經常寫CRUD程序的小伙伴們可能都經歷過定義很多Repository
接口,分別做對應的實現,依賴注入并使用的場景。有的時候會發現,很多分散的XXXXRepository
的邏輯都是基本一致的,于是開始思考是否可以將這些操作抽象出去,當然是可以的,而且被抽象出去的部分是可以不加改變地在今后的任何有此需求的項目中直接引入使用。
那么我們本文的需求就是:如何實現一個可重用的Repository
模塊。
長文預警,包含大量代碼。
實現通用Repository
模式并進行驗證。
通用的基礎在于抽象,抽象的粒度決定了通用的程度,但是同時也決定了使用上的復雜度。對于自己的項目而言,抽象到什么程度最合適,需要自己去權衡,也許后面某個時候我會決定自己去實現一個完善的Repository
庫提供出來(事實上已經有很多人這樣做了,我們甚至可以直接下載Nuget包進行使用,但是自己親手去實現的過程能讓你更好地去理解其中的原理,也理解如何開發一個通用的類庫。)
總體思路是:在Application
中定義相關的接口,在Infrastructure
中實現基類的功能。
對于要如何去設計一個通用的Repository
庫,實際上涉及的面非常多,尤其是在獲取數據的時候。而且根據每個人的習慣,實現起來的方式是有比較大的差別的,尤其是關于泛型接口到底需要提供哪些方法,每個人都有自己的理解,這里我只演示基本的思路,而且盡量保持簡單,關于更復雜和更全面的實現,GIthub上有很多已經寫好的庫可以去學習和參考,我會列在下面:
很顯然,第一步要去做的是在Application/Common/Interfaces
中增加一個IRepository<T>
的定義用于適用不同類型的實體,然后在Infrastructure/Persistence/Repositories
中創建一個基類RepositoryBase<T>
實現這個接口,并有辦法能提供一致的對外方法簽名。
IRepository.cs
namespaceTodoList.Application.Common.Interfaces;publicinterfaceIRepository<T>whereT:class{}
RepositoryBase.cs
usingMicrosoft.EntityFrameworkCore;usingTodoList.Application.Common.Interfaces;namespaceTodoList.Infrastructure.Persistence.Repositories;publicclassRepositoryBase<T>:IRepository<T>whereT:class{privatereadonlyTodoListDbContext_dbContext;publicRepositoryBase(TodoListDbContextdbContext)=>_dbContext=dbContext;}
在動手實際定義IRepository<T>
之前,先思考一下:對數據庫的操作都會出現哪些情況:
新增實體(Create)
新增實體在Repository
層面的邏輯很簡單,傳入一個實體對象,然后保存到數據庫就可以了,沒有其他特殊的需求。
IRepository.cs
//省略其他...//Create相關操作接口Task<T>AddAsync(Tentity,CancellationTokencancellationToken=default);
RepositoryBase.cs
//省略其他...publicasyncTask<T>AddAsync(Tentity,CancellationTokencancellationToken=default){await_dbContext.Set<T>().AddAsync(entity,cancellationToken);await_dbContext.SaveChangesAsync(cancellationToken);returnentity;}
更新實體(update)
和新增實體類似,但是更新時一般是單個實體對象去操作。
IRepository.cs
//省略其他...//update相關操作接口TaskUpdateAsync(Tentity,CancellationTokencancellationToken=default);
RepositoryBase.cs
//省略其他...publicasyncTaskUpdateAsync(Tentity,CancellationTokencancellationToken=default){//對于一般的更新而言,都是Attach到實體上的,只需要設置該實體的State為Modified就可以了_dbContext.Entry(entity).State=EntityState.Modified;await_dbContext.SaveChangesAsync(cancellationToken);}
刪除實體(delete)
對于刪除實體,可能會出現兩種情況:刪除一個實體;或者刪除一組實體。
IRepository.cs
//省略其他...//delete相關操作接口,這里根據key刪除對象的接口需要用到一個獲取對象的方法ValueTask<T?>GetAsync(objectkey);TaskDeleteAsync(objectkey);TaskDeleteAsync(Tentity,CancellationTokencancellationToken=default);TaskDeleteRangeAsync(IEnumerable<T>entities,CancellationTokencancellationToken=default);
RepositoryBase.cs
//省略其他...publicvirtualValueTask<T?>GetAsync(objectkey)=>_dbContext.Set<T>().FindAsync(key);publicasyncTaskDeleteAsync(objectkey){varentity=awaitGetAsync(key);if(entityisnotnull){awaitDeleteAsync(entity);}}publicasyncTaskDeleteAsync(Tentity,CancellationTokencancellationToken=default){_dbContext.Set<T>().Remove(entity);await_dbContext.SaveChangesAsync(cancellationToken);}publicasyncTaskDeleteRangeAsync(IEnumerable<T>entities,CancellationTokencancellationToken=default){_dbContext.Set<T>().RemoveRange(entities);await_dbContext.SaveChangesAsync(cancellationToken);}
獲取實體(Retrieve)
對于如何獲取實體,是最復雜的一部分。我們不僅要考慮通過什么方式獲取哪些數據,還需要考慮獲取的數據有沒有特殊的要求比如排序、分頁、數據對象類型的轉換之類的問題。
具體來說,比如下面這一個典型的LINQ查詢語句:
varresults=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
來表示查詢的各類條件:
usingSystem.Linq.Expressions;usingMicrosoft.EntityFrameworkCore.Query;namespaceTodoList.Application.Common.Interfaces;publicinterfaceISpecification<T>{//查詢條件子句Expression<Func<T,bool>>Criteria{get;}//Include子句Func<IQueryable<T>,IIncludableQueryable<T,object>>Include{get;}//OrderBy子句Expression<Func<T,object>>OrderBy{get;}//OrderByDescending子句Expression<Func<T,object>>OrderByDescending{get;}//分頁相關屬性intTake{get;}intSkip{get;}boolIsPagingEnabled{get;}}
并實現這個泛型接口,放在Application/Common
中:
usingSystem.Linq.Expressions;usingMicrosoft.EntityFrameworkCore.Query;usingTodoList.Application.Common.Interfaces;namespaceTodoList.Application.Common;publicabstractclassSpecificationBase<T>:ISpecification<T>{protectedSpecificationBase(){}protectedSpecificationBase(Expression<Func<T,bool>>criteria)=>Criteria=criteria;publicExpression<Func<T,bool>>Criteria{get;privateset;}publicFunc<IQueryable<T>,IIncludableQueryable<T,object>>Include{get;privateset;}publicList<string>IncludeStrings{get;}=new();publicExpression<Func<T,object>>OrderBy{get;privateset;}publicExpression<Func<T,object>>OrderByDescending{get;privateset;}publicintTake{get;privateset;}publicintSkip{get;privateset;}publicboolIsPagingEnabled{get;privateset;}publicvoidAddCriteria(Expression<Func<T,bool>>criteria)=>Criteria=Criteriaisnotnull?Criteria.AndAlso(criteria):criteria;protectedvirtualvoidAddInclude(Func<IQueryable<T>,IIncludableQueryable<T,object>>includeExpression)=>Include=includeExpression;protectedvirtualvoidAddInclude(stringincludeString)=>IncludeStrings.Add(includeString);protectedvirtualvoidApplyPaging(intskip,inttake){Skip=skip;Take=take;IsPagingEnabled=true;}protectedvirtualvoidApplyOrderBy(Expression<Func<T,object>>orderByExpression)=>OrderBy=orderByExpression;protectedvirtualvoidApplyOrderByDescending(Expression<Func<T,object>>orderByDescendingExpression)=>OrderByDescending=orderByDescendingExpression;}//https://stackoverflow.com/questions/457316/combining-two-expressions-expressionfunct-boolpublicstaticclassExpressionExtensions{publicstaticExpression<Func<T,bool>>AndAlso<T>(thisExpression<Func<T,bool>>expr1,Expression<Func<T,bool>>expr2){varparameter=Expression.Parameter(typeof(T));varleftVisitor=newReplaceExpressionVisitor(expr1.Parameters[0],parameter);varleft=leftVisitor.Visit(expr1.Body);varrightVisitor=newReplaceExpressionVisitor(expr2.Parameters[0],parameter);varright=rightVisitor.Visit(expr2.Body);returnExpression.Lambda<Func<T,bool>>(Expression.AndAlso(left??thrownewInvalidOperationException(),right??thrownewInvalidOperationException()),parameter);}privateclassReplaceExpressionVisitor:ExpressionVisitor{privatereadonlyExpression_oldValue;privatereadonlyExpression_newValue;publicReplaceExpressionVisitor(ExpressionoldValue,ExpressionnewValue){_oldValue=oldValue;_newValue=newValue;}publicoverrideExpressionVisit(Expressionnode)=>node==_oldValue?_newValue:base.Visit(node);}}
為了在RepositoryBase
中能夠把所有的Spcification串起來形成查詢子句,我們還需要定義一個用于組織Specification的SpecificationEvaluator
類:
usingTodoList.Application.Common.Interfaces;namespaceTodoList.Application.Common;publicclassSpecificationEvaluator<T>whereT:class{publicstaticIQueryable<T>GetQuery(IQueryable<T>inputQuery,ISpecification<T>?specification){varquery=inputQuery;if(specification?.Criteriaisnotnull){query=query.Where(specification.Criteria);}if(specification?.Includeisnotnull){query=specification.Include(query);}if(specification?.OrderByisnotnull){query=query.OrderBy(specification.OrderBy);}elseif(specification?.OrderByDescendingisnotnull){query=query.OrderByDescending(specification.OrderByDescending);}if(specification?.IsPagingEnabled!=false){query=query.Skip(specification!.Skip).Take(specification.Take);}returnquery;}}
在IRepository
中添加查詢相關的接口,大致可以分為以下這幾類接口,每類中又可能存在同步接口和異步接口:
IRepository.cs
//省略其他...//1.查詢基礎操作接口IQueryable<T>GetAsQueryable();IQueryable<T>GetAsQueryable(ISpecification<T>spec);//2.查詢數量相關接口intCount(ISpecification<T>?spec=null);intCount(Expression<Func<T,bool>>condition);Task<int>CountAsync(ISpecification<T>?spec);//3.查詢存在性相關接口boolAny(ISpecification<T>?spec);boolAny(Expression<Func<T,bool>>?condition=null);//4.根據條件獲取原始實體類型數據相關接口Task<T?>GetAsync(Expression<Func<T,bool>>condition);Task<IReadOnlyList<T>>GetAsync();Task<IReadOnlyList<T>>GetAsync(ISpecification<T>?spec);//5.根據條件獲取映射實體類型數據相關接口,涉及到Group相關操作也在其中,使用selector來傳入映射的表達式TResult?SelectFirstOrDefault<TResult>(ISpecification<T>?spec,Expression<Func<T,TResult>>selector);Task<TResult?>SelectFirstOrDefaultAsync<TResult>(ISpecification<T>?spec,Expression<Func<T,TResult>>selector);Task<IReadOnlyList<TResult>>SelectAsync<TResult>(Expression<Func<T,TResult>>selector);Task<IReadOnlyList<TResult>>SelectAsync<TResult>(ISpecification<T>?spec,Expression<Func<T,TResult>>selector);Task<IReadOnlyList<TResult>>SelectAsync<TGroup,TResult>(Expression<Func<T,TGroup>>groupExpression,Expression<Func<IGrouping<TGroup,T>,TResult>>selector,ISpecification<T>?spec=null);
有了這些基礎,我們就可以去Infrastructure/Persistence/Repositories
中實現RepositoryBase
類剩下的關于查詢部分的代碼了:
RepositoryBase.cs
//省略其他...//1.查詢基礎操作接口實現publicIQueryable<T>GetAsQueryable()=>_dbContext.Set<T>();publicIQueryable<T>GetAsQueryable(ISpecification<T>spec)=>ApplySpecification(spec);//2.查詢數量相關接口實現publicintCount(Expression<Func<T,bool>>condition)=>_dbContext.Set<T>().Count(condition);publicintCount(ISpecification<T>?spec=null)=>null!=spec?ApplySpecification(spec).Count():_dbContext.Set<T>().Count();publicTask<int>CountAsync(ISpecification<T>?spec)=>ApplySpecification(spec).CountAsync();//3.查詢存在性相關接口實現publicboolAny(ISpecification<T>?spec)=>ApplySpecification(spec).Any();publicboolAny(Expression<Func<T,bool>>?condition=null)=>null!=condition?_dbContext.Set<T>().Any(condition):_dbContext.Set<T>().Any();//4.根據條件獲取原始實體類型數據相關接口實現publicasyncTask<T?>GetAsync(Expression<Func<T,bool>>condition)=>await_dbContext.Set<T>().FirstOrDefaultAsync(condition);publicasyncTask<IReadOnlyList<T>>GetAsync()=>await_dbContext.Set<T>().AsNoTracking().ToListAsync();publicasyncTask<IReadOnlyList<T>>GetAsync(ISpecification<T>?spec)=>awaitApplySpecification(spec).AsNoTracking().ToListAsync();//5.根據條件獲取映射實體類型數據相關接口實現publicTResult?SelectFirstOrDefault<TResult>(ISpecification<T>?spec,Expression<Func<T,TResult>>selector)=>ApplySpecification(spec).AsNoTracking().select(selector).FirstOrDefault();publicTask<TResult?>SelectFirstOrDefaultAsync<TResult>(ISpecification<T>?spec,Expression<Func<T,TResult>>selector)=>ApplySpecification(spec).AsNoTracking().select(selector).FirstOrDefaultAsync();publicasyncTask<IReadOnlyList<TResult>>SelectAsync<TResult>(Expression<Func<T,TResult>>selector)=>await_dbContext.Set<T>().AsNoTracking().select(selector).ToListAsync();publicasyncTask<IReadOnlyList<TResult>>SelectAsync<TResult>(ISpecification<T>?spec,Expression<Func<T,TResult>>selector)=>awaitApplySpecification(spec).AsNoTracking().select(selector).ToListAsync();publicasyncTask<IReadOnlyList<TResult>>SelectAsync<TGroup,TResult>(Expression<Func<T,TGroup>>groupExpression,Expression<Func<IGrouping<TGroup,T>,TResult>>selector,ISpecification<T>?spec=null)=>null!=spec?awaitApplySpecification(spec).AsNoTracking().GroupBy(groupExpression).select(selector).ToListAsync():await_dbContext.Set<T>().AsNoTracking().GroupBy(groupExpression).select(selector).ToListAsync();//用于拼接所有Specification的輔助方法,接收一個`IQuerybale<T>對象(通常是數據集合)//和一個當前實體定義的Specification對象,并返回一個`IQueryable<T>`對象為子句執行后的結果。privateIQueryable<T>ApplySpecification(ISpecification<T>?spec)=>SpecificationEvaluator<T>.GetQuery(_dbContext.Set<T>().AsQueryable(),spec);
為了驗證通用Repsitory的用法,我們可以先在Infrastructure/DependencyInjection.cs
中進行依賴注入:
//inAddInfrastructure,省略其他services.AddScoped(typeof(IRepository<>),typeof(RepositoryBase<>));
用于初步驗證(主要是查詢接口),我們在Application
項目里新建文件夾TodoItems/Specs
,創建一個TodoItemSpec
類:
usingTodoList.Application.Common;usingTodoList.Domain.Entities;usingTodoList.Domain.Enums;namespaceTodoList.Application.TodoItems.Specs;publicsealedclassTodoItemSpec:SpecificationBase<TodoItem>{publicTodoItemSpec(booldone,PriorityLevelpriority):base(t=>t.Done==done&&t.Priority==priority){}}
然后我們臨時使用示例接口WetherForecastController
,通過日志來看一下查詢的正確性。
privatereadonlyIRepository<TodoItem>_repository;privatereadonlyILogger<WeatherForecastController>_logger;//為了驗證,臨時在這注入IRepository<TodoItem>對象,驗證完后撤銷修改publicWeatherForecastController(IRepository<TodoItem>repository,ILogger<WeatherForecastController>logger){_repository=repository;_logger=logger;}
在Get
方法里增加這段邏輯用于觀察日志輸出:
//記錄日志_logger.LogInformation($"maybethislogisprovidedbySerilog...");varspec=newTodoItemSpec(true,PriorityLevel.High);varitems=_repository.GetAsync(spec).Result;foreach(variteminitems){_logger.LogInformation($"item:{item.Id}-{item.Title}-{item.Priority}");}
啟動Api項目然后請求示例接口,觀察控制臺輸出:
#以上省略,Controller日志開始...[16:49:59INF]maybethislogisprovidedbySerilog...[16:49:59INF]EntityFrameworkCore6.0.1initialized'TodoListDbContext'usingprovider'Microsoft.EntityFrameworkCore.SqlServer:6.0.1'withoptions:MigrationsAssembly=TodoList.Infrastructure,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null[16:49:59INF]ExecutedDbCommand(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:59INF]item:87f1ddf1-e6cd-4113-74ed-08d9c5112f6b-Apples-High[16:49:59INF]ExecutingObjectResult,writingvalueoftype'TodoList.Api.WeatherForecast[]'.[16:49:59INF]ExecutedactionTodoList.Api.Controllers.WeatherForecastController.Get(TodoList.Api)in160.5517ms
到此,關于“如何實現Repository模式”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注本站網站,小編會繼續努力為大家帶來更多實用的文章!
本文由 貴州做網站公司 整理發布,部分圖文來源于互聯網,如有侵權,請聯系我們刪除,謝謝!
c語言中正確的字符常量是用一對單引號將一個字符括起表示合法的字符常量。例如‘a’。數值包括整型、浮點型。整型可用十進制,八進制,十六進制。八進制前面要加0,后面...
2022年天津專場考試原定于3月19日舉行,受疫情影響確定延期,但目前延期后的考試時間推遲。 符合報名條件的考生,須在規定時間登錄招考資訊網(www.zha...
:喜歡聽,樂意看。指很受歡迎?!巴卣官Y料”喜聞樂見:[ xǐ wén lè jiàn ]詳細解釋1. 【解釋】:喜歡聽,樂意看。指很受歡迎。2. 【示例】:這是...
國企高管的薪酬規定有哪些?新規定的內容涉及國有企業負責人的薪酬結構、薪酬體系、基本年薪、績效年薪、補充保險、崗位消費標準模式和監督體系。重點是奠定我國國有企業高管薪酬體系的總體框架和薪酬水平的大致范圍。鑒于金融業的特殊性,本規定頒布后,相關主管部門將針對金融和非金融企業等發布具體實施細則。各地也將參照本規定出臺相關地方性法規,全面規范國有企業負責人的薪酬管理。與員工平均工資掛鉤為確保條例的實施和有...
亞洲金融危機是指發生于哪一年?亞洲金融危機發生在1997年,它最開是席卷了泰國,此后波及到日本、新加坡、韓國等等國家,導致貨幣、股市大幅度下跌,造成經濟甚至政治危機。整個亞洲金融危機分為三個階段,直到1999年才結束。金融危機是指金融投資、金融市場以及金融機構的危機,是金融界的一種術語,其具體的表現情況就是金融資產價格大幅度下降或者大部分金融易購倒閉、臨近破產等等,是整個經濟體系的大災難。1998...
大學生買基金怎么買?大學生可以買基金,并且購買非常方便,支付寶平臺、微信等平臺都可以直接購買基金,基金對于投資者年齡沒有限制。大學生購買基金建議選一些風險性比較低的,如貨幣基金、債券基金等,雖然收益不高但是勝在穩定,比較適合大學生投資。大學生買基金買多少合適?基金的門檻并不高,有的一元就可以購買,有的十元就可以購買,買多少都是可以的,但是要根據自身情況來考慮,比如說:某學生除去日常開銷的生活費,還...