简体   繁体   中英

Reflection only assembly loading in .Net Core

I have a .Net Framework WPF application that I'm currently migrating to .Net6. At startup it examines certain assemblies in the executable folder looking for any with a custom assembly attribute. Those that have this are then loaded into the current appdomain. (Note that some of these assemblies may already be in the appdomain, as they are projects in the running application's solution).

This is the 4.x code:

private void LoadAssemblies(string folder)
{
    AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve +=
        (s, e) => Assembly.ReflectionOnlyLoad(e.Name);

    var assemblyFiles = Directory.GetFiles(folder, "*.Client.dll");
    foreach (var assemblyFile in assemblyFiles)
    {
        var reflectionOnlyAssembly = Assembly.ReflectionOnlyLoadFrom(assemblyFile);
        if (ContainsCustomAttr(reflectionOnlyAssembly))
        {
            var assembly = Assembly.LoadFrom(assemblyFile);
            ProcessAssembly(assembly);
        }
    }
}
  

The custom assembly attribute (that this code is looking for) has a string property containing a path to a XAML resource file within that assembly. The ProcessAssembly() method adds this resource file to the application's merged dictionary, something like this:

var resourceUri = string.Format(
    "pack://application:,,,/{0};component/{1}",
    assembly.GetName().Name,
    mimicAssemblyAttribute.DataTemplatePath);

var uri = new Uri(resourceUri, UriKind.RelativeOrAbsolute);
application.Resources.MergedDictionaries.Add(new ResourceDictionary { Source = uri });

Just to reiterate, all this works as it should in the .Net 4.x application.

.Net6 on the other hand doesn't support reflection-only loading, nor can you create a second app domain in which to load the assemblies. I rewrote the above code by loading the assemblies being examined into what I understand is a temporary, unloadable context:

    private void LoadAssemblies(string folder)
    {
        var assemblyFiles = Directory.GetFiles(folder, "*.Client.dll");
        using (var ctx = new TempAssemblyLoadContext(AppDomain.CurrentDomain.BaseDirectory))
        {
            foreach (var assemblyFile in assemblyFiles)
            {
                var assm = ctx.LoadFromAssemblyPath(assemblyFile);
                if (ContainsCustomAttr(assm))
                {
                    var assm2 = Assembly.LoadFrom(assemblyFile);
                    ProcessAssembly(assm2);
                }
            }
        }
    }

    private class TempAssemblyLoadContext : AssemblyLoadContext, IDisposable
    {
        private AssemblyDependencyResolver _resolver;

        public TempAssemblyLoadContext(string readerLocation)
            : base(isCollectible: true)
        {
            _resolver = new AssemblyDependencyResolver(readerLocation);
        }

        public void Dispose()
        {
            Unload();
        }

        protected override Assembly Load(AssemblyName assemblyName)
        {
            var path = _resolver.ResolveAssemblyToPath(assemblyName);
            if (path != null)
            {
                return LoadFromAssemblyPath(path);
            }

            return null;
        }

        protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
        {
            var path = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
            if (path != null)
            {
                return LoadUnmanagedDllFromPath(path);
            }

            return IntPtr.Zero;
        }
    }

(Note the ProcessAssembly() method is unchanged).

This code "works" in so much as it goes through the motions without crashing. However at a later point when the application starts creating the views, I get the following exception:

The component ' . .ModeSelectorView' does not have a resource identified by the URI '/ . ;component/views/modeselector/modeselectorview.xaml'.

This particular view resides in a project of this application's solution, so the assembly will already be in the appdomain. The assembly also contains that custom attribute so the above code will be trying to load it, although I believe that Assembly.LoadFrom() should not load the same assembly again?

Just in case, I modified the "if" block in my LoadAssemblies() method to ignore assemblies already in the app domain:

if (ContainsCustomAttr(assm) && !AppDomain.CurrentDomain.GetAssemblies().Contains(assm))

Sure enough, a breakpoint shows that the assembly in question (containing that view) is ignored and not loaded into the app domain. However I still get the same exception further down the line. In fact I can comment out the entire "if" block so no assemblies are being loaded into the app domain, and I still get the exception, suggesting that it's caused by loading the assembly into that AssemblyLoadContext. Also, a breakpoint shows that context is being unloaded via its Dispose() method, upon dropping out of the "using" block in the LoadAssemblies() method.

Edit : even with the "if" block commented out, a breakpoint at the end of the method shows that all the assemblies being loaded by ctx.LoadFromAssemblyPath() are ending up in AppDomain.Current. What am I not understanding? Is the context part of the appdomain and not a separate "area"? How can I achieve this "isolated" loading of assemblies in a similar way to the "reflection only" approach that I was using in .Net 4.x?

Okay, so I found the answer, which is to use MetadataLoadContext . This is essentially the .Net Core replacement for reflection-only loading:

private void LoadAssemblies(string folder)
{
    // The load context needs access to the .Net "core" assemblies...
    var allAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.Client.dll").ToList();
    // .. and the assemblies that I need to examine.
    var assembliesToExamine = Directory.GetFiles(folder, "NuIns.CoDaq.*.Client.dll");
    allAssemblies.AddRange(assembliesToExamine);

    var resolver = new PathAssemblyResolver(allAssemblies);
    using (var mlc = new MetadataLoadContext(resolver))
    {
        foreach (var assemblyFile in assembliesToExamine)
        {
            var assm = mlc.LoadFromAssemblyPath(assemblyFile);
            if (ContainsCustomAttr(assm))
            {
                var assm2 = Assembly.LoadFrom(assemblyFile);
                AddMimicAssemblyInfo(assm2);
            }
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM