Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
dotnetjunkie opened this issue Sep 30, 2019 · 15 comments
Closed
Labels
area-hosting Includes Hosting area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions
Milestone

Comments

@dotnetjunkie
Copy link

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.

@Tratcher
Copy link
Member

Why is DI still being modified in Configure?

@dotnetjunkie
Copy link
Author

Because we need a fully configured and built MS.DI container to cross-wire to framework services such as IServiceScopeFactory.

@Tratcher
Copy link
Member

@davidfowl @halter73 who are more familiar with DI interop.

Configure is not the place for that, it's just too late. There are several different supported ways to configure DI containers and we should be able to find one of them that works for you.

Have you tried working with the IServiceProviderFactory?
https://github.com/aspnet/Extensions/blob/32042787e1eb7f16c6f2e6b73c4c382571c32527/src/DependencyInjection/DI.Abstractions/src/IServiceProviderFactory.cs
https://github.com/aspnet/Extensions/blob/32042787e1eb7f16c6f2e6b73c4c382571c32527/src/Hosting/Hosting/src/HostBuilder.cs#L82-L86

That should let you intercept the container as it's completed and wrap it as needed.

@dotnetjunkie
Copy link
Author

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.

@remcoros
Copy link

remcoros commented Sep 30, 2019

I think this is not so much about the serviceprovider, but more the configuration phase. In Configure, routes are setup, additional options set, etc. They wouldn't be available now, or components wrongly setup with defaults when hosted services start if I'm understanding this correctly.

e.g.

class Startup
{
  ConfigureServices(..)
  {
    services.AddSingleton<IComponent, Component>>(..)
  }

  Configure()
  {
    provider.GetService<IComponent>(...).AdditionalSetup(...)
  }
}

class HostedService {
  Start()
  {
    provider.GetService<IComponent>().Run(...);
  }
}

@davidfowl
Copy link
Member

So is this specifically about resolving things from an IHostedService using non-conforming containers? Is that the broken scenario?

@dotnetjunkie
Copy link
Author

This is about hosted services being resolved too early in the pipeline, breaking non-conforming containers.

@davidfowl
Copy link
Member

davidfowl commented Oct 1, 2019

Help me understand the actual problem though. Are you saying non-conforming containers are broken all up? As in you replace the controller factory and instantiate MVC controllers anymore? Or is something more narrow broken? Sorry, I'm being a bit dense but I'm not putting it altogether and this issue is very specific. Some background would help.

@dotnetjunkie
Copy link
Author

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.

@davidfowl
Copy link
Member

OK thanks for that. The IServiceProviderFactory is actually the right place for simple injector to plug in here. Though the usage might be a bit atypical. Here's how it could look:

public class SimpleInjectorServiceProviderFactory : IServiceProviderFactory<(Container, IServiceCollection)>
{
    public (Container, IServiceCollection) CreateBuilder(IServiceCollection services)
    {
        // Adds Simple Injector integration (using SimpleInjector.Intagration.ServiceCollection)
        var container = new Container();
        services.AddSimpleInjector(container);
        return (container, services);
    }

    public IServiceProvider CreateServiceProvider((Container, IServiceCollection) containerBuilder)
    {
        var (container, services) = containerBuilder;
        var serviceProvider = services.BuildServiceProvider();
        // Finalizes the Simple Injector configuration by enabling cross-wiring.
        serviceProvider.UseSimpleInjector(container);
        return serviceProvider;
    }
}
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>();
            })
            .UseServiceProviderFactory(new SimpleInjectorServiceProviderFactory()); // Wire up simple injector
}

The factory is wiring up simple injector before ConfigureServices is called and also gets a chance to run logic before the service provider is built. Notice I'm still returning the default service provider to the system but in the mean time, the simple injector container has been configured and cross wires framework dependencies at the right and appropriately.

Now for the part that you may not like:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using SimpleInjector;

namespace WebApplication351
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
        }

        public void ConfigureContainer((Container, IServiceCollection) builder)
        {
            var (container, services) = builder;

            // 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
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Hello World!");
                });
            });
        }
    }
}

