Post-Build Addition of IHostedService

87 Views Asked by At

Is it possible to add an IHostedService to an IHost after calling HostApplicationBuilder.Build() and IHost.StartAsync()?

The application needs to be able to load plug-ins during use and have services provided in those plug-ins become hosted by the already-started IHost. Is this possible?

2

There are 2 best solutions below

2
Guru Stron On BEST ANSWER

AFAIK - no, at least in non-brittle way. The IHostedService infrastructure is build on top of the DI and build-in DI container is immutable after it has been build. Even if you substitute the container with some which allows runtime management of dependencies it still will not work because theIHostedService's are resolved and started upon the application start (docs, internal Host implementation starting services):

The hosted service is activated once at app startup

So working over default infrastructure to "just add" the hosted service is not possible.

The application needs to be able to load plug-ins during use and have services provided in those plug-ins become hosted by the already-started IHost. Is this possible?

But this requirement is totally satisfiable - just create a hosted service which will somehow (depended on your needs) determine/watch new plugins available and manage them (start and stop). Those plugins can be build over standard IHostedService interface or any one you deem appropriate. Something to get you started:

public class PluginWorker : BackgroundService
{
    private readonly ILogger<PluginWorker> _logger;
    private readonly List<IHostedService> _services = new();

    public PluginWorker(ILogger<PluginWorker> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            IHostedService newOne = await GetNewPlugin(stoppingToken);
            _services.Add(newOne);
            await newOne.StartAsync(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken cancellationToken)
    {
        await base.StopAsync(cancellationToken);
        await Task.WhenAny(
            Task.WhenAll(_services.Select(s => s.StopAsync(cancellationToken)).ToArray()), 
            Task.Delay(Timeout.Infinite, cancellationToken));
    }

    // TODO:
    private Task<IHostedService> GetNewPlugin(CancellationToken ct) => throw new NotImplementedException();
}
0
PatrickV On

In the unlikely event anyone else needs to build a similar house of cards, the only solution I came up with to this was to have two HostApplicationBuilders. The first one exclusively dealt with setting up the initial plug-ins. What it composed was then used as an input into the second builder. The only reason this was needed was because we needed to dynamically detect and load plug-ins, and we wanted logging to be working the same throughout the life of the application, including during plug-in discovery. For DI to know about the plug-ins, they had to be available before DI composition, but logging is not available until after DI composition. This approach did seem to work, but our need to support plug-ins went away, so it was never really fully tested.