如何使用 IOC 从存储库中删除工作单元功能 [英] How to remove unit of work functionality from repositories using IOC

本文介绍了如何使用 IOC 从存储库中删除工作单元功能的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have an application using ASP.NET MVC, Unity, and Linq to SQL.

统一容器注册类型 AcmeDataContext,它继承自 System.Data.Linq.DataContext,使用 HttpContext 使用 LifetimeManager.

The unity container registers the type AcmeDataContext which inherits from System.Data.Linq.DataContext, with a LifetimeManager using HttpContext.

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

There is a controller factory which gets the controller instances using the unity container. I set-up all my dependencies on the constructors, like this:

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

每当需要构造函数时,统一容器都会解析一个连接,该连接用于解析数据上下文、存储库、服务,最后是控制器.

Whenever a constructor is needed, the unity container resolves a connection, which is used to resolve a data context, then a repository, then a service, and finally the controller.

问题在于 IEmployeeRepository 公开了 SubmitChanges 方法,因为服务类没有 DataContext 引用.强>

The issue is that IEmployeeRepository exposes the SubmitChanges method, since the service classes DO NOT have a DataContext reference.

有人告诉我应该从存储库外部管理工作单元,所以我似乎应该从我的存储库中删除 SubmitChanges.这是为什么?

I have been told that the unit of work should be managed from outside the repositories, so it would seem I ought to remove SubmitChanges from my repositories. Why is that?

如果这是真的,这是否意味着我必须声明一个 IUnitOfWork 接口并使每个服务类依赖于它?我还能如何让我的服务类管理工作单元?

If this is true, does this mean that I have to declare an IUnitOfWork interface and make every service class dependent upon it? How else can I allow my service classes to manage the unit of work?

推荐答案

您不应该尝试将 AcmeDataContext 本身提供给 EmployeeRepository.我什至会扭转整个事情:

You shouldn't try to supply the AcmeDataContext itself to the EmployeeRepository. I would even turn the whole thing around:

  1. 定义一个允许为 Acme 域创建新工作单元的工厂:
  2. 创建一个抽象的 AcmeUnitOfWork,将 LINQ to SQL 抽象出来.
  3. 创建一个具体工厂,允许创建新的 LINQ to SQL 工作单元.
  4. 在您的 DI 配置中注册那个具体工厂.
  5. 为单元测试实现 InMemoryAcmeUnitOfWork.
  6. 可选择为您的 IQueryable 存储库中的常见操作实现方便的扩展方法.
  1. Define a factory that allows creating a new unit of work for the Acme domain:
  2. Create an abstract AcmeUnitOfWork that abstracts away LINQ to SQL.
  3. Create a concrete factory that can allows creating new LINQ to SQL unit of works.
  4. Register that concrete factory in your DI configuration.
  5. Implement an InMemoryAcmeUnitOfWork for unit testing.
  6. Optionally implement convenient extension methods for common operations on your IQueryable<T> repositories.

更新:我写了一篇关于这个主题的博客文章:伪造您的 LINQ 提供程序.

下面是一步一步的例子:

Below is a step-by-step with examples:

警告:这将是一个很长的帖子.

第一步:定义工厂:

public interface IAcmeUnitOfWorkFactory
{
    AcmeUnitOfWork CreateNew();
}

创建工厂很重要,因为 DataContext 实现了 IDisposable,因此您希望拥有实例的所有权.虽然有些框架允许您在不再需要时处理对象,但工厂对此非常明确.

Creating a factory is important, because the DataContext implement IDisposable so you want to have ownership over the instance. While some frameworks allow you to dispose objects when not needed anymore, factories make this very explicit.

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

Step 2: Creating an abstract unit of work for the Acme domain:

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 的东西.存储库实现返回特定存储库的属性.这会阻止用户调用 uow.GetRepository() 并且这会创建一个非常接近您已经使用 LINQ to SQL 或实体框架所做的模型.

There are some interesting things to note about this abstract class. The Unit of Work controls and creates the Repositories. A repository is basically something that implements IQueryable<T>. The repository implements properties that return a specific repository. This prevents users from calling uow.GetRepository<Employee>() and this creates a model that is very close to what you are already doing with LINQ to SQL or Entity Framework.

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

The unit of work implements Insert and Delete operations. In LINQ to SQL these operations are placed on the Table<T> classes, but when you try to implement it this way it will prevent you from abstracting LINQ to SQL away.

步骤 3. 创建一个具体的工厂:

Step 3. Create a concrete factory:

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:

The factory created a LinqToSqlAcmeUnitOfWork based on the AcmeUnitOfWork base class:

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 配置中注册该具体工厂.

Step 4: Register that concrete factory in your DI configuration.

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

