dotnet / extensions

This repository contains a suite of libraries that provide facilities commonly needed when creating production-ready applications.
MIT License
2.59k stars 741 forks source link

Adding an initialization interface in generic host #1151

Closed Yves57 closed 5 years ago

Yves57 commented 5 years ago

Is your feature request related to a problem? Please describe.

I use the generic host architecture for a headless service. Initialization code (resource checking,...) is scattered in several assemblies. And I want to be sure that this code is called before the background services startup.

Describe the solution you'd like

I would like an interface like this:

public interface IHostInitialization
{
    Task InitializeAsync();
}

I would be able to register several implementations, and these interfaces would be called one by one somewhere around here.

davidfowl commented 5 years ago

Why not use IHostedService and just put it first in the list?

Yves57 commented 5 years ago

Because it is more difficult to control the order of registration.

Yves57 commented 5 years ago

And thank you for the quick answer!

davidfowl commented 5 years ago

Because it is more difficult to control the order of registration.

What happens when you have multiple IHostInitialization instances registered. What then? What are you trying to run before?

Yves57 commented 5 years ago

In my case, a module is related to a database (so I have to check database), another is related to blob storage (so I have to create the blob container if necessary), etc. The order of the IHostInitialization is not really important. My only request is that they have to be called before my background services.

davidfowl commented 5 years ago

Why can't you register the hosted service first reliably? Is it a library?

Yves57 commented 5 years ago

No, it is a backend service. I agree that it is possible to add the IHostedService first in the list. And I have found a solution for myself that works fine (I call manually my registered IHostInitialization instances one by one in a scope before starting the host).

And in my opinion I see the "add IHostedService first in list" solution more as a hack, no? It seems to be easy to break unintentionally.

I did the suggestion to have an out of the box more generic solution.

And about the IHostInitialization calling order, it may possible to imagine a Priority property.

davidfowl commented 5 years ago

I wouldn't consider it a hack it's actually by design. Registration order is the order in which hosted services run in, it's well defined actually.

I did the suggestion to have an out of the box more generic solution.

I don't see how this is more generic. I don't know what it solves.

And about the IHostInitialization calling order, it may possible to imagine a Priority property.

I think that's actually a much worse design. What's priority and how do you order things that have the same priority? What if somebody wants to really go first.

This all gets interesting when you don't own all of the components and you have no control over the composition order. I don't know of a system that works well other than declaring dependencies and doing a topological sort on that to find the right ordering. Other systems based on just a number, don't work as well and you always have the problem of "well this component really needs to go first".

We opted to go for a simple registration order since it's logical and works well. Where it falls over is the library scenario or the scenario where you don't control how those bits are composed in the pipeline but I don't have a good answer for that.

That said, I don't think there's anything fundamentally wrong having another interface, it just needs to be a bit more useful I think.

Yves57 commented 5 years ago

If I understand correctly your solution, I would have to write something like:

=========== Module A ============
public static IServiceCollection AddModuleAInitialization(this IServiceCollection services)
{
     services.AddSingleton<IHostedService, MyModuleAInitialization>();
}

public static IServiceCollection AddModuleAOtherServices(this IServiceCollection services) { ... }

=========== Module B ============
public static IServiceCollection AddModuleBInitialization(this IServiceCollection services)
{
     services.AddSingleton<IHostedService, MyModuleBInitialization>();
}

public static IServiceCollection AddModuleBOtherServices(this IServiceCollection services) { ... }

=========== Main ============

new HostBuilder()
    .ConfigureServices((_, services) => 
    {
        services.AddModuleAInitialization();
        services.AddModuleBInitialization();
        services.AddModuleAOtherServices();
        services.AddModuleBOtherServices();
    });

I don't find that very easy to use, but I understand your sentence "We opted to go for a simple registration order since it's logical and works well".

analogrelay commented 5 years ago

We could update the docs to make this pattern a little clearer in BackgroundService. In your background service, you can directly overload StartAsync to provide "initialization" logic that you want to complete before other hosted services are started:

public class MyBgService : BackgroundService
{
    public override async Task StartAsync(CancellationToken ct)
    {
        // Do my custom initialization logic.
        // All the HostedServices registered _after_ this one will wait until this
        // task completes before starting
        await Task.Delay(1000);
        await base.StartAsync(ct);
    }

    // My executeasync, etc..
}
analogrelay commented 5 years ago

If you need advanced scheduling like this, going directly to IHostedService is the best option.