EntityFramework系列:Repository模式与单元测试
1.依赖IRepository接口而不是直接使用EntityFramework
使用IRepository不只是架构上解耦的需要,更重要的意义在于Service的单元测试,Repository模式本身就是采用集合操作的方式简化数据访问,IRepository更容易Mock。先上图:

鉴于目前接触到的项目中,即使业务逻辑相对复杂的项目也只是应用逻辑复杂而非领域逻辑复杂,在实际使用中聚合根和单独Repository接口只是引入了更多的代码和类型定义,因此一般情况下使用泛型版本的Repository<T>接口即可。nopcommerce等开源项目中也是如此。Java中的伪泛型无法实现泛型版本的Repository<T>,简单的说你无法在Repository<T>的方法中获取T的类型。
namespace Example.Application
{
public interface IRepository<T> where T : class
{
T FindBy(object id); IQueryable<T> Query { get; } void Add(T entity); void Remove(T entity); void Update(T entity); int Commit();
}
}
2.封装DbContext的依赖项
(1)定义一个通用的EfDbContext,将DbContext对IDbConnectionFactory、ConnectionString、实体类配置等的依赖封装到DbSettings中,既可以在使用使方便依赖注入也方便进行单元测试。
namespace Example.Infrastructure.Repository
{
public class EfDbContext : DbContext, IDbContext
{
private DbSettings _dbSettings; public EfDbContext(IConfiguration configuration, ILogger logger, DbSettings dbSettings) : base(dbSettings.NameOrConnectionString)
{
this._dbSettings = dbSettings;
if (this._dbSettings.DbConnectionFactory != null)
{
#pragma warning disable
Database.DefaultConnectionFactory = this._dbSettings.DbConnectionFactory;
}
if (configuration.Get<bool>("database.log:", false))
{
this.Database.Log = sql => logger.Information(sql);
}
this.Database.Log = l => System.Diagnostics.Debug.WriteLine(l);
} protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder); modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
if (_dbSettings.EntityMaps != null)
{
foreach (var item in _dbSettings.EntityMaps)
{
modelBuilder.Configurations.Add((dynamic)item);
}
}
if (_dbSettings.ComplexMaps != null)
{
foreach (var item in _dbSettings.ComplexMaps)
{
modelBuilder.Configurations.Add((dynamic)item);
}
}
} public void SetInitializer<T>() where T : DbContext
{
if (this._dbSettings.Debug)
{
if (this._dbSettings.UnitTest)
{
Database.SetInitializer(new DropCreateDatabaseAlways<T>());
}
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<T>());
}
}
else
{
Database.SetInitializer<T>(null);
}
} public new IDbSet<T> Set<T>() where T : class
{
return base.Set<T>();
} public int Commit()
{
return base.SaveChanges();
}
}
}
(2)在DbSettings中按需定义依赖,这里将实体类的配置也通过DbSettings注入。
namespace Example.Infrastructure.Repository
{
public class DbSettings
{
public DbSettings()
{
this.RowVersionNname = "Version";
} public string NameOrConnectionString { get; set; } public string RowVersionNname { get; set; }
public bool Debug { get; set; } public bool UnitTest { get; set; } public IDbConnectionFactory DbConnectionFactory { get; set; } public List<object> EntityMaps { get; set; } = new List<object>(); public List<object> ComplexMaps { get; set; } = new List<object>();
}
}
3.定义SqlServerDbContext和VersionDbContext,解决使用开放式并发连接时,MySql等数据库无法自动生成RowVersion的问题。
(1)适用于SqlServer、SqlServeCe的SqlServerDbContext
namespace Example.Infrastructure.Repository
{
public class SqlServerDbContext : EfDbContext
{
private DbSettings _dbSettings; public SqlServerDbContext(IConfiguration configuration, ILogger logger, DbSettings dbSettings)
: base(configuration, logger, dbSettings)
{
this._dbSettings = dbSettings;
} protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Properties().Where(o => o.Name == this._dbSettings.RowVersionNname).Configure(o => o.IsRowVersion());
base.SetInitializer<SqlServerDbContext>();
}
}
}
(2)适用于Myql、Sqlite等数据库的VersionDbContext。使用手动更新Version,通过GUID保证版本号唯一。
namespace Example.Infrastructure.Repository
{
public class VersionDbContext : EfDbContext
{
private DbSettings _dbSettings; public VersionDbContext(IConfiguration configuration, ILogger logger, DbSettings dbSettings)
: base(configuration,logger,dbSettings)
{
this._dbSettings = dbSettings;
} protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Properties().Where(o => o.Name == this._dbSettings.RowVersionNname)
.Configure(o => o.IsConcurrencyToken().HasDatabaseGeneratedOption(DatabaseGeneratedOption.None));
base.SetInitializer<VersionDbContext>();
} public override int SaveChanges()
{
this.ChangeTracker.DetectChanges();
var objectContext = ((IObjectContextAdapter)this).ObjectContext;
foreach (ObjectStateEntry entry in objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified | EntityState.Added))
{
var v = entry.Entity;
if (v != null)
{
var property = v.GetType().GetProperty(this._dbSettings.RowVersionNname);
if (property != null)
{
var value = Encoding.UTF8.GetBytes(Guid.NewGuid().ToString());
property.SetValue(v, value);
}
}
}
return base.SaveChanges();
}
}
}
4.使用XUnit、Rhino.Mocks和SqlServerCe进行单元测试
这是参考nopcommerce中的做法,nopcommerce使用的NUnit需要安装NUnit扩展,XUnit只需要通过Nuget引入程序包,看看GitHub上的aspnet源码,微软也在使用XUnit。
namespace Example.Infrastructure.Test.Repository
{
public class CustomerPersistenceTest
{
private IRepository<T> GetRepository<T>() where T : class
{
string testDbName = "Data Source=" + (System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)) + @"\\test.sdf;Persist Security Info=False";
var configuration = MockRepository.GenerateMock<IConfiguration>();
var logger = MockRepository.GenerateMock<ILogger>();
var repository = new EfRepository<T>(new SqlServerDbContext(configuration,logger,new DbSettings
{
DbConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0"),
NameOrConnectionString = testDbName,
Debug = true,
UnitTest = true,
EntityMaps = new List<object> { new EntityTypeConfiguration<Customer>() }
}));
return repository;
} [Fact]
public void SaveLoadCustomerTest()
{
var repository = this.GetRepository<Customer>();
repository.Add(new Customer { UserName = "test" });
repository.Commit();
var customer = repository.Query.FirstOrDefault(o => o.UserName == "test");
Assert.NotNull(customer);
}
}
}
5.确保在ASP.NET中使用依赖注入时,配置DbContext的生命周期为Request范围
namespace Example.Web
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
ObjectFactory.Init();
ObjectFactory.AddSingleton<IConfiguration, AppConfigAdapter>();
ObjectFactory.AddSingleton<ILogger, Log4netAdapter>();
ObjectFactory.AddSingleton<DbSettings, DbSettings>(new DbSettings { NameOrConnectionString = "SqlCeConnection", Debug = true });
ObjectFactory.AddScoped<IDbContext, SqlServerDbContext>();
ObjectFactory.AddTransient(typeof(IRepository<>), typeof(EfRepository<>));
ObjectFactory.Build();
ObjectFactory.GetInstance<ILogger>().Information(String.Format("Start at {0}",DateTime.Now));
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
} protected void Application_EndRequest()
{
ObjectFactory.Dispose();
}
}
}
依赖注入这里采用的是StructureMap。HttpContextLifecycle提供了Request范围内的生命周期管理但未定义在StructureMap程序包中,需要引入StructureMap.Web程序包。使用HttpContextLifecycle时需要在Application_EndRequest调用HttpContextLifecycle.DisposeAndClearAll()方法。
EntityFramework系列:Repository模式与单元测试的更多相关文章
- 基于 EntityFramework 生成 Repository 模式代码
借助 WeihanLi.EntityFramework 实现简单的 Repository Intro 很多时候一些简单的业务都是简单的增删改查,动态生成一些代码完成基本的增删改查,而这些增删改查代码大 ...
- 使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【二】——使用Repository模式构建数据库访问层
系列导航地址http://www.cnblogs.com/fzrain/p/3490137.html 前言 在数据访问层应用Repository模式来隔离对领域对象的细节操作是很有意义的.它位于映射层 ...
- PHP 设计模式系列 —— 资源库模式(Repository)
1.模式定义 Repository 是一个独立的层,介于领域层与数据映射层(数据访问层)之间.它的存在让领域层感觉不到数据访问层的存在,它提供一个类似集合的接口提供给领域层进行领域对象的访问.Repo ...
- 关于Repository模式
定义(来自Martin Fowler的<企业应用架构模式>): Mediates between the domain and data mapping layers using a co ...
- LCLFramework框架之Repository模式
Respository模式在示例中的实际目的小结一下 Repository模式是架构模式,在设计架构时,才有参考价值: Repository模式主要是封装数据查询和存储逻辑: Repository模式 ...
- [转]关于Repository模式
原文链接:关于Repository模式 定义(来自Martin Fowler的<企业应用架构模式>): Mediates between the domain and data mappi ...
- Asp.Net Core + Dapper + Repository 模式 + TDD 学习笔记
0x00 前言 之前一直使用的是 EF ,做了一个简单的小项目后发现 EF 的表现并不是很好,就比如联表查询,因为现在的 EF Core 也没有啥好用的分析工具,所以也不知道该怎么写 Linq 生成出 ...
- Entity Framework Repository模式
Repository模式之前 如果我们用最原始的EF进行设计对每个实体类的“C(增加).R(读取).U(修改).D(删除)”这四个操作. 第一个:先来看看查询,对于实体类简单的查询操作,每次都是这样的 ...
- Repository模式介绍汇总
1.Linq To Sql中Repository模式应用场景 http://www.cnblogs.com/zhijianliutang/archive/2012/02/24/2367305.html ...
随机推荐
- MySQL 5.6 中的 TIMESTAMP 和 explicit_defaults_for_timestamp 参数
安装MySQL时,有warning: [root@localhost mysql]# scripts/mysql_install_db --user=mysql Installing MySQL sy ...
- JS对URL字符串进行编码/解码分析
一.为什么要进行js编码和解码? 只有字母和数字[0-9a-zA-Z].一些特殊符号“$-_.+!*'(),”[不包括双引号].以及某些保留字,才可以不经过编码直接用于URL. 出现的情况: 网址路径 ...
- IE6/7/8中parseInt第一个参数为非法八进制字符串且第二个参数不传时返回值为0
JavaScript中数字有十进制.八进制.十六进制.以"0"开头的是八进制,"0x"或"0X"开头的是十六进制. parseInt用来把字 ...
- 通过beego快速创建一个Restful风格API项目及API文档自动化
通过beego快速创建一个Restful风格API项目及API文档自动化 本文演示如何快速(一分钟内,不写一行代码)的根据数据库及表创建一个Restful风格的API项目,及提供便于在线测试API的界 ...
- SVN同步大坑
遇到的问题 这两天一直在搞svn的主从备份,使用的方法是svnsync做的主从同步,同步大部分的仓库都没有什么问题很顺利的就同步完成了,不了解svnsync同步的可以看我这篇,但是在在同步2个仓库的时 ...
- Linux 系统常用命令汇总(二) vi 文本编辑
文本编辑 vi 命令 作用 +文件名 编辑文本文件,若文件不存在同时创建该文件 Ctrl+f 向后翻一页 Ctrl+b 向前翻一页 Ctrl+d 向后翻半页 Ctrl+u 向前翻半页 + 光标移动到下 ...
- java读取word内容
暂时只写读取word内容的方法. 依赖的jar: poi-3.9-20121203.jarpoi-ooxml-3.9-20121203.jarxmlbeans-2.3.0.jar package co ...
- 该如何认识ZBrush中的2.5D绘画
ZBrush不仅对3D行业进行了改革.让艺术家感到无约束自由创作的3D设计,同时它还是一个强大的绘画程序!基于强大的Pixol功能,ZBrush®将数字绘画提升到一个新的层次.如下图所示,插画功能主要 ...
- Linux 删除文件中某一行的方法
如果有一个abc.txt文件,内容是: aaa bbb ccc ddd eee fff 如果要删除aaa,那么脚本可以这样写: sed -i '/aaa/d' abc.txt 如果删除的是一个变量的值 ...
- 分层开发MySchool总结
由于分层之间存在各层之间的关系窗体之间的方法跳转,故有需要者可以进行下载本地文件 MySchool.rar 3304KB 5/22/2016 9:43:28 AM ,代码中有注释,