Search code examples
c#asp.net-web-apiinversion-of-controlioc-containersimple-injector

Error registering generic UnitOfWork<> with IoC


I have a generic implementation of UnitOfWork for my DbContexts in my application, that works like below:

 public class UnitOfWork<TContext> : IDisposable, IUnitOfWork<TContext>
        where TContext : DbContext
 {
     private readonly TContext _context;

     [..UoW default implementations..]

     public void Dispose()
     {
        _context.Dispose();
     }
  }

And registration...:

public static class SimpleInjectorWebApiInitializer
    {
        /// <summary>Initialize the container and register it as Web API Dependency Resolver.</summary>
        public static void Initialize()
        {
            var container = new Container();
            container.Options.DefaultScopedLifestyle = new WebApiRequestLifestyle();

            InitializeContainer(container);

            container.RegisterWebApiControllers(GlobalConfiguration.Configuration);

            container.Verify();

            GlobalConfiguration.Configuration.DependencyResolver =
                new SimpleInjectorWebApiDependencyResolver(container);
        }

        private static void InitializeContainer(Container container)
        {

            // For instance:
            container.Register(typeof(IUnitOfWork<>), typeof(UnitOfWork<>), Lifestyle.Scoped);
            container.RegisterWebApiRequest<IBankRepository, BankRepository>();
            container.RegisterWebApiRequest<IBankService, BankService>();

        }
    }

When I try to register this type and other services, I'm getting these warnings:

-[Lifestyle Mismatch] UnitOfWork<CustomerContext> (Web API Request) depends on CustomerContext (Transient).

-[Disposable Transient Component] CustomerContext is registered as transient, but implements IDisposable.

My application architecture uses a default WebAPI implementation:

[RoutePrefix("customer/banks")]
    public class BankController : ApiController
    {
        private readonly IBankService _bankService;

        public BankController(IBankService bankService)
        {
            _bankService = bankService;
        }

        [Route]
        public IEnumerable<BancoModel> Get()
        {
            var result = _bankService.GetBanks();
            [...mappings and return...]            
        }
    }

I've already tried to supress these warnings:

Registration registration = container.GetRegistration(typeof(IUnitOfWork<>)).Registration;
            registration.SuppressDiagnosticWarning(DiagnosticType.DisposableTransientComponent, "Dispose called by application code");

..but I receive an InvalidOperationException.

Any ideas on how to register this?


Solution

  • The problem you face is that you didn't add a Registration for CustomerContext.

    Because CustomerContext has a default constructor, Simple Injector is able to autowire this dependency for you. But when Simple Injector makes a registration for this at the first call to GetInstance, the container has no other choice than register this servicetype using the default lifestyle. The default lifestyle for Simple Injector however is: Transient.

    In other words the diagnostic warnings you're receiving are correct, because CustomerContext is in fact registered as Transient.

    To fix this add a registration for the CustomerContext as:

    container.Register<CustomerContext>(Lifestyle.Scoped);
    

    You can remove the configuration of the suppression of the warnings, because these 'warnings' are actual problems!

    There is actually a third diagnostic warning available at this point. You can read about the Container-Registered Types warning here. This diagnostic warning is informing you that there are types needed in your object graph that aren't part of your configuration. This is an informational warning and therefore not thrown as exception when verifying the container. The reason for the informational category is that for a simple class that can be Transient this is no problem at all and there are valid use cases where you aren't able to register all classes at compile time.

    But it is good practice to register all your needed types in the container, because the container is in this case fully aware of all classes and thereby able to do the best job he can to Verify your configuration.

    Second reason why this is good practice is because every DI container has a different 'default lifestyle'. Simple Injector defaults to Transient, while there are others defaulting to Singleton. If you would swap your DI container later on, you could end up with an application which fails at runtime because some component suddenly became Singleton.

    The reason you received an InvalidOperationException on suppressing the warnings is caused by the container not (yet) being able of suppressing warnings on open generic types. Which is in this case a good thing, because you had to come for help!

    If you would have suppressed these warnings successfully you would have found yourself debugging strange problems pretty soon.

    Because what would have happened if your DbContext was Transient?

    Every service/commandhandler/queryhandler/etc. that has a dependency on this DbContext will get a different one!! This could possibly result in this scenario:

    • you fetch a record from the database, using a queryhandler/repository or whatever service
    • you'll update the entity with some new information in yet another service/commandhandler, which has its own dependency on DbContext, and thus a different instance
    • you call DbContext.SaveChanges() in possibly another class, again with its own instance of the DbContext, a SaveChangesCommandHandlerDecorator for examlple. Because this DbContext didn't track any changes the database isn't updated.

    For the sake of completeness:

    The warnings are given on CustomerContext and UnitOfWork<CustomerContext>, so you should have suppressed the warnings on the registrations of this servicetypes. Fortunately you did not have a registration for CustomerContext and you had to do a little more work for suppressing the warnings on the UnitOfwork<CustomerContext!

    The documentation of the warnings you received is here and here