Reputation:
Below is pseudo code from my textbook, and I'm confused about use a service in configure method
public class ConcreteA
{
public static Run(IServiceProvider serviceProvider)
{
ConcreteB _concrete = serviceProvider.GetRequiredService<ConcreteB>();
... //use ConcreteB instance
}
}
__________________________________________________________
// startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<ConcreteA>;
services.AddScoped<ConcreteB>;
}
public void Configure(IApplicationBuilder app) {
app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app.UseMvcWithDefaultRoute();
ConcreteA.Run(app.ApplicationServices);
}
I was told that becuase I use ConcreteA
in Configure
,method, so I'm running this outside of a scope. Any dependency(ConcreteB instance in this case) I create will hang around.
I'm very confused, below is my questions:
Q1- I registered ConcreteA
and ConcreteB
both as AddScoped
, so there shouldn't be any captured dependencies issues as they are in same scope, so why ConcreteB
will still hang around.
Q2- I didn't even create a ConcreteA instance as the method I access is a static method, so no ConcreteA instance needs to be created. So it is even more impossible for ConcreteB
to hang around.
Upvotes: 0
Views: 6177
Reputation: 387835
To answer in regards to your confusion explicitly: You have to think about the service provider as a cache of objects. When it creates a service that is not registered as transient, then it will store that instance locally so that it can provide the same instance again at a later time.
For example, when you do the following (assuming IFoo
is not registered as transient), then it will resolve the same object instance:
serviceProvider.GetService<IFoo>();
serviceProvider.GetService<IFoo>();
In order to do that, the service provider has to remember the IFoo
it returned in the very first call, so it can return the same instance on the second call (and any other call).
So what is a scope? A scope basically tells the service provider to resolve services from a separate cache. When you are within a scope and you now resolve a scoped service IScopedBar
, then the instance the service provider creates for you will be cached in that scoped cache:
serviceProvider.GetService<IScopedBar>();
serviceProvider.GetService<IScopedBar>();
When you are within a scope and you resolve a singleton service, then that service will still be looked up in the main cache. But scoped services will be looked up in the scoped cache.
That all wouldn’t really make a difference if a scope wouldn’t be closed. When a scope is closed, which is done when it gets disposed (e.g. when the using
block ends), then the services in the scoped cache are disposed and the cache is cleared. The main cache however stays.
If we were to implement that in a simplified pseudo service provider class, it could look like this:
public class SimplifiedServiceProvider
{
private Dictionary<Type, object> mainCache = new Dictionary<Type, object>();
private Dictionary<Type, object> scopeCache = new Dictionary<Type, object>();
public object GetService(Type type)
{
var serviceLifetime = GetLifetimeForService(type);
if (serviceLifetime == ServiceLifetime.Transient)
{
// transients are created directly
return CreateNewInstance(type);
}
else if (serviceLifetime == ServiceLifetime.Singleton)
{
// try to get from the cache
if (!mainCache.TryGetValue(type, out var service))
{
// create the service first
service = CreateNewInstance(type);
mainCache.Add(type, service);
}
return service;
}
else if (serviceLifetime == ServiceLifetime.Scoped)
{
// try to get from the scope cache
if (!scopeCache.TryGetValue(type, out var service))
{
// create the service first
service = CreateNewInstance(type);
scopeCache.Add(type, service);
}
return service;
}
}
public void DisposeScope()
{
// dispose all created (disposable) instances
foreach (var instance in scopeCache.Values)
(instance as IDisposable)?.Dispose();
// reset cache
scopeCache.Clear();
}
private ServiceLifetime GetLifetimeForService(Type type) { … }
private object CreateNewInstance(Type type) { … }
}
(The real implementation of a service provider and service scopes is obviously a bit more complicated than this, but this should still give a good idea of how scoped dependencies differ from singletons.)
With that idea and pseudo implementation in mind, imagine what happens when you resolve a scoped service outside of a scope, so DisposeScope
would never be called: The created scoped service would just stay permanently inside of the scope cache; just like singletons stay permanently within the main cache.
So by resolving a scoped service outside of a service scope, you effectively lifted the lifetime of that instance up to be a singleton service. This will not affect instances that actually are created within a scope, but those instances that are created outside of a scope will live for the lifetime of the service provider, which usually is the lifetime of the application.
That is why you usually want to create a temporary scope when you want to consume scoped services outside of “natural scopes” (i.e. scopes that are automatically created for you, like ASP.NET Core does when handling a request). That way, you restrict the lifetime of that scope, and as such also the lifetime of the instances that you resolve.
Upvotes: 4
Reputation: 247223
This appears to be an XY problem.
I believe you are trying to achieve the following but first you would need to refactor ConcreteA
to explicitly depend on ConcreteB
via constructor injection (though dependency on concretions as a code smell is outside of the scope (pardon the pun) of this question in its current form)
public class ConcreteA {
private ConcreteB B;
public ConcreteA(ConcreteB B) {
this.B = B;
}
public void Run() {
... //use ConcreteB instance
}
}
And then in start up you register them as scoped, just as before in ConfigureServices. However in Configure
you will access them as scoped.
For example
startup.cs
public void ConfigureServices(IServiceCollection services) {
services.AddScoped<ConcreteA>();
services.AddScoped<ConcreteB>();
//...
}
public void Configure(IApplicationBuilder app) {
app.UseStatusCodePages();
app.UseDeveloperExceptionPage();
app.UseMvcWithDefaultRoute();
// Create a new IServiceScope that can be used to resolve scoped services.
using(var scope = app.ApplicationServices.CreateScope()) {
// resolve the services within this scope
ConcreteA A = scope.ServiceProvider.GetRequiredService<ConcreteA>();
//ConcreteA instance and injected ConcreteB are used in the same scope
//do something
A.Run();
}
//both will be properly disposed of here when they both got out of scope.
}
The ConcreteA
and by extension ConcreteB
used above will be in the same scope.
Upvotes: 3