如何使用IOC从存储库中删除工作单元function

我有一个使用ASP.NET MVC,Unity和Linq到SQL的应用程序。

统一容器注册从System.Data.Linq.DataContextinheritance的typesAcmeDataContext ,并使用HttpContextLifetimeManager

有一个控制器工厂使用统一容器获取控制器实例。 我在构造函数中设置了所有依赖关系,如下所示:

 // 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 

无论何时需要构造函数,统一容器都会parsing一个连接,该连接用于parsing数据上下文,然后是存储库,然后是服务,最后是控制器。

问题是IEmployeeRepository暴露了SubmitChanges方法,因为服务类没有DataContext引用。

我被告知应该从仓库外部pipe理工作单元,所以看起来我应该从仓库中删除SubmitChanges 。 这是为什么?

如果这是真的,这是否意味着我必须声明一个IUnitOfWork接口并使每个服务类都依赖于它? 我还可以如何让我的服务class来pipe理工作单位?

您不应该尝试将AcmeDataContext本身提供给EmployeeRepository 。 我甚至会把整个事情都转过来:

  1. 定义一个工厂,允许为Acme域创build一个新的工作单元:
  2. 创build一个将LINQ to SQL抽象出来的抽象AcmeUnitOfWork
  3. 创build一个可以创build新的LINQ to SQL工作单元的具体工厂。
  4. 在您的DIconfiguration中注册混凝土工厂。
  5. 实施unit testing的InMemoryAcmeUnitOfWork
  6. 可以为IQueryable<T>存储库上的常见操作实现方便的扩展方法。

更新:我写了一个关于这个主题的博客文章: 伪装你的LINQ提供者 。

下面是一个循序渐进的例子:

警告:这将是一个懒惰的职位。

第一步:定义工厂:

 public interface IAcmeUnitOfWorkFactory { AcmeUnitOfWork CreateNew(); } 

创build一个工厂很重要,因为DataContext实现了IDisposable,所以你想拥有实例的所有权。 虽然有些框架允许您在不再需要的时候处理对象,但工厂对此非常明确。

第2步:为Acme域创build抽象工作单元:

 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) { } } 

关于这个抽象类有一些有趣的事情需要注意。 工作单元控制并创build知识库。 存储库基本上是实现IQueryable<T>东西。 存储库实现了返回特定存储库的属性。 这可以防止用户调用uow.GetRepository<Employee>() ,这样就创build了一个非常接近于您已经在使用LINQ to SQL或Entity Framework的模型。

工作单元实现InsertDelete操作。 在LINQ to SQL中,这些操作放置在Table<T>类上,但是当您尝试以这种方式实现时,它将阻止您将LINQ to SQL抽象出来。

第3步。创build一个混凝土工厂:

 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); } } 

工厂基于AcmeUnitOfWork基类创build了一个LinqToSqlAcmeUnitOfWork

 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:在您的DIconfiguration中注册混凝土工厂。

您最清楚如何注册IAcmeUnitOfWorkFactory接口来返回IAcmeUnitOfWorkFactory一个实例,但它看起来像这样:

 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 。 您不需要此接口进行unit testing,因为您可以在testing期间replace工作单元,以防止EmployeeService访问数据库。 这也可能为您节省大量的DIconfiguration,因为大多数DI框架知道如何实例化一个具体的类。

第5步:实施InMemoryAcmeUnitOfWork进行unit testing。

所有这些抽象是有原因的。 unit testing。 现在让我们创build一个AcmeUnitOfWork进行unit testing:

 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) { } } 

你可以在你的unit testing中使用这个类。 例如:

 [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:可select实施方便的扩展方法:

预计存储库有方便的方法,如GetByIdGetByLastName 。 当然, IQueryable<T>是一个通用的接口,不包含这样的方法。 我们可以使用context.Employees.Single(e => e.Id == employeeId)调用代码,但这真的很难看。 这个问题的完美解决scheme是:扩展方法:

 // 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); 

这个代码(我在生产中使用它)最好的事情就是 – 只需要一个地方 – 它可以帮你避免为unit testing写很多代码。 当新实体添加到系统中时,您会发现自己向AcmeRepositoryExtensions类和属性添加了方法到AcmeRepositoryExtensions类,但不需要为生产或testing创build新的存储库类。

这个模型当然有一些缺点。 最重要的或许是LINQ to SQL不是完全抽象出来的,因为你仍然使用LINQ to SQL生成的实体。 那些实体包含特定于LINQ to SQL的EntitySet<T>属性。 我没有发现他们是在适当的unit testing的方式,所以对我来说这不是一个问题。 如果你想要,你总是可以使用POCO对象与LINQ to SQL。

另一个缺点是,复杂的LINQ查询可以在testing中成功,但由于查询提供程序(尤其是EF 3.5查询提供程序)的限制(或错误)而导致生产失败。 当你不使用这个模型时,你可能正在编写定制的存储库类,它们被unit testing版本完全替代,你仍然有在unit testing中不能testing你的数据库查询的问题。 为此,您将需要集成testing,由事务包装。

这种devise的最后一个缺点是在工作单元上使用InsertDelete方法。 将它们移动到存储库时,会强制您使用特定的class IRepository<T> : IQueryable<T>接口进行devise,从而防止出现其他错误。 在我自己使用的解决scheme中,我也有InsertAll(IEnumerable)DeleteAll(IEnumerable)方法。 然而,这很容易input这个错误,并写上context.Delete(context.Messages) (注意使用Delete而不是DeleteAll )。 这将编译好,因为Delete接受一个object 。 对存储库进行删除操作的devise会阻止编译这样的语句,因为存储库是键入的。

更新:我写了一个关于这个主题的博客文章,更详细地描述了这个解决scheme: 伪装你的LINQ提供者 。

我希望这有帮助。

如果将工作单元和存储库模式结合在一起,有人主张应该在存储库之外pipe理UoW,这样就可以创build两个存储库(比如CustomerRepository和OrderRepository)并将它们传递给相同的UoW实例,以确保对数据库的所有更改当你最后调用UoW.Complete()时,可以自动完成。

然而,在成熟的DDD解决scheme中,不应该需要UoW和存储库。 这是因为这样的解决scheme聚合边界是这样定义的,即不需要涉及多个存储库的primefaces更改。

这回答了你的问题了吗?