1.依賴IRepository介面而不是直接使用EntityFramework
使用IRepository不只是架構上解耦的需要,更重要的意義在於Service的單元測試,Repository模式本身就是採用集合操作的方式簡化資料訪問,IRepository更容易Mock。先上圖:
鑑於目前接觸到的專案中,即使業務邏輯相對複雜的專案也只是應用邏輯複雜而非領域邏輯複雜,在實際使用中聚合根和單獨Repository介面只是引入了更多的程式碼和型別定義,因此一般情況下使用泛型版本的Repository<T>介面即可。nopcommerce等開源專案中也是如此。Java中的偽泛型無法實現泛型版本的Repository<T>,簡單的說你無法在Repository<T>的方法中獲取T的型別。
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.封裝DbContext的依賴項
(1)定義一個通用的EfDbContext,將DbContext對IDbConnectionFactory、ConnectionString、實體類配置等的依賴封裝到DbSettings中,既可以在使用使方便依賴注入也方便進行單元測試。
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 disable 13 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 : DbContext 44 { 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 else 56 { 57 Database.SetInitializer<T>(null); 58 } 59 } 60 61 public new IDbSet<T> Set<T>() where T : class 62 { 63 return base.Set<T>(); 64 } 65 66 public int Commit() 67 { 68 return base.SaveChanges(); 69 } 70 } 71 }
(2)在DbSettings中按需定義依賴,這裡將實體類的配置也通過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.定義SqlServerDbContext和VersionDbContext,解決使用開放式併發連線時,MySql等資料庫無法自動生成RowVersion的問題。
(1)適用於SqlServer、SqlServeCe的SqlServerDbContext
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)適用於Myql、Sqlite等資料庫的VersionDbContext。使用手動更新Version,通過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.使用XUnit、Rhino.Mocks和SqlServerCe進行單元測試
這是參考nopcommerce中的做法,nopcommerce使用的NUnit需要安裝NUnit擴充套件,XUnit只需要通過Nuget引入程式包,看看GitHub上的aspnet原始碼,微軟也在使用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 DbSettings 11 { 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.確保在ASP.NET中使用依賴注入時,配置DbContext的生命週期為Request範圍
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 }
依賴注入這裡採用的是StructureMap。HttpContextLifecycle提供了Request範圍內的生命週期管理但未定義在StructureMap程式包中,需要引入StructureMap.Web程式包。使用HttpContextLifecycle時需要在Application_EndRequest呼叫HttpContextLifecycle.DisposeAndClearAll()方法。