question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Microsoft.Extensions.Hosting.Host creates hosted services too early.

See original GitHub issue

Describe the bug

ASP.NET Core 3.0’s new Microsoft.Extensions.Hosting.Host creates hosted services between Startup’s ConfigureServices and Configure methods, where the ‘old’ Microsoft.AspNetCore.WebHost creates the hosted services only after the Configure method has run.

To Reproduce

  1. Create a vanilla ASP.NET Core (Model-View-Controller) Web Application project for ASP.NET Core 3.0.
  2. Create an empty MyHostedService class that implements IHostedService.
  3. Add the following registration to the Startup.ConfigureServices method: services.AddSingleton<IHostedService>(p => new MyHostedService());
  4. Run the application
  5. The p => new MyHostedService() is invoked after Startup.ConfigureServices ran, but before Startup.Configure runs.

Expected behavior

The delegate should run only after Startup.Configure ran.

Additional context

This behavior difference between Microsoft.AspNetCore.WebHost and Microsoft.Extensions.Hosting.Host is significant, because it disallows users of non-conforming containers (like Simple Injector, Castle Windsor, and Ninject) to resolve hosted services from their container, because the configuration of those containers needs to be finished in the Startup.Configure phase, while resolving hosted services from that container before that configuration is finished, can lead to problems. Simple Injector, for instance, blocks any registrations made after the first resolve (which will be a resolve for MyHostedService if you assume the hosted service to be added using p => container.GetInstance<MyHostedService>()). But even if the container doesn’t block registrations, the early request for hosted services can cause trouble, because the hosted service’s dependencies might not be registered at that point.

Issue Analytics

  • State:closed
  • Created 4 years ago
  • Comments:15 (7 by maintainers)

github_iconTop GitHub Comments

4reactions
dotnetjunkiecommented, Sep 30, 2019

Hi @Tratcher,

Thank you for trying to help me out on this, but please keep in mind that Simple Injector is non conforming. With Simple Injector, it is not possible to replace the built-in container. This is really important to understand. The suggestions you make only work for conforming containers like Autofac; not for Simple Injector, Castle Windsor, and Ninject.

2reactions
dotnetjunkiecommented, Oct 1, 2019

Hi @davidfowl,

Thanks for chiming in.

As in you replace the controller factory and instantiate MVC controllers anymore?

No. Replacing the default controller factory still works and AFAIK all non-conformers still take this approach in ASP.NET Core 3.

Or is something more narrow broken?

The specific scenario that is broken for non-conformers is the case where an application developer wants to add a hosted service (i.e. IHostedService) to the ASP.NET Core 3 pipeline, while resolving that hosted service from their application/non-conforming container.

Let me give you a full repro to understand. Here are ASP.NET Core v3 Program and Startup classes. Program is the default using Host, while Startup is adapted using Simple Injector:

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>();
            });
}

public class Startup
{
    // Create the application container (in this case Simple Injector)
    private readonly Container container = new Container();

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

    public IConfiguration Configuration { get; }

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

        // Adds Simple Injector integration (using SimpleInjector.Intagration.ServiceCollection)
        services.AddSimpleInjector(this.container);

        // Registers application components into the application container
        container.RegisterSingleton<MyApplicationService>();

        // Register the hosted service in the application container
        container.RegisterSingleton<MyHostedService>();

        // Cross-wire the hosted service into the (MS.DI) framework container.
        // The ASP.NET Core Host resolves all hosted services.
        // Here it is done by hand, but Simple Injector also contains an AddHostedService
        // extension method, but that does basically the same.
        services.AddSingleton<IHostedService>(
            p => container.GetInstance<MyHostedService>()); // ## <-- called before Configure
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // Finalizes the Simple Injector configuration by enabling cross-wiring.
        app.ApplicationServices.UseSimpleInjector(this.container);

        // What .UseSimpleInjector() does under the covers (among other things) is cross-wiring
        // an Microsoft.Extensions.DependencyInjection.IServiceScope.
        container.Register<IServiceScope>( // ## <-- This breaks with the new Host class.
            ()=>app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope(),
            Lifestyle.Scoped);
            
        // Usual ASP.NET Core stuff here.
    }
}

The MyHostedService and MyApplicationService are simple stubs:

// Some application component
public class MyApplicationService { }

// Some hosted service depending on an application component
public class MyHostedService : IHostedService
{
    private readonly MyApplicationService service;

    public MyHostedService(MyApplicationService service)
    {
        this.service = service;
    }

    public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

The problem here is that Simple Injector needs to be further configured inside the Configure method to be able to achieve “cross wiring.” With cross wiring, the application container pulls in missing registrations from the framework container. The ability to cross wire is important, because application components obviously need to integrate with framework and third-party components.

This cross-wiring can only be applied after an IServiceProvider has become available. The IServiceProvider is used, for instance, to resolve the IServiceScopeFactory, and it is used to register an event that is triggered when Simple Injector detects an unregistered service. Because IServiceProvider is not available in during the Startup.Configure stage, it is impossible to complete Simple Injector’s configuration at that point.

With the new Host however, IHostedService implementations are requested from the built IServiceProvider before Startup.ConfigureServices is called and, more importantly, the hosted services are started at that point. So when users lets Simple Injector resolve their hosted services, it means that Simple Injector’s build process is triggered at that point. This locks the container, which is similar to the two-phase build process that MS.DI uses (use a ServiceCollection to build and a ServiceProvider to resolve). But after a ‘lock down’, no changes can be made to the container, which means that the application breaks at that point.

Note that, even though other non-conforming containers might not lock the container, they still have the same problem, because a resolved hosted service might require a framework component that hasn’t been cross-wired at that point. This would also break the application.

I’ve been thinking about ways to mitigate this, for instance by adding new abstractions to ASP.NET Core. But as long as Host keeps requesting and starting hosted services before Startup.Configure, the problem will remain and it becomes impossible for non-conformers to register hosted services as application components.

Read more comments on GitHub >

github_iconTop Results From Across the Web

An error occurred while accessing the Microsoft.Extensions ...
The tools first try to obtain the service provider by invoking the Program.CreateHostBuilder() , calling Build() , then accessing the Services ...
Read more >
Extending the shutdown timeout setting to ensure graceful ...
In this post I describe a problem where you get an exception when shutting down an app that used IHostedService, when the service...
Read more >
NET Generic Host
Learn about the .NET Generic Host, which is responsible for app startup and lifetime management.
Read more >
Worker Services - .NET
Hosted services are logging, configuration, and dependency injection (DI) ready. They're a part of the extensions suite of libraries, meaning ...
Read more >
ASP.NET Core Web Host
Learn about Web Host in ASP.NET Core, which is responsible for app startup and lifetime management.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found