natemcmaster / DotNetCorePlugins

.NET Core library for dynamically loading code
Apache License 2.0
1.61k stars 227 forks source link

[Question] How do I load Plugin that depends on another plugin #240

Closed Palatis closed 1 year ago

Palatis commented 2 years ago

I have a WPF+Net6 solution, say:

Problem:

When Plugin2 (which refs Plugin1), the xaml (like Plugin1UserControl) loads fine, but Plugin3 fails to find the xaml resources. throws XamlParseException complaining about "/Plugin1;component/plugin1usercontrol.xaml" not found.

The behavior is pretty random, somethings Plugin2 gets the xaml, sometimes Plugin1.

I tried:

load all abstract plugins (ie. Plugin1 with its satellites (ie. NuGet dlls) in this case) into the AssemblyLoadContext.Default, it complains about InvalidCastException ([A]Plugin2 vs. [B]Plugin2), I also add types from Plugin1 into shared types.

var shared = ...;
var shared2 = shared;
var dlls = ...;
foreach (var dll in dlls) 
{
    // tried with both config.PreferSharedTypes = true and false
    var loader = PluginLoader.CreateFromAssemblyFile(dll, true, shared, config => config.PreferSharedTypes = true);
    var assembly = loader.LoadDefaultAssembly();
    var references = assembly.GetReferencedAssemblies();
    if (assembly.GetExportedTypes().Any(p => typeof(IPlugin).IsAssignableFrom(p) && p.IsAbstract))
    {
        AssemblyLoadContext.Default.LoadFromAssemblyPath(assembly.Location);
        foreach (var reference in references)
        {
            try { AssemblyLoadContext.Default.LoadFromAssemblyPath($"{PLUGIN_DIR}{Path.DirectorySeraratorChar}{reference.Name}.dll"); }
            catch { }
        }
        // tried either with or without the next line
        shared2 = shared2.Concat(assembly.GetExportedTypes().Where(p => p.IsAbstract)).ToArray();
    } else {
        loader.Dispose();
    }
}

foreach (var dll in dlls)
{
    var loader = PluginLoader.CreateFromAssemblyFile(dll, true, shared2, config => config.PreferSharedTypes = true);
    var assembly = loader.LoadDefaultAssembly();
    var plugins = assembly.GetExportedTypes().Where(p => typeof(IPlugin).IsAssignableFrom(p) && !p.IsAbstract);
    if (plugin.FirstOrDefault() != null)
    {
        foreach (var plugin in plugins)
            try { Plugins.Add((IPlugin)Activator.CreateInstance(plugin)); }
            catch { }
    } else {
        loader.Dispose();
    }
}

but neither of the 4 combinations works.

What can I do now? Currently I'm falling-back to NOT sharing UI classes between plugins (ie. no Plugin1UserControl classes). Is there any other solution to this?

rokenbuzz commented 2 years ago

I am working toward similar goals, .net 6, WPF, with this plugin library. I'm just building a simple test solution to exercise the concepts. Mine is working, but it's slightly more simple than yours. I'd be willing to add to mine in the hope of reproducing your problem, but I'm not sure what yours does, especially Plugin1.dll.

I have a Contracts library with just an interface file, defining a method to return a UserControl. Two wpf libraries, each with a custom user control, and separate resource dictionary to define a style it uses. The interface implementation returns an instance of this control. And my wpf test app that finds the two plugins, instantiates, and gets the controls to populate the UI.

So I would need to add the abstract class like you say you have in Plugin1, and any xaml defined in it. Can you share you project or explain it further?

mphipps1 commented 2 years ago

I am having the same problem without WPF (so you can rule that out) as the problem. I have multiple plugins that depend on a common class. The common class is in the plugins directory. I can load the common class, but trying to cast it fails. @natemcmaster - any ideas? Is this something that changed for .NET Core 6, maybe? Edit - another data point - the shared dependency is strongly named (aka signed).

rokenbuzz commented 2 years ago

I can load the common class, but trying to cast it fails.

What the failure you're seeing? I'd don't want to commit to this library or others if I'm going to encounter issues like this later.

mphipps1 commented 2 years ago

It's an invalid cast issue. When Plugin1 and Plugin2 are created, they have some common dependency (PluginBase). When loaded, Plugin1 loads PluginBase. When Plugin2 is loaded, it also loads PluginBase. They are considered 2 different types.

In my case, PluginBase is also part of the program LOADING the plugins (call it Main). Main has a PluginBase type, but I can't cast between any of the three.

The documentation says that this should work, so my guess is that something has changed in .NET to break this.

rokenbuzz commented 2 years ago

This is in my "Main". I have a separate library for the base class and each of two plugins.

foreach (string dir in Directory.GetDirectories(pluginsDir))
{
    var dirName = Path.GetFileName(dir);
    var pluginDll = Path.Combine(dir, dirName + ".dll");
    if (File.Exists(pluginDll))
    {
        var loader = PluginLoader.CreateFromAssemblyFile(
            pluginDll,
            sharedTypes: new[] { typeof(PluginControlBase) });
        pluginLoaders.Add(loader);
    }
}

foreach (var loader in pluginLoaders)
{
    foreach (var pluginType in loader
        .LoadDefaultAssembly()
        .GetTypes()
        .Where(t => typeof(PluginControlBase).IsAssignableFrom(t) && !t.IsAbstract))
    {
        PluginControlBase pluginControl = (PluginControlBase)Activator.CreateInstance(pluginType);
        myPluginControlBases.Add(pluginControl);
    }
}

I tried to simulate what I understand you're trying to do. It works for me. But I'm not sure I'm casting in the ways you are. I signed my base plugin library and it still works, though I'm also not sure that simulates what you're doing thoroughly enough. I'd be willing to try to replicate your situation more closely. Or, could you share your solution?

mphipps1 commented 2 years ago

I altered my code this morning to remove the issue (I merged a bunch of plugins to eliminate the issue). Now I can't reproduce the issue - I made a test case and it works. There was some subtle interaction in my old code that caused an issue, but since I can't reproduce this now, I can't help any more. Sorry.

github-actions[bot] commented 1 year ago

This issue has been automatically marked as stale because it has no recent activity. It will be closed if no further activity occurs. Please comment if you believe this should remain open, otherwise it will be closed in 14 days. Thank you for your contributions to this project.

github-actions[bot] commented 1 year ago

Closing due to inactivity. If you are looking at this issue in the future and think it should be reopened, please make a commented here and mention natemcmaster so he sees the notification.