1、背景

  最近,有空了,想着把之前一些乱七八糟的小项目给整理一下,尤其是涉及到Repository、UoW几处。为此,专门查阅了博客园中几个大神 关于Repository的实践,到最后都感觉依然莫衷一是,于是感觉这玩意儿不能深究,自己还是紧扣Martin老爷子关于Repository及UoW的核心定义,自己实践核心概念就是了,其他的都不重要了。

2、整个项目架构

红框框起来的部分,就是关于Repository的那些部分,其中,Account.Infrustructure.Contract和Account.Infrusture.EF是核心,可以跨解决方案或工程存在,前者是Repository基础契约定义,后者是该契约基于EF的实现。接下来,分别就两部分实现详细说明。

3、Repository、UoW核心实现

先看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的应该都知道;

2)接口有个泛型参数TEntity,很明显,是要每个实体对应一个Repository实现的将来。

接下来再看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的实现简直大同小异。

4、应用

基础架构定义好了,接下来就是我们仓储层的具体应用,这里以一个简单的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());
int count = await source.CountAsync();
List<Manifest> manifests = null;
if (count > )
{
manifests = await source.OrderBy(x => x.Date).Skip((pageIndex - ) * 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接口的作用。记住一个词,面向接口。。。

5、总结

  本文是针对Repository、UoW的核心概念的实现,即,Repository用于解耦应用服务层或者说叫业务逻辑层与具体数据存取,UoW用于维护事务。在此之前,曾拜读过园子中大神们的一些文章,最终得出结论,这玩意儿,没必要深究,只要抓住了Martin老爷子对二者的核心定义,在此基础上按照自己的理解去实践就OK了。这玩意儿就像ML,在XX和获得GC的大前提下,采用何种姿势,各位随意,只要自己爽就成。如果你非要尝试各种不同姿势,未尝不可,只要自己不嫌累,是不是。。。

Repository个人实践的更多相关文章

  1. DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(3)

    上一篇:<DDD 领域驱动设计-谈谈 Repository.IUnitOfWork 和 IDbContext 的实践(2)> 这篇文章主要是对 DDD.Sample 框架增加 Transa ...

  2. DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(2)

    上一篇:<DDD 领域驱动设计-谈谈 Repository.IUnitOfWork 和 IDbContext 的实践(1)> 阅读目录: 抽离 IRepository 并改造 Reposi ...

  3. DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(1)

    好久没写 DDD 领域驱动设计相关的文章了,嘎嘎!!! 这几天在开发一个新的项目,虽然不是基于领域驱动设计的,但我想把 DDD 架构设计的一些东西运用在上面,但发现了很多问题,这些在之前的短消息项目中 ...

  4. DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(转)

    http://www.cnblogs.com/xishuai/p/ddd-repository-iunitofwork-and-idbcontext.html 好久没写 DDD 领域驱动设计相关的文章 ...

  5. 谈谈 Repository、IUnitOfWork 和 IDbContext 的实践

    谈谈 Repository.IUnitOfWork 和 IDbContext 的实践 上一篇:<DDD 领域驱动设计-谈谈 Repository.IUnitOfWork 和 IDbContext ...

  6. Repository、IUnitOfWork 和 IDbContext 的实践

    Repository.IUnitOfWork 和 IDbContext 的实践 好久没写 DDD 领域驱动设计相关的文章了,嘎嘎!!! 这几天在开发一个新的项目,虽然不是基于领域驱动设计的,但我想把 ...

  7. EF6 CodeFirst+Repository+Ninject+MVC4+EasyUI实践(完)

    前言 这一篇是本系列的最后一篇,虽然示例讲到这里就停止呢,但对于这些技术的学习远不能停止.虽然本示例讲的比较基础,但是正如我第一篇说到的,这个系列的目的不是说一些高端的架构设计,而是作为一个入门级,对 ...

  8. EF6 CodeFirst+Repository+Ninject+MVC4+EasyUI实践(九)

    前言 这一篇我们将完成系统的权限设置功能以及不同角色用户登录系统后动态加载菜单.注意:此示例权限只针对菜单级,如果园友需要更复杂的系统权限设置,可以拓展到按钮级或属性级. 用户的登录采用Form认证来 ...

  9. EF6 CodeFirst+Repository+Ninject+MVC4+EasyUI实践(八)

    前言 本篇幅将对系统的菜单管理模块进行说明,系统的菜单采用树形结构,这样可以更好地方便层级设计和查看.本示例将说明如何通过EntityFramework读取递归的菜单树形结构,以及结合EasyUI的t ...

随机推荐

  1. css(外部样式表)中各种选择器(定义属性时)的优先级

    今天在学css的时候遇到一个问题,用css的外部样式表改变一个<p>元素的颜色,死活就是改变不了,最后才发现是优先级的问题(我自己想当然成后面的优先级就高了,犯了经验主义错误). 先给大家 ...

  2. 关于vue如何解决数据渲染完成之前,dom树显示问题

    在id="app"以下的标签中添加属性v-cloak 并且在css文件中添加[v-cloak]{display:none} 如果效果失效,这种原因是有几种可能,游览器大的解析加载速 ...

  3. Codeforces Round #432 Div. 1 C. Arpa and a game with Mojtaba

    首先容易想到,每种素数是独立的,相互sg就行了 对于一种素数来说,按照的朴素的mex没法做... 所以题解的简化就是数位化 多个数同时含有的满参数因子由于在博弈中一同变化的,让他们等于相当于,那么这样 ...

  4. 玲珑杯#2.5 A-B

    这个题解错了网上还没有题解 囧 = (i%2)? 1 : -1 = - * * *= m #include<bits/stdc++.h> using namespace std; type ...

  5. Linux之shell编程

    一.Bash变量 1) Bash变量与变量分类 1. 定义:变量是计算机内存的单元,其中存放的值可以改变 2. 变量命令规则 #变量名必须以字母或下划线开头,名字中间只能由字母.数字和下划线组成 #变 ...

  6. android小程序之幸运菜谱

    android小程序之幸运菜谱 前言:刚刚结束短短5天的android公开课程,收获不少,写下来记录一下吧!(因为学校校企公开课的缘故才偶然接触的android,所以只学了这几天,不喜勿喷) 一开始得 ...

  7. LCT 模板及套路总结

    这一个月貌似已经考了无数次\(LCT\)了..... 保险起见还是来一发总结吧..... A. LCT 模板 \(LCT\) 是由大名鼎鼎的 \(Tarjan\) 老爷发明的. 主要是用来维护树上路径 ...

  8. 【NOIP2014】解方程(枚举)

    题面 题目描述 已知多项式方程: a0+a1x+a2x^2+..+anx^n=0 求这个方程在[1, m ] 内的整数解(n 和m 均为正整数) 输入格式 输入共n + 2 行. 第一行包含2 个整数 ...

  9. 2014NOIP普及组 子矩阵

    觉得题目水的离开 觉得普及组垃圾的请离开 不知道 DFS 和 DP 的请离开 不屑的大佬请离开 ……. 感谢您贡献的访问量 ————————————————华丽的分割线 ——————————————— ...

  10. [SCOI2016]美味

    按位从高往低贪心,枚举到第i位,只需要判断这2^i长度的区间是否有菜,用主席树就可以了 # include <bits/stdc++.h> # define RG register # d ...