Skip to content

分层架构中的数据访问层设计 ​

概述 ​

在企业级应用中,良好的架构设计是保证系统可维护性、可扩展性和可测试性的关键。数据访问层(Data Access Layer, DAL)作为应用与数据库之间的桥梁,其设计质量直接影响整个系统的健康度。本节深入探讨如何在 .NET 应用中设计优雅的数据访问层。

经典分层架构 ​

┌─────────────────────────────────────┐
│      Presentation Layer (UI)        │  ← Controllers, Razor Pages, APIs
├─────────────────────────────────────┤
│      Application Layer (Services)   │  ← Business Logic, DTOs, Interfaces
├─────────────────────────────────────┤
│       Domain Layer (Entities)       │  ← Entities, Value Objects, Interfaces
├─────────────────────────────────────┤
│    Data Access Layer (Repository)   │  ← DbContext, Implementations
├─────────────────────────────────────┤
│         Database (SQL Server)       │  ← Tables, Views, Stored Procedures
└─────────────────────────────────────┘

方案 1: 传统三层架构 ​

项目结构 ​

MyApp.sln
├── src/
│   ├── MyApp.API/              # 表示层 (Presentation)
│   │   ├── Controllers/
│   │   └── Program.cs
│   │
│   ├── MyApp.Application/      # 应用层 (Application)
│   │   ├── Services/
│   │   ├── DTOs/
│   │   └── Interfaces/
│   │
│   ├── MyApp.Domain/           # 领域层 (Domain)
│   │   ├── Entities/
│   │   └── Interfaces/
│   │
│   └── MyApp.Infrastructure/   # 基础设施层 (Infrastructure)
│       ├── Data/
│       │   ├── AppDbContext.cs
│       │   └── Repositories/
│       └── Migrations/
│
└── tests/
    ├── MyApp.UnitTests/
    └── MyApp.IntegrationTests/

1. 定义领域实体 ​

csharp
// MyApp.Domain/Entities/Product.cs
namespace MyApp.Domain.Entities;

public class Product : BaseEntity
{
    public string Name { get; set; } = string.Empty;
    public string Description { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Stock { get; set; }
    public int CategoryId { get; set; }
    
    // 导航属性
    public Category? Category { get; set; }
}

// MyApp.Domain/Entities/Category.cs
namespace MyApp.Domain.Entities;

public class Category : BaseEntity
{
    public string Name { get; set; } = string.Empty;
    
    // 导航属性
    public ICollection<Product> Products { get; set; } = new List<Product>();
}

// MyApp.Domain/BaseEntity.cs
namespace MyApp.Domain.Entities;

public abstract class BaseEntity
{
    public int Id { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime? UpdatedAt { get; set; }
}

2. 定义仓储接口(在领域层) ​

csharp
// MyApp.Domain/Interfaces/IRepository.cs
namespace MyApp.Domain.Interfaces;

public interface IRepository<T> where T : BaseEntity
{
    Task<T?> GetByIdAsync(int id);
    Task<List<T>> GetAllAsync();
    Task AddAsync(T entity);
    void Update(T entity);
    void Delete(T entity);
    Task SaveChangesAsync();
    IQueryable<T> Query();
}

// MyApp.Domain/Interfaces/IProductRepository.cs
namespace MyApp.Domain.Interfaces;

public interface IProductRepository : IRepository<Product>
{
    Task<List<Product>> GetByCategoryAsync(int categoryId);
    Task<List<Product>> GetLowStockProductsAsync(int threshold = 10);
    Task<decimal> GetAveragePriceAsync();
}

3. 实现仓储(在基础设施层) ​

csharp
// MyApp.Infrastructure/Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using MyApp.Domain.Entities;

namespace MyApp.Infrastructure.Data;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) 
        : base(options) { }
    
    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>(entity =>
        {
            entity.HasKey(p => p.Id);
            entity.Property(p => p.Name).IsRequired().HasMaxLength(200);
            entity.Property(p => p.Price).HasColumnType("decimal(18,2)");
            
            entity.HasOne(p => p.Category)
                .WithMany(c => c.Products)
                .HasForeignKey(p => p.CategoryId);
        });
        
