How to load assemblies using new AssemblyLoadContext in .NET 7?

214 Views Asked by At

I am looking for community guidance to help me translate the below mentioned code to a new .NET 7 compatible version. Especially the bits and pieces inside LoadModuleCatalog method.

As per my research so far, the old AppDomain is no more applicable in .NET 7.0 version, instead a new AssemblyLoadContext has been introduced by Microsoft to load assemblies but due to a lack of online documentation and resources on this topic, I am struggling to convert this code snippet to .NET 7 compliant version.

public class DynamicDirectoryModuleCatalog : ModuleCatalog
{
    void LoadModuleCatalog(string path, bool isFile = false)
    {
        AppDomain? parentDomain = AppDomain.CurrentDomain;
        Evidence evidence = new Evidence(parentDomain.Evidence);
        AppDomainSetup setup = parentDomain.SetupInformation;
        AppDomain childDomain =  AppDomain.CreateDomain("DiscoveryRegion", evidence, setup);

        try
        {
            List<string> loadedAssemblies = new List<string>();

            var assemblies = (
                 from Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()
                 where !(assembly is System.Reflection.Emit.AssemblyBuilder)
                    && assembly.GetType().FullName != "System.Reflection.Emit.InternalAssemblyBuilder"
                    && !String.IsNullOrEmpty(assembly.Location)
                 select assembly.Location
                 );


            loadedAssemblies.AddRange(assemblies);

            Type loaderType = typeof(InnerModuleInfoLoader);
            if (loaderType.Assembly != null)
            {
                var loader = (InnerModuleInfoLoader)childDomain.CreateInstanceFrom(loaderType.Assembly.Location, loaderType.FullName).Unwrap();
                loader.LoadAssemblies(loadedAssemblies);

                ModuleInfo[] modules = loader.GetModuleInfos(path, isFile);

                this.Items.AddRange(modules);

                if (isFile) LoadModules(modules);
            }
        }
        finally
        {
            AppDomain.Unload(childDomain);
        }
    }

    private class InnerModuleInfoLoader : MarshalByRefObject
    {
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        internal ModuleInfo[] GetModuleInfos(string path, bool isFile = false)
        {
            Assembly moduleReflectionOnlyAssembly =
                AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().First(
                    asm => asm.FullName == typeof(IModule).Assembly.FullName);

            Type IModuleType = moduleReflectionOnlyAssembly.GetType(typeof(IModule).FullName);

            FileSystemInfo info = null;
            if (isFile)
                info = new FileInfo(path);
            else
                info = new DirectoryInfo(path);

            ResolveEventHandler resolveEventHandler = delegate(object sender, ResolveEventArgs args) { return OnReflectionOnlyResolve(args, info); };
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += resolveEventHandler;
            IEnumerable<ModuleInfo> modules = GetNotAllreadyLoadedModuleInfos(info, IModuleType);
            AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= resolveEventHandler;

            return modules.ToArray();
        }

        private static IEnumerable<ModuleInfo> GetNotAllreadyLoadedModuleInfos(FileSystemInfo info, Type IModuleType)
        {
            List<FileInfo> validAssemblies = new List<FileInfo>();
            Assembly[] alreadyLoadedAssemblies = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies();

            FileInfo fileInfo = info as FileInfo;
            if (fileInfo != null)
            {
                if (alreadyLoadedAssemblies.FirstOrDefault(assembly => String.Compare(Path.GetFileName(assembly.Location), fileInfo.Name, StringComparison.OrdinalIgnoreCase) == 0) == null)
                {
                    var moduleInfos = Assembly.ReflectionOnlyLoadFrom(fileInfo.FullName).GetExportedTypes()
                    .Where(IModuleType.IsAssignableFrom)
                    .Where(t => t != IModuleType)
                    .Where(t => !t.IsAbstract).Select(t => CreateModuleInfo(t));

                    return moduleInfos;
                }
            }

            DirectoryInfo directory = info as DirectoryInfo;

            var files = directory.GetFiles("*.dll").Where(file => alreadyLoadedAssemblies.
                FirstOrDefault(assembly => String.Compare(Path.GetFileName(assembly.Location), file.Name, StringComparison.OrdinalIgnoreCase) == 0) == null);

            foreach (FileInfo file in files)
            {
                try
                {
                    Assembly.ReflectionOnlyLoadFrom(file.FullName);
                    validAssemblies.Add(file);
                }
                catch (BadImageFormatException)
                {
                    // skip non-.NET Dlls
                }
            }

            return validAssemblies.SelectMany(file => Assembly.ReflectionOnlyLoadFrom(file.FullName)
                                        .GetExportedTypes()
                                        .Where(IModuleType.IsAssignableFrom)
                                        .Where(t => t != IModuleType)
                                        .Where(t => !t.IsAbstract)
                                        .Select(type => CreateModuleInfo(type)));
        }


        private static Assembly OnReflectionOnlyResolve(ResolveEventArgs args, FileSystemInfo info)
        {
            Assembly loadedAssembly = AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies().FirstOrDefault(
                asm => string.Equals(asm.FullName, args.Name, StringComparison.OrdinalIgnoreCase));
            if (loadedAssembly != null)
            {
                return loadedAssembly;
            }

            DirectoryInfo directory = info as DirectoryInfo;
            if (directory != null)
            {
                AssemblyName assemblyName = new AssemblyName(args.Name);
                string dependentAssemblyFilename = Path.Combine(directory.FullName, assemblyName.Name + ".dll");
                if (File.Exists(dependentAssemblyFilename))
                {
                    return Assembly.ReflectionOnlyLoadFrom(dependentAssemblyFilename);
                }
            }

            return Assembly.ReflectionOnlyLoad(args.Name);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        internal void LoadAssemblies(IEnumerable<string> assemblies)
        {
            foreach (string assemblyPath in assemblies)
            {
                try
                {
                    Assembly.ReflectionOnlyLoadFrom(assemblyPath);
                }
                catch (FileNotFoundException)
                {
                    // Continue loading assemblies even if an assembly can not be loaded in the new AppDomain
                }
            }
        }

        private static ModuleInfo CreateModuleInfo(Type type)
        {
            // create ModuleInfo, implementation is not relevant
        }
    }
}

Appreciate any help in this regard.

This code is from Brian Lagunas's blog post.

1

There are 1 best solutions below

1
Panagiotis Kanavos On

Prism has evolved a lot since 2013. That article was written for a completely different runtime and a completely different version of Prism. Back then there was no DI built into .NET Framework so Prism ended up being a "bit" overengineered.

Module loading from a directory is available out of the box using the built-in DirectoryModuleCatalog class. The class doesn't use AssemblyLoadContext at all. In fact, the code looks quite similar to the original article so one would have to do a diff to find what's different.

The HelloWorld example app shows how to use various module catalogs by overriding App.CreateModuleCatalog, eg :

public partial class App
{
    protected override Window CreateShell()
    {
        return Container.Resolve<MainWindow>();
    }

    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {
        containerRegistry.RegisterSharedSamples();
    }

    protected override IModuleCatalog CreateModuleCatalog()
    {
        return new DirectoryModuleCatalog() { ModulePath = "Modules" }; 
    };
}

ConfigureModuleCatalog is replaced by CreateModuleCatalog that adds compiled modules from the Modules folder