Finally, I understand how the scope of singleton, transient and scoped is implemented

1: Background
1. Tell a story
A few days ago, a friend asked me to have time to analyze why classes injected into ServiceCollection in aspnetcore can be Singleton, Transient and Scoped. It's very interesting. Let's talk about this topic. Since ServiceCollection in core and the popular DDD mode, I believe many friends rarely see new in their projects, At least spring did it more than ten years ago.

2: Basic usage of Singleton,Transient,Scoped
Before analyzing the source code, I think it is necessary to introduce their playing methods. To facilitate the demonstration, I will create a new webapi project and define an interface and concrete. The code is as follows:

 

    public class OrderService : IOrderService
    {
        private string guid;

        public OrderService()
        {
            guid = $"time:{DateTime.Now}, guid={ Guid.NewGuid()}";
        }

        public override string ToString()
        {
            return guid;
        }
    }

    public interface IOrderService
    {
    }


1. AddSingleton
As the name indicates, it can maintain an instance in your process, that is, it can only be instantiated once. If you don't believe it, the code will demonstrate it.


   

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddSingleton<IOrderService, OrderService>();
        }
    }

    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        IOrderService orderService1;
        IOrderService orderService2;

        public WeatherForecastController(IOrderService orderService1, IOrderService orderService2)
        {
            this.orderService1 = orderService1;
            this.orderService2 = orderService2;
        }

        [HttpGet]
        public string Get()
        {
            Debug.WriteLine($"{this.orderService1}\r\n{this.orderService2} \r\n ------");
            return "helloworld";
        }
    }


Then run it and refresh the page several times, as shown in the following figure:

You can see that no matter how you refresh the page, the guid is the same, indicating that it is indeed a single example.

2. AddScoped
Just from the name: Scope is a Scope. How big is the Scope in webapi or mvc? Yes, it is a request. Of course, the request will penetrate Presentation, Application, Repository and other layers. In the process of penetrating the layer, there must be multiple injections of the same class. These multiple injections maintain a single example under this Scope, as shown in the following code:


   

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddScoped<IOrderService, OrderService>();
        }


Refresh the page several times after running, as shown in the following figure:

Obviously, the guid changes every time you brush the UI, and the GUID is the same in the same request (scope).

3. AddTransient
As you can see earlier, either the scope is the whole process or the scope is a request, and the Transient here has no scope concept. It is injected and instantiated once. If you don't believe it, let's show you the code.


     

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();

            services.AddTransient<IOrderService, OrderService>();
        }


As can be seen from the figure, it is very simple to inject new once. Of course, each has its own application scenario.

Friends who didn't know before should also understand these three scopes by now. The next question to continue to think about is how to do this scope? To answer this question, you can only study the source code.

3: Source code analysis
The IOC container in aspnetcore is ServiceCollection. You can inject classes with different scopes into the IOC and finally generate a provider, as shown in the following code:

            var services = new ServiceCollection();

            services.AddSingleton<IOrderService, OrderService>();

            var provider = services.BuildServiceProvider();


1. How is the scope of addsingleton implemented
Generally speaking, when it comes to single cases, the first reaction is static. However, in general, there are hundreds of AddSingleton types in ServiceCollection, which are static variables. Since they are not static, there should be a cache dictionary or something. In fact, there is one.

1) Realized services dictionary
Each provider will have a dictionary called realized services, which will act as a cache later, as shown in the following figure:

As can be seen from the above figure, the dictionary has nothing during initialization. Next, execute var orderservice = provider GetService(); The effect is as follows:

You can see that there is already a service record in realized services, and then proceed to var orderservice2 = provider GetService();, Finally, it will enter callsiteruntimeresolver The visitcache method determines whether an instance exists, as shown in the following figure:

Take a closer look at the sentence in the above code: if (!resolvedServices.TryGetValue(callSite.Cache.Key, out obj)) returns directly once the dictionary exists. Otherwise, execute the new link, that is, this VisitCallSiteMain.

On the whole, this is the reason why a single case can be used. If you don't understand, you can take dnspy and think about it carefully...

2. Exploration of addtransient source code
As you can see, there will be a DynamicServiceProviderEngine engine class in the provider. The engine class uses dictionary cache to solve the single case problem. It can be imagined that there must be no dictionary logic in AddTransient. Is it right? Debug it.

Like the singleton, the final resolution is the responsibility of CallSiteRuntimeResolver. AddTransient will go to the VisitDisposeCache method, and this will continue here VisitCallSiteMain (transient callsite, context) is used to perform the new operation of an instance. Remember how a single instance is done? It will package a layer of resolvedServices judgment on the VisitCallSiteMain, 🤭 Continue to follow the VisitCallSiteMain method, which will eventually go to callsitekind The constructor branch calls your constructor. The code is as follows:

        protected virtual TResult VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
        {
            switch (callSite.Kind)
            {
            case CallSiteKind.Factory:
                return this.VisitFactory((FactoryCallSite)callSite, argument);
            case CallSiteKind.Constructor:
                return this.VisitConstructor((ConstructorCallSite)callSite, argument);
            case CallSiteKind.Constant:
                return this.VisitConstant((ConstantCallSite)callSite, argument);
            case CallSiteKind.IEnumerable:
                return this.VisitIEnumerable((IEnumerableCallSite)callSite, argument);
            case CallSiteKind.ServiceProvider:
                return this.VisitServiceProvider((ServiceProviderCallSite)callSite, argument);
            case CallSiteKind.ServiceScopeFactory:
                return this.VisitServiceScopeFactory((ServiceScopeFactoryCallSite)callSite, argument);
            }
            throw new NotSupportedException(string.Format("Call site type {0} is not supported", callSite.GetType()));
        }


Finally, the VisitConstructor calls the constructor of my instance code, so you should understand why new is injected every time. As shown below:

3. Exploration of addscoped source code
When you understand the principle of AddSingleton and AddTransient, I think scoped is also very easy to understand. It must be scoped and realized services, right? If you don't believe it, continue to code.


   

        static void Main(string[] args)
        {
            var services = new ServiceCollection();

            services.AddScoped<IOrderService, OrderService>();

            var provider = services.BuildServiceProvider();

            var scoped1 = provider.CreateScope();

            var scoped2 = provider.CreateScope();

            while (true)
            {
                var orderService = scoped1.ServiceProvider.GetService<IOrderService>();

                var orderService2 = scoped2.ServiceProvider.GetService<IOrderService>();

                Console.WriteLine(orderService);

                Thread.Sleep(1000);
            }
        }


Then see if scope1 and scope2 have separate cache dictionaries.

As can be seen from the figure, ResolvedServices in scope1 and scope2 have unused count s, which means that they exist independently and do not affect each other.

4: Summary
Many times we are so used to it. Suddenly one day, we are still a little confused when asked, so it is necessary to ask ourselves why 😄😄😄.
--------
Copyright notice: This article is the original article of CSDN blogger "first line code farmer", which follows the CC 4.0 BY-SA copyright agreement. Please attach the original source link and this notice for reprint.
Original link: https://blog.csdn.net/huangxinchen520/article/details/108334554

Keywords: .NET

Added by spyrral on Tue, 25 Jan 2022 14:30:59 +0200