Asp.net-Mvc

如何使用 IOC 從儲存庫中刪除工作單元功能

  • August 11, 2014

我有一個使用 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. 我什至會扭轉整個局面:

  1. 定義一個允許為 Acme 域創建新工作單元的工廠:
  2. 創建一個抽像AcmeUnitOfWork出 LINQ to SQL 的抽象。
  3. 創建一個可以允許創建新的 LINQ to SQL 工作單元的具體工廠。
  4. 在您的 DI 配置中註冊該具體工廠。
  5. 實施InMemoryAcmeUnitOfWork單元測試。
  6. 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 或實體框架所做的模型。

工作單位實施InsertDelete操作。在 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 步:可選擇實現方便的擴展方法:

儲存庫應該有方便的方法,例如GetByIdGetByLastName。當然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 查詢提供程序很爛)。當您不使用此模型時,您可能正在編寫完全被單元測試版本替換的自定義儲存庫類,並且您仍然會遇到無法在單元測試中測試對數據庫的查詢的問題。為此,您將需要由事務包裝的集成測試。

這種設計的最後一個缺點是工作單元的使用InsertDelete方法。雖然將它們移動到儲存庫會迫使您擁有具有特定class IRepository<T> : IQueryable<T>界面的設計,但它可以防止您出現其他錯誤。在我自己使用的解決方案中,我也有InsertAll(IEnumerable)方法DeleteAll(IEnumerable)。然而,很容易輸入錯誤並寫出類似的東西context.Delete(context.Messages)(注意使用 ofDelete代替DeleteAll)。這會編譯得很好,因為Delete接受一個object. 對儲存庫進行刪除操作的設計將阻止此類語句編譯,因為儲存庫是類型化的。

更新:我寫了一篇關於這個主題的部落格文章,更詳細地描述了這個解決方案:Faking your LINQ provider

我希望這有幫助。

引用自:https://stackoverflow.com/questions/4128640