无私分享:从入门到精通ASP.NET MVC 从0开始,一起搭框架、做项目(3)公共基础数据操作类 RepositoryBase
在ASP.NET MVC(或ASP.NET Core MVC)项目开发中,数据访问层的重复代码是每个开发者都会遇到的痛点:每个业务实体的增删改查逻辑几乎大同小异,反复编写不仅低效,还会导致维护成本飙升。RepositoryBase作为通用基础数据操作类,基于Repository模式封装了通用CRUD、异步查询、分页、事务等核心能力,能帮助我们彻底告别重复代码,专注于业务逻辑实现。
本文将从零开始,逐步实现一个功能完整、符合最佳实践的RepositoryBase,并结合示例项目演示如何在实际业务中使用,同时分享常见问题与最佳实践。
目录#
- 前置知识:Repository模式与基类设计思路
- 从零实现RepositoryBase核心功能 2.1 基础架构与泛型约束定义 2.2 通用CRUD方法(同步+异步) 2.3 过滤、排序、分页通用查询 2.4 高效批量操作(基于EF Core 7.0+) 2.5 事务管理封装
- 基于RepositoryBase实现业务Repository
- 最佳实践与常见问题
- 示例项目整合演示
- 总结
- 参考资料
1. 前置知识:Repository模式与基类设计思路#
1.1 Repository模式简介#
Repository模式是DDD领域驱动设计中的核心模式之一,它的核心目标是:
- 解耦领域层与数据访问层:业务逻辑无需直接操作EF Core DbContext,而是通过Repository接口访问数据
- 提高可测试性:可通过Mock Repository实现单元测试,无需依赖真实数据库
- 统一数据访问规范:所有数据操作遵循一致的接口标准
1.2 RepositoryBase设计目标#
我们需要设计一个满足以下要求的通用基类:
- 泛型支持:适配任意实体类型,无需为每个实体重复编写基础CRUD
- 异步优先:全面支持异步操作,提升Web应用并发性能
- 可扩展:允许业务Repository自定义扩展方法,不限制灵活性
- 高效查询:封装过滤、排序、分页、导航属性加载等通用查询逻辑
- 事务支持:提供统一的事务管理能力,支持跨Repository事务
- 低侵入性:仅依赖EF Core DbContext,无其他强绑定
2. 从零实现RepositoryBase核心功能#
2.1 基础架构与泛型约束定义#
首先,我们需要定义统一的实体接口和Repository接口,确保基类的泛型约束合理。
步骤1:定义实体基接口#
为了统一实体主键规范,定义IEntity<TKey>接口:
/// <summary>
/// 实体基接口,统一主键定义
/// </summary>
/// <typeparam name="TKey">主键类型(int、long、Guid等)</typeparam>
public interface IEntity<TKey>
{
/// <summary>
/// 实体主键
/// </summary>
TKey Id { get; set; }
}
/// <summary>
/// 软删除实体接口(可选,根据业务需求添加)
/// </summary>
public interface ISoftDelete
{
bool IsDeleted { get; set; }
DateTime? DeleteTime { get; set; }
}步骤2:定义通用Repository接口#
/// <summary>
/// 通用Repository接口
/// </summary>
/// <typeparam name="T">实体类型</typeparam>
/// <typeparam name="TKey">主键类型</typeparam>
public interface IRepository<T, TKey> where T : class, IEntity<TKey>
{
// 单实体操作
Task<T> GetByIdAsync(TKey id, string includeProperties = "", CancellationToken cancellationToken = default);
Task<T> AddAsync(T entity, bool saveChanges = true, CancellationToken cancellationToken = default);
Task<T> UpdateAsync(T entity, bool saveChanges = true, CancellationToken cancellationToken = default);
Task<int> DeleteByIdAsync(TKey id, bool saveChanges = true, CancellationToken cancellationToken = default);
// 批量操作
Task<List<T>> AddRangeAsync(IEnumerable<T> entities, bool saveChanges = true, CancellationToken cancellationToken = default);
Task<int> DeleteRangeAsync(Expression<Func<T, bool>> filter, bool saveChanges = true, CancellationToken cancellationToken = default);
Task<int> BatchUpdateAsync(Expression<Func<T, bool>> filter, Expression<Func<SetPropertyCalls<T>, SetPropertyCalls<T>>> setProperties, CancellationToken cancellationToken = default);
// 通用查询
Task<PagedResult<T>> GetPagedListAsync(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, string includeProperties = "", int pageIndex = 1, int pageSize = 10, CancellationToken cancellationToken = default);
// 事务管理
Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
Task CommitTransactionAsync(IDbContextTransaction transaction, CancellationToken cancellationToken = default);
}2.2 通用CRUD方法(同步+异步)#
接下来实现RepositoryBase类,继承自上述接口,泛型约束限定实体必须实现IEntity<TKey>。
public abstract class RepositoryBase<T, TKey> : IRepository<T, TKey>
where T : class, IEntity<TKey>
{
protected readonly AppDbContext _dbContext;
protected readonly DbSet<T> _dbSet;
public RepositoryBase(AppDbContext dbContext)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_dbSet = _dbContext.Set<T>();
}
#region 单实体CRUD
public async Task<T> GetByIdAsync(TKey id, string includeProperties = "", CancellationToken cancellationToken = default)
{
IQueryable<T> query = _dbSet;
// 加载导航属性
foreach (var includeProperty in includeProperties.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
return await query.FirstOrDefaultAsync(e => e.Id.Equals(id), cancellationToken);
}
public async Task<T> AddAsync(T entity, bool saveChanges = true, CancellationToken cancellationToken = default)
{
if (entity == null) throw new ArgumentNullException(nameof(entity));
await _dbSet.AddAsync(entity, cancellationToken);
if (saveChanges) await _dbContext.SaveChangesAsync(cancellationToken);
return entity;
}
public async Task<T> UpdateAsync(T entity, bool saveChanges = true, CancellationToken cancellationToken = default)
{
if (entity == null) throw new ArgumentNullException(nameof(entity));
_dbSet.Update(entity);
if (saveChanges) await _dbContext.SaveChangesAsync(cancellationToken);
return entity;
}
public async Task<int> DeleteByIdAsync(TKey id, bool saveChanges = true, CancellationToken cancellationToken = default)
{
var entity = await GetByIdAsync(id, cancellationToken: cancellationToken);
if (entity == null) return 0;
_dbSet.Remove(entity);
return saveChanges ? await _dbContext.SaveChangesAsync(cancellationToken) : 0;
}
#endregion
}2.3 过滤、排序、分页通用查询#
分页是Web应用中高频需求,我们封装通用分页方法,同时支持过滤、排序、导航属性加载:
// 定义分页结果模型
public class PagedResult<T>
{
public int PageIndex { get; set; }
public int PageSize { get; set; }
public int TotalCount { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public List<T> Items { get; set; } = new List<T>();
}
// 在RepositoryBase中添加分页方法
public async Task<PagedResult<T>> GetPagedListAsync(Expression<Func<T, bool>> filter = null,
Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null,
string includeProperties = "",
int pageIndex = 1,
int pageSize = 10,
CancellationToken cancellationToken = default)
{
IQueryable<T> query = _dbSet;
// 应用过滤条件
if (filter != null) query = query.Where(filter);
// 加载导航属性
foreach (var includeProperty in includeProperties.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
// 统计总数
var totalCount = await query.CountAsync(cancellationToken);
// 排序
if (orderBy != null) query = orderBy(query);
else query = query.OrderBy(e => e.Id); // 默认按主键排序
// 分页查询
var items = await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken);
return new PagedResult<T>
{
PageIndex = pageIndex,
PageSize = pageSize,
TotalCount = totalCount,
Items = items
};
}2.4 高效批量操作(基于EF Core 7.0+)#
EF Core 7.0+支持ExecuteDeleteAsync和ExecuteUpdateAsync,无需加载实体即可直接执行数据库操作,性能远高于传统的加载再删除/更新:
#region 批量操作
public async Task<List<T>> AddRangeAsync(IEnumerable<T> entities, bool saveChanges = true, CancellationToken cancellationToken = default)
{
if (entities == null) throw new ArgumentNullException(nameof(entities));
await _dbSet.AddRangeAsync(entities, cancellationToken);
if (saveChanges) await _dbContext.SaveChangesAsync(cancellationToken);
return entities.ToList();
}
public async Task<int> DeleteRangeAsync(Expression<Func<T, bool>> filter, bool saveChanges = true, CancellationToken cancellationToken = default)
{
if (filter == null) throw new ArgumentNullException(nameof(filter));
// 高效批量删除,无需加载实体
return await _dbSet.Where(filter).ExecuteDeleteAsync(cancellationToken);
}
public async Task<int> BatchUpdateAsync(Expression<Func<T, bool>> filter, Expression<Func<SetPropertyCalls<T>, SetPropertyCalls<T>>> setProperties, CancellationToken cancellationToken = default)
{
if (filter == null) throw new ArgumentNullException(nameof(filter));
if (setProperties == null) throw new ArgumentNullException(nameof(setProperties));
// 高效批量更新
return await _dbSet.Where(filter).ExecuteUpdateAsync(setProperties, cancellationToken);
}
#endregion2.5 事务管理封装#
跨业务操作的事务管理是常见需求,我们在RepositoryBase中封装事务的开启、提交、回滚逻辑:
#region 事务管理
public async Task<IDbContextTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default)
{
return await _dbContext.Database.BeginTransactionAsync(cancellationToken);
}
public async Task CommitTransactionAsync(IDbContextTransaction transaction, CancellationToken cancellationToken = default)
{
if (transaction == null) throw new ArgumentNullException(nameof(transaction));
try
{
await _dbContext.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
}
catch
{
await transaction.RollbackAsync(cancellationToken);
throw; // 重新抛出异常,上层处理
}
}
#endregion3. 基于RepositoryBase实现业务Repository#
有了RepositoryBase,业务Repository只需实现接口+自定义业务逻辑即可,无需重复编写基础CRUD。
3.1 定义业务实体与Repository接口#
// 业务实体:Product
public class Product : IEntity<long>, ISoftDelete
{
public long Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
public bool IsHot { get; set; }
// 软删除字段
public bool IsDeleted { get; set; }
public DateTime? DeleteTime { get; set; }
}
// 业务Repository接口
public interface IProductRepository : IRepository<Product, long>
{
// 自定义业务查询:获取热门商品
Task<List<Product>> GetHotProductsAsync(int topN, CancellationToken cancellationToken = default);
}3.2 实现业务Repository#
public class ProductRepository : RepositoryBase<Product, long>, IProductRepository
{
public ProductRepository(AppDbContext dbContext) : base(dbContext)
{
}
public async Task<List<Product>> GetHotProductsAsync(int topN, CancellationToken cancellationToken = default)
{
return await _dbSet
.Where(p => !p.IsDeleted && p.IsHot && p.Stock > 0)
.OrderByDescending(p => p.Price)
.Take(topN)
.ToListAsync(cancellationToken);
}
}4. 最佳实践与常见问题#
4.1 最佳实践#
- 异步优先:所有数据库操作优先实现异步版本,避免线程阻塞,提升Web应用并发能力
- 基类只存通用逻辑:业务相关查询必须放在具体Repository中,禁止在RepositoryBase中编写业务代码
- 高效批量操作优先:EF Core 7.0+场景下,优先使用
ExecuteDeleteAsync/ExecuteUpdateAsync替代加载实体后删除/更新 - 依赖注入正确配置:在Program.cs中注册泛型Repository和业务Repository,使用Scoped生命周期:
// 注册DbContext builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); // 注册泛型Repository基类 builder.Services.AddScoped(typeof(IRepository<,>), typeof(RepositoryBase<,>)); // 注册业务Repository builder.Services.AddScoped<IProductRepository, ProductRepository>(); - 软删除统一处理:如果项目使用软删除,可以在基类中封装默认过滤逻辑(比如查询时自动排除已删除实体)
4.2 常见问题#
Q1:何时需要在RepositoryBase中扩展方法,何时在业务Repository中实现?#
A:如果方法适用于所有实体(如分页、批量删除),则封装在基类;如果仅针对单个业务实体(如获取热门商品),则在具体Repository中实现。
Q2:跨多个Repository的事务如何处理?#
A:由于ASP.NET Core中DbContext默认是Scoped生命周期,多个Repository会共享同一个DbContext实例。只需在任意一个Repository中开启事务,操作多个Repository后统一提交即可:
public async Task CreateOrderWithStockDeductionAsync(Order order, List<long> productIds, List<int> quantities)
{
using var transaction = await _productRepository.BeginTransactionAsync();
try
{
// 1. 批量扣减库存
for (int i = 0; i < productIds.Count; i++)
{
await _productRepository.BatchUpdateAsync(
p => p.Id == productIds[i],
s => s.SetProperty(p => p.Stock, p => p.Stock - quantities[i]));
}
// 2. 创建订单
await _orderRepository.AddAsync(order);
// 3. 提交事务
await _productRepository.CommitTransactionAsync(transaction);
}
catch
{
await transaction.RollbackAsync();
throw;
}
}Q3:如何在Repository中进行单元测试?#
A:可以使用Moq框架Mock AppDbContext或直接使用EF Core In-Memory数据库,无需依赖真实数据库即可完成单元测试。
5. 示例项目整合演示#
在ASP.NET Core MVC的Controller中注入IProductRepository,即可直接使用所有功能:
public class ProductController : Controller
{
private readonly IProductRepository _productRepository;
public ProductController(IProductRepository productRepository)
{
_productRepository = productRepository;
}
// 分页展示商品列表
public async Task<IActionResult> Index(int pageIndex = 1, int pageSize = 10)
{
var pagedResult = await _productRepository.GetPagedListAsync(
filter: p => !p.IsDeleted && p.Stock > 0,
orderBy: q => q.OrderByDescending(p => p.Price),
pageIndex: pageIndex,
pageSize: pageSize);
return View(pagedResult);
}
// 批量删除过期商品
public async Task<IActionResult> DeleteExpiredProducts()
{
var affectedRows = await _productRepository.DeleteRangeAsync(p => p.Stock == 0);
TempData["Message"] = $"成功删除{affectedRows}个过期商品";
return RedirectToAction(nameof(Index));
}
}6. 总结#
RepositoryBase作为数据访问层的核心基础类,通过封装通用CRUD、异步查询、分页、批量操作、事务等能力,能极大提升开发效率,同时保证代码的可维护性与可扩展性。本文实现的RepositoryBase具备以下特点:
- 泛型适配所有实体类型
- 全面支持异步操作
- 高效批量操作(基于EF Core 7.0+特性)
- 灵活的事务管理
- 符合DDD与依赖注入最佳实践
在实际项目中,可根据业务需求进一步扩展基类功能(如缓存整合、多租户支持等),但需始终遵循"通用逻辑放基类,业务逻辑放具体Repository"的原则。
7. 参考资料#
- 微软官方:Repository模式设计指南
- EF Core 异步操作文档
- EF Core 高效批量操作
- 《领域驱动设计精粹》(Eric Evans)