Dynamically pre-load assemblies in a ASP.NET Core (or any C#) project
c# asp.net-core

Dynamically pre-load assemblies in a ASP.NET Core (or any C#) project

This article explains how to preload some assemblies for a .Net project and provides all the code required to replicate the behavior.

AppDomain.CurrentDomain.GetAssemblies() returns only assemblies that have actually been loaded. In some case, like if you are looking on how to detect all classes implementing a specific interface, you may want to pre-load all assemblies that are referenced in a project.

Build a function that is able to pre-load all assemblies of a project

This article will show how to implement the following function, and use it in a relevant context:

static void LoadAllAssemblies(bool includeFramework = false)

How will we get there? The idea will be to load all assemblies that are referenced by the already-loaded assemblies.

The starting library usually references all dependencies (direct or indirect) that will be required during the functioning of the program.

You should note that this does not cover dynamically loaded plugins DLLs from the file system that are not already loaded. If you intend to add autonomous plugins (from a plugin folder for example), that are not referenced by your program, please load them manually first or they will not be taken into consideration.

Specifications of the function

The specifications for this tool that we are going to follow will be:

  • The developed function should load all relevant assemblies into memory
  • It should be able to skip the framework libraries, to avoid loading too many libs
  • The call should be required only once
  • Regarding performances, it should be very fast, few milliseconds maximum

Primary investigations

We are going to build a recursive loop, to load all assemblies and all assemblies referenced by these assemblies. Our entry point will be the -already-loaded- assemblies, got by calling an AppDomain method: AppDomain.CurrentDomain.GetAssemblies().

In practice, we call this code from Startup or any other file in the program. The starting and calling libraries are already loaded for the application, therefore it is a good starting point to get all dependencies.

Now we have identified some assemblies to start with. If we take a single assembly, we can find all its dependencies by calling assembly.GetReferencedAssemblies(). This will give us the list of all assemblies referenced by assembly.

Once we get new assemblies, we can load them by calling an Assembly static method: Assembly.Load(assembly).

From there we can make a recursive call to load these assemblies and their dependencies.

Our pseudo-code would therefore look like:

function LoadAssemblyAndReference(Assembly assembly)
{
    Assembly.Load(assembly);
    foreach(var refAssembly in assembly.GetReferencedAssemblies())
    	LoadAssemblyAndReference(refAssembly);
}
var allAssemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach(var a in allAssemblies)
    LoadAssemblyAndReference(a);

Optimizations

Of course it has to be enhanced and optimized to avoid over-work or infinite loops.

C# ensures that there is no circular dependency between two libraries, we will take advantage of this feature to optimize our loading.

In order to optimize, what we can do is to maintain a list of assemblies already loaded by the tool. This will avoid trying to load assemblies twice. We will use a ConcurrentDictionary<string, bool> to take advantage of the hash indexed search (we could have used ConcurrentSet but it is still missing from the framework) .

We will also take advantage of local functions introduced with C# 7.0 in 2017 to make the function autonomous.

This loading function skips all the .Net Framework assemblies, on my solution of more than 100 projects it takes 0ms to execute, so I consider that it has no performance issues.

Final optimized code

Here is the final code that will help to solve our case, how to load all assemblies for a c# program:

private static void LoadAllAssemblies(bool includeFramework = false)
{
    // Storage to ensure not loading the same assembly twice and optimize calls to GetAssemblies()
    ConcurrentDictionary<string, bool> Loaded = new ConcurrentDictionary<string, bool>();

    // Filter to avoid loading all the .net framework
    bool ShouldLoad(string assemblyName)
    {
        return (includeFramework || NotNetFramework(assemblyName))
            && !Loaded.ContainsKey(assemblyName);
    }
    bool NotNetFramework(string assemblyName)
    {
        return !assemblyName.StartsWith("Microsoft.") 
            && !assemblyName.StartsWith("System.")
            && !assemblyName.StartsWith("Newtonsoft.") 
            && assemblyName != "netstandard";
    }
    void LoadReferencedAssembly(Assembly assembly)
    {
        // Check all referenced assemblies of the specified assembly
        foreach (AssemblyName an in assembly.GetReferencedAssemblies().Where(a => ShouldLoad(a.FullName)))
        {
            // Load the assembly and load its dependencies
            LoadReferencedAssembly(Assembly.Load(an)); // AppDomain.CurrentDomain.Load(name)
            Loaded.TryAdd(an.FullName, true);
            System.Diagnostics.Debug.WriteLine($"\n>> Referenced assembly => {an.FullName}");
        }
    }

    // Populate already loaded assemblies
    System.Diagnostics.Debug.WriteLine($">> Already loaded assemblies:");
    foreach (var a in AppDomain.CurrentDomain.GetAssemblies().Where(a => ShouldLoad(a.FullName)))
    {
        Loaded.TryAdd(a.FullName, true);
        System.Diagnostics.Debug.WriteLine($">>>> {a.FullName}");
    }
    int alreadyLoaded = Loaded.Keys.Count();
    System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();

    // Loop on loaded assembliesto load dependencies (it includes Startup assembly so should load all the dependency tree) 
    foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies().Where(a => NotNetFramework(a.FullName)))
        LoadReferencedAssembly(assembly);

    // Debug
    System.Diagnostics.Debug.WriteLine($"\n>> Assemblies loaded after scann ({(Loaded.Keys.Count-alreadyLoaded)} assemblies in {sw.ElapsedMilliseconds} ms):");
    foreach (var a in Loaded.Keys.OrderBy(k=>k))
        System.Diagnostics.Debug.WriteLine($">>>> {a}");
}

With this article, we learned how to load all assemblies of a csharp program. The final function loads all dependencies of the already-loaded assemblies in less than a millisecond. Like and share it if you found it useful 😃


Send
Please sign-in to comment
.X0001-01-01_00-00