Royi Namir
Royi Namir

Reputation: 148514

ApplicationServices resolves a different scoped instance in net core?

I'm using .net core 3.1 with this configuration :

public interface IFoo
{
    public void Work();
}

public class Foo : IFoo
{
    readonly string MyGuid;
    public Foo()
    {
        MyGuid = Guid.NewGuid().ToString();
    }
    public void Work() { Console.WriteLine(MyGuid); }
}

And this is the configuration :

public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IFoo, Foo>();
        ...
    }

    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IFoo foo, IServiceProvider myServiceProvider)
    {
        
        Console.WriteLine("Via ApplicationServices");
        app.ApplicationServices.GetService<IFoo>().Work();

        Console.WriteLine("Via IServiceProvider");
        myServiceProvider.GetService<IFoo>().Work();

        Console.WriteLine("Via IFoo injection");
        foo.Work();

    }

Result are :

Via ApplicationServices 27e61428-2adf-4ffa-b27a-485b9c45471d <----different
Via IServiceProvider c9e86865-2eeb-44db-b625-312f92533beb
Via IFoo injection c9e86865-2eeb-44db-b625-312f92533beb

More , in the controller action , If I use IserviceProvider :

   [HttpGet]
    public IActionResult Get([FromServices] IServiceProvider serviceProvider)
    {
     serviceProvider.GetService<IFoo>().Work();
     }

I see another different Guid : 45d95a9d-4169-40a0-9eae-e29e85a3cc19.

Question:

Why does the IServiceProvider and Injection of IFoo in the Configure method yield the same Guid , while the controller action and app.ApplicationServices.GetService yield different ones?

There are 4 different guids in this example

It's a scoped service. It supposed to be the same Guid.

Upvotes: 6

Views: 7189

Answers (3)

Nenad
Nenad

Reputation: 26607

TL;DR;

IFoo foo is resolved using myServiceProvider.GetService<IFoo>() before both get passed into Configure method. So you're resolving same IFoo instance from the same myServiceProvider instance for the 2nd time.

app.ApplicationServices is special root service provider of the application. Parent of myServiceProvider. Thus it resolves different IFoo.

**Default behavior of app.ApplicationServices should be to throw exception when you try to resolve scoped service.

LONGER EXPLANATION

If you have three dummy classes:

class Singleton { }
class Scoped { }
class Transient { }

And you register them as such in IServiceConfiguration:

public static void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<Singleton>();
    services.AddScoped<Scoped>();
    services.AddTransient<Transient>();
}

Now, you create your "root" IServiceProvider from IServiceCollection - in command-line app, it would look like this:

ServiceCollection sc = new ServiceCollection();
ConfigureServices(sc);
ServiceProvider root = sc.BuildServiceProvider();

ROOT SERVICE PROVIDER BEHAVIOR (equivalent to app.ApplicationServices):

If you now test root:

  1. root.GetService<Singleton>(); - every time it's called returns same object instance.
  2. root.GetService<Scoped>(); - every time it's called returns same object instance.
  3. root.GetService<Transient>(); every time it's called returns new object instance.

CHILD-SCOPE SERVICE PROVIDER BEHAVIOR (eg: IServiceProvider in Configure method):

If you now create child scope and use it's own IServiceProvider:

IServiceScope scope1 = root.CreateScope();
IServiceProvider sp1 = scope1.ServiceProvider;
  1. sp1.GetService<Singleton>(); - every time it's called returns same object instance whichroot.GetService<Singleton>(); returns. Singleton is the same instance no matter from which scope you call it. It is resolved climbing the hierarchy of scopes back to the root service provider (scopeless one).
  2. sp1.GetService<Scoped>(); - every time it's called returns same object instance, but not the same instance that root returns. Object instance is cached on the current scope. Every scope creates/caches it's own scoped instance.
  3. sp1.GetService<Transient>(); every time it's called returns new object instance, same behavior like for the root.

root scope is "special" only because it has no parent scope, so resolving scoped or singleton service from the root technically does the same thing - object instance returned is cached in the root itself.

