SimpleInjector: Services added with AddHostedService are unable to resolve registrations in ASP.NET Core 3

Description of the bug

Adding a service with AddHostedService<T>() is unable to resolve types registered with Simple Injector in ASP.NET Core 3. The same code worked in 2.2.

Trying to step through the code it appears that services added with AddHostedService() are being started (and subsequently resolved) earlier in Core 3 than in 2.2, causing registrations to not yet have been added. The Startup.Configure() method is not hit at the time the exception is raised.

Moving the registrations to occur at the end of ConfigureServices() doesn’t help, as despite it fixing the missing registrations it is not possible to then call app.UseSimpleInjector() in Configure() as the hosted service being resolved before Configure() is called locks the container:

The container can’t be changed after the first call to GetInstance, GetAllInstances, Verify, and some calls of GetRegistration. Please see https://simpleinjector.org/locked to understand why the container is locked. The following stack trace describes the location where the container was locked:

at SimpleInjector.Container.GetInstance[TService]()
at SimpleInjector.SimpleInjectorGenericHostExtensions.<>c__DisplayClass0_0`1.<AddHostedService>b__0(IServiceProvider _)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitIEnumerable(IEnumerableCallSite enumerableCallSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite singletonCallSite, RuntimeResolverContext context)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass1_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
at System.Runtime.CompilerServices.AsyncMethodBuilderCore.Start[TStateMachine](TStateMachine& stateMachine)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token)
at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host)
at Test.Web.Program.Main(String[] args) in E:\projects\test\src\backend\Test.Web\Program.cs:line 14

Expected behavior

Type of HostedService is resolved.

Actual behavior

Exception:

The container can’t be changed after the first call to GetInstance, GetAllInstances, Verify, and some calls of GetRegistration. Please see https://simpleinjector.org/locked to understand why the container is locked. The following stack trace describes the location where the container was locked:

To Reproduce

Replace the default Startup with the following:

public class HostedService : IHostedService
{
    public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

public class Startup
{
    private readonly Container container = new Container();

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

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

        services.AddSimpleInjector(this.container, options =>
        {
            options.AddHostedService<HostedService>();
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.ApplicationServices.UseSimpleInjector(this.container);

        app.UseStaticFiles();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");
        });
    }
}

Additional context

  • Simple Injector 4.7.1
  • .NET Core 3

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 15

Commits related to this issue

Most upvoted comments

btw thank you for this detailed and excellent bug report. This is how I rather see everyone pointing their bugs and questions.

Crêpe! Yet another unfortunate breaking change in ASP.NET Core v3. I will investigate this and report back to you when I found a workaround or have a fix available. Might take a few days for me to get to this point though.

In case others are still having issues after reading this. I used a modified version of the suggestion by @dotnetjunkie from Sep 30, 2019.

public static void Main(string[] args)
{
    CreateHostBuilder(args)
        .UseDefaultServiceProvider(options => { })
        .Build()
        .Run();
}

public static IWebHostBuilder CreateHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args).UseStartup<Startup>();

Note the addition of .UseDefaultServiceProvider(options => { }) in the Main() method. Not sure why, but in my .NET Core 3.1.8 web API this was the only way I could find to get AddHostedService() to work without throwing an exception.

This problem is caused by the behavioral change of the new Host class compared to the previous WebHost class. I filed a bug.

As workaround, switch back to using WebHost in your Program.cs instead of the new Host, which is used in the default Visual Studio template. When creating a new ASP.NET Core 3.0 application, the Program.cs looks like this:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

You should change it to the following:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

The reason this works is because Host starts creating hosted services (too) early in the pipeline, which causes problems for containers like Simple Injector, Castle Windsor, and Ninject, that can’t conform to the presented Microsoft container abstraction.

As it currently stands, there is no way for Simple Injector to work around this issue, as this behavior is embedded deeply into the Host class.

Hi @paolopiaggio,

That difference in behavior between Host and WebHost is the core incombatibility that this issue describes (and explained in this post), that I reported to Microsoft, and that Simple Injector v4.8 has a fix/workaround for.

In the Simple Injector v4.8 ASP.NET integration, the integration gets notified when ASP.NET starts to resolve from the container before the Configure method is called. This allows the integration to do its required last-minute registrations. This allows the integration to succeed, but it doesn’t prevent the container from being locked. This is something that can’t be prevented. But if you use a version of Simple Injector (and the integration packages) < v4.8 with the new Host, it will certainly break once you start adding Hosted Services.

I hope this makes sense.

Hi @paolopiaggio,

I’m unable to reproduce this using ASP.NET Core v3.1. I’m very interested in learning more about the issue you are having, so I can act accordingly. Would you mind creating a new issue with a minimal reproducible example that demonstrates the bug?

Thanks in advance.

We just release Simple Injector v4.8, which fixed this.

Just given the v4.8 beta2 a spin and it seems to work for me after following the obsoletion messages.

I published beta2 release of v4.8 to NuGet that aims to fix this problem. The package marked some methods obsolete and guides you towards the right methods. Do note that the obsolete messages reference the documentation, but the documentation has not yet been updated.

I appreciate any feedback on this.