        modelBuilder.Entity<Category>(entity =>
        {
            entity.HasKey(c => c.Id);
            entity.Property(c => c.Name).IsRequired().HasMaxLength(100);
        });
    }
    
    public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        // 自动更新时间戳
        var entries = ChangeTracker.Entries<BaseEntity>()
            .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
        
        foreach (var entry in entries)
        {
            if (entry.State == EntityState.Added)
                entry.Entity.CreatedAt = DateTime.UtcNow;
            else
                entry.Entity.UpdatedAt = DateTime.UtcNow;
        }
        
        return base.SaveChangesAsync(cancellationToken);
    }
}

// MyApp.Infrastructure/Data/Repositories/BaseRepository.cs
using MyApp.Domain.Entities;
using MyApp.Domain.Interfaces;
using Microsoft.EntityFrameworkCore;

namespace MyApp.Infrastructure.Data.Repositories;

public class BaseRepository<T> : IRepository<T> where T : BaseEntity
{
    protected readonly AppDbContext _context;
    protected readonly DbSet<T> _dbSet;
    
    public BaseRepository(AppDbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }
    
    public virtual async Task<T?> GetByIdAsync(int id)
    {
        return await _dbSet.FindAsync(id);
    }
    
    public virtual async Task<List<T>> GetAllAsync()
    {
        return await _dbSet.ToListAsync();
    }
    
    public virtual async Task AddAsync(T entity)
    {
        await _dbSet.AddAsync(entity);
    }
    
    public virtual void Update(T entity)
    {
        _dbSet.Update(entity);
    }
    
    public virtual void Delete(T entity)
    {
        _dbSet.Remove(entity);
    }
    
    public virtual async Task SaveChangesAsync()
    {
        await _context.SaveChangesAsync();
    }
    
    public virtual IQueryable<T> Query()
    {
        return _dbSet.AsQueryable();
    }
}

// MyApp.Infrastructure/Data/Repositories/ProductRepository.cs
using MyApp.Domain.Entities;
using MyApp.Domain.Interfaces;
using Microsoft.EntityFrameworkCore;

namespace MyApp.Infrastructure.Data.Repositories;

public class ProductRepository : BaseRepository<Product>, IProductRepository
{
    public ProductRepository(AppDbContext context) : base(context) { }
    
    public async Task<List<Product>> GetByCategoryAsync(int categoryId)
    {
        return await _dbSet
            .Include(p => p.Category)
            .Where(p => p.CategoryId == categoryId)
            .ToListAsync();
    }
    
    public async Task<List<Product>> GetLowStockProductsAsync(int threshold = 10)
    {
        return await _dbSet
            .Include(p => p.Category)
            .Where(p => p.Stock <= threshold)
            .OrderBy(p => p.Stock)
            .ToListAsync();
    }
    
    public async Task<decimal> GetAveragePriceAsync()
    {
        return await _dbSet.AverageAsync(p => p.Price);
    }
}

4. 应用服务层 ​

csharp
// MyApp.Application/DTOs/ProductDto.cs
namespace MyApp.Application.DTOs;

public record ProductDto(
    int Id,
    string Name,
    string Description,
    decimal Price,
    int Stock,
    int CategoryId,
    string? CategoryName
);

public record CreateProductRequest(
    string Name,
    string Description,
    decimal Price,
    int Stock,
    int CategoryId
);

public record UpdateProductRequest(
    int Id,
    string Name,
    string Description,
    decimal Price,
    int Stock
);

// MyApp.Application/Services/IProductService.cs
namespace MyApp.Application.Services;

