一、背景编程
最近,有空了,想着把以前一些乱七八糟的小项目给整理一下,尤为是涉及到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的大前提下,采用何种姿式,各位随意,只要本身爽就成。若是你非要尝试各类不一样姿式,何尝不可,只要本身不嫌累,是否是。。。