如何使用 IOC 從儲存庫中刪除工作單元功能
我有一個使用 ASP.NET MVC、Unity 和 Linq to SQL 的應用程序。
unity 容器使用 using 註冊繼承自
AcmeDataContext的類型。System.Data.Linq.DataContext``LifetimeManager``HttpContext有一個控制器工廠,它使用統一容器獲取控制器實例。我設置了對建構子的所有依賴項,如下所示:
// Initialize a new instance of the EmployeeController class public EmployeeController(IEmployeeService service) // Initializes a new instance of the EmployeeService class public EmployeeService(IEmployeeRepository repository) : IEmployeeService // Initialize a new instance of the EmployeeRepository class public EmployeeRepository(AcmeDataContext dataContext) : IEmployeeRepository每當需要建構子時,統一容器會解析一個連接,該連接用於解析數據上下文,然後是儲存庫,然後是服務,最後是控制器。
問題在於
IEmployeeRepository暴露了該SubmitChanges方法,因為服務類沒有DataContext引用。有人告訴我,工作單元應該從儲存庫外部進行管理,所以我似乎應該
SubmitChanges從我的儲存庫中刪除。這是為什麼?如果這是真的,這是否意味著我必須聲明一個
IUnitOfWork介面並使每個服務類都依賴於它?我還能如何讓我的服務類管理工作單元?
您不應該嘗試將
AcmeDataContext自身提供給EmployeeRepository. 我什至會扭轉整個局面:
- 定義一個允許為 Acme 域創建新工作單元的工廠:
- 創建一個抽像
AcmeUnitOfWork出 LINQ to SQL 的抽象。- 創建一個可以允許創建新的 LINQ to SQL 工作單元的具體工廠。
- 在您的 DI 配置中註冊該具體工廠。
- 實施
InMemoryAcmeUnitOfWork單元測試。IQueryable<T>可選擇為您的儲存庫上的常見操作實現方便的擴展方法。更新:我寫了一篇關於這個主題的部落格文章:Faking your LINQ provider。
下面是一步一步的例子:
警告:這將是一個很長的文章。
第 1 步:定義工廠:
public interface IAcmeUnitOfWorkFactory { AcmeUnitOfWork CreateNew(); }創建工廠很重要,因為
DataContext實現 IDisposable 所以您希望擁有實例的所有權。雖然一些框架允許您在不再需要時處理對象,但工廠使這一點非常明確。第 2 步:為 Acme 域創建一個抽象的工作單元:
public abstract class AcmeUnitOfWork : IDisposable { public IQueryable<Employee> Employees { [DebuggerStepThrough] get { return this.GetRepository<Employee>(); } } public IQueryable<Order> Orders { [DebuggerStepThrough] get { return this.GetRepository<Order>(); } } public abstract void Insert(object entity); public abstract void Delete(object entity); public abstract void SubmitChanges(); public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } protected abstract IQueryable<T> GetRepository<T>() where T : class; protected virtual void Dispose(bool disposing) { } }關於這個抽像類,有一些有趣的事情需要注意。工作單元控制並創建儲存庫。儲存庫基本上是實現
IQueryable<T>. 儲存庫實現返回特定儲存庫的屬性。這可以防止使用者呼叫uow.GetRepository<Employee>(),並創建一個非常接近您已經使用 LINQ to SQL 或實體框架所做的模型。工作單位實施
Insert和Delete操作。在 LINQ to SQL 中,這些操作放在Table<T>類上,但是當您嘗試以這種方式實現它時,它將阻止您將 LINQ to SQL 抽像出來。步驟 3. 創建具體工廠:
public class LinqToSqlAcmeUnitOfWorkFactory : IAcmeUnitOfWorkFactory { private static readonly MappingSource Mapping = new AttributeMappingSource(); public string AcmeConnectionString { get; set; } public AcmeUnitOfWork CreateNew() { var context = new DataContext(this.AcmeConnectionString, Mapping); return new LinqToSqlAcmeUnitOfWork(context); } }
LinqToSqlAcmeUnitOfWork工廠基於基類創建了一個AcmeUnitOfWork:internal sealed class LinqToSqlAcmeUnitOfWork : AcmeUnitOfWork { private readonly DataContext db; public LinqToSqlAcmeUnitOfWork(DataContext db) { this.db = db; } public override void Insert(object entity) { if (entity == null) throw new ArgumentNullException("entity"); this.db.GetTable(entity.GetType()).InsertOnSubmit(entity); } public override void Delete(object entity) { if (entity == null) throw new ArgumentNullException("entity"); this.db.GetTable(entity.GetType()).DeleteOnSubmit(entity); } public override void SubmitChanges(); { this.db.SubmitChanges(); } protected override IQueryable<TEntity> GetRepository<TEntity>() where TEntity : class { return this.db.GetTable<TEntity>(); } protected override void Dispose(bool disposing) { this.db.Dispose(); } }第 4 步:在您的 DI 配置中註冊該具體工廠。
您最了解如何註冊
IAcmeUnitOfWorkFactory介面以返回 的實例LinqToSqlAcmeUnitOfWorkFactory,但它看起來像這樣:container.RegisterSingle<IAcmeUnitOfWorkFactory>( new LinqToSqlAcmeUnitOfWorkFactory() { AcmeConnectionString = AppSettings.ConnectionStrings["ACME"].ConnectionString });現在您可以更改對 的依賴項
EmployeeService以使用IAcmeUnitOfWorkFactory:public class EmployeeService : IEmployeeService { public EmployeeService(IAcmeUnitOfWorkFactory contextFactory) { ... } public Employee[] GetAll() { using (var context = this.contextFactory.CreateNew()) { // This just works like a real L2S DataObject. return context.Employees.ToArray(); } } }請注意,您甚至可以刪除
IEmployeeService介面並讓控制器EmployeeService直接使用。您不需要此介面進行單元測試,因為您可以在測試期間替換工作單元以防止EmployeeService訪問數據庫。這可能還會為您節省大量 DI 配置,因為大多數 DI 框架都知道如何實例化具體類。第 5 步:實施
InMemoryAcmeUnitOfWork單元測試。所有這些抽像都是有原因的。單元測試。現在讓我們創建一個
AcmeUnitOfWork用於單元測試的目的:public class InMemoryAcmeUnitOfWork: AcmeUnitOfWork, IAcmeUnitOfWorkFactory { private readonly List<object> committed = new List<object>(); private readonly List<object> uncommittedInserts = new List<object>(); private readonly List<object> uncommittedDeletes = new List<object>(); // This is a dirty trick. This UoW is also it's own factory. // This makes writing unit tests easier. AcmeUnitOfWork IAcmeUnitOfWorkFactory.CreateNew() { return this; } // Get a list with all committed objects of the requested type. public IEnumerable<TEntity> Committed<TEntity>() where TEntity : class { return this.committed.OfType<TEntity>(); } protected override IQueryable<TEntity> GetRepository<TEntity>() { // Only return committed objects. Same behavior as L2S and EF. return this.committed.OfType<TEntity>().AsQueryable(); } // Directly add an object to the 'database'. Useful during test setup. public void AddCommitted(object entity) { this.committed.Add(entity); } public override void Insert(object entity) { this.uncommittedInserts.Add(entity); } public override void Delete(object entity) { if (!this.committed.Contains(entity)) Assert.Fail("Entity does not exist."); this.uncommittedDeletes.Add(entity); } public override void SubmitChanges() { this.committed.AddRange(this.uncommittedInserts); this.uncommittedInserts.Clear(); this.committed.RemoveAll( e => this.uncommittedDeletes.Contains(e)); this.uncommittedDeletes.Clear(); } protected override void Dispose(bool disposing) { } }您可以在單元測試中使用此類。例如:
[TestMethod] public void ControllerTest1() { // Arrange var context = new InMemoryAcmeUnitOfWork(); var controller = new CreateValidController(context); context.AddCommitted(new Employee() { Id = 6, Name = ".NET Junkie" }); // Act controller.DoSomething(); // Assert Assert.IsTrue(ExpectSomething); } private static EmployeeController CreateValidController( IAcmeUnitOfWorkFactory factory) { return new EmployeeController(return new EmployeeService(factory)); }第 6 步:可選擇實現方便的擴展方法:
儲存庫應該有方便的方法,例如
GetById或GetByLastName。當然IQueryable<T>是泛型介面,不包含這樣的方法。我們可以用類似的呼叫來混淆我們的程式碼context.Employees.Single(e => e.Id == employeeId),但這真的很難看。這個問題的完美解決方案是:擴展方法:// Place this class in the same namespace as your LINQ to SQL entities. public static class AcmeRepositoryExtensions { public static Employee GetById(this IQueryable<Employee> repository,int id) { return Single(repository.Where(entity => entity.Id == id), id); } public static Order GetById(this IQueryable<Order> repository, int id) { return Single(repository.Where(entity => entity.Id == id), id); } // This method allows reporting more descriptive error messages. [DebuggerStepThrough] private static TEntity Single<TEntity, TKey>(IQueryable<TEntity> query, TKey key) where TEntity : class { try { return query.Single(); } catch (Exception ex) { throw new InvalidOperationException("There was an error " + "getting a single element of type " + typeof(TEntity) .FullName + " with key '" + key + "'. " + ex.Message, ex); } } }有了這些擴展方法,它允許您
GetById從程式碼中呼叫這些方法和其他方法:var employee = context.Employees.GetById(employeeId);這段程式碼最好的地方(我在生產中使用它)是——一旦到位——它可以讓你不用為單元測試編寫大量程式碼。當新實體添加到系統中時,您會發現自己向類添加方法
AcmeRepositoryExtensions和向類添加屬性AcmeUnitOfWork,但您不需要為生產或測試創建新的儲存庫類。這種模式當然有一些缺點。最重要的可能是 LINQ to SQL 並沒有完全抽像出來,因為您仍然使用 LINQ to SQL 生成的實體。這些實體包含
EntitySet<T>特定於 LINQ to SQL 的屬性。我沒有發現它們妨礙正確的單元測試,所以對我來說這不是問題。如果您願意,您始終可以將 POCO 對象與 LINQ to SQL 一起使用。另一個缺點是複雜的 LINQ 查詢可以在測試中成功但在生產中失敗,因為查詢提供程序中的限制(或錯誤)(尤其是 EF 3.5 查詢提供程序很爛)。當您不使用此模型時,您可能正在編寫完全被單元測試版本替換的自定義儲存庫類,並且您仍然會遇到無法在單元測試中測試對數據庫的查詢的問題。為此,您將需要由事務包裝的集成測試。
這種設計的最後一個缺點是工作單元的使用
Insert和Delete方法。雖然將它們移動到儲存庫會迫使您擁有具有特定class IRepository<T> : IQueryable<T>界面的設計,但它可以防止您出現其他錯誤。在我自己使用的解決方案中,我也有InsertAll(IEnumerable)方法DeleteAll(IEnumerable)。然而,很容易輸入錯誤並寫出類似的東西context.Delete(context.Messages)(注意使用 ofDelete代替DeleteAll)。這會編譯得很好,因為Delete接受一個object. 對儲存庫進行刪除操作的設計將阻止此類語句編譯,因為儲存庫是類型化的。更新:我寫了一篇關於這個主題的部落格文章,更詳細地描述了這個解決方案:Faking your LINQ provider。
我希望這有幫助。