.NET Framework 的 Microsoft.Extensions.DependencyInjection 导致内存泄漏

use*_*922 5 .net c# dependency-injection

由于 Unity 已被弃用,我最近将 .Net Framework 4.7.2 MVC 项目从 Unity 移至 Microsoft.Extensions.DependencyInjection。切换似乎很简单,主要变化是需要创建自定义DependencyResolver,因为这之前是由 Unity 处理的。

现在这些更改已在生产中进行,我开始注意到一些严重的内存问题。获取内存使用情况的转储显示内存中最大的项目来自ServiceProviderMicrosoft.Extensions.DependencyInjection,其中包含数千个尚未处理的控制器。

DependencyResolver 看起来像:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我已经根据我阅读的 stackoverflow 文章实现了这一点:How do I insert dependency in webapi in .net Framework using Microsoft.Extensions.DependencyInjection?

Startup 类如下所示:

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;
}
Run Code Online (Sandbox Code Playgroud)

由于我们仍在使用 .Net 框架,控制器似乎没有自动注册,因此我必须将它们显式注册为 Transient。

我的问题是,我完全错过了什么吗?我对转向 Microsoft DI 包的希望是它能以与新版本的 .Net 中相同的方式运行,但我觉得转向完全不同的 IoC 框架会更容易,例如Autofaq,解决这些内存问题。

Ste*_*ven 5

你的MicrosoftDefaultDependencyResolver有缺陷。

如果您查看 的定义System.Web.Mvc.IDependencyResolver,您会发现没有BeginScope方法。MVC 不会自动启动作用域。这意味着所有 MVC 控制器都是从根作用域/容器解析的,并且从根容器解析的任何作用域或临时一次性依赖项都将被永远引用。

您可能已经注意到,只有 MVC 控制器会被引用;Web API 控制器不存在这个问题。这是因为System.Web.Http.Dependencies.IDependencyResolver定义实际上包含一个BeginScope方法,并且 ASP.NET Web API 在每个请求开始时主动调用该方法。这意味着 Web API 控制器是从特定于请求的 解析的IServiceScope,并且此范围将被自动垃圾收集。

尽管如此,在您的 Web API 实现中仍然存在一个错误,因为您没有处理该IServiceScope实例。尽管 Web API 控制器会自动被丢弃,但来自容器的其他服务不会。

旁注:我想说 ASP.NET MVC 框架本身的 DI 实现有点缺陷,但这可能就是 Web API 团队IDependencyResolver首先创建新界面的原因。

因此,MVC 的解决方案是确保(以某种方式)IServiceScope在每个请求上创建 MVC 控制器,从该范围解析,以及在请求结束时处理该范围。

有多种方法可以做到这一点,但最简单的可能是挂钩事件Application。例如:

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
   
        // Configure and build your container here
        Container = BuildServiceProvider();

        System.Web.Mvc.DependencyResolver.SetResolver(
            new MsDiMvcDependencyResolver(Container));
    }
}
Run Code Online (Sandbox Code Playgroud)

之后,最好为该功能创建一个单独的类作为System.Web.Mvc.IDependencyResolver. 此实现可以利用存储的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);
}
Run Code Online (Sandbox Code Playgroud)

为了完整起见,您的 Web API 依赖项解析器可能应如下所示:

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

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

    public System.Web.Http.Dependencies.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
        : System.Web.Http.Dependencies.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);
    }
}
Run Code Online (Sandbox Code Playgroud)

最后要注意的是,两个依赖解析器实现仍然会导致控制器类型被处置两次,这是因为 MVC 和 Web API 也会处置控制器。然而,在正常情况下,这应该不会造成任何问题。


use*_*922 0

我本可以尝试史蒂文的答案,但它对于我们使用 Unity 时几乎不需要输入的东西来说非常复杂。为简单起见,我在最少的工作量内改用 AutoFac,内存问题就消失了。