如何使用IOC从存储库中删除工作单元功能

Ed *_*d I 10 asp.net-mvc domain-driven-design dependency-injection inversion-of-control linq-to-sql

我有一个使用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
Run Code Online (Sandbox Code Playgroud)

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

问题是IEmployeeRepository暴露SubmitChanges方法,因为服务类没有DataContext引用.

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

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

Ste*_*ven 24

你不应该试图为AcmeDataContext自己提供自己EmployeeRepository.我甚至会把整个事情都转过来:

  1. 定义允许为Acme域创建新工作单元的工厂:
  2. 创建一个摘要AcmeUnitOfWork,摘要LINQ to SQL.
  3. 创建一个可以允许创建新的LINQ to SQL工作单元的具体工厂.
  4. 在DI配置中注册该混凝土工厂.
  5. 实施InMemoryAcmeUnitOfWork单元测试.
  6. (可选)为IQueryable<T>存储库中的常见操作实现方便的扩展方法.

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

以下是一些示例:

警告:这将是一个loooong帖子.

第1步:定义工厂:

public interface IAcmeUnitOfWorkFactory
{
    AcmeUnitOfWork CreateNew();
}
Run Code Online (Sandbox Code Playgroud)

创建工厂很重要,因为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) { }
}
Run Code Online (Sandbox Code Playgroud)

关于这个抽象类有一些有趣的事情需要注意.工作单元控制并创建存储库.存储库基本上是实现的东西IQueryable<T>.存储库实现返回特定存储库的属性.这可以防止用户调用uow.GetRepository<Employee>(),这会创建一个非常接近LINQ to SQL或Entity Framework的模型.

工作单元实施InsertDelete操作.在LINQ to SQL中,这些操作放在Table<T>类上,但是当您尝试以这种方式实现它时,它将阻止您将LINQ抽象为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);
    }
}
Run Code Online (Sandbox Code Playgroud)

工厂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(); }
}
Run Code Online (Sandbox Code Playgroud)

第4步:在DI配置中注册具体工厂.

你知道如何注册IAcmeUnitOfWorkFactory接口以返回一个实例LinqToSqlAcmeUnitOfWorkFactory,但它看起来像这样:

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

现在您可以更改依赖关系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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,您甚至可以删除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)
    { 
    }
}
Run Code Online (Sandbox Code Playgroud)

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

[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));
}
Run Code Online (Sandbox Code Playgroud)

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

预计存储库有方便的方法,如GetByIdGetByLastName.当然IQueryable<T>是通用接口,不包含这样的方法.我们可以通过类似的调用来混乱我们的代码context.Employees.Single(e => 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用这些扩展方法,它允许您GetById从代码中调用这些和其他方法:

var employee = context.Employees.GetById(employeeId);
Run Code Online (Sandbox Code Playgroud)

关于这段代码(我在生产中使用它)最好的事情就是-once到位 - 它可以避免编写大量代码进行单元测试.在将新实体添加到系统时,您会发现自己将AcmeRepositoryExtensions类和属性添加到类中AcmeUnitOfWork,但是您不需要为生产或测试创建新的存储库类.

这个模型当然有一些短缺.最重要的可能是LINQ to SQL并不完全抽象,因为你仍然使用LINQ to SQL生成的实体.这些实体包含EntitySet<T>特定于LINQ to SQL的属性.我没有发现它们妨碍了正确的单元测试,所以对我来说这不是问题.如果需要,可以始终将POCO对象与LINQ to SQL一起使用.

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

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

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

我希望这有帮助.