A: background

1. Tell a story

A few days ago, a friend of mine gave me the time to analyze why aspNetCore can inject Singleton, Transient, Scoped into ServiceCollection. This is interesting. Since the introduction of ServiceCollection in core, coupled with the popular DDD mode, I believe that many of your projects will rarely see new, at least spring did this more than a decade ago.

2: the Singleton, Transient, Scoped basic usage

Before analyzing the source code, I think it is necessary to introduce their gameplay first. For demonstration purposes, 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()
        {
            returnguid; }}public interface IOrderService{}Copy the code

1. AddSingleton

As the name suggests, it can hold an instance in your process, that is, only once.


    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"; }}Copy the code

Then run to refresh the page several times, as shown below:

As you can see, the GUID is the same no matter how you refresh the page, indicating that it is indeed a singleton.

2. AddScoped

As the name suggests: Scope is a Scope. How big is the Scope in WebAPI or MVC? The request will penetrate the Presentation, Application, Repository, and so on. There must be multiple injections of the same class in the process of crossing the layer. These injections will maintain a singleton in this scope, as shown in the following code:


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

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

Copy the code

Run to refresh the page several times, as shown below:

Obviously, the GUID changes every time the UI is swiped, but the GUID is the same in the same request (Scope).

3. AddTransient

As you can see before, the scope is either the entire process or the scope is a request, but there is no concept of scope in Transient, injection once instantiate once, if you don’t believe it, the code will show you.


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

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

Copy the code

As you can see from the diagram, injection once new, very simple, of course, each has its own application scenario.

Those of you who were not aware of these three scopes should now understand them, and the next question to ponder is, how does this scope work? The only way to answer this question is to look at the source code.

Three: source code analysis

The IOC container in AspNetCore is a ServiceCollection. You can inject different scoped classes into IOC, and finally generate a provider, as shown in the following code:


            var services = new ServiceCollection();

            services.AddSingleton<IOrderService, OrderService>();

            var provider = services.BuildServiceProvider();

Copy the code

1. How is AddSingleton scoped

A ServiceCollection will have hundreds of AddSingleton types. This is not possible. If it is not static, it should have a cache dictionary or something. There is actually one.

1) Real Services dictionary

Inside each provider is a dictionary called RealizedServices, which will act as a cache, as shown below:

Var orderService = provider.getService

(); The effect is shown below:

Var orderService2 = provider.getService

(); , will eventually into CallSiteRuntimeResolver VisitCache method to determine whether a instance, the following figure:

Look closely at this sentence in the code above: if (! ResolvedServices. TryGetValue (callSite. Cache. The Key, the out obj)) once the dictionary is returned directly, otherwise will implement the new link, also is this. VisitCallSiteMain.

All in all, this is why you can singleton. If you don’t understand, you can take dnSpy and think about it carefully.

2. AddTransient source code exploration

In the front you can see, the provider will have a DynamicServiceProviderEngine engine classes, using the dictionary cache engine classes to solve the problem of single case, it is conceivable that AddTransient internal logic is certainly no dictionary, does it? Let’s debug it.

As with singletons, the final parsing is done by CallSiteRuntimeResolver, and the AddTransient inside goes to the VisitDisposeCache method, Call this.visitCallSitemain (Context callsite, context) for instance new. Remember how singletons work? It will package a layer of resolvedServices on this VisitCallSiteMain. 🤭 Go ahead and find the VisitCallSiteMain method. This method will eventually call your Constructor via the Callsitekind. Constructor branch 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()));
		}

Copy the code

The final call to my instance code’s constructor comes from VisitConstructor, so you can see why each injection is new. The diagram below:

AddScoped source code exploration

When you understand the AddSingleton AddTransient principle, I think Scoped is also very easy to understand. It must be a Scoped, a realizedservice, right? If you don’t believe me, 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); }}Copy the code

Then see if scoped1 and scoped2 both have separate cache dictionaries.

As can be seen from the figure, ResolvedServices in Scoped1 and Scoped2 have no count, indicating that they exist independently and do not affect each other.

Four:

Most of the time we are so used to using, suddenly one day was asked a little meng forced, so often ask yourself why it is necessary ha 😄😄😄.