public interface IProductService
{
    Task<ProductDto?> GetByIdAsync(int id);
    Task<List<ProductDto>> GetAllAsync();
    Task<List<ProductDto>> GetByCategoryAsync(int categoryId);
    Task<List<ProductDto>> GetLowStockProductsAsync(int threshold = 10);
    Task<int> CreateAsync(CreateProductRequest request);
    Task UpdateAsync(UpdateProductRequest request);
    Task DeleteAsync(int id);
}

// MyApp.Application/Services/ProductService.cs
using MyApp.Application.DTOs;
using MyApp.Domain.Interfaces;
using MyApp.Domain.Entities;

namespace MyApp.Application.Services;

public class ProductService : IProductService
{
    private readonly IProductRepository _productRepository;
    private readonly IRepository<Category> _categoryRepository;
    
    public ProductService(
        IProductRepository productRepository,
        IRepository<Category> categoryRepository)
    {
        _productRepository = productRepository;
        _categoryRepository = categoryRepository;
    }
    
    public async Task<ProductDto?> GetByIdAsync(int id)
    {
        var product = await _productRepository.GetByIdAsync(id);
        return product != null ? MapToDto(product) : null;
    }
    
    public async Task<List<ProductDto>> GetAllAsync()
    {
        var products = await _productRepository.GetAllAsync();
        return products.Select(MapToDto).ToList();
    }
    
    public async Task<List<ProductDto>> GetByCategoryAsync(int categoryId)
    {
        var products = await _productRepository.GetByCategoryAsync(categoryId);
        return products.Select(MapToDto).ToList();
    }
    
    public async Task<List<ProductDto>> GetLowStockProductsAsync(int threshold = 10)
    {
        var products = await _productRepository.GetLowStockProductsAsync(threshold);
        return products.Select(MapToDto).ToList();
    }
    
    public async Task<int> CreateAsync(CreateProductRequest request)
    {
        // 验证分类是否存在
        var category = await _categoryRepository.GetByIdAsync(request.CategoryId);
        if (category == null)
            throw new InvalidOperationException($"Category {request.CategoryId} not found");
        
        var product = new Product
        {
            Name = request.Name,
            Description = request.Description,
            Price = request.Price,
            Stock = request.Stock,
            CategoryId = request.CategoryId
        };
        
        await _productRepository.AddAsync(product);
        await _productRepository.SaveChangesAsync();
        
        return product.Id;
    }
    
    public async Task UpdateAsync(UpdateProductRequest request)
    {
        var product = await _productRepository.GetByIdAsync(request.Id);
        if (product == null)
            throw new InvalidOperationException($"Product {request.Id} not found");
        
        product.Name = request.Name;
        product.Description = request.Description;
        product.Price = request.Price;
        product.Stock = request.Stock;
        
        _productRepository.Update(product);
        await _productRepository.SaveChangesAsync();
    }
    
    public async Task DeleteAsync(int id)
    {
        var product = await _productRepository.GetByIdAsync(id);
        if (product == null)
            throw new InvalidOperationException($"Product {id} not found");
        
        _productRepository.Delete(product);
        await _productRepository.SaveChangesAsync();
    }
    
    private static ProductDto MapToDto(Product product)
    {
        return new ProductDto(
            product.Id,
            product.Name,
            product.Description,
            product.Price,
            product.Stock,
            product.CategoryId,
            product.Category?.Name
        );
    }
}

5. 依赖注入配置 ​

csharp
// MyApp.Infrastructure/DependencyInjection.cs
using Microsoft.Extensions.DependencyInjection;
using MyApp.Domain.Interfaces;
using MyApp.Infrastructure.Data;
using MyApp.Infrastructure.Data.Repositories;

namespace MyApp.Infrastructure;

public static class DependencyInjection
{
    public static IServiceCollection AddInfrastructure(this IServiceCollection services)
    {
        // 注册 DbContext
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(ConfigurationExtensions.GetConnectionString("Default")));
        
