Microsoft.Extensions.DependencyInjection对.NET Framework引起内存泄漏。

5

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

现在这些更改已经在生产环境上实施,我开始注意到一些严重的内存问题。获取内存使用情况的转储显示,Microsoft.Extensions.DependencyInjection中的ServiceProvider是内存中占用最大的项,其中包含数千个未被处理的控制器。

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

我已经根据我读过的stackoverflow文章进行了实现:如何在使用Microsoft.Extensions.DependencyInjection的.NET框架中注入WebAPI依赖项? 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;
}

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

我的问题是,我是否完全错过了什么? 我希望迈向Microsoft DI包的原因是它会像较新版本的.Net一样运行,但是我现在感到更容易转移到完全不同的IoC框架(例如Autofaq),以解决这些内存问题。


1
@LexLi:您提出的方案不容易实现。问题是关于ASP.NET(经典版)而不是ASP.NET Core。Autofac可以通过MS.DI提供的抽象轻松集成到ASP.NET Core中,但是与ASP.NET的集成是使用Autofac提供的集成包完成的;在这种情况下,它们不使用MS.DI。这肯定是可行的,但仍需要手写IDependencyResolver实现。 - Steven
另一个选择当然是选择一个具有内置支持ASP.NET MVC和Web API的DI容器,实际上除了MS.DI之外都可以。 - Steven
2个回答

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

在此之后,最好为其创建一个单独的类,作为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);
}

为了完整性,这是你的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);
    }
}

最后一点,两种依赖解析器的实现仍然会导致控制器类型被释放两次,这是因为MVC和Web API都会释放控制器。然而,在正常情况下,这不应该造成任何问题。

IDependencyScope来自哪里? - Kristóf Tóth
System.Web.Http.Dependencies - Steven
当新的请求在之前的请求完成之前到达时会发生什么情况?Application_BeginRequest钩子会在HttpContext.Current上设置一个新的范围。那么,这是否意味着从第一个请求中对MsDiMvcDependencyResolver.GetService()的任何额外调用都会使用第二个请求的范围?如果是这样,这会有问题吗?同样地,当第一个请求结束时,Application_EndRequest钩子被调用,当前的范围将被释放,所以我假设在第一个请求之后的任何活动请求中,范围将被释放。这两种情况会引起问题吗? - undefined
@br3nt,如果HttpContext.Current的功能如你所描述的那样,那肯定会引起问题。幸运的是,它并不是这样工作的。微软的文档说明了HttpContext.Current是用来获取或设置当前HTTP请求的HttpContext对象,我的经验是你可以依赖这个描述和行为。许多工具(如DI容器)依赖于HttpContext.Current的范围仅限于单个请求。 - undefined
好的,谢谢你提供的信息和链接。顺便说一句,实施这个解决方案完全消除了我们的内存泄漏问题。再次感谢你的回答 :) - undefined

0

我本可以尝试Steven的答案,但对于在使用Unity时几乎不需要输入就能正常工作的东西来说,那个方法过于复杂。为了简单起见,我转而使用AutoFac进行最小限度的工作,内存问题已经消失。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接