Is there a way to manually start BackgroundService in Asp.Net core 3.1
Asked Answered
L

1

7

I know prior to Asp.Net 3.0 (or 3.1), to manually start a BackgroundService, we could derive it from IHostedService instead and change the DI registration to:

services.AddSingleton<IHostedService, CustomHostedService>();

and then manually trigger the service start by injecting the service in the constructor, and calling StartAsync().

However, I can't seem to do that in Asp.Net Core 3.1. Having a look at the code for the StartAsync() method, background services are started before the app startup is completed.

public async Task StartAsync(CancellationToken cancellationToken = default)
{
    _logger.Starting();
    await _hostLifetime.WaitForStartAsync(cancellationToken);

    cancellationToken.ThrowIfCancellationRequested();
    _hostedServices = Services.GetService<IEnumerable<IHostedService>>();

    foreach (var hostedService in _hostedServices)
    {
        // Fire IHostedService.Start
        await hostedService.StartAsync(cancellationToken).ConfigureAwait(false);
    }

    // Fire IApplicationLifetime.Started
    _applicationLifetime?.NotifyStarted();

    _logger.Started();
}

What's the best way to manually trigger a background service to start?

Essentially, this is my setup:

Background Service

public MyBackgroundService(Func<string, IConsumer> consumerFactory)
{
    _consumerFactory = consumerFactory ?? throw new ArgumentNullException(nameof(consumerFactory));
}

Consumer factory registration

services.AddSingleton<Func<string, IConsumer>>(provider => providerName => provider.ConfigureConsumer(environment, providerName));

private static IConsumer ConfigureConsumer(this IServiceProvider provider, IHostEnvironment environment, string provideName)
{
    if (string.IsNullOrEmpty(provideName))
        throw new ArgumentNullException(nameof(provideName));

    var options = provider.GetRequiredService<IOptions<ConsumerConfig>>();
    if (options?.Value == null)
        throw new ArgumentNullException(nameof(options));

    return environment.IsDevelopment() 
        ? new Consumer(options, provider.GetTopics()) 
        : new Consumer((IOptions<ConsumerConfig>)options.SetSecurityOptions(provider), provider.GetTopics());
}

private static IOptions<Config> SetSecurityOptions(this IOptions<Config> config, IServiceProvider provider)
{
    var certService = provider.GetRequiredService<IVaultCertificateService>();
    ...
    return config;
}

Essentially this certService properties are only set when a request comes in and executes a middleware. And since when ExecuteAsync() in the background service tries to get an instance of consumer from the factory, is executed regardless, I dont have the IConsumer properly configured Thanks

Labrie answered 7/4, 2020 at 23:42 Comment(8)
Why do you need to manually start it when the host will do that as part of Startup process?Leverage
Some of the configuration information that is required to build one of the dependencies of the bckground service is only available when a certian middleware executes (when a request comes in). And that dependency needs to be registered as singleton, so hence I was wondering if I could manually start the HostedService once the the first request comes in? I welcome any suggestions, if there is a better approach to handle this scenario?Labrie
Another way I can think is to create a StartupFilter and load the configuration there and register the hostetservices after registering the StartupFilter?Labrie
Just double checked. The automatic start does not happen in our 3.1 implementation. We explicitely need _ = app.ApplicationServices.GetService<IXyzService>(); in Startup.Configure() to automatically start the background services. If this is not present the service is not resolved and thus not instantiated. We would need to send a request to one of the controllers where this service is injected, just as you describe.Spotty
@MarkusDeibel we don't. The interfaces haven't changed. What the OP did, using AddSingleton wasn't needed and was actually a bad idea, as it interferes with the background service lifecycle management - even after a service stops, it remains active in a zombie state. Worse, it introduces the entire scoped service problem - where is a singleton service going to get any scoped services it needs?Indisputable
@Labrie the original code was problematic from the start. You don't need to start/stop the service manually, you need to have it start/stop processing entries. Making it a singleton is a problem to begin with as it stays alive even after a call to Stop. That's why the doc samples don't use singletons.Indisputable
@Labrie are you sure the problem is one of start/stop instead of scoped services? A request in ASP.NET Core uses its own scope. Your own code could do the same, creating a scope each time it needs to process a requestIndisputable
Thanks for the comments guys. @PanagiotisKanavos, I don't have a singleton registration, that's what I said about how we could it in earlier versions. I am aware of scoped service issue with Singleton registrations. However, you are right in your comment, that the real issue is not to start/stop the service, instead triggering it to start processing messages.Labrie
S
7

Injected Hosted services start with the app and stops with it. Framework doesn't provide you manual control. There should be underlying reasons for that like. Of course, you can implement your own solutions with threads but if your wish to go on with Microsoft's built-in solutions you can use Queued Background Task. You will start a background service with an empty task queue. Service will be listening to your first task to be added to the queue. You can add a task whenever you wish then it will be running at background.

Sulphide answered 8/4, 2020 at 6:46 Comment(2)
yes I am aware of where HostedServices sits in the pipeline. The issue that I have is regarding construction of a dependency in the constructor of the service. The data required to construct that dependency is available when a certian middleware executes and since ExecuteAsync is fired parallely to the app startup, which is using that dependency, I dont have a correctly constucted dependency at that time.Labrie
Ok than. you can try lazily injecting the service to DI. So you can control when the DI will load the service to container and run the constructor of the service. I can be clearer after seeing your current app structure if it's open source project but theoretically, adding lazy service may be applied as solution like that : services.AddTransient<Lazy<LazyService>>();Sulphide

© 2022 - 2024 — McMap. All rights reserved.