This also explains why you cannot resolve service from IServiceCollection directly. IServiceCollection does not have hierarchy of scopes and caching infrastructure which IServiceProvider has. It just contains list of ServiceDescriptor. In addition, it would be unclear in which scope service instance should be cached.

ASP.NET Core

For ASP.NET Core root IServiceProvider is app.ApplicationServices. Configure method receives first child-scope created from the root - application-scope. For every HTTP request application-scope creates child-scope which is used to resolve all services and is itself injected in controllers and views of that HTTP request. It is also used to inject all other types in a controller constructor or a view.

IFoo resolution

So, your foo from Configure method is resolved using myServiceProvider, then they both get used as input parameters for Configure. Framework does something like this:

ServiceProvider root = sc.BuildServiceProvider(validateScopes: true);
var appScope = root.CreateScope();
IFoo foo = appScope.ServiceProvider.GetService<IFoo>();
ConfigureServices(foo, appScope.ServiceProvider);

When you call sp.GetService<IFoo>() inside of Configure method it is identical to appScope.ServiceProvider.GetService<IFoo>(); that was already called from the outside. root.GetService<IFoo>() creates different IFoo instance, as it should.

More ASP.NET Core:

To prevent developers making mistake trying to resolve scoped service from the app.ApplicationServices and not realizing that it is application scoped (global), instead of being scoped to HTTP request, by default, ASP.NET Core creates root ServiceProvider using BuildServiceProvider overload:

ServiceProvider root = sc.BuildServiceProvider(validateScopes: true);

validateScopes: true to perform check verifying that scoped services never gets resolved from root provider; otherwise false.

However, this might depend on compatibility mode you're using. I'm guessing that's the reason why it allows you to resolve scoped service via app.ApplicationServices.

Upvotes: 10

david-ao
david-ao

Reputation: 3160

The IApplicationBuilder "app" is created and passed to the Configure method.

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/startup?view=aspnetcore-5.0#the-configure-method

IApplicationBuilder is available to the Configure method, but it isn't registered in the service container. Hosting creates an IApplicationBuilder and passes it directly to Configure.

On the other hand the IServiceProvider "myServiceProvider" is actually activated/created and injected:

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-5.0#services-injected-into-startup

Services can be injected into the Startup constructor and the Startup.Configure method.

Any service registered with the DI container can be injected into the Startup.Configure method

So we are in two different scopes, so the guid, as expected, is different.

Via ApplicationServices 27e61428-2adf-4ffa-b27a-485b9c45471d <---- created in scope "A"

Via IServiceProvider c9e86865-2eeb-44db-b625-312f92533beb <-- created in scope "B"

Via IFoo injection c9e86865-2eeb-44db-b625-312f92533beb < -- created in scope "B"

When you are in your controller each request will have it's brand new scope, a different scope than the ones discussed above:

https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#scoped

It seems to me that you expected that also the guid in the controller to be the same, you should be aware that for each request an instance of your controller is created and a DI chain starts in a fresh scope for each request.

Upvotes: 2

Rojan Gh.
Rojan Gh.

Reputation: 1540

Update answer: For being able to provide you with the same instance, the DI engine will need to know the current scope.

The Configure method in the Startup class is called initially by the ASP.NET Core engine, not by a request from the user which is how a scoped service is meant to be used.

Now if you try this example instead:

IServiceScope scope = app.ApplicationServices.CreateScope();

scope.ServiceProvider.GetService<IFoo>().Work();

As long as you use the same ServiceProvider from the same IServiceScope instance, you will have access to the same IFoo object and will get the same GUID.

Old answer: A scoped service is meant to be living for a short period (scope) and get removed when they are needed anymore.

From Dependency injection in .NET

a scoped lifetime indicates that services are created once per client request (connection)

furthermore,

Do not resolve a scoped service from a singleton and be careful not to do so indirectly, for example, through a transient service.

app.ApplicationServices.GetService will try to provide you a Singleton which is not what the service is registered as.

What you see is the expected behavior. If you need the service to have the same instance for the whole period of your application's lifetime, you will have to register it as a Singleton.

Read more about the singleton lifetime and change your code from services.AddScoped<IFoo, Foo>() to services.AddSingleton<IFoo, Foo>() if you need the same value everywhere.

Upvotes: 2

Related Questions