Code Monkey home page Code Monkey logo

cogenity.extensions's Introduction

Cogenity.Extensions.Hosting.Composition

Light-weight, runtime-composition for the .NET Core 3.0 Generic Host.

Background

The .NET Generic Host provides scaffolding for implementing software that runs across a variety of environments and platforms. It provides common resources - and patterns to supplement these resources - such as:

  • Dependency injection (DI)
  • Logging
  • Configuration

Furthermore, by encapsulating/decorating your software as an IHostedService, the Generic Host provides graceful start-up and shutdown in accordance with the lifetime under which it is currently running (i.e. console application, windows service, web host, etc).

In short, it's great! Since it's introduction in .NET Core 2.1, the Generic Host has quickly become the go-to pattern for implementing long-running services in .NET Core.

Unfortunately, the Generic Host doesn't provide any means of composing services from non-referenced assemblies at runtime. While the WebHost has support for IHostingStartup and other libraries for accomplishing runtime composition are available, they tend to be quite complex and/or there is little to no guidance on how to integrate these libraries in a way that works reliably with the Generic Host.

This library aims to address these issue by providing a light-weight means to quickly and reliably load and register services from non-referenced assemblies into the Generic Host.

NOTE: This library does not intend to be a generic, zero-knowledge plugin system. Microsoft.Extensions.Hosting.Composition uses configuration to specify and configure modules providing increased reliability and flexibility while decreasing start-up times compared to the directory / assembly scanning approaches typically used by plug-in systems. If you feel you require a plug-in system, you can find a good example of one here.

Usage

Usage is very straight-forward and can be accomplished in a few steps. Here are the steps I used to implement the GenericHostConsole sample:

Step 1 - UseComposition

In the GenericHostConsole project, add a reference to Cogenity.Extensions.Hosting.Composition and change the line Host.CreateDefaultBuilder() to ComposableHost.CreateDefaultBuilder() as shown below:

private static async Task Main(string[] args)
{
    var builder = ComposableHost.CreateDefaultBuilder(args) // <-- Change 'Host' to 'ComposableHost'
        .ConfigureHostConfiguration(
            configurationBuilder => 
            {
                configurationBuilder
                    .AddCommandLine(args)
                    .AddYamlFile(args[0]) // <-- Add configuration
            });

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

As you can see, we pass an additional configuration file into the Host configuration but we'll get back to that in section 3.

Step 2 - Implement IModule

For any assembly you'd like to compose into your Generic Framework host, add a reference to Cogenity.Extensions.Hosting.Composition.Abstractions and add a new class that implements IModule. Within the Configure method of the interface, compose your services as you would from a normal generic host. Here we're registering configuration, services and logging within the GenericHostConsole.Writer sample:

public class Module : IModule
{
    public IHostBuilder Configure(IHostBuilder hostbuilder, string configurationSection)
    {
        return hostbuilder
            .ConfigureServices(
                (hostBuilderContext, serviceCollection) =>
                {
                    serviceCollection.AddOptions<Configuration>().Bind(hostBuilderContext.Configuration.GetSection(configurationSection));
                    serviceCollection.AddSingleton<IHostedService, Service>();
                })
            .ConfigureLogging((hostingContext, logging) => logging.AddConsole());
    }
}

Step 3 - Configuration

Back in the GenericHostConsole project, we need to supply configuration information to ComposableHost call. I like using yaml for this kind of configuration so I first install the NetEscapades.Configuration.Yaml package then add a new yaml file to the project named 'config.yml' (remembering to set it's Copy To Output Directory setting to Copy If Newer).

Then I populate the config.yaml file with the following:

composition:
  modules:
    - name: ConsoleWriter
      assembly: GenericHostConsole.Writer
      configurationSection: consolewriterConfiguration
      optional: true

consolewriterConfiguration:
  writeIntervalInSeconds: 2

When loaded, this configuration will do the following:

  1. Instruct the module loader to load the GenericHostConsole.Writer assembly. If no path is supplied, it looks in the directory containing the currently executing loading assembly.
  2. Give the loaded module a distinct name which allows multiple modules of the same type to be loaded concurrently.
  3. Pass the specified configurationSection to the module from which to load it's configuration
  4. State that loading this module is optional - no exception will be thrown if the module could not be located.

Step 4 - Run

If you run the GenericHostConsole app now.... you'll see the following:

Warning: The module named 'ConsoleWriter' could not be loaded as the assembly 'GenericHostConsole.Writer' could not be found
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Source\Repositories\Microsoft.Extensions.Hosting.PlugIns\samples\GenericHostConsole\bin\Debug\netcoreapp3.0

Yup, it starts with a warning that it couldn't locate a named module then does nothing. Now, if you copy the build artifacts from GenericHostConsole.Writer (.\samples\GenericHostConsole.Writer\bin\debug\netstandard2.0) to the build directory for GenericHostConsole (.\samples\GenericHostConsole\bin\Debug\netcoreapp3.0) then re-run the GenericHostConsole app you should now see the following:

info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Source\Repositories\Microsoft.Extensions.Hosting.PlugIns\samples\GenericHostConsole\bin\Debug\netcoreapp3.0
info: GenericHostConsole.Writer.Service[0]
      Here!
info: GenericHostConsole.Writer.Service[0]
      Here!

Yup, no warning and the the text Here! written to the console every two seconds. This is the GenericHostConsole.Writer.Service following it's configuration and logging settings.

Done, you've composed functionality into your generic host!

Contributing

Any suggestions/contributions of enhancements/bug fixes gratefully received.

cogenity.extensions's People

Contributors

ibebbs avatar

Stargazers

 avatar

Watchers

 avatar  avatar

cogenity.extensions's Issues

Composed assemblies unable to resolve hosted services - RFC

Description

Due to the way in which composed assemblies are loaded into a [semi-]isolated AssemblyLoadContext, implementations of services defined in 'common' assemblies are not available to assemblies loaded as part of the compostion.

This issue outlines the cause of this behaviour and considers various ways to resolve this issue. Additional suggestions regarding alternatives are welcomed.

Cause

Due to the requirement of needing to allow additional modules to participate in host composition, assemblies are being loaded at composition, not build time, as shown below:

var builder = Host.CreateDefaultBuilder(args)
    .ConfigureHostConfiguration(configurationBuilder => configurationBuilder.AddCommandLine(args))
    .UseComposition(config => config.AddYamlFile(args[0]), "composition"); // <- Assemblies loaded here
    .ConfigureServices(, serviceCollection) => serviceCollection.AddSingleton<IEventBus, EventBus>())

await builder
    .Build() // <- Not here
    .RunAsync();

This results in the AssemblyLoadContext used to isolate module assemblies loading new instances of 'common' assemblies rather than sharing those loaded by the host service (i.e. the IEventBus in the above example).

Suggestion 1

Remove the use of AssemblyLoadContext to ensure all assemblies share a common set of dependencies.

Pros:

  • Simplifies everything

Cons:

  • No longer able to load multiple instances/versions of specific assemblies

Suggestion 2

"Touch" common assemblies to ensure they're loaded prior to loading addition module assemblies.

Something like this:

var builder = Host.CreateDefaultBuilder(args)
    .ConfigureHostConfiguration(configurationBuilder => configurationBuilder.AddCommandLine(args))
    .UseComposition<IEventBus>(config => config.AddYamlFile(args[0]), "composition"); // <- Assemblies loaded here
    .ConfigureServices(, serviceCollection) => serviceCollection.AddSingleton<IEventBus, EventBus>())

