Reputation: 4458
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
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