您现在的位置是:网站首页> 编程资料编程资料
.NET 6开发TodoList应用之实现Repository模式_实用技巧_
2023-05-24
402人已围观
简介 .NET 6开发TodoList应用之实现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-bool public 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
相关内容
- .NET 6开发TodoList应用之使用MediatR实现POST请求_实用技巧_
- .NET 6开发TodoList应用引入数据存储_实用技巧_
- .NET 6开发TodoList应用引入第三方日志库_实用技巧_
- .NET 6开发TodoList应用实现结构搭建_实用技巧_
- .NET 6开发TodoList应用实现系列背景_实用技巧_
- 浅析.netcore中的Configuration具体使用_实用技巧_
- Visual Studio 2022智能辅助编码介绍_实用技巧_
- 正确使用dotnet-*工具的方法_实用技巧_
- ASP.NET Core中Startup类、Configure()方法及中间件详解_基础应用_
- 详解Asp.net 5中的ApplicationBuilder_基础应用_
点击排行
本栏推荐
