Implement Microsoft DI container in ASP.NET MVC 5

By Mirek on (tags: ASP.NET MVC, dependency injection, mvc 5, categories: architecture, code, web)

Dependency injection is very important mechanism in web applications. There are many third party packages that serves dependency injection in ASP.NET MVC 5, like Autofac or Unity. However, there is also Microsoft solution, which is already part of ASP.NET Core framework. In this post We’ll implement it in ASP.NET MVC 5 application.

Sample ASP.NET MVC 5 web application will be targeting .net 4.8.

Now acording to some resources we can find on internet here or here the thing seems to be fairly easy.

We can acomplish it by following these steps:

1. Install Microsoft.Extensions.DependencyInjection package

2. Create a DefaultDependencyResolver class that implements IDependencyResolver

public class DefaultDependencyResolver : IDependencyResolver
{
     protected IServiceProvider serviceProvider;

     public DefaultDependencyResolver(IServiceProvider serviceProvider)
     {
         this.serviceProvider = serviceProvider;
     }

     public object GetService(Type serviceType)
     {
         return this.serviceProvider.GetService(serviceType);
     }

     public IEnumerable<object> GetServices(Type serviceType)
     {
         return this.serviceProvider.GetServices(serviceType);
     } }

Here we pass the IServiceProvider root which is used to resolve services from the container.

3. Then we register our services and set the custom dependency resolver in the framework

public void ConfigureServices()
{
     var services = new ServiceCollection();

     services.AddScoped<HomeController>();
     services.AddScoped<MyService>();

     var resolver = new DefaultDependencyResolver(services.BuildServiceProvider());
     DependencyResolver.SetResolver(resolver); }

And that’s it. We have working dependency injection using Microsoft.Extensions.DependencyInjection implemented in our web application right ?

Yes, except it does not work.


Well, it works, but not as we expect it to work. In particular everything is resolved from the root container and stays in memory forever. To be more precise, if we’ve deployed the application on production IIS, then every service and controller resolved during the request, stays in memory until the application pool is recycled. This behavior exhaust the available resources very soon.

As it turned out there is no build in mechanism neither in ASP.NET MVC framework nor in Microsoft.Extensions.DependencyInjection, that would create new service scope for each new http request and dispose it when requests ends. We need to implement it by our own and a quite lean approach to handle it can be found here.
Here is how it works:

1. We create new ServiceScope from the root dependency container and maintain it along each single http request. We use HttpContext.Items collection/cache for that.

2. We create and hookup custom http module and in the EndRequest event we dispose the IServiceScope associated with current http request

The implementation is fairly simple.
Here is the new ScopedDependencyResolver class

public class ScopedDependencyResolver : IDependencyResolver
{
     private readonly IServiceProvider _serviceProviderRoot;

     public ScopedDependencyResolver(IServiceProvider serviceProviderRoot)
     {
         _serviceProviderRoot = serviceProviderRoot;
     }

     public object GetService(Type serviceType)
     {
         return resolveServiceProvider().GetService(serviceType);
     }

     public IEnumerable<object> GetServices(Type serviceType)
     {
         return resolveServiceProvider().GetServices(serviceType);
     }

     private IServiceProvider resolveServiceProvider()
     {
         if (HttpContext.Current == null)
         {
             Debug.WriteLine("Resolving services from root container !");
             return _serviceProviderRoot;
         }
         var scope = HttpContext.Current.Items[typeof(IServiceScope)] as IServiceScope;
         if (scope == null)
         {
             scope = _serviceProviderRoot.CreateScope();
             HttpContext.Current.Items[typeof(IServiceScope)] = scope;
         }
         return scope.ServiceProvider;
     } }

As you can see, we try to retreive service scope from the request context and if not found we create new one.

Then we create the http module just to catch the moment the request ends and dispose all created services by disposing the service scope

public class ScopedDependencyHttpModule : IHttpModule
{
     public void Init(HttpApplication context)
     {
         context.EndRequest += Context_EndRequest;
     }

     private void Context_EndRequest(object sender, EventArgs e)
     {
         var context = ((HttpApplication)sender).Context;
         if (context.Items[typeof(IServiceScope)] is IServiceScope scope)
         {
             scope.Dispose();
             context.Items.Remove(typeof(IServiceScope));
         }
     }

     public void Dispose() { } }

then we need to register the module in our mvc application and configure the resolver

[assembly: PreApplicationStartMethod(typeof(MvcApplication), "InitModules")]
 
namespace MyAspNetMVC5App
{
     public class MvcApplication : System.Web.HttpApplication
     {
         public static void InitModules()
         {
             RegisterModule(typeof(ScopedDependencyHttpModule));
         }

public void ConfigureServices()
         {
            var services = new ServiceCollection();
            /* register your scoped services here*/
            var rootProvider = services.BuildServiceProvider(true);
            var resolver = new ScopedDependencyResolver(rootProvider);
            DependencyResolver.SetResolver(resolver);
         }
     } }

Now it works as expected. Additionally we have passed true flag to the BuildServiceProvider(true) method so the exception is being thrown every time the service is resolved from the root container.

Sample solution is available to download below.

Another approach (described in this post) is to define custom ControllerFactory and handle the service scope creation and disposal in GetControllerInstance() and ReleaseController() methods.

Download attachement - 23 KB