10

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?

1 Answer 1

14

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, "*.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);
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

This is great. Just curious - how did you find this answer?
@RobSiklos I forget to be honest with it being so long ago, but knowing me it would have been a little trial and error involving the above MS article. You have me worried now that I've taken it from a blog that I didn't give attribution to!
I'm trying to use this approach, but what I am seeing is that attempting to do any sort of reflection on the assemblies in MetadataLoadContext loads their dependencies too (it happens when you start doing reflection, not when you load the assembly). A key feature of reflection-only loading in .NET FW was that it would not load dependent assemblies. Do you know how to stop the dependencies from loading?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.