如何使用国际奥委会库中删除的工作单位功能 [英] How to remove unit of work functionality from repositories using IOC

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

问题描述

我有使用ASP.NET MVC,团结和LINQ to SQL应用程序。

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

团结容器注册类型 AcmeDataContext System.Data.Linq.DataContext 继承,以 LifetimeManager 使用的HttpContext

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

有一个控制器工厂使用Unity容器,得到控制器实例。我设置了我所有的依赖在构造函数,像这样的:

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到作品的SQL单位。

  4. 注册混凝土厂的DI配置。

  5. 实施的 InMemoryAcmeUnitOfWork 进行单元测试。

  6. 选择实现对常用操作方便的扩展方法你的的IQueryable< T>

  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提供程序

下面是一步一步用实例

警告:这将是一个loooong后

第1步:定义工厂:

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.

第二步:创建工作的抽象单位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&LT; T&GT; 。该信息库实现了返回特定的资源库属性。这prevents用户调用 uow.GetRepository&LT;员工&GT;(),这将创建一个模型,是非常接近你已经在使用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.

工作器具的单位插入删除操作。在LINQ to SQL这些操作都放在表&LT; T&GT; 类,但是当你试图实现这种方式,将$ P $您pvent从抽象的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);
    }
}

工厂创建一个 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(); }
}

第四步:注册混凝土厂的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 直接使用。你不需要这个接口进行单元测试,因为你可以测试$ P $访问数据库pventing的的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.

第五步:实现一个 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));
}

第六步:选择实现方便的扩展方法:

Step 6: Optionally implement convenient extension methods:

库预计将有便捷的方法,如 GetById GetByLastName 。当然,的IQueryable&LT; T&GT; 是一个通用的接口,不包含这样的方法。我们可以扰乱我们的code与像通话context.Employees.Single(E =&GT; e.Id ==雇员),但是这真难看。这个问题的完美解决方案是:扩展方法:

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 并从code其他方法:

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

最好的事情是什么这个code(我用它在生产中)是该-once地方 - 它写了很多code的单元测试可以节省你。你会发现自己添加方法的 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.

该模型当然有一些shortcomes。最重要的也许是,LINQ to SQL的不是抽象的完全消失,因为你仍然使用LINQ到SQL生成的实体。这些实体包括的EntitySet&LT; T&GT;所特有的LINQ to SQL的属性。我还没有发现他们在适当的单元测试的方式,所以对我来说这不是一个问题。如果你愿意,你可以随时使用POCO对象使用LINQ到SQL。

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.

另一个shortcome是复杂的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.

这种设计的最后shortcome是使用插入删除的工作单元方法。虽然它们移动到存储库会迫使你必须使用特定类IRepository℃的设计; T&GT; :IQueryable的&LT; T&GT; 接口,它从$其他错误p $ pvents你。在我用我自己的解决方案,我也有 InsertAll(IEnumerable的) DeleteAll(IEnumerable的)方法。这不过是很容易输错这一点,写的东西像 context.Delete(context.Messages)(注意使用中删除而不是 DeleteAll )。这将编译很好,因为删除接受一个对象。与库删除操作的设计将进行编译prevent这样的说法,因为存储库类型的。

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提供程序

我希望这有助于。

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

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