5

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

Jinish
  • 1,983
  • 1
  • 17
  • 22
  • Why do you need to manually start it when the host will do that as part of Startup process? – Nkosi Apr 07 '20 at 23:51
  • 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? – Jinish Apr 08 '20 at 00:03
  • Another way I can think is to create a StartupFilter and load the configuration there and register the hostetservices after registering the StartupFilter? – Jinish Apr 08 '20 at 00:27
  • Just double checked. The automatic start does _not_ happen in our 3.1 implementation. We explicitely need `_ = app.ApplicationServices.GetService();` 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. – Markus Deibel Apr 08 '20 at 06:02
  • @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? – Panagiotis Kanavos Apr 08 '20 at 07:08
  • @Jinish 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. – Panagiotis Kanavos Apr 08 '20 at 07:10
  • @Jinish 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 request – Panagiotis Kanavos Apr 08 '20 at 07:13
  • 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. – Jinish Apr 08 '20 at 07:59

1 Answers1

5

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.

  • 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. – Jinish Apr 08 '20 at 08:04
  • 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>(); – zekeriya kocairi Apr 08 '20 at 09:30