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

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

问题描述

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



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



有一个控制器工厂,它使用unity容器来获取控制器实例。我设置了所有我对构造函数的依赖,如下所示:

  //初始化EmployeeController类的新实例
public EmployeeController(IEmployeeService service)

//初始化EmployeeService类的新实例
public EmployeeService(IEmployeeRepository存储库):IEmployeeService

//初始化一个EmployeeRepository类的新实例
public EmployeeRepository(AcmeDataContext dataContext):IEmployeeRepository

每当一个构造函数需要,unity容器解析一个连接,用于解析一个数据上下文,一个存储库,一个服务,最后是控制器。



问题是, IEmployeeRepository 公开了 SubmitChanges 方法,因为服务类不具有 DataContext 参考。



我被告知工作单位应该是从存储库外部进行管理,所以看起来我应该从我的存储库中删除 SubmitChanges 。为什么是这样?



如果这是真的,这是否意味着我必须声明一个 IUnitOfWork 接口并使每个服务类依赖于它?还有什么可以允许我的服务类来管理工作单位?

解决方案

你不应该尝试提供 AcmeDataContext 本身到 EmployeeRepository 。我甚至会改变整个事情:


  1. 定义一个允许为Acme域创建新工作单位的工厂:

  2. 创建一个抽象的 AcmeUnitOfWork ,将LINQ to SQL抽象出来。

  3. 创建一个可以允许的具体工厂创建新的LINQ to SQL工作单元。

  4. 在DI配置中注册具体的工厂。

  5. 实现一个 InMemoryAcmeUnitOfWork 用于单元测试。

  6. 可选地,在 IQueryable 存储库中实现常用操作的方便扩展方法。

更新:我写了一篇有关此主题的博文:伪造您的LINQ提供商



是一个一步一步的例子:



警告:这将是一个很糟糕的帖子。



步骤1:定义进入工厂:

  public interface IAcmeUnitOfWorkFactory 
{
AcmeUnitOfWork CreateNew();
}

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



步骤2:为Acme域创建抽象工作单元: p>

  public abstract class AcmeUnitOfWork:IDisposable 
{
public IQueryable< Employee>员工
{
[DebuggerStepThrough]
get {return this.GetRepository< Employee>(); }
}

public IQueryable< Order>订单
{
[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>()
其中T:class;

protected virtual void Dispose(bool disposal){}
}

有关这个抽象类有一些有趣的事情要注意。工作单位控制和创建存储库。存储库基本上是实现 IQueryable< T> 的内容。存储库实现返回特定存储库的属性。这阻止用户调用 uow.GetRepository< Employee>(),并创建一个非常接近您正在使用LINQ to SQL或Entity Framework的模型。 / p>

工作单位实施插入删除操作。在LINQ to SQL中,这些操作位于 Table< T> 类中,但是当您尝试以这种方式实现时,将阻止您将LINQ抽象到SQL。 / p>

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

  public class LinqToSqlAcmeUnitOfWorkFactory: IAcmeUnitOfWorkFactory 
{
private static readonly MappingSource Mapping =
new AttributeMappingSource();

public string AcmeConnectionString {get;组; }

public AcmeUnitOfWork CreateNew()
{
var context = new DataContext(this.AcmeConnectionString,Mapping);
返回新的LinqToSqlAcmeUnitOfWork(context);
}
}

工厂创建了一个 LinqToSqlAcmeUnitOfWork 基于 AcmeUnitOfWork 基类:

 内部密封类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>()
其中TEntity:class
{
return this.db.GetTable< TEntity>();
}

protected override void Dispose(bool disposal){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())
{
//这只是一个真正的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列表< object> uncommittedInserts = new List< object>();
private readonly列表< object> uncommittedDeletes = new List< object>();

//这是一个肮脏的技巧。这个UoW也是自己的工厂。
//这使得写单元测试更容易。
AcmeUnitOfWork IAcmeUnitOfWorkFactory.CreateNew(){return this; }

//获取所请求类型的所有提交对象的列表。
public IEnumerable< TEntity>承诺< TEntity>()其中TEntity:class
{
return this.committed.OfType< TEntity>();
}

protected override IQueryable< TEntity> GetRepository< TEntity>()
{
//仅返回已提交的对象。与L2S和EF相同的行为。
return this.committed.OfType< TEntity>()。AsQueryable();
}

//直接向数据库添加一个对象。在测试设置期间有用。
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 实体不存在);

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

您可以在单元测试中使用此类。例如:

  [TestMethod] 
public void ControllerTest1()
{
//排列
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)
{
返回新的EmployeeController(返回新的EmployeeService(factory));
}

步骤6:选择实现方便的扩展方法:



存储库预计将具有方便的方法,例如 GetById GetByLastName 。当然 IQueryable< T> 是一个通用接口,不包含这样的方法。我们可以用 context.Employees.Single(e => e.Id == employeeId)之类的调用来混淆我们的代码,但这真的很丑陋。这个问题的完美解决方案是:扩展方法:

  //将此类放置在与LINQ to SQL实体相同的命名空间中。 
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 => id == id),id);
}

//此方法允许报告更多描述性错误消息。
[DebuggerStepThrough]
private static TEntity Single< TEntity,TKey>(IQueryable< TEntity>查询,
TKey key)其中TEntity:class
{
try
{
return query.Single();
}
catch(Exception ex)
{
throw new InvalidOperationException(有一个错误+
获取一个类型为+ 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< T> 属性。我没有发现他们是在正确的单元测试的方式,所以对我来说这不是一个问题。如果您希望您可以随时使用具有LINQ to SQL的POCO对象。



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



此设计的最后一个简便是使用插入删除工作单位的方法。将它们移动到存储库时,将迫使您使用特定的类IRepository< T> :IQueryable< T> 接口,它阻止您出现其他错误。在我自己的解决方案中,我也有 InsertAll(IEnumerable) DeleteAll(IEnumerable)方法。然而,这样做很容易出现,并写出像 context.Delete(context.Messages)(注意使用删除而不是 DeleteAll )。这将编译好,因为 Delete 接受一个对象。在存储库中进行删除操作的设计将阻止此类语句的编译,因为存储库是键入的。



更新:我写了一篇有关此主题的博客文章更详细地描述这个解决方案:伪造你的LINQ提供者



我希望这有助于您。


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

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.

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

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?

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?

解决方案

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

  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.

UPDATE: I wrote a blog post on this subject: Faking your LINQ provider.

Below is a step-by-step with examples:

WARNING: This will be a loooong post.

Step 1: Defining the factory:

public interface IAcmeUnitOfWorkFactory
{
    AcmeUnitOfWork CreateNew();
}

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.

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

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.

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.

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

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

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

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

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

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.

Step 5: Implement an InMemoryAcmeUnitOfWork for unit testing.

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:

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

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

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.

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.

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.

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.

UPDATE: I wrote a blog post on this subject that describes this solution in even more detail: Faking your LINQ provider.

I hope this helps.

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

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