weikio / PluginFramework

Everything is a Plugin in .NET
MIT License
557 stars 106 forks source link

`FolderPluginCatalog` doesn't automatically load dependent assemblies in the same folder #46

Open nitz opened 3 years ago

nitz commented 3 years ago

Hello!

Real quick I wanted to say that I'm so glad I found this project, it's the exact amount of auto-magic I was hoping for In a plugin system!

So, I'm making use of a FolderPluginCatalog for now, just as I'm setting up the scaffolding for a project. One of the plugin assembles references a nuget package. I'm using the "xcopy deploy" method to copy my plugin assemblies (and their dependencies via <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>) to my plugins folder.

This is a slimmed down version of my PluginCoordinator:

    public class PluginCoordinator
    {
        private static readonly DirectoryInfo[] _pluginPaths = new[]
        {
            new DirectoryInfo("./plugins")
        };

        private static readonly Type _pluginBaseType = typeof(IPlugin);

        private static readonly Type[] _pluginTypes = new[]
        {
            typeof(IFooPlugin),
            typeof(IBarPlugin),
            typeof(IBazPlugin),
            /* and so on */
        };

        private static ILoggerFactory _loggerFactory { get; set; }

        public static int LoadPlugins(ILoggerFactory loggerFactory)
        {
            _loggerFactory = loggerFactory;

            var allCatalogs = new CompositePluginCatalog();

            foreach (DirectoryInfo pluginPath in _pluginPaths)
            {
                foreach (Type type in _pluginTypes)
                {
                    IPluginCatalog catalog = LoadPluginsOfType(pluginPath, type);
                    allCatalogs.AddCatalog(catalog);
                }
            }

            // initialize all catalogs syncronously
            allCatalogs.Initialize().Wait();

            // get all the plugins and do something fun with them!
            var plugins = allCatalogs.GetPlugins();

            return plugins.Count;
        }

        private static IPluginCatalog LoadPluginsOfType(DirectoryInfo pluginDirectory, Type pluginType)
        {
            TypeFinderCriteria? pluginCriteria = TypeFinderCriteriaBuilder.Create()
                .Implements(_pluginBaseType)
                .Implements(pluginType)
                .Build();

            if (pluginCriteria == null)
            {
                throw new InvalidOperationException("Failed to build plugin search criteria.");
            }

            var contextOptions = new PluginLoadContextOptions()
            {
                LoggerFactory = () => _loggerFactory.CreateLogger<PluginAssemblyLoadContext>(),
                //AdditionalRuntimePaths = new List<string> { pluginDirectory.FullName }
                /* ^^^ This is key! ^^^ */
            };

            FolderPluginCatalogOptions options = new()
            {
                IncludeSubfolders = false,
                PluginLoadContextOptions = contextOptions,
                TypeFinderOptions = new TypeFinderOptions()
                {
                    TypeFinderCriterias = new List<TypeFinderCriteria> { pluginCriteria }
                }
            };

            return new FolderPluginCatalog(pluginDirectory.FullName, options);
        }
    }

The line commented in the creation of the PluginLoadContextOptions is where I've found my problem. With the code as shown (not setting the additional runtime paths to include the exact same folder causes a FileNotFound exception when say the assembly containing the IFoos tries to load (thus needing it's NuGet dependency -- which is in the folder with it). However, adding the AdditionalRuntimePaths back in like it's written there: Everything loads peachy just like you'd imagine it should.

I can't tell if this is an issue from me using it wrong (and I'm expected to specify the folder not only in the FolderPluginCatalog constructor, but also through the context options,) or if it's something that the framework should handle and just doesn't.

Thanks again for the awesome project!

mikoskinen commented 3 years ago

Thank for reporting this! Based on the description it might be a bug and it should automatically include the plugin directory. I'll check and get back to you.

nitz commented 3 years ago

Awesome, no rush as the workaround I found seems to work fine! The more I was thinking about it too, it's very likely "technically correct" as is: The plugin directory wouldn't be on PATH, and the application's working directory isn't switched in there. It definitely does seem like it might be expected behavior by most users... but you know, they wouldn't call it DLL hell if loading them always made sense either 🙂

franklupo commented 2 years ago

News?