TheHvidsten
TheHvidsten

Reputation: 4458

Multiple hosts with the same DI container

In C#.NET Core you can create a generic host using the following code:

IHostBuilder builder = new HostBuilder()
    .ConfigureServices((context, collection) => {
        collection.AddSingleton<IMyClass, MyClass>();
        collection.AddHostedService<MyService>();
    });
await builder.RunConsoleAsync();

This creates a new instance of MyService with the default DI container.

Now, say that I want to create a new host inside MyService. This is easy enough (a web host in this case):

IWebHost webHost = WebHost.CreateDefaultBuilder()
    .UseStartup<MyStartup>()
    .Build();
    .RunAsync();

This webhost will have its own Dependency Injection container, so it will not have access to all dependencies I've already added to the generic host container: i.e. it will not be able to have IMyClass injected into MyStartup.

I've also tried adding a custom IServiceProviderFactory<> using the following code (based on the .UseDefaultServiceProvider() code where they use IServiceCollection as the builder type):

public class CustomServiceProviderFactory :  IServiceProviderFactory<IServiceCollection>
{
    private readonly IServiceProvider _provider;

    public CustomServiceProviderFactory(IServiceProvider provider)
    {
        _provider = provider;
    }

    public IServiceCollection CreateBuilder(IServiceCollection services)
    {
        return services;
    }

    public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder)
    {
        return _provider;
    }
}

Then in my HostBuilder I added it through .UseServiceProviderFactory(new CustomServiceProviderFactory(_serviceProvider)), but for some reason the HostedService is instantiated before this is created, causing DI exceptions about not finding the required objects.

However, seeing as WebHost.CreateDefaultBuilder() is now the preferred way to create a webhost (in .NET Core 3.0), and an IWebHostBuilder does not have an option to set a custom IServiceProviderFactory this does seem like a dead end.

How can I have the webhost use the same DI container as the initial generic host?

Upvotes: 7

Views: 1797

Answers (1)

Eza
Eza

Reputation: 53

I've tried to do the same thing and this is what I have landed on. Not fully tested but it does appear to work.

First, in my base/first HostBuilder, add the service collection as a service so an IServiceCollection can be resolved via DI later on.

IHostBuilder builder = new HostBuilder()
    .ConfigureServices((ctx, services) =>
    {
        services.AddSingleton<IMyService, MyService>();
        services.AddHostedService<MyApp>();
        services.AddSingleton(services);
    });

In IHostedService.StartAsync() I create the WebHost. I copied the use of services.Replace from the functionality inside UseDefaultServiceProvider():

IWebHost host = WebHost
            .CreateDefaultBuilder()
            .ConfigureServices(services =>
            {
                var options = new ServiceProviderOptions();               
                services.Replace(ServiceDescriptor.Singleton<IServiceProviderFactory<IServiceCollection>>(new CustomServiceProviderFactory(_services, options)));
            })
            .UseStartup<MyStartup>()
            .Build();

In the constructor of my CustomServicesProvider, I also need to remove any IHostedService services or else it appears you enter an infinite loop of the service starting. When creating the service provider, I add everything from the constructor-passed service collection to the local service collection.

class CustomServiceProviderFactory : IServiceProviderFactory<IServiceCollection>
{
    private readonly IServiceCollection _baseServices;
    private readonly ServiceProviderOptions _options;

    public CustomServiceProviderFactory(IServiceCollection baseServices, ServiceProviderOptions options)
    {
        _baseServices = baseServices;
        _options = options;
        _baseServices.RemoveAll<IHostedService>();
    }
    
    public IServiceCollection CreateBuilder(IServiceCollection services)
    {
        return services;
    }

    public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder)
    {
        foreach (var service in _baseServices)
        {
            containerBuilder.Add(service);
        }

        return containerBuilder.BuildServiceProvider(_options);
    }
}

I was then able to create a Controller after adding app.UseRouting() and app.UseEndpoints(...) in my startup class. Injecting IMyService was successfully resolved and I could use it as normal.

You could also test it by just adding app.ApplicationServices.GetRequiredService<IMyService>() in your Startup.Configure() method and see that the correct service is returned.

Upvotes: 1

Related Questions