aspnet / DependencyInjection

[Archived] Contains common DI abstractions that ASP.NET Core and Entity Framework Core use. Project moved to https://github.com/aspnet/Extensions

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

MVC 4.5.1 : scoped services never disposed

Ghost-cruiser opened this issue · comments

Hello,

I'm currently using aspnetcore dependency injection system in a MVC 4.5.1 application. I based my code on Scott Dorman's example (http://scottdorman.github.io/2016/03/17/integrating-asp.net-core-dependency-injection-in-mvc-4/). Although the injection works fine, it appears Scoped services are never disposed : they're basically singleton.

Would you have any clue to explain or fix this behaviour?

In advance, thanks,
G. M.

Which version are you using? Where/how are you resolving the service? How is it registered, i.e. by type or factory? What lifetime does the services it's injected into have?

My first hunch: you're creating a captive dependency by either injecting a scoped service into a singleton or resolving a scoped service from the application container (not from an actual scope).

I'm using v.1.1.1.0.

The services are resolved automatically in the constructors (I'm not sure if this is the expected answer). I'm registering them by type. I'm injecting a scoped service into a transient controllers ; the service depends on scoped services and one singleton.

Below the source code.
Note : InjectController is the same function that ControllerAsServices in Scott Dorman's example and DefaultDepenedencyResolver is the same as the example as well.

public partial class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IHttpService, Services.RisHttpService>();
        services.AddSingleton<IConfiguration, Services.Configuration.RisConfiguration>();

        services.AddScoped<IDbContext, BACKBONERISEntities>();

        ScanForDependency(services);
        SetResolver(services);
    }

    private void ScanForDependency(IServiceCollection services)
    {
        var types = typeof(Startup).Assembly.GetExportedTypes()
            .Where(t => !t.IsAbstract && !t.IsGenericTypeDefinition);

        // Controllers
        services.InjectControllers(types
           .Where(t => typeof(IController).IsAssignableFrom(t)
              || t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase
              )));

        // Modules
        services.RegisterModulesDependencies(types.Where(
            t => t.GetInterfaces().Any(
                x => x == typeof(IModuleConfiguration)
                ))
           );
    }

    private void SetResolver(IServiceCollection services)
    {
        var resolver = new DefaultDependencyResolver(services.BuildServiceProvider());
        DependencyResolver.SetResolver(resolver);
    }
}




public class GestionHl7Module : IModuleConfiguration
{

    public void Configure(IServiceCollection services, IConfiguration config)
    {
        RegisterDependencies(services, config);
    }

    private void RegisterDependencies(IServiceCollection services, IConfiguration config)
    {
        services.AddScoped<IGestionHl7Service, GestionHl7Service>();
    }
}





public class GestionHl7Service : GenericService, IGestionHl7Service
{
    #region DI
    private readonly IMessageSender<MESSAGE> _hl7Service;
    private readonly IConfiguration _config;
    #endregion

    #region CTOR
    public GestionHl7Service(
        IConfiguration config, //singleton
        IDbContext context,  // scoped - never disposed
        IMessageSender<MESSAGE> hl7Service // scoped - never disposed
        ) 
        : base(context)
    {
        ApplicationLog.Info("Instantiating GestionHl7Service");
        _hl7Service = hl7Service;
        _config = config;
    }
    #endregion

[...]

}

public class GestionHl7Controller : GenericAuthorizedController
{
    #region DI

    private readonly IGestionHl7Service _service;
    #endregion

    #region PROPS
    public const string JsonP = "{0}({1})";
    #endregion

    #region CTOR
    public GestionHl7Controller(IGestionHl7Service service)
    {
        _service = service;
    }
    #endregion
[...]

}

So, as usual, the main problem is between the chair and the keyboard...

It was indeed a captive dependency, the context was injected in the configuration, which I didn't think to check.

Sorry if any time was lost due to my error!

Nop, spoke too soon, problem is still there, even without any singleton registered, behaviour is still the same.

Could you indicate how the created serviceProvider is supposed to be destroyed? What event is supposed to trigger the dispose?

In advance, thanks.

I'd be interested in the answer to this. I am using DI package 1.1.1 in UWP, I create the service provider
this.ServiceProvider = this.ServiceCollection.BuildServiceProvider();
then I create the scope:
this.ServiceScope = this.ServiceProvider.GetRequiredService().CreateScope();
Then I create IDisposable scoped services using GetService. Eventually I try to end the scope like this:
this.ServiceScope.Dispose();
However, Dispose is not called in the services, and evidently the scope does not end because when I later call GetService, it hands back the same instance as before.

Disposing scoped services obviously works, because this test (part of the test suite in this repository) is passing:

public void DisposingScopeDisposesService()
{
// Arrange
var collection = new TestServiceCollection();
collection.AddSingleton<IFakeSingletonService, FakeService>();
collection.AddScoped<IFakeScopedService, FakeService>();
collection.AddTransient<IFakeService, FakeService>();
var provider = CreateServiceProvider(collection);
FakeService disposableService;
FakeService transient1;
FakeService transient2;
FakeService singleton;
// Act and Assert
var transient3 = Assert.IsType<FakeService>(provider.GetService<IFakeService>());
using (var scope = provider.CreateScope())
{
disposableService = (FakeService)scope.ServiceProvider.GetService<IFakeScopedService>();
transient1 = (FakeService)scope.ServiceProvider.GetService<IFakeService>();
transient2 = (FakeService)scope.ServiceProvider.GetService<IFakeService>();
singleton = (FakeService)scope.ServiceProvider.GetService<IFakeSingletonService>();
Assert.False(disposableService.Disposed);
Assert.False(transient1.Disposed);
Assert.False(transient2.Disposed);
Assert.False(singleton.Disposed);
}
Assert.True(disposableService.Disposed);
Assert.True(transient1.Disposed);
Assert.True(transient2.Disposed);
Assert.False(singleton.Disposed);
var disposableProvider = provider as IDisposable;
if (disposableProvider != null)
{
disposableProvider.Dispose();
Assert.True(singleton.Disposed);
Assert.True(transient3.Disposed);
}
}

It must be some misconfiguration in your app somewhere. Have you tried to call BuildServiceProvider(validateScopes: true)? This is turned on by default in 2.0, but not in 1.x. It will prevent you from shooting yourself in the foot with mismatching lifetime configurations 😄

Well, the problem was that I was obtaining the service from this.ServiceProvider instead of this.ServiceScope.ServiceProvider. As you can see, I have a class that holds the service provider and the current scope, which is created/disposed when certain conditions occur (file open / close, as it happens).

Assuming that a scope either may or may not be active, then to get a service you have to either get it from the active scope or else fall back to the non-scoped service provider, like this:

var service = this.ServiceScope?.ServiceProvider.GetService<T>() ?? this.ServiceProvider.GetService<T>().

Based on my experience, I recommend a change to the message in the exception thrown by ServiceProvider when the validateScopes: true option is set. It should include a statement that scoped services should be obtained from the ServiceProvider within the ServiceScope.

This change will make the correct usage more discoverable.

This issue was moved to dotnet/aspnetcore#2337