Where using a generic overload of UseComposition would cause the assemblies to be loaded into the host prior to loading the external assemblies.

Pros:

  • Still pretty simple

Cons:

  • Ugly and could get extremely onorous

Suggestion 3

Provide a means of interacting more closely with the HostBuilder's Build() process such that external assemblies are loaded only after common assemblies have been loaded/registered with the service container.

A PoC of this approach has been implemented in this branch and seems to be working well. This is achieved by calling ComposableHost.CreateDefaultBuilder instead of Host.CreateDefaultBuilder and works by decorating/wrapping the HostBuilder in a ComposableHostBuilder instance allowing configuration and service registrations to be interrogated prior to loading external assemblies and building the host. An example is shown below:

private static async Task Main(string[] args)
{
    var builder = ComposableHost.CreateDefaultBuilder(args) // <- Use ComposableHost
        .ConfigureHostConfiguration(
            config =>
            {
                config.AddYamlFile(args[0]); // <- Provides host runtime composition configuration
                config.AddCommandLine(args);
            })
        .UseComposition("composition") // <- Provides composition configuration section
        .ConfigureServices(
            services =>
            {
                services.AddSingleton<Common.IEventBus, Common.EventBus>();
                services.AddSingleton<IHostedService, Ping.Service>();
            });

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

Pros:

  • Address issues outlined above
  • Removes the need to provide configuration directly to the UseComposition extension method
  • Allows logging providers/loggers for library debugging to be configured/injected in exactly the same way as logging providers/loggers for application code
  • Looks more like canonical Microsoft.Extensions code

Cons:

WRT the second point, the PoC doesn't suffer from the issues outlined in the justification for removing the second DI container (i.e. duplicate singleton services getting created) as it is only used to resolve the module loader instance but neatly causes dependent assemblies to be loaded before this happens. It may be possible to remove the second DI container but with significant increase in complexity (i.e. exacerbating the first point).

Suggestion 4

Allow both the current functionality and the solution provided in #3 via different method overloads.

Pros:

  • Versatility and speed where required
    Cons:
  • Complexity and confusion when reporting/debugging issues

Others?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.