【问题标题】:How to remove unit of work functionality from repositories using IOC如何使用 IOC 从存储库中删除工作单元功能
【发布时间】:2011-05-06 22:21:12
【问题描述】:

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

统一容器注册类型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 接口并使每个 服务类都依赖于它?我还能如何让我的服务类管理工作单元?

【问题讨论】:

    标签: asp.net-mvc linq-to-sql dependency-injection domain-driven-design inversion-of-control


    【解决方案1】:

    您不应尝试将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&lt;T&gt; 的东西。存储库实现返回特定存储库的属性。这可以防止用户调用uow.GetRepository&lt;Employee&gt;(),并创建一个非常接近您已经使用 LINQ to SQL 或实体框架所做的模型。

    工作单元实现InsertDelete 操作。在 LINQ to SQL 中,这些操作放在 Table&lt;T&gt; 类上,但是当您尝试以这种方式实现它时,它会阻止您将 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);
        }
    }
    

    工厂基于AcmeUnitOfWork基类创建了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 步:在您的 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&lt;T&gt;是一个泛型接口,不包含这样的方法。我们可以用context.Employees.Single(e =&gt; 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 生成的实体。这些实体包含特定于 LINQ to SQL 的 EntitySet&lt;T&gt; 属性。我没有发现它们妨碍正确的单元测试,所以对我来说这不是问题。如果您愿意,您可以随时将 POCO 对象与 LINQ to SQL 一起使用。

    另一个缺点是复杂的 LINQ 查询可以在测试中成功但在生产中失败,因为查询提供程序中的限制(或错误)(尤其是 EF 3.5 查询提供程序很烂)。当您不使用此模型时,您可能正在编写完全被单元测试版本替换的自定义存储库类,并且您仍然会遇到无法在单元测试中测试对数据库的查询的问题。为此,您将需要由事务包装的集成测试。

    这种设计的最后一个缺点是在工作单元上使用了InsertDelete 方法。虽然将它们移动到存储库会迫使您拥有一个具有特定 class IRepository&lt;T&gt; : IQueryable&lt;T&gt; 接口的设计,但它可以防止您出现其他错误。在我自己使用的解决方案中,我也有InsertAll(IEnumerable)DeleteAll(IEnumerable) 方法。然而,很容易输入错误并写成context.Delete(context.Messages) 之类的东西(注意使用Delete 而不是DeleteAll)。这会编译得很好,因为Delete 接受object。对存储库进行删除操作的设计会阻止此类语句编译,因为存储库是类型化的。

    更新:我写了一篇关于这个主题的博文,更详细地描述了这个解决方案:Faking your LINQ provider

    我希望这会有所帮助。

    【讨论】:

    • 也许我应该把它写成一篇博文:-)
    • 你应该 - 这是一个非常有用的答案。现在,我正试图理解 Szymon 所说的话:“然而,在成熟的 DDD 解决方案中,不应该同时需要 UoW 和存储库”
    • 我认为 LinqToSqlAcmeUnitOfWork : IAcmeUnitOfWorkFactory 应该是 LinqToSqlAcmeUnitOfWork : AcmeUnitOfWork
    • @Tymek:非常敏锐。我修好了。
    • @Steven,可能是因为它有太多的编辑(见here。(我还要做更多......)顺便说一句,在我看来,如果你做了你的@987654374 @ 和 Delete 通用,你可能会避免你的最后一个缺点,不是吗?
    【解决方案2】:

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

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

    这能回答你的问题吗?

    【讨论】:

    • 你能举个例子吗?
    猜你喜欢
    • 2016-06-27
    • 1970-01-01
    • 2010-12-10
    • 1970-01-01
    • 2011-06-28
    • 2013-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多