You know self best how to register the IAcmeUnitOfWorkFactory interface to return an instance of the LinqToSqlAcmeUnitOfWorkFactory, but it would look something like this:

container.RegisterSingle<IAcmeUnitOfWorkFactory>(
    new LinqToSqlAcmeUnitOfWorkFactory()
    {
        AcmeConnectionString =
            AppSettings.ConnectionStrings["ACME"].ConnectionString
    });

现在您可以更改对 EmployeeService 的依赖以使用 IAcmeUnitOfWorkFactory:

Now you can change the dependencies on the EmployeeService to use the 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 框架都知道如何实例化一个具体的类.

Note that you could even remove the IEmployeeService interface and let the controller use the EmployeeService directly. You don't need this interface for unit testing, because you can replace the unit of work during testing preventing the EmployeeService from accessing the database. This will probably also save you a lot of DI configuration, because most DI frameworks know how to instantiate a concrete class.

第 5 步:为单元测试实现 InMemoryAcmeUnitOfWork.

Step 5: Implement an InMemoryAcmeUnitOfWork for unit testing.

所有这些抽象都是有原因的.单元测试.现在让我们创建一个 AcmeUnitOfWork 用于单元测试目的:

All these abstractions are there for a reason. Unit testing. Now let's create a AcmeUnitOfWork for unit testing purposes:

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

您可以在单元测试中使用这个类.例如:

You can use this class in your unit tests. For instance:

[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 步:可选地实现方便的扩展方法:

Step 6: Optionally implement convenient extension methods:

存储库应该有方便的方法,例如 GetByIdGetByLastName.当然 IQueryable 是一个通用接口,不包含这样的方法.我们可以使用诸如 context.Employees.Single(e => e.Id == employeeId) 之类的调用来弄乱我们的代码,但这真的很难看.这个问题的完美解决方案是:扩展方法:

Repositories are expected to have convenient methods such as GetById or GetByLastName. Of course IQueryable<T> is a generic interface and does not contains such methods. We could clutter our code with calls like context.Employees.Single(e => e.Id == employeeId), but that's really ugly. The perfect solution to this problem is: extension methods:

// 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 和其他方法:

With these extension methods in place, it allows you to call those GetById and other methods from your code:

var employee = context.Employees.GetById(employeeId);

这段代码最棒的地方(我在生产中使用它)是 - 一旦到位 - 它可以让您免于为单元测试编写大量代码.当新实体添加到系统中时,您会发现自己将方法添加到 AcmeRepositoryExtensions 类并将属性添加到 AcmeUnitOfWork 类,但您不需要创建新的存储库类用于生产或测试.

What the nicest thing is about this code (I use it in production) is that -once in place- it saves you from writing a lot of code for unit testing. You will find yourself adding methods to the AcmeRepositoryExtensions class and properties to the AcmeUnitOfWork class when new entities are added to the system, but you don't need to create new repository classes for production or testing.

这个模型当然有一些缺点.最重要的也许是 LINQ to SQL 并没有完全抽象出来,因为您仍然使用 LINQ to SQL 生成的实体.这些实体包含特定于 LINQ to SQL 的 EntitySet 属性.我还没有发现它们妨碍了正确的单元测试,所以对我来说这不是问题.如果您愿意,您始终可以在 LINQ to SQL 中使用 POCO 对象.

This model has of course some shortcomes. The most important perhaps is that LINQ to SQL isn't abstract away completely, because you still use the LINQ to SQL generated entities. Those entity contain EntitySet<T> properties which are specific to LINQ to SQL. I haven't found them to be in the way of proper unit testing, so for me it's not a problem. If you want you can always use POCO objects with LINQ to SQL.

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

Another shortcome is that complicated LINQ queries can succeed in test but fail in production, because of limitations (or bugs) in the query provider (especially the EF 3.5 query provider sucks). When you do not use this model, you are probably writing custom repository classes that are completely replaced by unit test versions and you will still have the problem of not being able to test queries to your database in unit tests. For this you will need integration tests, wrapped by a transaction.

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

A last shortcome of this design is the use of Insert and Delete methods on the Unit of Work. While moving them to the repository would force you to have a design with an specific class IRepository<T> : IQueryable<T> interface, it prevents you from other errors. In the solution I use myself I also have InsertAll(IEnumerable) and DeleteAll(IEnumerable) methods. It is however easy to mistype this and write something like context.Delete(context.Messages) (note the use of Delete instead of DeleteAll). This would compile fine, because Delete accepts an object. A design with delete operations on the repository would prevent such statement from compiling, because the repositories are typed.

更新:我写了一篇关于这个主题的博客文章,更详细地描述了这个解决方案:伪造您的 LINQ 提供程序.

我希望这会有所帮助.

这篇关于如何使用 IOC 从存储库中删除工作单元功能的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