一、背景編程
最近,有空了,想着把以前一些亂七八糟的小項目給整理一下,尤爲是涉及到Repository、UoW幾處。爲此,專門查閱了博客園中幾個大神 關於Repository的實踐,到最後都感受依然莫衷一是,因而感受這玩意兒不能深究,本身仍是緊扣Martin老爺子關於Repository及UoW的核心定義,本身實踐核心概念就是了,其餘的都不重要了。架構
二、整個項目架構app
紅框框起來的部分,就是關於Repository的那些部分,其中,Account.Infrustructure.Contract和Account.Infrusture.EF是核心,能夠跨解決方案或工程存在,前者是Repository基礎契約定義,後者是該契約基於EF的實現。接下來,分別就兩部分實現詳細說明。async
三、Repository、UoW核心實現ide
先看Repository核心契約的定義:模塊化
很簡單,一個基於netstandard的類庫,其中就兩個接口定義,分別對應Repository和UoW的核心概念,IRepository的定義以下:函數
public interface IRepository { } /// <summary> /// Repository標記接口 /// </summary> public interface IRepository<TEntity> : IRepository where TEntity : class { IQueryable<TEntity> Get( Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = ""); TEntity GetByID(object id); void Insert(TEntity entity); void Delete(object id); void Delete(TEntity entityToDelete); void Update(TEntity entityToUpdate); void Save(); }
非泛型空接口IRepository用來標記倉儲,是面向接口編程中很常見的作法,這個待會咱們會在使用環節進一步說明。泛型IRepository接口用來規範全部倉儲都應該具備的基礎增刪查改方法,這裏有2點須要注意:優化
1)方法返回類型爲IQueryable,目的是延遲查詢,用過相似EF的ORM的應該都知道;ui
2)接口有個泛型參數TEntity,很明顯,是要每一個實體對應一個Repository實現的未來。this
接下來再看UoW契約的定義:
public interface IUnitOfWork { DbTransaction BeginTransaction(); void CommitTransaction(); void RollbackTransaction(); }
這個契約更簡單,由於我給其的職責,就只有將多個操做歸入統一事務並有效管理。這已經足夠實現Martin老爺子關於UoW的核心概念了。
以後,咱們看看IRepository、IUoW的基於EF的實現:
能夠看見,也很簡單,就是基於契約基礎工程中的兩個接口的實現,整個類庫也是基於standard的。
IUnityOfWork的實現以下:
public class EFUnitOfWork : IUnitOfWork { private readonly DbContext _context; public EFUnitOfWork(DbContext context) { _context = context; } public DbTransaction BeginTransaction() { _context.Database.BeginTransaction(); return _context.Database.CurrentTransaction.GetDbTransaction(); } public void CommitTransaction() { _context.SaveChanges(); _context.Database.CommitTransaction(); } public void RollbackTransaction() { _context.Database.RollbackTransaction(); } }
你們注意工做單元中用到的上下文,很明顯,DBContext是基於EF的數據上下文的,並且,通常,咱們具體項目中才用到的上下文,都是SchoolDBContext之類的,那麼這裏如何註冊進來呢?若是是自定義系統服務,直接Registet<XXDbContext>().As<DbContext>()就成了(若是Autofac的話),問題是咱們注入上下文時候,是相似這樣:
services.AddDbContext<AccountContext>(options => options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));
翻遍了EF的AddDBContext的重載,也沒發現能夠註冊爲DBContext的實現啊,怎麼整。。。答案來了,這裏有個小技巧,既然咱們都明白,自定義服務是能夠註冊爲接口或基類的,那這裏咱們把XXXDBContext也當作自定義服務來註冊,你前面不是EF標準註冊了XXDBContext了麼,好,下一步,我就再把XXDBContext註冊爲DBContext,無非控制下生命週期就成,具體實現以下:
services.AddDbContext<AccountContext>(options => options.UseMySql(Configuration.GetConnectionString("DefaultConnection"))); services.AddScoped<DbContext>(provider => provider.GetService<AccountContext>());
上述操做是在Startup中完成的。注意,這一步比較重要,由於它直接決定了你EFUnityOfWork中是否能接收到DBContext,不這樣作,你就得在EFUnityOfWork中直接接受XXDBContext了,那還談何抽象,還談何基礎架構。。。
接下來,再看EF基礎實現中Repository的實現,以下:
public abstract class Repository<TEntity> : IRepository<TEntity> where TEntity : class { protected DbContext Context; protected DbSet<TEntity> DbSet; public Repository(DbContext context) { this.Context = context; this.DbSet = context.Set<TEntity>(); } public virtual IQueryable<TEntity> Get( Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "") { IQueryable<TEntity> query = this.DbSet; if (filter != null) { query = query.Where(filter); } foreach (var includeProperty in includeProperties.Split (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) { query = query.Include(includeProperty); } if (orderBy != null) { return orderBy(query); } else { return query; } } public virtual TEntity GetByID(object id) { return DbSet.Find(id); } public virtual void Insert(TEntity entity) { DbSet.Add(entity); } public virtual void Delete(object id) { TEntity entityToDelete = DbSet.Find(id); Delete(entityToDelete); } public virtual void Delete(TEntity entityToDelete) { if (Context.Entry(entityToDelete).State == EntityState.Detached) { DbSet.Attach(entityToDelete); } DbSet.Remove(entityToDelete); } public virtual void Update(TEntity entityToUpdate) { DbSet.Attach(entityToUpdate); Context.Entry(entityToUpdate).State = EntityState.Modified; } public void Save() { this.Context.SaveChanges(); } }
這個很簡單,無非就是你平時寫的直接基於XXDBContext的CRUD給抽象一下,泛型一下,而後蒸到這裏來。注意最後邊的那個save,有些實踐中會把save直接整到UoW裏邊去,我沒有,由於我對UoW的惟一指望就是,管理好事務,不涉及到事務的狀況下,應用服務層連UoW的影子都不要出現,有Repository就夠了,那就涉及到簡單CUD的保存,尤爲是像基於EF的這種實現中,還他媽必須savechanges才行。。。這裏特別說明,可能save放這裏並不合適,由於有些orm犯不着必須save才行,在非事務的狀況下,好比Dapper,再好比Chloe,因此這裏能夠更進一步優化或抽象。只能說,fuck EF,非事務性寫操做,你給我直接寫庫不就完了。。。
你們注意,這裏既然這裏抽象出了Account.Infrustructure.Contract,以及有了Account.Infrustructure.EF的實現,以及我上邊說了那麼多各ORM關於save的不一樣,你就應該想到,抽象的目的,是爲了切換ORM準備的,假如我想切換爲Chloe的實現,那麼很簡單,改動只須要3處:
1)startup中EFDBContext的註冊改成Chole Context的註冊,如MsSqlContext;
2)ChloeUnityOfWork實現IUnitOfWork,構造函數中傳入IDbContext,下面的方法實現切換爲MsSQLContext的相關事務操做;
3)Repository中DBContext切換爲IDBContext,對應的CRUD及save切換爲基於IDBContext的實現(其實Chloe根本他媽就不須要save。。。);
上述IDbContext是Chloe的數據上下文,用過的應該清楚。另外,涉及到多ORM或切換ORM,直接更改不推薦,鍋鍋們,面向對象或者抽象的目的,不是爲了改動,而是爲了擴展,我上邊只是爲了說明要基於其餘ORM去實現,很是簡單而已,正確作法是,直接新建Account.Infrustructure.Chloe工程,而後實現兩個契約接口,跟EF的實現簡直大同小異。
四、應用
基礎架構定義好了,接下來就是咱們倉儲層的具體應用,這裏以一個簡單的ManifestRepository爲例看下如何實現:
public class ManifestRepository : Repository<Manifest>, IManifestRepository { public ManifestRepository(AccountContext context) :base(context) { } public async Task<PaginatedList<Manifest>> GetManifests(DateTime start, DateTime end, int pageIndex, int pageSize) { var source = DbSet.Where(x => x.Date >= start && x.Date < new DateTime(end.Year, end.Month, end.Day).AddDays(1)); int count = await source.CountAsync(); List<Manifest> manifests = null; if (count > 0) { manifests = await source.OrderBy(x => x.Date).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); } return new PaginatedList<Manifest>(pageIndex, pageSize, count, manifests ?? new List<Manifest>()); } }
典型的,繼承基類泛型實現獲取基本CRUD方法,這裏多了一個,是由於這個查詢相對複雜,若是實際項目中,沒有這種複雜查詢,或者這種查詢只出現一次,實際上不必在ManifestRepository裏邊抽取,直接在應用服務層經過IRepository暴露的接口獲取便可。具體Repository有了,接下來咱們看應用服務層如何調用:
public class ManifestService : IManifestService { private readonly IManifestRepository _manifestRepository; private readonly IDailyRepository _dailyRepository; private readonly IUnitOfWork _unitOfWork; public ManifestService(IManifestRepository manifestRepository, IDailyRepository dailyRepository, IUnitOfWork unitOfWork) { _manifestRepository = manifestRepository; _dailyRepository = dailyRepository; _unitOfWork = unitOfWork; }
看見沒有,典型的構造函數注入,注入 所需的倉儲,以及UoW。咱們再看看具體的一個Add方法,看下它是如何與Repository、UoW交互的:
public Manifest AddManifest(Manifest manifest) { try { _unitOfWork.BeginTransaction(); _manifestRepository.Insert(manifest); var daily = _dailyRepository.Get(x => x.Date.Date == manifest.Date.Date).FirstOrDefault(); if (daily != null) { daily.Cost += manifest.Cost; _dailyRepository.Update(daily); } else { daily = new Daily { ID = Guid.NewGuid().ToString(), Date = manifest.Date, Cost = manifest.Cost }; _dailyRepository.Insert(daily); } _unitOfWork.CommitTransaction(); return manifest; } catch(Exception e) { _unitOfWork.RollbackTransaction(); throw e; } }
看到沒有,UoW開啓事務,而後各類倉儲原子操做,操做完畢,UoW 提交事務,或者異常出現,UoW回滾事務。是否是So easy。。。另外,假如倉儲層切換了ORM或者數據源,對應用服務層是徹底透明的,是否是so happy。。。
另外,以前曾有園友問過,在Autofac模塊化注入中,若是不想以名字結尾來匹配,如何註冊服務或倉儲,這裏也貼出解決方案:
public class RepositoryModule : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType<EFUnitOfWork>() .As<IUnitOfWork>() .InstancePerLifetimeScope(); builder.RegisterAssemblyTypes(this.ThisAssembly) .Where(t => t.IsAssignableTo<IRepository>()) .AsImplementedInterfaces() .InstancePerLifetimeScope(); } }
你們注意看紅色部分,這就是以前定義那個空IRepository接口的做用。記住一個詞,面向接口。。。
五、總結
本文是針對Repository、UoW的核心概念的實現,即,Repository用於解耦應用服務層或者說叫業務邏輯層與具體數據存取,UoW用於維護事務。在此以前,曾拜讀過園子中大神們的一些文章,最終得出結論,這玩意兒,不必深究,只要抓住了Martin老爺子對兩者的核心定義,在此基礎上按照本身的理解去實踐就OK了。這玩意兒就像ML,在XX和得到GC的大前提下,採用何種姿式,各位隨意,只要本身爽就成。若是你非要嘗試各類不一樣姿式,何嘗不可,只要本身不嫌累,是否是。。。