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

r7s23pms  于 2023-01-10  发布在  .NET
关注(0)|答案(2)|浏览(296)

我最近把一个.Net Framework 4.7.2 MVC项目从Unity移到了Microsoft.Extensions.DependencyInjection,因为Unity已经被弃用了。这个切换看起来很简单,主要的变化是需要创建一个自定义的DependencyResolver,因为这以前是由Unity处理的。
现在,这些更改已投入生产,我开始注意到一些严重的内存问题。获取内存使用情况转储显示,内存中最大的项目是Microsoft.Extensions.DependencyInjection的ServiceProvider,其中包含数千个尚未处理的控制器。
依赖关系解析程序如下所示:

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文章实现了这个:How do I inject 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;
}

由于我们仍然在使用.Net framework,看起来控制器没有自动注册,所以我不得不将它们显式注册为Transient。
我的问题是,我是不是完全忽略了什么?我希望迁移到Microsoft DI包时,它的工作方式与新版本的.Net相同,但我现在觉得迁移到一个完全不同的IoC框架(如Autofaq)来解决这些内存问题更容易。

pexxcrt2

pexxcrt21#

你的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实现中仍然存在一个bug,因为您没有处理IServiceScope方法。虽然Web API控制器会自动被处理,但来自容器的其他服务不会。
注意:我想说MVC中的DI实现有点缺陷,但这可能是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
   
        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也会释放控制器。然而,在正常情况下,这应该不会导致任何问题。

jjjwad0x

jjjwad0x2#

我本可以尝试Steven的答案,但对于使用Unity时需要很少输入的东西来说,这是非常复杂的。为了简单起见,我切换到使用AutoFac,在最小的工作量内,内存问题已经消失。

相关问题