Microsoft.Extensions.DependencyInjection for .NET Framework causing memory leak

Viewed 89

I have recently moved a .Net Framework 4.7.2 MVC project from Unity to Microsoft.Extensions.DependencyInjection due to Unity being deprecated. The switch-over seemed simple enough with the main change being the need to create a custom DependencyResolver, as this was previously handled by Unity.

Now these changes are on Production and I am starting to notice some severe memory issues. Getting a dump of the memory usage shows that largest item in memory is the ServiceProvider from Microsoft.Extensions.DependencyInjection which contains thousands of Controllers that have not been disposed of.

The DependencyResolver looks like:

public class MicrosoftDefaultDependencyResolver
    : System.Web.Mvc.IDependencyResolver
    , System.Web.Http.Dependencies.IDependencyResolver
{
    protected IServiceProvider serviceProvider;

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

    public IDependencyScope BeginScope()
    {
        return new MicrosoftDefaultDependencyResolver(
            this.serviceProvider.CreateScope().ServiceProvider);
    }

    public void Dispose()
    {
    }

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

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

I have implemented this based off of a stackoverflow article that I read: How do I inject dependency in webapi in .net framework using Microsoft.Extensions.DependencyInjection?

The Startup class looks like:

public void Configuration(IAppBuilder app)
{
    // Set MVC Resolver
    MicrosoftDefaultDependencyResolver resolver = GetDependencyResolver();

    DependencyResolver.SetResolver(resolver);

    // Any connection or hub wire up and configuration should go here
    app.MapAzureSignalR(GetType().FullName);

    // Turn tracing on programmatically
    GlobalHost.TraceManager.Switch.Level = SourceLevels.Error;
}

As we are still using .Net framework, it seemed like the Controllers were not automatically being registered, so I instead had to register them explicitly as Transient.

My question is, am I completely missing something? My hope for moving towards the Microsoft DI package was that it would function in the same way that it does in the newer version of .Net, but I am at the point where it feels easier to move to a completely different IoC framework, such as Autofaq, to resolve these memory issues.

1 Answers

Your MicrosoftDefaultDependencyResolver is flawed.

If you take a look at the definition of System.Web.Mvc.IDependencyResolver, you'll notice that there is no BeginScope method. MVC will not start automatically start a scope. This means that all your MVC controllers are resolved from the root scope/container. Any scoped or transient disposable dependency that gets resolved from the root container will be referenced forever.

As you probably noticed, only your MVC controllers stay referenced; Web API controllers don't have this problem. This is because the System.Web.Http.Dependencies.IDependencyResolver definition actually contains a BeginScope method and ASP.NET Web API actively calls this method at the begin of each request. This means that Web API controllers are resolved from a request-specific IServiceScope, and this scope will be automatically garbage collected.

Still, in your Web API implementation there is a bug, because you don't dispose of the IServiceScope method. Although Web API controllers automatically get disposed of, other services that come from the container won't.

NOTE: I would say the DI implementation in MVC is a bit flawed, but that's likely the reason why the Web API team created a new IDependencyResolver interface in the first place.

The solution for MVC, therefore, is to ensure (somehow) for the IServiceScope to be created on each request, MVC controllers to get resolved from that scope, and the scope to be disposed of at the end of the request.

There are several ways to do this, but probably the easiest is to hook into the Application events. For instance:

public class WebApiApplication : System.Web.HttpApplication
{
    private static IServiceProvider Container;

    // Store scope in HttpContext.Items
    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        HttpContext.Current.Items[typeof(IServiceScope)] =
            Container.CreateScope();
    }

    // Dispose the scope
    protected void Application_EndRequest(object sender, EventArgs e)
    {
        var scope =
            HttpContext.Current.Items[typeof(IServiceScope)] as IServiceScope;

        scope?.Dispose();
    }

    protected void Application_Start()
    {
        // All your usual MVC stuff here
   
        Container = BuildServiceProvider();

        System.Web.Mvc.DependencyResolver.SetResolver(
            new MsDiMvcDependencyResolver(Container));
    }
}

After this, it's best to create a separate class for that functions as adapter for System.Web.Mvc.IDependencyResolver. This implementation can make use of the stored IServiceScope:

public class MsDiMvcDependencyResolver : System.Web.Mvc.IDependencyResolver
{
    private readonly IServiceProvider root;

    public MsDiMvcDependencyResolver(IServiceProvider root) => this.root = root;

    // Pulls the scope from the HttpContext and falls back to the root container.
    private IServiceProvider Current
    {
        get
        {
            var context = HttpContext.Current;

            if (context is null) return this.root;

            var scope = context.Items[typeof(IServiceScope)] as IServiceScope;

            if (scope is null) return this.root;

            return scope.ServiceProvider;
        }
    }

    public object GetService(Type serviceType) =>
        this.Current.GetService(serviceType);

    public IEnumerable<object> GetServices(Type serviceType) =>
        this.Current.GetServices(serviceType);
}

For completeness, this is what your Web API dependency resolver should probably look like:

public class MsDiWebAPiDependencyResolver
    : System.Web.Http.Dependencies.IDependencyResolver
{
    private readonly IServiceProvider root;

    public MsDiWebAPiDependencyResolver(IServiceProvider root) => this.root = root;

    public IDependencyScope BeginScope() =>
            new DependencyScope(this.root.CreateScope());

    public void Dispose() => (this.root as IDisposable)?.Dispose();

    public object GetService(Type serviceType) => this.root.GetService(serviceType);

    public IEnumerable<object> GetServices(Type serviceType) =>
            this.root.GetServices(serviceType);

    private sealed class DependencyScope : IDependencyScope
    {
        private readonly IServiceScope scope;

        public DependencyScope(IServiceScope scope) => this.scope = scope;

        public void Dispose() => this.scope.Dispose();

        public object GetService(Type serviceType) =>
            this.scope.ServiceProvider.GetService(serviceType);

        public IEnumerable<object> GetServices(Type serviceType) =>
            this.scope.ServiceProvider.GetServices(serviceType);
    }
}

Last note, both dependency resolver implementations still cause controller types to get disposed of twice, which happens because both MVC and Web API also dispose of controllers. This should, however, under normal conditions, not cause any problem.

Related