Search code examples
c#.netasp.net-coreweb-applicationswebapi

HostBuilder ServiceProvider able to resolve scoped services?


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/


Solution

  • 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

    enter image description here