gpt4 book ai didi

asp.net-mvc - 如何使用 IOC 从存储库中删除工作单元功能

转载 作者:行者123 更新时间:2023-12-04 02:46:58 25 4
gpt4 key购买 nike

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

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

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

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

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

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

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

如果这是真的,这是否意味着我必须声明 IUnitOfWork接口(interface)和制作 服务类依赖吗?我还能如何让我的服务类管理工作单元?

最佳答案

您不应该尝试提供 AcmeDataContext自己到 EmployeeRepository .我什至会扭转整个事情:

  • 定义允许为 Acme 域创建新工作单元的工厂:
  • 创建摘要 AcmeUnitOfWork将 LINQ to SQL 抽象出来。
  • 创建一个具体工厂,允许创建新的 LINQ to SQL 工作单元。
  • 在您的 DI 配置中注册那个具体工厂。
  • 实现 InMemoryAcmeUnitOfWork用于单元测试。
  • 可选地为您的 IQueryable<T> 上的常见操作实现方便的扩展方法存储库。

  • 更新:我写了一篇关于这个主题的博客文章: Faking your LINQ provider .

    下面是一步一步的例子:

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

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

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

    第 4 步:在您的 DI 配置中注册那个具体工厂。

    您最了解自己如何注册 IAcmeUnitOfWorkFactory返回 LinqToSqlAcmeUnitOfWorkFactory 实例的接口(interface),但它看起来像这样:
    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接口(interface)并让 Controller 使用 EmployeeService直接地。单元测试不需要这个接口(interface),因为你可以在测试过程中替换工作单元,防止 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<T>是一个通用接口(interface),不包含此类方法。我们可以通过像 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);
    }
    }
    }

    有了这些扩展方法,您就可以调用这些 GetById和代码中的其他方法:
    var employee = context.Employees.GetById(employeeId);

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

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

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

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

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

    我希望这有帮助。

    关于asp.net-mvc - 如何使用 IOC 从存储库中删除工作单元功能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/4128640/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com