Scrutor icon indicating copy to clipboard operation
Scrutor copied to clipboard

How To Handle Modular Startup Config from Plugins?

Open aloksharma1 opened this issue 5 years ago • 1 comments

please read below scenario:

in a plugin based application built with Plugin Library DotnetCorePlugins where a single host application serves with micro plugins everything is working well and good. But i need to do startup service registration from these plugins as needed so say plugin1 introduces a middleware, plugin2 a pipeline, plugin3 some mediatr service etc...

i dig into OrchardCore & Orchard does that by using a StartupBase class but i am unable to find out how they are doing it [if my assumption is correct orchard uses msbuild for plugins unlike loadcontext of this library].

my requirements and structure is different from orchard, but i like the idea of having a StartupBase class where i can define configuration order and service init order and it gets called on main host app initilization can someone guide me to the right way to do this, i am ok with even minimal flow steps as long as its clear to understand. the Plugin Startup files must handle the defined order in host and be injected into main startup bus.

i found this lib and it seems to work similar to what i want in microsoft docs, all i want to know how to handle services in defined order?

thanks

aloksharma1 avatar Apr 09 '21 13:04 aloksharma1

You can use MEF (Managed Extensibility Framework) to implement this process. I have done it myself using an interface as follows :

/// <summary>
/// Provides with an interface that allows external libraries to register types to the services collection.
/// This interface is used with MEF : Managed Extensibility Framework.
/// The implementation class must be decorated with the attribute <see cref="System.ComponentModel.Composition.ExportAttribute"/> attribute,
/// with <see cref="IAddServiceExport"/> type as contract type.
/// </summary>
public interface IAddServiceExport
{
    /// <summary>
    /// When implemented, this method should add types to the services collection.
    /// </summary>
    /// <param name="services">The services collection to act on.</param>
    /// <param name="configuration">The application configuration.</param>
    void AddServices(IServiceCollection services, IConfiguration configuration);
}

The I define options for export ...

/// <summary>
/// Defines options to configure export services.
/// </summary>
public sealed class ExportServiceOptions
{
    /// <summary>
    /// Initializes a default instance of <see cref="ExportServiceOptions"/> class.
    /// </summary>
    public ExportServiceOptions() { }

    /// <summary>
    /// Gets or sets the path to the directory to scan for assemblies to add to the catalog.
    /// if not defined, the system will look to the application current directory.
    /// </summary>
    public string Path { get; set; } = System.IO.Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)!;

    /// <summary>
    /// Gets or sets the pattern to search with. The format of the pattern should be the same as specified for GetFiles.
    /// If not defined, the system will use the <see langword="*.dll"/> pattern.
    /// </summary>
    public string SearchPattern { get; set; } = "*.dll";

    /// <summary>
    /// Gets or sets whether or not to search in sub-directories.
    /// The default value is <see langword="false"/>.
    /// </summary>
    public bool SearchSubDirectories { get; set; }
}

used by an extension method :

 /// <summary>
    /// Adds and configures registration of services using the<see cref="IAddServiceExport"/> implementations found in the path.
    /// This method uses MEF : Managed Extensibility Framework.
    /// </summary>
    /// <param name="services">The collection of services.</param>
    /// <param name="configuration">The application configuration.</param>
    /// <param name="configureOptions">A delegate to configure the <see cref="ExportServiceOptions"/>.</param>
    /// <returns>The <see cref="IServiceCollection"/> instance.</returns>
    /// <exception cref="ArgumentNullException">The <paramref name="services"/> is null.</exception>
    /// <exception cref="ArgumentNullException">The <paramref name="configureOptions"/> is null.</exception>
    /// <exception cref="InvalidOperationException">The operation failed. See inner exception.</exception>
    public static IServiceCollection AddXServiceExport(
        this IServiceCollection services,
        IConfiguration configuration,
        Action<ExportServiceOptions> configureOptions)
    {
        ArgumentNullException.ThrowIfNull(services);
        ArgumentNullException.ThrowIfNull(configuration);
        ArgumentNullException.ThrowIfNull(configureOptions);

        var definedOptions = new ExportServiceOptions();
        configureOptions.Invoke(definedOptions);
        services.AddServiceExport(configuration, definedOptions);

        return services;
    }

    private static void AddServiceExport(
        this IServiceCollection services,
        IConfiguration configuration,
        ExportServiceOptions options)
    {
        try
        {
            using var directoryCatalog = options.SearchSubDirectories
                ? new RecursiveDirectoryCatalog(options.Path, options.SearchPattern)
                : (ComposablePartCatalog)new DirectoryCatalog(options.Path, options.SearchPattern);

            var importDefinition = BuildAddImportDefinition();

            using var aggregateCatalog = new AggregateCatalog();
            aggregateCatalog.Catalogs.Add(directoryCatalog);

            using var compositionContainer = new CompositionContainer(aggregateCatalog);
            var exportServices = compositionContainer
                .GetExports(importDefinition)
                .Select(def => def.Value)
                .OfType<IAddServiceExport>();

            foreach (var export in exportServices)
                export.AddServices(services, configuration);
        }
        catch (Exception exception) when (exception is NotSupportedException
                                        || exception is System.IO.DirectoryNotFoundException
                                        || exception is UnauthorizedAccessException
                                        || exception is ArgumentException
                                        || exception is System.IO.PathTooLongException
                                        || exception is ReflectionTypeLoadException)
        {
            throw new InvalidOperationException("Adding exports failed. See inner exception.", exception);
        }
    }

    private static ImportDefinition BuildAddImportDefinition()
        => new(
                _ => true,
                typeof(IAddServiceExport).FullName,
                ImportCardinality.ZeroOrMore,
                false,
                false);
}

You can find the complete code here : https://github.com/Francescolis/Xpandables.Net/tree/Net6.0/Xpandables.Net/Cqrs/Extensibility

Francescolis avatar Mar 14 '22 16:03 Francescolis