Augmenting the .NET Core 3.0 Generic Host

Battling a SOLID implementation of the Open-closed Principle

Published on 18 November 2019

Background

I love the .NET Core 3.0 Generic Host, I really do. As a framework for simplifying common scaffolding and lifetime management of long-running services, it's almost faultless. Unfortunately, the mere fact of being a framework rather than a library can lead to issues where, as a user of the framework, you're unable to accomplish a specific goal. For me, this happened while trying to get instrumentation written to a various EventSource instances to be output - in a configurable manner - through the Generic Host's logging infrastructure. Sounds simple huh, and it really ought to be. But it wasn't. Here's why:

The Open-Closed Principle

In the unlikely event you're not familiar with this principle, you can read about it here.

The Generic Host is a beacon of SOLID-design and seems to embrace the 'Open-closed principal' particularly closely. Perhaps this is due to the fact that, almost by definition, the Generic Host needs to be extremely extensible. Therefore, to ensure it's able to guide it's users into the pit of success, the implementation of the Generic Host closes many avenues for modifying its default behaviours.

Unfortunately there are instances where users have good reason to want to modify default behaviour. For example, there have been multiple feature requests for the Generic Host to provide a means for consumers to add behaviour to the start-up routine after the dependency injection container has been created but before it's used to resolve any IHostedService instances. In each of these instances, the maintainers of the Microsoft.Extensions repository have suggested alternatives for the specific use-case being mooted even though, in my opinion, these alternatives seem to be rather poor workarounds for what I believe to be a valid feature request.

Why do I believe it's a valid feature when so many other awesome .NET developers believe differently? Well, because the generic host itself uses this feature, as can be seen here:

_appServices = _serviceProviderFactory.CreateServiceProvider(containerBuilder);

if (_appServices == null)
{
    throw new InvalidOperationException($"The IServiceProviderFactory returned a null IServiceProvider.");
}

// resolve configuration explicitly once to mark it as resolved within the
// service provider, ensuring it will be properly disposed with the provider
_ = _appServices.GetService<IConfiguration>();

As you can see, for perfectly valid reasons, the Generic Host needs to perform an operation after the service provider has been created but before any further types are resolved. I do not believe it's only the Generic Host infrastructure code that requires this entry-point. Indeed, this is the feature I required to ensure I could correctly instantiate and configure a singleton EventListener instance prior to events being written to (and potentially lost from) various EventSource instances throughout my code.

So, rather than accept a workaround and potentially compromise some core requirements, I decided to see if there was a way I could implement the above feature without straying too far from canonical Generic Host start-up code.

IHostApplicationLifetime

The GenericHost has an IHostApplicationLifetime interface which is responsible for coordinating application lifetime notifications. Registered with the dependency injection container as a singleton, it is ordinarily injected into IHostedService instances, providing the ability for any service to request that the application be stopped.

However, this interface is also injected into the various IHostLifetime implementations (i.e. ConsoleLifetime for console applications, WindowsServiceLifetime for Windows services, etc) and, as such, it is instantiated once, after the dependency injection container is created but before any IHostedService instances are resolved.

Sounds useful huh.

Best of all, this interface provides the means to react to application start and stop events via a novel use of CancellationToken instances. I therefore chose this as my entry point for providing required functionality.

ApplicationLifetimeEx (ugh)

Inheriting from the default the IHostApplicationLifetime implementation, registering callbacks for the ApplicationStarted and ApplicationStopping cancellation tokens and then registering the derived class as the new IHostApplicationLifetime implementation in the Generic Host's DI container worked perfectly. Using this approach I was able to leverage standard generic host start-up code and functionality to resolve an EventListener, with configuration, prior to any further types being instantiated.

An example of this can be found in the development branch my Cogenity.Extensions repository here.

Sounds good. So why only in the development branch?

Well, this approach has a lot of drawbacks, the most significant of which being that it fails to adhere to another important OO design principal; that of 'Composition over inheritance'. In fact, the above approach - in it's current form - is not composable at all. If another library wanted to hijack the IHostApplicationLifetime interface in a similar way, then my implementation (and associated functionality) would be entirely lost.

As the Generic Host goes to great lengths to ensure that composability is maintained at all times and in all configurations this approach is probably not one I would recommend. Given there are no better options at the current time, I will probably proceed with this implementation as, to me, it is a white box, but I don't intend to make a packaged version available for general consumption.

That said, when the suggested decoration extensions are made available, decorating the default IHostApplicationLifetime implementation will become a composable operation and this approach could suddenly become a lot more attractive.

Until then...

... I kind of hope the project maintainers will take another look at these feature requests and provide a more holistic solution. Perhaps something along the lines of:

private static async Task Main(string[] args)
{
    var builder = Host.CreateDefaultBuilder(args)
        .ConfigureServices(
            services =>
            {
                ...
            })
        .ConfigureApplicationLifetime(
            (applicationLifetime, appServices) =>
            {
                applicationLifetime.ApplicationStarted.Register(() => [do something with appServices])
            }
        );

    await builder
        .Build()
        .RunAsync();
}

I think this could be quite nice as the GenericHost could then move the

// resolve configuration explicitly once to mark it as resolved within the
// service provider, ensuring it will be properly disposed with the provider
_ = _appServices.GetService<IConfiguration>();

block into a ConfigureApplicationLifetime configuration call back within the CreateDefaultBuilder call.

Of course, this signature would provide all sorts of opportunities for naughtiness and smells and, while I could probably implement it using the method described above, I'm very much hoping the great minds behind this project might be able to propose something a little safer.