Search code examples
c#dependency-injectioninversion-of-controldbcontextsolid-principles

Using the DbContext the SOLID way


By depending directly on the DbContext in command- and query handlers, I understand that I am violating the SOLID-principles as of this comment from a StackOverflow 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.

This makes totally sense, but I am unsure how to solve it probably using IoC and DI?

By initial though was to create a IUnitOfWork with a single method which could be used to query the context:

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

Now that I can depend on IUnitOfWork in my query handlers (receiving data), I have solved this part.

Next I needed to look at my commands (modifying data) I could solve the saving of changes to the context with a decorator for my commands:

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

This works fine as well.

First question is: How do I modify objects in the context from my command handlers, as I can not depend directly on the DbContext anymore?

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

As far as I understand I have to create another interfaces for this to work with the SOLID-principles. For example a ICommandEntities which would contain methods like void Create<TEntity>(TEntity entity), update, delete, rollback and even reload. Then depend on this interface in my commands, but am I missing a point here and are we abstracting too deep?

Second questions is: Is this the only way respect the SOLID-principles when working with the DbContext or is this a place where its "okay" to violate the principles?

If needed, I use Simple Injector as my IoC container.


Solution

  • With your EntityFrameworkUnitOfWork, you are still violating the following part:

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

    Your object graphs should be stateless and state should be passed through the object graph at runtime. Your EntityFrameworkUnitOfWork should look as follows:

    internal sealed class EntityFrameworkUnitOfWork : IUnitOfWork
    {
        private readonly Func<DbContext> contextProvider;
    
        public EntityFrameworkUnitOfWork(Func<DbContext> contextProvider)
        {
            this.contextProvider = contextProvider;
        }
    
        // etc
    }
    

    Having an abstraction with a single IQueryable<T> Set<T>() methods works great for queries. It makes it childs play to add aditional permission based filtering to the IQueryable<T> later on, without having to change any line of code in your query handlers.

    Do note though, that an abstraction that exposes IQueryable<T> (as this IUnitOfWork) abstraction does, IS still a violation of the SOLID principles. This is because IQueryable<T> is a leaky abstraction, which basically means a Dependency Inversion Principle Violation. IQueryable is a leaky abstraction, because a LINQ query that runs on EF will not automatically run on NHibernate or vice versa. But at least we're a little bit more SOLID in this case, because it prevents us from having to do sweeping changes through our query handlers in case we need to apply permission filtering or other kinds of filtering.

    Trying to completely abstract away the O/RM from your query handlers is useless, and will only cause you to move the LINQ queries to yet another layer, or will make you revert to SQL queries or Stored Procedures. But again, abstracting the O/RM is not the issue here, being able to apply cross-cutting concerns at the right place in the application is the issue.

    In the end, if you migrate to NHibernate, you will most likely have to rewrite some of your query handlers. Your integration tests will in that case will directly tell you which handlers need to be changed.

    But enough said about query handlers; let's talk about command handlers. They need to do much more work with the DbContext. So much that you in the end might consider letting command handlers depend directly on DbContext. But I still prefer them not to do so and let my command handlers depend on SOLID abstraction only. How this looks like might vary from application to application, but since command handlers usually are really focused, and change just a few entities, I prefer things like:

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

    In the systems I work we hardly ever delete anything. So this prevents us from having a Delete method on that IRepository<TEntity>. When having both GetById and Create on that interface, changes however are already high that you will be violating the Interface Segregation Principle, and be very careful not to add more methods. You might even want to split them. If you see that your command handlers get big, with many dependencies, you might want to split them up in Aggregate Services, or if the result is worse, you could consider returning repositories from your IUnitOfWork, but you will have to be careful not losing the possibility to add cross-cutting concerns.

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

    This is absolutely not the only way. I would say the most pleasant way is to apply Domain-Driven Design and work with aggregate roots. In the background, you might have a O/RM that persists a complete aggregate for you, completely hidden away from the command handler and the entity itself. Even more pleasant is it if you can just completely serialize this object graph to JSON and store it as blob in your database. This completely removes the need to have an O/RM tool in the first place, but this practically means you are having a document database. You'd better use a real document database, because otherwise it will become almost impossible to query that data.

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

    No matter what you do, you will have to violate the SOLID principles somewhere. It's up to you where it's good to violate them and where it's good to stick with them.