Reputation: 148514
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
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
:
root.GetService<Singleton>();
- every time it's called returns same object instance.root.GetService<Scoped>();
- every time it's called returns same object instance.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;
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).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.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
Reputation: 3160
The IApplicationBuilder "app" is created and passed to 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:
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
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