EntityFramework series: Repository mode and unit test,
1. Rely on the IRepository interface instead of directly using EntityFramework
The use of IRepository is not only an architectural decoupling requirement, but also a unit test of the Service. The Repository mode itself simplifies data access through set operations, making IRepository easier to use Mock. First:
In view of the current project, even if the business logic is relatively complex, the application logic is complex, not the domain logic is complex, in actual use, the aggregate root and individual Repository interfaces only introduce more code and Type Definitions. Therefore, you can use the generic version of Repository <T> interface. This is also true for open-source projects such as nopcommerce. The pseudo-generic type in Java cannot implement the Repository of the generic version. Simply put, you cannot obtain the T type in the Repository <T> method.
1 namespace Example.Application 2 { 3 public interface IRepository<T> where T : class 4 { 5 T FindBy(object id); 6 7 IQueryable<T> Query { get; } 8 9 void Add(T entity);10 11 void Remove(T entity);12 13 void Update(T entity);14 15 int Commit();16 }17 }
2. encapsulate DbContext Dependencies
(1) define a common EfDbContext, and encapsulate DbContext dependencies on IDbConnectionFactory, ConnectionString, and object configuration into DbSettings, you can use it to facilitate dependency injection and unit testing.
1 namespace Example.Infrastructure.Repository 2 { 3 public class EfDbContext : DbContext, IDbContext 4 { 5 private DbSettings _dbSettings; 6 7 public EfDbContext(IConfiguration configuration, ILogger logger, DbSettings dbSettings) : base(dbSettings.NameOrConnectionString) 8 { 9 this._dbSettings = dbSettings;10 if (this._dbSettings.DbConnectionFactory != null)11 {12 #pragma warning disable13 Database.DefaultConnectionFactory = this._dbSettings.DbConnectionFactory;14 }15 if (configuration.Get<bool>("database.log:", false))16 {17 this.Database.Log = sql => logger.Information(sql);18 }19 this.Database.Log = l => System.Diagnostics.Debug.WriteLine(l);20 }21 22 protected override void OnModelCreating(DbModelBuilder modelBuilder)23 {24 base.OnModelCreating(modelBuilder);25 26 modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();27 if (_dbSettings.EntityMaps != null)28 {29 foreach (var item in _dbSettings.EntityMaps)30 {31 modelBuilder.Configurations.Add((dynamic)item);32 }33 }34 if (_dbSettings.ComplexMaps != null)35 {36 foreach (var item in _dbSettings.ComplexMaps)37 {38 modelBuilder.Configurations.Add((dynamic)item);39 }40 }41 }42 43 public void SetInitializer<T>() where T : DbContext44 {45 if (this._dbSettings.Debug)46 {47 if (this._dbSettings.UnitTest)48 {49 Database.SetInitializer(new DropCreateDatabaseAlways<T>());50 }51 {52 Database.SetInitializer(new DropCreateDatabaseIfModelChanges<T>());53 }54 }55 else56 {57 Database.SetInitializer<T>(null);58 }59 }60 61 public new IDbSet<T> Set<T>() where T : class62 {63 return base.Set<T>();64 }65 66 public int Commit()67 {68 return base.SaveChanges();69 }70 }71 }
(2) define dependencies as needed in DbSettings. Here, the object class configuration is also injected through DbSettings.
1 namespace Example.Infrastructure.Repository 2 { 3 public class DbSettings 4 { 5 public DbSettings() 6 { 7 this.RowVersionNname = "Version"; 8 } 9 10 public string NameOrConnectionString { get; set; }11 12 public string RowVersionNname { get; set; }13 public bool Debug { get; set; }14 15 public bool UnitTest { get; set; }16 17 public IDbConnectionFactory DbConnectionFactory { get; set; }18 19 public List<object> EntityMaps { get; set; } = new List<object>();20 21 public List<object> ComplexMaps { get; set; } = new List<object>();22 }23 }
3. Define SqlServerDbContext and VersionDbContext to solve the problem that RowVersion cannot be automatically generated by databases such as MySql when open concurrent connections are used.
(1) SqlServerDbContext applicable to SqlServer and SqlServeCe
1 namespace Example.Infrastructure.Repository 2 { 3 public class SqlServerDbContext : EfDbContext 4 { 5 private DbSettings _dbSettings; 6 7 public SqlServerDbContext(IConfiguration configuration, ILogger logger, DbSettings dbSettings) 8 : base(configuration, logger, dbSettings) 9 {10 this._dbSettings = dbSettings;11 }12 13 protected override void OnModelCreating(DbModelBuilder modelBuilder)14 {15 base.OnModelCreating(modelBuilder);16 modelBuilder.Properties().Where(o => o.Name == this._dbSettings.RowVersionNname).Configure(o => o.IsRowVersion());17 base.SetInitializer<SqlServerDbContext>();18 }19 }20 }
(2) VersionDbContext applicable to databases such as Myql and Sqlite. Use manual Version update to ensure that the Version number is unique through GUID.
1 namespace Example.Infrastructure.Repository 2 { 3 public class VersionDbContext : EfDbContext 4 { 5 private DbSettings _dbSettings; 6 7 public VersionDbContext(IConfiguration configuration, ILogger logger, DbSettings dbSettings) 8 : base(configuration,logger,dbSettings) 9 {10 this._dbSettings = dbSettings;11 }12 13 protected override void OnModelCreating(DbModelBuilder modelBuilder)14 {15 base.OnModelCreating(modelBuilder);16 modelBuilder.Properties().Where(o => o.Name == this._dbSettings.RowVersionNname)17 .Configure(o => o.IsConcurrencyToken().HasDatabaseGeneratedOption(DatabaseGeneratedOption.None));18 base.SetInitializer<VersionDbContext>();19 }20 21 public override int SaveChanges()22 {23 this.ChangeTracker.DetectChanges();24 var objectContext = ((IObjectContextAdapter)this).ObjectContext;25 foreach (ObjectStateEntry entry in objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Modified | EntityState.Added))26 {27 var v = entry.Entity;28 if (v != null)29 {30 var property = v.GetType().GetProperty(this._dbSettings.RowVersionNname);31 if (property != null)32 {33 var value = Encoding.UTF8.GetBytes(Guid.NewGuid().ToString());34 property.SetValue(v, value);35 }36 }37 }38 return base.SaveChanges();39 }40 }41 }
4. Use XUnit, Rhino. Mocks, and SqlServerCe for unit testing
This is a reference to nopcommerce practices. NUnit extensions must be installed for nopcommerce NUnit. XUnit only needs to introduce the program package through Nuget to check the aspnet source code on GitHub. Microsoft is also using XUnit.
1 namespace Example.Infrastructure.Test.Repository 2 { 3 public class CustomerPersistenceTest 4 { 5 private IRepository<T> GetRepository<T>() where T : class 6 { 7 string testDbName = "Data Source=" + (System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)) + @"\\test.sdf;Persist Security Info=False"; 8 var configuration = MockRepository.GenerateMock<IConfiguration>(); 9 var logger = MockRepository.GenerateMock<ILogger>();10 var repository = new EfRepository<T>(new SqlServerDbContext(configuration,logger,new DbSettings11 {12 DbConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0"),13 NameOrConnectionString = testDbName,14 Debug = true,15 UnitTest = true,16 EntityMaps = new List<object> { new EntityTypeConfiguration<Customer>() }17 }));18 return repository;19 }20 21 [Fact]22 public void SaveLoadCustomerTest()23 {24 var repository = this.GetRepository<Customer>();25 repository.Add(new Customer { UserName = "test" });26 repository.Commit();27 var customer = repository.Query.FirstOrDefault(o => o.UserName == "test");28 Assert.NotNull(customer);29 }30 }31 }
5. Make sure that the lifecycle of DbContext is set to the Request range when dependency injection is used in ASP. NET.
1 namespace Example.Web 2 { 3 public class MvcApplication : System.Web.HttpApplication 4 { 5 protected void Application_Start() 6 { 7 ObjectFactory.Init(); 8 ObjectFactory.AddSingleton<IConfiguration, AppConfigAdapter>(); 9 ObjectFactory.AddSingleton<ILogger, Log4netAdapter>();10 ObjectFactory.AddSingleton<DbSettings, DbSettings>(new DbSettings { NameOrConnectionString = "SqlCeConnection", Debug = true });11 ObjectFactory.AddScoped<IDbContext, SqlServerDbContext>();12 ObjectFactory.AddTransient(typeof(IRepository<>), typeof(EfRepository<>));13 ObjectFactory.Build();14 ObjectFactory.GetInstance<ILogger>().Information(String.Format("Start at {0}",DateTime.Now));15 AreaRegistration.RegisterAllAreas();16 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);17 RouteConfig.RegisterRoutes(RouteTable.Routes);18 BundleConfig.RegisterBundles(BundleTable.Bundles);19 }20 21 protected void Application_EndRequest()22 {23 ObjectFactory.Dispose();24 }25 }26 }
Here, StructureMap is used for dependency injection. HttpContextLifecycle provides lifecycle management within the Request range, but is not defined in the StructureMap package. You need to introduce the StructureMap. Web package. When using HttpContextLifecycle, you must call HttpContextLifecycle. DisposeAndClearAll () in Application_EndRequest.