如何使用IOC从存储库中删除工作单元function
我有一个使用ASP.NET MVC,Unity和Linq到SQL的应用程序。
统一容器注册从System.Data.Linq.DataContext
inheritance的typesAcmeDataContext
,并使用HttpContext
与LifetimeManager
。
有一个控制器工厂使用统一容器获取控制器实例。 我在构造函数中设置了所有依赖关系,如下所示:
// 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
。 我甚至会把整个事情都转过来:
- 定义一个工厂,允许为Acme域创build一个新的工作单元:
- 创build一个将LINQ to SQL抽象出来的抽象
AcmeUnitOfWork
。 - 创build一个可以创build新的LINQ to SQL工作单元的具体工厂。
- 在您的DIconfiguration中注册混凝土工厂。
- 实施unit testing的
InMemoryAcmeUnitOfWork
。 - 可以为
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的模型。
工作单元实现Insert
和Delete
操作。 在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实施方便的扩展方法:
预计存储库有方便的方法,如GetById
或GetByLastName
。 当然, 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的最后一个缺点是在工作单元上使用Insert
和Delete
方法。 将它们移动到存储库时,会强制您使用特定的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更改。
这回答了你的问题了吗?