        // 注册仓储
        services.AddScoped<IProductRepository, ProductRepository>();
        services.AddScoped(typeof(IRepository<>), typeof(BaseRepository<>));
        
        return services;
    }
}

// MyApp.Application/DependencyInjection.cs
using Microsoft.Extensions.DependencyInjection;
using MyApp.Application.Services;

namespace MyApp.Application;

public static class DependencyInjection
{
    public static IServiceCollection AddApplication(this IServiceCollection services)
    {
        services.AddScoped<IProductService, ProductService>();
        // 注册其他服务...
        
        return services;
    }
}

6. API 控制器 ​

csharp
// MyApp.API/Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
using MyApp.Application.DTOs;
using MyApp.Application.Services;

namespace MyApp.API.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;
    
    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }
    
    [HttpGet]
    public async Task<ActionResult<List<ProductDto>>> GetAll()
    {
        var products = await _productService.GetAllAsync();
        return Ok(products);
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<ProductDto>> GetById(int id)
    {
        var product = await _productService.GetByIdAsync(id);
        if (product == null)
            return NotFound();
        
        return Ok(product);
    }
    
    [HttpPost]
    public async Task<ActionResult<int>> Create([FromBody] CreateProductRequest request)
    {
        try
        {
            var productId = await _productService.CreateAsync(request);
            return CreatedAtAction(nameof(GetById), new { id = productId }, productId);
        }
        catch (InvalidOperationException ex)
        {
            return BadRequest(new { error = ex.Message });
        }
    }
    
    [HttpPut("{id}")]
    public async Task<IActionResult> Update(int id, [FromBody] UpdateProductRequest request)
    {
        if (id != request.Id)
            return BadRequest();
        
        try
        {
            await _productService.UpdateAsync(request);
            return NoContent();
        }
        catch (InvalidOperationException ex)
        {
            return NotFound(new { error = ex.Message });
        }
    }
    
    [HttpDelete("{id}")]
    public async Task<IActionResult> Delete(int id)
    {
        try
        {
            await _productService.DeleteAsync(id);
            return NoContent();
        }
        catch (InvalidOperationException ex)
        {
            return NotFound(new { error = ex.Message });
        }
    }
}

方案 2: Clean Architecture (推荐) ​

Clean Architecture 强调依赖倒置,领域层不依赖任何外部层。

项目结构 ​

MyApp.sln
├── src/
│   ├── Core/
│   │   ├── MyApp.Domain/         # 纯领域逻辑
│   │   └── MyApp.Application/    # 应用逻辑和接口
│   │
│   ├── Infrastructure/
│   │   └── MyApp.Infrastructure/ # 外部依赖实现
│   │
│   └── API/
│       └── MyApp.API/            # 表示层
│
└── tests/

核心差异 ​

Application 层定义接口:

csharp
// MyApp.Application/Common/Interfaces/IApplicationDbContext.cs
namespace MyApp.Application.Common.Interfaces;

public interface IApplicationDbContext
{
    DbSet<Product> Products { get; }
    DbSet<Category> Categories { get; }
    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

Infrastructure 层实现接口:

csharp
// MyApp.Infrastructure/Data/AppDbContext.cs
namespace MyApp.Infrastructure.Data;

public class AppDbContext : DbContext, IApplicationDbContext
{
    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();
    
    // ... 实现细节
}

使用 MediatR 实现 CQRS:

csharp
// MyApp.Application/Products/Commands/CreateProductCommand.cs
using MediatR;

namespace MyApp.Application.Products.Commands;

public record CreateProductCommand(
    string Name,
    string Description,
    decimal Price,
    int Stock,
    int CategoryId) : IRequest<int>;

// MyApp.Application/Products/Commands/CreateProductCommandHandler.cs
namespace MyApp.Application.Products.Commands;

public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, int>
{
    private readonly IApplicationDbContext _context;
    
    public CreateProductCommandHandler(IApplicationDbContext context)
    {
        _context = context;
    }
    
    public async Task<int> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        var product = new Product
        {
            Name = request.Name,
            Description = request.Description,
            Price = request.Price,
            Stock = request.Stock,
            CategoryId = request.CategoryId
        };
        
        _context.Products.Add(product);
        await _context.SaveChangesAsync(cancellationToken);
        
        return product.Id;
    }
}

