weikio / PluginFramework

Everything is a Plugin in .NET
MIT License
549 stars 105 forks source link

ASP.NET Core: Configure the default plugin #24

Closed mikoskinen closed 3 years ago

mikoskinen commented 4 years ago

Background

Plugin Framework's ASP.NET Core package contains code which allows the developer to automatically register the plugins into the dependency injection system. Given the following code:

        services.AddPluginFramework()
            .AddPluginCatalog(folderPluginCatalog)
            .AddPluginType<IOperator>();

All the IOperators are available through IEnumerable:

    public CalculatorController(IEnumerable<IOperator> operators)
    {
        _operators = operators;
    }

In addition, a single IOperator can also be injected:

    public CalculatorController(IOperator myOperator)
    {
        _myOperator = myOperator;
    }

The problem:

The problem with injecting a single IOperator is that the developer can't control which IOperator is returned. This is hard coded as FirstOrDefault in AddPluginType:

            var result = GetTypes<T>(sp);

            return result.FirstOrDefault();

Enhancement:

The developer should be able to control which IOperator is the "default". This would be useful in scenarios where only one plugin of a given type can be enabled (through UI or through configuration, for example).

Maybe the ideal solution for this could be through IOptions. The AddPluginType should contain a parameter which can be used as a syntactic sugar but under the hood, this should just configure the default option.

mikoskinen commented 3 years ago

The thing that needs to be modified is Weikio.PluginFramework.AspNetCore / AddPluginType. The code for registering the default type is currently this:

        var serviceDescriptorSingle = new ServiceDescriptor(typeof(T), sp =>
        {
            var pluginProvider = sp.GetService<PluginProvider>();
            var result = pluginProvider.GetTypes<T>();

            return result.FirstOrDefault();
        }, serviceLifetime);

We should be able to approach this with a new option class, something like "DefaultPluginOption". This class should require only one property, a func which takes an IEnumarable of types and returns a single Type. As the default implementation it should use types.FirstOrDefault() like the current implementation.

The configuration should happen using named options where the name is the name of type. In the code above we should resolve IOptionsMonitor from the service provider and then get the named option using the typeof(T).Name. The option returned from the sp should be used to resolve the default type.

This feature should be usable in two ways:

  1. Manually configuring the DefaultPluginOption using services.Configure:

        services.Configure<DefaultPluginOption>(nameof(IOperator), option =>
        {
    
        });
  2. Through an optional parameter in AddPluginType:

        services.AddPluginFramework()
            .AddPluginCatalog(folderPluginCatalog)
            .AddPluginType<IOperator>(configureDefault: option =>
            {
    
            });

Samples/WebApp should be updated to show this feature.

mikoskinen commented 3 years ago

Let us tweak this a bit. Instead of "a func which takes an IEnumarable of types and returns a single Type", this should be:

Func<IServiceProvider, IEnumerable<Type>, Type>