Appearance
分层架构中的数据访问层设计
概述
在企业级应用中,良好的架构设计是保证系统可维护性、可扩展性和可测试性的关键。数据访问层(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
记住:没有银弹,选择适合你项目规模和复杂度的架构!