title: AspNetCore底层源码剖析(三)IOC
date: 2022-09-21 13:20:01
categories: 后端
tags:
- .NET

介绍

image-20220921132113295

每个 ASP.NET Core 应用程序都有一个根级别的IServiceProvider,除了Root级别的IServiceProvider之外,IServiceProvider还可以创建多个新的ScopeIServiceScope),Scope内有自己的IServiceProvider,当Scope被释放时,它也会释放其中所有的ScopeTransient级别的对象。

关于服务的生命周期范围一共就三种:

  1. Singleton 跟应用的生命周期一致
  2. Scoped 跟容器的生命周期一致
  3. Transient 每次获取都创建新的对象

在 ASP.NET Core 中会为每个请求创建一个新范围。这意味着给定请求的所有 Scoped 服务都是从同一个容器中解析的,因此对于任意一个请求,在任何地方都使用相同的 Scoped 服务实例。在请求结束时,Scope本身和所有已解析的服务一起被释放。每个请求都有一个新的范围,因此 Scoped 服务彼此隔离。

重点:如果有一个服务不是从http请求处理的过程中解析出来的,比如自己加了一个BackgroundService在后台运行,并解析了一些Scoped和Transient级别的服务,那这些服务还会被释放吗?答案是不会,下面我们来看下源码,到底发生了什么

分析源码

HTTP请求

注意,下面给出的源码中省略了大部分与IOC无关的代码逻辑

当一个http请求进来的时候,首先是调用如下方法,开始处理这个请求:

 private async Task ProcessRequests<TContext>(IHttpApplication<TContext> application) where TContext : notnull
 {

     BeginRequestProcessing();
	 // 重点
     var context = application.CreateContext(this);

     await application.ProcessRequestAsync(context);

 }

CreateContext函数用于初始化HttpContext对象,下面是这个函数的完整代码:

public Context CreateContext(IFeatureCollection contextFeatures)
{
    Context? hostContext;
    if (contextFeatures is IHostContextContainer<Context> container)
    {
        hostContext = container.HostContext;
        if (hostContext is null)
        {
            hostContext = new Context();
            container.HostContext = hostContext;
        }
    }
    else
    {
        // Server doesn't support pooling, so create a new Context
        hostContext = new Context();
    }

    HttpContext httpContext;
    if (_defaultHttpContextFactory != null)
    {
        var defaultHttpContext = (DefaultHttpContext?)hostContext.HttpContext;
        if (defaultHttpContext is null)
        {
            httpContext = _defaultHttpContextFactory.Create(contextFeatures);
            hostContext.HttpContext = httpContext;
        }
        else
        {
            _defaultHttpContextFactory.Initialize(defaultHttpContext, contextFeatures);
            httpContext = defaultHttpContext;
        }
    }
    else
    {
        httpContext = _httpContextFactory!.Create(contextFeatures);
        hostContext.HttpContext = httpContext;
    }

    _diagnostics.BeginRequest(httpContext, hostContext);
    return hostContext;
}

这里面最重要的就是_defaultHttpContextFactory这个类,一看名字就知道是个HttpContext的工厂类,其创建代码如下:

public HttpContext Create(IFeatureCollection featureCollection)
{
    if (featureCollection is null)
    {
        throw new ArgumentNullException(nameof(featureCollection));
    }

    var httpContext = new DefaultHttpContext(featureCollection);
    Initialize(httpContext);
    return httpContext;
}

在内部它每次都会根据传进来的featureCollection初始化一个DefaultHttpContext,而这个DefaultHttpContext内部最重要的属性是下面这个:


// 核心
public override IServiceProvider RequestServices
{
    get { return ServiceProvidersFeature.RequestServices; }
    set { ServiceProvidersFeature.RequestServices = value; }
}

private IServiceProvidersFeature ServiceProvidersFeature =>
    _features.Fetch(ref _features.Cache.ServiceProviders, this, _newServiceProvidersFeature)!;

调试时发现_features的更新和清除比较复杂,就没有细研究,不过不影响最终结论

下面是Debug过程中初始化之后这个属性的值:

image-20220921140954453

可以看到并不是根范围的IServiceProvider,当HttpContext初始化完毕后,接下来再根据管道往下调用:

 public Task ProcessRequestAsync(Context context)
 {
     return _application(context.HttpContext!);
 }
       

当单步调试后,我们会进入一个很重要的管道:

public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object?[] args)
{
    // 上面省略大量代码
    var factory = Compile<object>(methodInfo, parameters);

    return context =>
    {
        // 核心
        var serviceProvider = context.RequestServices ?? applicationServices;
        if (serviceProvider == null)
        {
            throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
        }

        return factory(instance, context, serviceProvider);
    };
});
}
        

在这里,var serviceProvider = context.RequestServices ?? applicationServices;这句话决定当前这个处理的请求是使用了Scope级别的IServiceProvider还是Root级别的IServiceProvider,当然,一般情况下context.RequestServices这个值都是存在的,所以我们每个请求的内的IServiceProvider都是在子范围内,最后当请求结束时这个IServiceProvider会销毁,里面解析的Scoped和Transient级别的服务自然也就销毁了

后台服务

当我们在不是http请求进来的其他服务中使用IServiceProvider时,它其实是一个根级别的,下面来写个例子证实一下:


