Search code examples
c#dllimportmef.net-8.0system.composition

Loading native dependencies for an MEF plugin


I have a .NET 8.0 application using MEF (System.Composition) as its plugin architecture. Most of the plugins work properly however there are a couple that will throw exceptions at runtime. Both of these plugins have one thing in common, they rely on libraries that use native dlls from a "runtime" folder. One has Microsoft.Web.WebView2 and the other has LiveCharts2, which uses Skia. They have "runtime" trees that look like this (e.g. for the livecharts plugin):

runtimes
|-- osx
|   `-- native
|       |-- libHarfBuzzSharp.dylib
|       `-- libSkiaSharp.dylib
|-- win-arm64
|   `-- native
|       |-- libHarfBuzzSharp.dll
|       `-- libSkiaSharp.dll
|-- win-x64
|   `-- native
|       |-- libHarfBuzzSharp.dll
|       `-- libSkiaSharp.dll
`-- win-x86
    `-- native
        |-- libHarfBuzzSharp.dll
        `-- libSkiaSharp.dll

The webview plugin has the same tree, just with different DLLs. The runtimes folder is the project's build directory next to its dll.

I can get this system to work by moving the dependencies to the host application (i.e. webview2 and skia), but that is obviously not ideal since any future plugin that wants to use a native library would have to have its dependencies added to the host.

If I do not have Skia in the host application the livecharts plugin will throw: DllNotFoundException: Unable to load DLL 'libSkiaSharp' or one of its dependencies: The specified module could not be found. (0x8007007E)

Here is my plugin loading code:

    private void Init(IEnumerable<Assembly> assemblies)
    {
        _pluginManager.ImportsSatisfied += OnImportsSatisfied;

        var configuration = new ContainerConfiguration()
            .WithAssemblies(assemblies);

        try
        {
            using (CompositionHost host = configuration.CreateContainer())
            {
                host.SatisfyImports(_pluginManager);
            }
        }
        catch (ReflectionTypeLoadException ex)
        {
            Log.Error("Could not load extension pluginNames: {0}\nLoader exceptions:{1} ", ex, ex.LoaderExceptions);
        }
        catch (Exception ex)
        {
            Log.Error("Could not load extension pluginNames: {0}", ex);
        }
    }

Where assemblies is a list of assemblies loaded with:

    private static IEnumerable<Assembly> GetAssembliesFromNames(IEnumerable<string> pluginNames, string pluginDirectory)
    {
        List<Assembly> assemblies = new();
        foreach (string pluginName in pluginNames)
        {
            try
            {
                assemblies.Add(GetAssemblyForPluginByName(pluginName, pluginDirectory));
            }
            catch (Exception ex)
            {
                Log.Error("Could not load plugin assembly {0}: {1}", pluginName, ex);
            }
        }
        return assemblies;
    }
    public static Assembly GetAssemblyForPluginByName(string name, string workingDirectory)
    {
        string pluginName = Path.GetFileName(name);
        string pluginFolderFilePath = Path.Combine(workingDirectory, pluginName);
        string pluginDllPath = Path.Combine(pluginFolderFilePath, pluginName + ".dll");
        if (Directory.Exists(pluginFolderFilePath) && File.Exists(pluginDllPath))
        {
            return Assembly.LoadFrom(pluginDllPath);
        }

        throw new FileNotFoundException(name);
    }

Solution

  • I figured it out with postings from around the web. Basically you need to define your own load context like this (I sadly don't remember where I found this code):

        using System.Reflection;
    using System.Runtime.Loader;
    
    namespace MonitoringDisplayTv.Plugins;
    
    public class PluginLoadContext : AssemblyLoadContext
    {
        private readonly AssemblyDependencyResolver _resolver;
        private readonly IEnumerable<string> _sharedAssemblyNames;
    
        public PluginLoadContext(string pluginPath) : this(pluginPath, [])
        {
        }
    
        public PluginLoadContext(string pluginPath, IEnumerable<string> assemblies) : base(true)
        {
            _resolver = new AssemblyDependencyResolver(pluginPath);
    
            _sharedAssemblyNames = assemblies;
        }
    
        protected override Assembly? Load(AssemblyName assemblyName)
        {
            if (assemblyName.Name == null)
            {
                return null;
            }
    
            foreach (string name in _sharedAssemblyNames)
            {
                if (name == assemblyName.Name)
                {
                    return Default.LoadFromAssemblyName(assemblyName);
                }
            }
    
    
            string? assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
            if (assemblyPath != null)
            {
                return LoadFromAssemblyPath(assemblyPath);
            }
    
            return null;
        }
    
        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
        {
            string? libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
            if (libraryPath != null)
            {
                return LoadUnmanagedDllFromPath(libraryPath);
            }
    
            return IntPtr.Zero;
        }
    }