无私分享:从入门到精通ASP.NET MVC 从0开始,一起搭框架、做项目(3)公共基础数据操作类 RepositoryBase

在ASP.NET MVC(或ASP.NET Core MVC)项目开发中,数据访问层的重复代码是每个开发者都会遇到的痛点:每个业务实体的增删改查逻辑几乎大同小异,反复编写不仅低效,还会导致维护成本飙升。RepositoryBase作为通用基础数据操作类,基于Repository模式封装了通用CRUD、异步查询、分页、事务等核心能力,能帮助我们彻底告别重复代码,专注于业务逻辑实现。

本文将从零开始,逐步实现一个功能完整、符合最佳实践的RepositoryBase,并结合示例项目演示如何在实际业务中使用,同时分享常见问题与最佳实践。

目录#

  1. 前置知识:Repository模式与基类设计思路
  2. 从零实现RepositoryBase核心功能 2.1 基础架构与泛型约束定义 2.2 通用CRUD方法(同步+异步) 2.3 过滤、排序、分页通用查询 2.4 高效批量操作(基于EF Core 7.0+) 2.5 事务管理封装
  3. 基于RepositoryBase实现业务Repository
  4. 最佳实践与常见问题
  5. 示例项目整合演示
  6. 总结
  7. 参考资料

1. 前置知识:Repository模式与基类设计思路#

1.1 Repository模式简介#

Repository模式是DDD领域驱动设计中的核心模式之一,它的核心目标是:

  • 解耦领域层与数据访问层:业务逻辑无需直接操作EF Core DbContext,而是通过Repository接口访问数据
  • 提高可测试性:可通过Mock Repository实现单元测试,无需依赖真实数据库
  • 统一数据访问规范:所有数据操作遵循一致的接口标准

1.2 RepositoryBase设计目标#

我们需要设计一个满足以下要求的通用基类:

  1. 泛型支持:适配任意实体类型,无需为每个实体重复编写基础CRUD
  2. 异步优先:全面支持异步操作,提升Web应用并发性能
  3. 可扩展:允许业务Repository自定义扩展方法,不限制灵活性
  4. 高效查询:封装过滤、排序、分页、导航属性加载等通用查询逻辑
  5. 事务支持:提供统一的事务管理能力,支持跨Repository事务
  6. 低侵入性:仅依赖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+支持ExecuteDeleteAsyncExecuteUpdateAsync,无需加载实体即可直接执行数据库操作,性能远高于传统的加载再删除/更新:

#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);
}
#endregion

2.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; // 重新抛出异常,上层处理
    }
}
#endregion

3. 基于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 最佳实践#

  1. 异步优先:所有数据库操作优先实现异步版本,避免线程阻塞,提升Web应用并发能力
  2. 基类只存通用逻辑:业务相关查询必须放在具体Repository中,禁止在RepositoryBase中编写业务代码
  3. 高效批量操作优先:EF Core 7.0+场景下,优先使用ExecuteDeleteAsync/ExecuteUpdateAsync替代加载实体后删除/更新
  4. 依赖注入正确配置:在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>();
  5. 软删除统一处理:如果项目使用软删除,可以在基类中封装默认过滤逻辑(比如查询时自动排除已删除实体)

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. 参考资料#

  1. 微软官方:Repository模式设计指南
  2. EF Core 异步操作文档
  3. EF Core 高效批量操作
  4. 《领域驱动设计精粹》(Eric Evans)