// API Controller 使用 MediatR
[HttpPost]
public async Task<ActionResult<int>> Create([FromBody] CreateProductCommand command)
{
    var productId = await _mediator.Send(command);
    return CreatedAtAction(nameof(GetById), new { id = productId }, productId);
}

最佳实践 ​

1. 使用工作单元模式 ​

csharp
public interface IUnitOfWork : IDisposable
{
    IProductRepository Products { get; }
    IOrderRepository Orders { get; }
    ICustomerRepository Customers { get; }
    
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

public class UnitOfWork : IUnitOfWork
{
    private readonly AppDbContext _context;
    private IProductRepository? _products;
    private IOrderRepository? _orders;
    
    public UnitOfWork(AppDbContext context)
    {
        _context = context;
    }
    
    public IProductRepository Products => 
        _products ??= new ProductRepository(_context);
    
    public IOrderRepository Orders => 
        _orders ??= new OrderRepository(_context);
    
    public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return await _context.SaveChangesAsync(cancellationToken);
    }
    
    public void Dispose()
    {
        _context.Dispose();
    }
}

// 使用示例
public class OrderService
{
    private readonly IUnitOfWork _unitOfWork;
    
    public OrderService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    
    public async Task ProcessOrderAsync(int orderId)
    {
        var order = await _unitOfWork.Orders.GetByIdAsync(orderId);
        var product = await _unitOfWork.Products.GetByIdAsync(order.ProductId);
        
        // 业务逻辑...
        product.Stock -= order.Quantity;
        order.Status = OrderStatus.Processing;
        
        _unitOfWork.Products.Update(product);
        _unitOfWork.Orders.Update(order);
        
        await _unitOfWork.SaveChangesAsync();
    }
}

2. 规范模式(Specification Pattern) ​

csharp
public interface ISpecification<T>
{
    Expression<Func<T, bool>> Criteria { get; }
    List<Expression<Func<T, object>>> Includes { get; }
}

public class ProductSpecification : ISpecification<Product>
{
    public Expression<Func<Product, bool>> Criteria { get; private set; } = _ => true;
    public List<Expression<Func<Product, object>>> Includes { get; } = new();
    
    public ProductSpecification WithCategory(int categoryId)
    {
        Criteria = Criteria.And(p => p.CategoryId == categoryId);
        Includes.Add(p => p.Category!);
        return this;
    }
    
    public ProductSpecification InStock()
    {
        Criteria = Criteria.And(p => p.Stock > 0);
        return this;
    }
}

public static class SpecificationExtensions
{
    public static Expression<Func<T, bool>> And<T>(
        this Expression<Func<T, bool>> left,
        Expression<Func<T, bool>> right)
    {
        var parameter = Expression.Parameter(typeof(T));
        var body = Expression.AndAlso(
            Expression.Invoke(left, parameter),
            Expression.Invoke(right, parameter));
        return Expression.Lambda<Func<T, bool>>(body, parameter);
    }
}

总结 ​

分层架构的优势 ​

  • ✅ 关注点分离: 每层职责明确
  • ✅ 可测试性: 通过接口轻松 Mock
  • ✅ 可维护性: 修改一层不影响其他层
  • ✅ 可替换性: 可以轻松替换数据存储方案

选择建议 ​

  • 小型项目: 直接使用 DbContext,无需仓储
  • 中型项目: 使用传统三层架构 + 仓储模式
  • 大型项目: 采用 Clean Architecture + CQRS + MediatR

记住:没有银弹,选择适合你项目规模和复杂度的架构!

基于 MIT 许可发布