The ConfigureContainer method is used to pass the container builder (in this case the Container from SimpleInjector) to the Startup class. The only really gross part is the fact that I'm using a tuple here for illustration.

The above IMO plugs in a bit more cleaning to the container lifetime while not forcing non-conforming containers to suddenly conform.

Thoughts?

@dotnetjunkie
Copy link
Author

dotnetjunkie commented Oct 1, 2019

@davidfowl, I have to research what the consequences are of this approach and whether or not we would run into different problems using this approach. The IServiceProviderFactory<T> abstraction was introduced for conformers primarily, so this will be new territory for all of us.

My main observation, though, is that ASP.NET Core 3 introduces this breaking change that forces us to completely remodel, retest, and redocument our integration packages, and communicate these changes to our users. This will raise new questions, and support. This is a major effort on our part, and can be a major frustration or confusion for our users.

Two questions:

  1. Can you explain why the decision was made for this behavioral change in the new Host class and what the consequences would be for you to change this behavior back to how it was with WebHost?
  2. What’s the plan with WebHost? Will WebHost be deprecated? Does Host supersede WebHost and does it have -or will it have- features that WebHost doesn’t have? Or is it safe for Simple Injector users to depend on WebHost in their Program classes?

@davidfowl
Copy link
Member

My main observation, though, is that ASP.NET Core 3 introduces this breaking change that forces us to completely remodel, retest, and redocument our integration packages, and communicate these changes to our users. This will raise new questions, and support. This is a major effort on our part, and can be a major frustration or confusion for our users.

I can appreciate that frustration and in part, it's why we chose not to deprecate the WebHost in this release cycle. Also, we shipped 10 preview versions which unfortunately nobody tested this specific scenario with SimpleInjector.

Can you explain why the decision was made for this behavioral change in the new Host class and what the consequences would be for you to change this behavior back to how it was with WebHost?

The generic host has a single extensibility point, the IHostedService (and that's intentional), so the intent was to have everything re-plat on top of a single abstraction to unify the behavior. Want to run code before the IServer? Just add an IHostedService that runs before it, it's no longer a special piece of the stack, it just fits in. Startup is now solely a feature of the WebHostedService which is why it happens during the call to Start instead of happening before that.

Changing this behavior would mean introducing some new phase in the host that happens before hosted services are run and then re-implementing parts of the WebHost on top of that.

The other thing we did was make sure there was a single container for the entire application. As you know the WebHost ends up with 2 or 3 containers depending on the scenario which could lead to different instances of singletons existing in the dependency tree (which was always confusing to customers).

What’s the plan with WebHost? Will WebHost be deprecated? Does Host supersede WebHost and does it have -or will it have- features that WebHost doesn’t have? Or is it safe for Simple Injector users to depend on WebHost in their Program classes?

Yes, we'll likely deprecate it in 5.0 and remove it in 6 or 7. Which should be more than enough time to adjust.

PS: The extensibility point above is a much cleaner way to expose the non-conforming container (albeit a new way), that plugs in at the "right time".

@dotnetjunkie
Copy link
Author

I just published Simple Injector v4.8, which now allows Simple Injector users to integrate hosted services while using the new ASP.NET Core 3 Microsoft.Extensions.Hosting.Host model. The fix refrains from using the IServiceProviderFactory<T> model, as demonstrated above by David.

During my research I found a way to circumvent the behavioral difference between Host and WebHost in a way that doesn't require the use of IServiceProviderFactory<T>. Using IServiceProviderFactory<T> would be both a risky and time-consuming endeavor for me and the other Simple Injector contributors, as I explained above, which is why every other solution had my preference.

@davidfowl
Copy link
Member

Should I look?

@danilobreda
Copy link

Upgraded to 4.8 and now hosted services works very well... I had to change some behaviors of my code because of the 3.0 changes. 👍

@ghost ghost locked as resolved and limited conversation to collaborators Jan 15, 2020
@amcasey amcasey added the area-hosting Includes Hosting label Jun 1, 2023
@amcasey amcasey added area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions and removed area-runtime labels Aug 24, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-hosting Includes Hosting area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions
Projects
None yet
Development

No branches or pull requests

9 participants