public class TestService : BackgroundService
{
    private readonly ILogger<TestService> _log;
    private readonly IServiceProvider _provider;
    private readonly IServiceScope _scope;

    public TestService(ILogger<TestService> logger,IServiceProvider provider)
    {
        _log = logger;
        _provider = provider;
        _scope=_provider.CreateScope();

    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
        
            //执行任务
            Console.WriteLine($"{DateTime.Now}");
            var t2=_provider.GetService<Test>();
            t2.Tag = "Root";
            // var t3=_scope.ServiceProvider.GetService<Test>();
            t3.Tag = "Root";
            //周期性任务,于上次任务执行完成后,等待50毫秒,执行下一次任务
            await Task.Delay(50);
        }
        
    }

    public override void Dispose()
    {
        Console.WriteLine("Disposed");
        base.Dispose();
    }

    public class Test:IDisposable
    {
        public string Tag { get; set; } = "Default";
        public List<long> data = new List<long>(10000);

        public Test()
        {
            for (int i = 0; i < 10000; i++)
            {
                data.Add(i);
            }
        }
        ~Test()
        {
            Console.WriteLine($"DisConstructor  Test {this.GetHashCode().ToString() } {Tag}");

        }
        public void Dispose()
        {
            Console.WriteLine($"Disposed Test {this.GetHashCode().ToString() } {Tag}");
        }
    }
    
    public class RootTest:IDisposable
    {
        public string Tag { get; set; } = "Default";
        
        ~RootTest()
        {
            Console.WriteLine($"DisConstructor Root Test {this.GetHashCode().ToString() } {Tag}");

        }
        public void Dispose()
        {
            Console.WriteLine($"Disposed Root Test {this.GetHashCode().ToString() } {Tag}");
        }
    }
}

接下来注入这三个服务:

builder.Services.AddTransient(typeof(TestService.Test)); // 瞬时级别
builder.Services.AddHostedService<TestService>(); // 后台服务
builder.Services.AddSingleton<TestService.RootTest>(); // 全局级别

我们需要关注的是var t2=_provider.GetService<Test>();这一行,我们启动程序后单步调试,可以看到进入了下面这段代码:

public bool IsRootScope { get; }

internal ServiceProvider RootProvider { get; }

public object GetService(Type serviceType)
{
    if (_disposed)
    {
        ThrowHelper.ThrowObjectDisposedException();
    }

    return RootProvider.GetService(serviceType, this);
}

很显然,从名字上都可以知道,这个RootProvider其实是一个根级别的IServiceProvider

内存泄漏

当我们在根容器内解析Scoped或者Transient级别的服务时,就会出现内存泄漏,因为除非根容器销毁(等同于程序退出),否则所有内部解析出来的服务都不会被销毁,还是以上一小节的Test类为例子,内部每次都会创建一个包含10000个long类型变量的数组,如果我们像下面这样解析服务:

while (!stoppingToken.IsCancellationRequested)
{

    //执行任务
    Console.WriteLine($"{DateTime.Now}");
    var t3=_scope.ServiceProvider.GetService<Test>();
    t3.Tag = "Root";
    //周期性任务,于上次任务执行完成后,等待50毫秒,执行下一次任务
    await Task.Delay(50);
}

Test在注册的时候用的是AddTransient,同时_scope并没有调用自己的Dispose,那么上面每50毫秒都会解析出一个新Test实例,并且不会被销毁!下面这段运行时的内存分析截图可以证明结论:

image-20220921142454635

同时Test类的析构函数和Dispose函数都没有被调用,这也可以证明结论是正确的

不仅Transient级别是这样的,Scoped级别同理也不会被释放,Singleton除外,Scoped级别的容器被销毁Singleton也不会被销毁,除非根容器销毁

最后总结

  1. 每个http请求都会创建一个IServiceScope,内部有一个自己的IServiceProvider,包括在控制器中注入的也是,可以通过HttpContext.RequestService获取
  2. 如果不是从Http请求进来的,比如后台服务,那么获取到的是根级别的IServiceProvider,我们需要自己创建一个范围容器来解析服务
  3. Singleton级别的服务如果需要Dipose,需要自己手动调用Dispose方法,否则不会被释放(socket连接,文件句柄等),或者使用委托方法注册,这样就由IOC容器来管理了,建议用第二种方式

如果需要用Rider调试,要在设置中勾上下面两项:

image-20220921143551001

否则会出现这个问题:Evaluation is not allowed: The thread is not at a GC-safe point site:stackoverflow.com,导致看不到调试过程中一些中间变量的值

参考

  1. https://github.com/dotnet/aspnetcore/issues/31478
  2. https://andrewlock.net/the-dangers-and-gotchas-of-using-scoped-services-when-configuring-options-in-asp-net-core/
  3. https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/web-host?view=aspnetcore-6.0&viewFallbackFrom=aspnetcore-2.2#scope-validation
  4. https://www.cnblogs.com/wucy/p/13268296.html
  5. https://github.com/dotnet/aspnetcore/issues/2826
  6. https://youtrack.jetbrains.com/issue/RIDER-45516/Cannot-evaluate-debug-assertion-in-net-core-31
  7. https://stackoverflow.com/questions/56032041/how-can-i-access-iservicecollection-from-a-background-thread