Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composed assemblies unable to resolve hosted services - RFC #5

Closed
ibebbs opened this issue Nov 11, 2019 · 0 comments
Labels

Comments

@ibebbs
Copy link
Owner

@ibebbs ibebbs commented Nov 11, 2019

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?

@ibebbs ibebbs added the discussion label Nov 11, 2019
@ibebbs ibebbs closed this in 432773a Nov 18, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
1 participant
You can’t perform that action at this time.