Why is it when using Host.CreateDefaultBuilder()
hostBuilder, the service provider when configuring services can provide scoped services but not with WebApplication
The set up is as follows:
ConfigureServices(IServiceCollection services)
{
services.AddSingleton<Config>();
services.AddScoped<Transporter>();
services.AddHostedService<MyBackgroundService>(sp => new(
sp.GetRequiredService<Config>(),
sp.GetRequiredService<Transporter>(),
false
));
}
When using the HostBuilder
this works perfectly fine, but when I instead use a WebApplicationBuilder
it will throw:
System.InvalidOperationException: 'Cannot resolve scoped service 'Transporter' from root provider.'
I think it makes sense that the service provider wouldn't be able to use the scoped service for the HostedService as it isn't running in a scope, but how is the HostBuilder able to instantiate it anyway?
Either way as I change it over to use a WebApplicationBuilder
I would still like the HostedService to use the scoped Transporter
. Should MyBackgroundService
just accept an IServiceProvider
and create a scope? But I would like to know why there is a difference between the two/
Reason
When the HostBuilder
creates an IHostedService
instance, it automatically creates a scope, allowing scoped services to be resolved.
In contrast, when the WebApplicationBuilder
creates an IHostedService
instance, it does so within the root service provider, which cannot resolve scoped services.
Therefore, it is necessary to manually create a scope within the MyBackgroundService
class to resolve scoped services correctly. By using IServiceProvider
in MyBackgroundService
and creating a scope manually in the ExecuteAsync
method, scoped services can be resolved properly. This approach not only solves the scoped service resolution issue but also maintains code clarity and logical consistency.
Below is the adjusted code.
Config.cs
namespace WebApplication2
{
public class Config
{
public string Setting { get; set; } = "Default Setting";
}
}
Transporter.cs
using System;
namespace WebApplication2
{
public class Transporter
{
public void TransportData(string data)
{
Console.WriteLine($"Transporting data: {data}");
}
}
}
MyBackgroundService.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace WebApplication2
{
public class MyBackgroundService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly bool _someFlag;
public MyBackgroundService(IServiceProvider serviceProvider, bool someFlag)
{
_serviceProvider = serviceProvider;
_someFlag = someFlag;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (var scope = _serviceProvider.CreateScope())
{
var config = scope.ServiceProvider.GetRequiredService<Config>();
var transporter = scope.ServiceProvider.GetRequiredService<Transporter>();
transporter.TransportData(config.Setting);
}
await Task.Delay(1000, stoppingToken);
}
}
}
}
(.net version <=5.0) Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddSingleton<Config>();
services.AddScoped<Transporter>();
services.AddHostedService<MyBackgroundService>(sp => new MyBackgroundService(sp, false));
//Or simplified it like below
//services.AddHostedService(sp => new MyBackgroundService(sp, false));
}
(.net version >=6.0) Program.cs
builder.Services.AddSingleton<Config>();
builder.Services.AddScoped<Transporter>();
builder.Services.AddHostedService<MyBackgroundService>(sp => new MyBackgroundService(sp, false));
//builder.Services.AddHostedService(sp => new MyBackgroundService(sp, false));
Test Result