如何实现Repository模式
这篇文章主要介绍"如何实现Repository模式",在日常操作中,相信很多人在如何实现Repository模式问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"如何实现Repository模式"的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
需求
经常写CRUD程序的小伙伴们可能都经历过定义很多Repository接口,分别做对应的实现,依赖注入并使用的场景。有的时候会发现,很多分散的XXXXRepository的逻辑都是基本一致的,于是开始思考是否可以将这些操作抽象出去,当然是可以的,而且被抽象出去的部分是可以不加改变地在今后的任何有此需求的项目中直接引入使用。
那么我们本文的需求就是:如何实现一个可重用的Repository模块。
长文预警,包含大量代码。
目标
实现通用Repository模式并进行验证。
原理和思路
通用的基础在于抽象,抽象的粒度决定了通用的程度,但是同时也决定了使用上的复杂度。对于自己的项目而言,抽象到什么程度最合适,需要自己去权衡,也许后面某个时候我会决定自己去实现一个完善的Repository库提供出来(事实上已经有很多人这样做了,我们甚至可以直接下载Nuget包进行使用,但是自己亲手去实现的过程能让你更好地去理解其中的原理,也理解如何开发一个通用的类库。)
总体思路是:在Application中定义相关的接口,在Infrastructure中实现基类的功能。
实现
通用Repository实现
对于要如何去设计一个通用的Repository库,实际上涉及的面非常多,尤其是在获取数据的时候。而且根据每个人的习惯,实现起来的方式是有比较大的差别的,尤其是关于泛型接口到底需要提供哪些方法,每个人都有自己的理解,这里我只演示基本的思路,而且尽量保持简单,关于更复杂和更全面的实现,GIthub上有很多已经写好的库可以去学习和参考,我会列在下面:
很显然,第一步要去做的是在Application/Common/Interfaces中增加一个IRepository的定义用于适用不同类型的实体,然后在Infrastructure/Persistence/Repositories中创建一个基类RepositoryBase实现这个接口,并有办法能提供一致的对外方法签名。
IRepository.cs
namespace TodoList.Application.Common.Interfaces;public interface IRepositorywhere T : class{}
RepositoryBase.cs
using Microsoft.EntityFrameworkCore;using TodoList.Application.Common.Interfaces;namespace TodoList.Infrastructure.Persistence.Repositories;public class RepositoryBase: IRepository where T : class{ private readonly TodoListDbContext _dbContext; public RepositoryBase(TodoListDbContext dbContext) => _dbContext = dbContext;}
在动手实际定义IRepository之前,先思考一下:对数据库的操作都会出现哪些情况:
新增实体(Create)
新增实体在Repository层面的逻辑很简单,传入一个实体对象,然后保存到数据库就可以了,没有其他特殊的需求。
IRepository.cs
// 省略其他...// Create相关操作接口TaskAddAsync(T entity, CancellationToken cancellationToken = default);
RepositoryBase.cs
// 省略其他...public async TaskAddAsync(T entity, CancellationToken cancellationToken = default){ await _dbContext.Set ().AddAsync(entity, cancellationToken); await _dbContext.SaveChangesAsync(cancellationToken); return entity;}
更新实体(Update)
和新增实体类似,但是更新时一般是单个实体对象去操作。
IRepository.cs
// 省略其他...// Update相关操作接口Task UpdateAsync(T entity, CancellationToken cancellationToken = default);
RepositoryBase.cs
// 省略其他...public async Task UpdateAsync(T entity, CancellationToken cancellationToken = default){ // 对于一般的更新而言,都是Attach到实体上的,只需要设置该实体的State为Modified就可以了 _dbContext.Entry(entity).State = EntityState.Modified; await _dbContext.SaveChangesAsync(cancellationToken);}删除实体(Delete)
对于删除实体,可能会出现两种情况:删除一个实体;或者删除一组实体。
IRepository.cs
// 省略其他...// Delete相关操作接口,这里根据key删除对象的接口需要用到一个获取对象的方法ValueTaskGetAsync(object key);Task DeleteAsync(object key);Task DeleteAsync(T entity, CancellationToken cancellationToken = default);Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default);
RepositoryBase.cs
// 省略其他...public virtual ValueTaskGetAsync(object key) => _dbContext.Set ().FindAsync(key);public async Task DeleteAsync(object key){ var entity = await GetAsync(key); if (entity is not null) { await DeleteAsync(entity); }}public async Task DeleteAsync(T entity, CancellationToken cancellationToken = default){ _dbContext.Set ().Remove(entity); await _dbContext.SaveChangesAsync(cancellationToken);}public async Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default){ _dbContext.Set ().RemoveRange(entities); await _dbContext.SaveChangesAsync(cancellationToken);}
获取实体(Retrieve)
对于如何获取实体,是最复杂的一部分。我们不仅要考虑通过什么方式获取哪些数据,还需要考虑获取的数据有没有特殊的要求比如排序、分页、数据对象类型的转换之类的问题。
具体来说,比如下面这一个典型的LINQ查询语句:
var results = await _context.A.Join(_context.B, a => a.Id, b => b.aId, (a, b) => new { // ... }) .Where(ab => ab.Name == "name" && ab.Date == DateTime.Now) .Select(ab => new { // ... }) .OrderBy(o => o.Date) .Skip(20 * 1) .Take(20) .ToListAsync();可以将整个查询结构分割成以下几个组成部分,而且每个部分基本都是以lambda表达式的方式表示的,这转化成建模的话,可以使用Expression相关的对象来表示:
1.查询数据集准备过程,在这个过程中可能会出现Include/Join/GroupJoin/GroupBy等等类似的关键字,它们的作用是构建一个用于接下来将要进行查询的数据集。
2.Where子句,用于过滤查询集合。
3.Select子句,用于转换原始数据类型到我们想要的结果类型。
4.Order子句,用于对结果集进行排序,这里可能会包含类似:OrderBy/OrderByDescending/ThenBy/ThenByDescending等关键字。
5.Paging子句,用于对结果集进行后端分页返回,一般都是Skip/Take一起使用。
6.其他子句,多数是条件控制,比如AsNoTracking/SplitQuery等等。
为了保持我们的演示不会过于复杂,我会做一些取舍。在这里的实现我参考了Edi.Wang的Moonglade中的相关实现。有兴趣的小伙伴也可以去找一下一个更完整的实现:Ardalis.Specification。
首先来定义一个简单的ISpecification来表示查询的各类条件:
using System.Linq.Expressions;using Microsoft.EntityFrameworkCore.Query;namespace TodoList.Application.Common.Interfaces;public interface ISpecification{ // 查询条件子句 Expression > Criteria { get; } // Include子句 Func , IIncludableQueryable > Include { get; } // OrderBy子句 Expression > OrderBy { get; } // OrderByDescending子句 Expression > OrderByDescending { get; } // 分页相关属性 int Take { get; } int Skip { get; } bool IsPagingEnabled { get; }}
并实现这个泛型接口,放在Application/Common中:
using System.Linq.Expressions;using Microsoft.EntityFrameworkCore.Query;using TodoList.Application.Common.Interfaces;namespace TodoList.Application.Common;public abstract class SpecificationBase: ISpecification { protected SpecificationBase() { } protected SpecificationBase(Expression > criteria) => Criteria = criteria; public Expression > Criteria { get; private set; } public Func , IIncludableQueryable > Include { get; private set; } public List IncludeStrings { get; } = new(); public Expression > OrderBy { get; private set; } public Expression > OrderByDescending { get; private set; } public int Take { get; private set; } public int Skip { get; private set; } public bool IsPagingEnabled { get; private set; } public void AddCriteria(Expression > criteria) => Criteria = Criteria is not null ? Criteria.AndAlso(criteria) : criteria; protected virtual void AddInclude(Func , IIncludableQueryable > includeExpression) => Include = includeExpression; protected virtual void AddInclude(string includeString) => IncludeStrings.Add(includeString); protected virtual void ApplyPaging(int skip, int take) { Skip = skip; Take = take; IsPagingEnabled = true; } protected virtual void ApplyOrderBy(Expression > orderByExpression) => OrderBy = orderByExpression; protected virtual void ApplyOrderByDescending(Expression > orderByDescendingExpression) => OrderByDescending = orderByDescendingExpression;}// https://stackoverflow.com/questions/457316/combining-two-expressions-expressionfunct-boolpublic static class ExpressionExtensions{ public static Expression > AndAlso (this Expression > expr1, Expression > expr2) { var parameter = Expression.Parameter(typeof(T)); var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter); var left = leftVisitor.Visit(expr1.Body); var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter); var right = rightVisitor.Visit(expr2.Body); return Expression.Lambda >( Expression.AndAlso(left ?? throw new InvalidOperationException(), right ?? throw new InvalidOperationException()), parameter); } private class ReplaceExpressionVisitor : ExpressionVisitor { private readonly Expression _oldValue; private readonly Expression _newValue; public ReplaceExpressionVisitor(Expression oldValue, Expression newValue) { _oldValue = oldValue; _newValue = newValue; } public override Expression Visit(Expression node) => node == _oldValue ? _newValue : base.Visit(node); }}
为了在RepositoryBase中能够把所有的Spcification串起来形成查询子句,我们还需要定义一个用于组织Specification的SpecificationEvaluator类:
using TodoList.Application.Common.Interfaces;namespace TodoList.Application.Common;public class SpecificationEvaluatorwhere T : class{ public static IQueryable GetQuery(IQueryable inputQuery, ISpecification ? specification) { var query = inputQuery; if (specification?.Criteria is not null) { query = query.Where(specification.Criteria); } if (specification?.Include is not null) { query = specification.Include(query); } if (specification?.OrderBy is not null) { query = query.OrderBy(specification.OrderBy); } else if (specification?.OrderByDescending is not null) { query = query.OrderByDescending(specification.OrderByDescending); } if (specification?.IsPagingEnabled != false) { query = query.Skip(specification!.Skip).Take(specification.Take); } return query; }}
在IRepository中添加查询相关的接口,大致可以分为以下这几类接口,每类中又可能存在同步接口和异步接口:
IRepository.cs
// 省略其他...// 1. 查询基础操作接口IQueryableGetAsQueryable();IQueryable GetAsQueryable(ISpecification spec);// 2. 查询数量相关接口int Count(ISpecification ? spec = null);int Count(Expression > condition);Task CountAsync(ISpecification ? spec);// 3. 查询存在性相关接口bool Any(ISpecification ? spec);bool Any(Expression >? condition = null);// 4. 根据条件获取原始实体类型数据相关接口Task GetAsync(Expression > condition);Task > GetAsync();Task > GetAsync(ISpecification ? spec);// 5. 根据条件获取映射实体类型数据相关接口,涉及到Group相关操作也在其中,使用selector来传入映射的表达式TResult? SelectFirstOrDefault (ISpecification ? spec, Expression > selector);Task SelectFirstOrDefaultAsync (ISpecification ? spec, Expression > selector);Task > SelectAsync (Expression > selector);Task > SelectAsync (ISpecification ? spec, Expression > selector);Task > SelectAsync (Expression > groupExpression, Expression , TResult>> selector, ISpecification ? spec = null);
有了这些基础,我们就可以去Infrastructure/Persistence/Repositories中实现RepositoryBase类剩下的关于查询部分的代码了:
RepositoryBase.cs
// 省略其他...// 1. 查询基础操作接口实现public IQueryableGetAsQueryable() => _dbContext.Set ();public IQueryable GetAsQueryable(ISpecification spec) => ApplySpecification(spec);// 2. 查询数量相关接口实现public int Count(Expression > condition) => _dbContext.Set ().Count(condition);public int Count(ISpecification ? spec = null) => null != spec ? ApplySpecification(spec).Count() : _dbContext.Set ().Count();public Task CountAsync(ISpecification ? spec) => ApplySpecification(spec).CountAsync();// 3. 查询存在性相关接口实现public bool Any(ISpecification ? spec) => ApplySpecification(spec).Any();public bool Any(Expression >? condition = null) => null != condition ? _dbContext.Set ().Any(condition) : _dbContext.Set ().Any();// 4. 根据条件获取原始实体类型数据相关接口实现public async Task GetAsync(Expression > condition) => await _dbContext.Set ().FirstOrDefaultAsync(condition);public async Task > GetAsync() => await _dbContext.Set ().AsNoTracking().ToListAsync();public async Task > GetAsync(ISpecification ? spec) => await ApplySpecification(spec).AsNoTracking().ToListAsync();// 5. 根据条件获取映射实体类型数据相关接口实现public TResult? SelectFirstOrDefault (ISpecification ? spec, Expression > selector) => ApplySpecification(spec).AsNoTracking().Select(selector).FirstOrDefault();public Task SelectFirstOrDefaultAsync (ISpecification ? spec, Expression > selector) => ApplySpecification(spec).AsNoTracking().Select(selector).FirstOrDefaultAsync();public async Task > SelectAsync (Expression > selector) => await _dbContext.Set ().AsNoTracking().Select(selector).ToListAsync();public async Task > SelectAsync (ISpecification ? spec, Expression > selector) => await ApplySpecification(spec).AsNoTracking().Select(selector).ToListAsync();public async Task > SelectAsync ( Expression > groupExpression, Expression , TResult>> selector, ISpecification ? spec = null) => null != spec ? await ApplySpecification(spec).AsNoTracking().GroupBy(groupExpression).Select(selector).ToListAsync() : await _dbContext.Set ().AsNoTracking().GroupBy(groupExpression).Select(selector).ToListAsync();// 用于拼接所有Specification的辅助方法,接收一个`IQuerybale 对象(通常是数据集合)// 和一个当前实体定义的Specification对象,并返回一个`IQueryable `对象为子句执行后的结果。private IQueryable ApplySpecification(ISpecification ? spec) => SpecificationEvaluator .GetQuery(_dbContext.Set ().AsQueryable(), spec);
引入使用
为了验证通用Repsitory的用法,我们可以先在Infrastructure/DependencyInjection.cs中进行依赖注入:
// in AddInfrastructure, 省略其他services.AddScoped(typeof(IRepository<>), typeof(RepositoryBase<>));
验证
用于初步验证(主要是查询接口),我们在Application项目里新建文件夹TodoItems/Specs,创建一个TodoItemSpec类:
using TodoList.Application.Common;using TodoList.Domain.Entities;using TodoList.Domain.Enums;namespace TodoList.Application.TodoItems.Specs;public sealed class TodoItemSpec : SpecificationBase{ public TodoItemSpec(bool done, PriorityLevel priority) : base(t => t.Done == done && t.Priority == priority) { }}
然后我们临时使用示例接口WetherForecastController,通过日志来看一下查询的正确性。
private readonly IRepository_repository;private readonly ILogger _logger;// 为了验证,临时在这注入IRepository 对象,验证完后撤销修改public WeatherForecastController(IRepository repository, ILogger logger){ _repository = repository; _logger = logger;}
在Get方法里增加这段逻辑用于观察日志输出:
// 记录日志_logger.LogInformation($"maybe this log is provided by Serilog...");var spec = new TodoItemSpec(true, PriorityLevel.High);var items = _repository.GetAsync(spec).Result;foreach (var item in items){ _logger.LogInformation($"item: {item.Id} - {item.Title} - {item.Priority}");}启动Api项目然后请求示例接口,观察控制台输出:
# 以上省略,Controller日志开始...[16:49:59 INF] maybe this log is provided by Serilog...[16:49:59 INF] Entity Framework Core 6.0.1 initialized 'TodoListDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.1' with options: MigrationsAssembly=TodoList.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null [16:49:59 INF] Executed DbCommand (51ms) [Parameters=[@__done_0='?' (DbType = Boolean), @__priority_1='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']SELECT [t].[Id], [t].[Created], [t].[CreatedBy], [t].[Done], [t].[LastModified], [t].[LastModifiedBy], [t].[ListId], [t].[Priority], [t].[Title]FROM [TodoItems] AS [t]WHERE ([t].[Done] = @__done_0) AND ([t].[Priority] = @__priority_1)# 下面这句是我们之前初始化数据库的种子数据,可以参考上一篇文章结尾的验证截图。[16:49:59 INF] item: 87f1ddf1-e6cd-4113-74ed-08d9c5112f6b - Apples - High[16:49:59 INF] Executing ObjectResult, writing value of type 'TodoList.Api.WeatherForecast[]'.[16:49:59 INF] Executed action TodoList.Api.Controllers.WeatherForecastController.Get (TodoList.Api) in 160.5517ms
到此,关于"如何实现Repository模式"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!