Search code examples
.netloaderkephas

How to provide a different strategy for loading application assemblies in Kephas bootstrapper


The default implementation of loading the application assemblies in a Kephas application is to include all assemblies found in the application folder. What if I need to provide a different folder, or if I want to support plugins? How can I specify a different folder, or a list of folders to include in search?


Solution

  • Almost everything in Kephas is configurable, and the IAppRuntime service is no exception. However, unlike the common application services, this is not registered through the [AppServiceContract] attribute, instead it is set and configured during the bootstrap procedure. The main reason behind this behavior is that the IAppRuntime service is required before the composition/IoC container is initialized.

    You can find a possible implementation below:

    /// <summary>
    /// An application runtime supporting plugins.
    /// Each plugin is found in its own folder in the Plugins sub directory of the application root folder.
    /// </summary>
    public class PluginsAppRuntime : DefaultAppRuntime
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="PluginsAppRuntime"/> class.
        /// </summary>
        /// <param name="assemblyLoader">Optional. The assembly loader.</param>
        /// <param name="logManager">Optional. The log manager.</param>
        /// <param name="assemblyFilter">Optional. A filter for loaded assemblies.</param>
        /// <param name="appLocation">Optional. The application location. If not specified, the current
        ///                           application location is considered.</param>
        public PluginsAppRuntime(
            IAssemblyLoader assemblyLoader = null,
            ILogManager logManager = null,
            Func<AssemblyName, bool> assemblyFilter = null,
            string appLocation = null)
            : base(assemblyLoader, logManager, assemblyFilter, appLocation)
        {
            pluginsFolder = Path.Combine(this.GetAppLocation(), "Plugins");
            this.PluginsFolder = Path.GetFullPath(pluginsFolder);
        }
    
        /// <summary>
        /// Gets the pathname of the plugins folder.
        /// </summary>
        public string PluginsFolder { get; }
    
        /// <summary>
        /// Enumerates the application directories containing assemblies to be loaded.
        /// </summary>
        protected override IEnumerable<string> GetAppAssemblyDirectories()
        {
            var rootDirectory = this.GetAppLocation();
            var appDirectories = new List<string> { rootDirectory };
    
            appDirectories.AddRange(this.EnumeratePluginFolders());
    
            var logger = this.GetLogger();
            logger.Info($"Loading application from directories: {string.Join(", ", appDirectories)}");
    
            return appDirectories;
        }
    
        /// <summary>
        /// Enumerates the root bin folders for the plugins.
        /// </summary>
        public IEnumerable<string> EnumeratePluginFolders()
        {
            var binPluginsFolder = this.PluginsFolder;
    
            if (Directory.Exists(binPluginsFolder))
            {
                var pluginsDirectories = Directory.EnumerateDirectories(binPluginsFolder);
                return pluginsDirectories;
            }
    
            return new string[0];
        }
    }
    
    /// <summary>
    /// For the sake of simplicity, add an extension method to the ambient services builder to make use of this new service.
    /// </summary>
    public static class PluginsAmbientServicesBuilderExtensions
    {
        /// <summary>
        /// Sets the plugins-enabled application runtime to the ambient services.
        /// </summary>
        /// <param name="ambientServicesBuilder">The ambient services builder.</param>
        /// <param name="assemblyFilter">Optional. A filter specifying the assembly (optional).</param>
        /// <param name="appLocation">Optional. The application location (optional). If not specified, the
        ///                           assembly location is used.</param>
        /// <returns>
        /// The provided ambient services builder.
        /// </returns>
        public static AmbientServicesBuilder WithPluginsAppRuntime(
            this AmbientServicesBuilder ambientServicesBuilder,
            Func<AssemblyName, bool> assemblyFilter = null,
            string appLocation = null)
        {
            Requires.NotNull(ambientServicesBuilder, nameof(ambientServicesBuilder));
    
            var ambientServices = ambientServicesBuilder.AmbientServices;
            var assemblyLoader = new DefaultAssemblyLoader();
            ambientServices.RegisterService<IAssemblyLoader>(assemblyLoader);
    
            return ambientServicesBuilder.WithAppRuntime(
                new PluginsAppRuntime(
                    assemblyLoader,
                    ambientServices.LogManager,
                    assemblyFilter: assemblyFilter,
                    appLocation: appLocation));
        }
    }
    
    /// <summary>
    /// Last, but not least, define the application root object.
    /// </summary>
    public class App : AppBase
    {
        /// <summary>Configures the ambient services asynchronously.</summary>
        /// <param name="appArgs">The application arguments.</param>
        /// <param name="ambientServicesBuilder">The ambient services builder.</param>
        /// <param name="cancellationToken">The cancellation token.</param>
        /// <returns>The asynchronous result.</returns>
        protected override async Task ConfigureAmbientServicesAsync(
            string[] appArgs,
            AmbientServicesBuilder ambientServicesBuilder,
            CancellationToken cancellationToken)
        {
            ambientServicesBuilder
                .WithAppConfiguration(new DefaultAppConfiguration())
                .WithPluginsAppRuntime()
                .WithMefCompositionContainer();
        }
    }
    
    /// <summary>
    /// Now everything is ready to go!
    /// </summary>
    public static class Program
    {
        private static async Task<int> Main(string[] args)
        {
            try
            {
                var result = await new App().BootstrapAsync(args).PreserveThreadContext();
                return 0;
            }
            catch (Exception ex)
            {
                return 100;
            }
        }
    }