以 SOLID 方式使用 DbContext

Using the DbContext the SOLID way

通过直接依赖命令和查询处理程序中的 DbContext,我知道我违反了 SOLID-principles as of this comment from a Whosebug user:

The DbContext is a bag with request-specific runtime data and injecting runtime data into constructors causes trouble. Letting your code having a direct dependency of on DbContext causes your code to violate DIP and ISP and this makes hard to maintain.

这完全有道理,但我不确定如何使用 IoC 和 DI 来解决它?

尽管最初是创建一个 IUnitOfWork 具有可用于查询上下文的单一方法:

public interface IUnitOfWork
{
    IQueryable<T> Set<T>() where T : Entity;
}

internal sealed class EntityFrameworkUnitOfWork : IUnitOfWork
{
    private readonly DbContext _context;

    public EntityFrameworkUnitOfWork(DbContext context)
    {
        _context = context;
    }

    public IQueryable<T> Set<T>() where T : Entity
    {
        return _context.Set<T>();
    }
} 

现在我可以在我的查询处理程序(接收数据)中依赖 IUnitOfWork,我已经解决了这部分。

接下来我需要查看我的命令(修改数据)我可以使用装饰器为我的命令解决将更改保存到上下文的问题:

internal sealed class TransactionCommandHandler<TCommand> : IHandleCommand<TCommand> where TCommand : ICommand
{
    private readonly DbContext _context;
    private readonly Func<IHandleCommand<TCommand>> _handlerFactory;

    public TransactionCommandHandler(DbContext context, Func<IHandleCommand<TCommand>> handlerFactory)
    {
        _context = context;
        _handlerFactory = handlerFactory;
    }

    public void Handle(TCommand command)
    {
        _handlerFactory().Handle(command);
        _context.SaveChanges();
    }
}

这也很好用。

第一个问题是:如何从我的命令处理程序修改上下文中的对象,因为我不能再直接依赖于 DbContext?

喜欢:context.Set<TEntity>().Add(entity);

据我所知,我必须为此创建另一个接口才能使用 SOLID 原则。例如 ICommandEntities 将包含 void Create<TEntity>(TEntity entity)、更新、删除、回滚甚至重新加载等方法。然后在我的命令中依赖这个接口,但是我在这里是否漏掉了一点,我们是不是抽象得太深了?

第二个问题是:这是在使用 DbContext 时遵守 SOLID 原则的唯一方法,还是这是 "okay" 违反原则的地方?

如果需要,我使用 Simple Injector 作为我的 IoC 容器。

你的EntityFrameworkUnitOfWork,你仍然违反了以下部分:

The DbContext is a bag with request-specific runtime data and injecting runtime data into constructors causes trouble

您的对象图应该是无状态的,状态应该在 运行 时通过对象图传递。您的 EntityFrameworkUnitOfWork 应如下所示:

internal sealed class EntityFrameworkUnitOfWork : IUnitOfWork
{
    private readonly Func<DbContext> contextProvider;

    public EntityFrameworkUnitOfWork(Func<DbContext> contextProvider)
    {
        this.contextProvider = contextProvider;
    }

    // etc
}

使用单个 IQueryable<T> Set<T>() 方法进行抽象非常适合查询。稍后向 IQueryable<T> 添加额外的基于权限的过滤变得很容易,而无需更改查询处理程序中的任何代码行。

但请注意,公开 IQueryable<T>(如此 IUnitOfWork)抽象的抽象仍然违反 SOLID 原则。这是因为 IQueryable<T> 是一个有漏洞的抽象,这基本上意味着违反了依赖倒置原则。 IQueryable 是一个有漏洞的抽象,因为在 EF 上 运行s 的 LINQ 查询不会在 NHibernate 上自动 运行,反之亦然。但至少在这种情况下我们更加 SOLID 一点,因为它可以防止我们在需要应用权限过滤或其他类型的过滤时必须通过查询处理程序进行彻底的更改。

尝试从查询处理程序中完全抽象掉 O/RM 是没有用的,只会导致您将 LINQ 查询移动到另一层,或者会让您恢复到 SQL 查询或存储过程。但是同样,抽象 O/RM 不是这里的问题,能够在应用程序的正确位置应用横切关注点才是问题。

最后,如果您迁移到 NHibernate,您很可能不得不重写一些查询处理程序。在这种情况下,您的集成测试将直接告诉您需要更改哪些处理程序。

但是关于查询处理程序已经说得够多了;让我们谈谈命令处理程序。他们需要用 DbContext 做更多的工作。这么多,你最终可能会考虑让命令处理程序直接依赖于 DbContext。但我仍然希望他们不要这样做,让我的命令处理程序仅依赖于 SOLID 抽象。这看起来可能因应用程序而异,但由于命令处理程序通常非常专注,并且只更改几个实体,所以我更喜欢这样的东西:

interface IRepository<TEntity> {
    TEntity GetById(Guid id);
    // Creates an entity that gets saved when the transaction is committed,
    // optionally using an id supplied by the client.
    TEntity Create(Guid? id = null);
}

在我工作的系统中,我们几乎从不删除任何东西。所以这会阻止我们在 IRepository<TEntity> 上使用 Delete 方法。当在该接口上同时具有 GetByIdCreate 时,更改已经很高,您将违反接口隔离原则,并且要非常小心不要添加更多方法。您甚至可能想拆分它们。如果你发现你的命令处理程序变得很大,有很多依赖项,你可能想将它们拆分成 Aggregate Services,或者如果结果更糟,你可以考虑从你的 IUnitOfWork 返回存储库,但是你必须小心不要失去添加横切关注点的可能性。

Is this the only way respect the SOLID-principles when working with the DbContext

这绝对不是唯一的方法。我想说最愉快的方法是应用领域驱动设计并使用聚合根。在后台,您可能有一个 O/RM 为您保留一个完整的聚合,完全隐藏在命令处理程序和实体本身之外。如果您可以将此对象图完全序列化为 JSON 并将其作为 blob 存储在数据库中,那就更令人愉快了。这完全消除了首先需要 O/RM 工具的需要,但这实际上意味着您拥有一个文档数据库。您最好使用真实的文档数据库,否则几乎无法查询该数据。

or is this a place where its "okay" to violate the principles?

无论你做什么,你都必须在某处违反 SOLID 原则。哪些地方可以违反,哪些地方可以坚持,完全取决于您。