Gapotchenko.FX.Reflection.Loader 2022.1.4 The ID prefix of this package has been reserved for one of the owners of this package by NuGet.org. Prefix Reserved

.NET Core 2.0 .NET Standard 2.0 .NET Framework 4.6
There is a newer version of this package available.
See the version list below for details.
Install-Package Gapotchenko.FX.Reflection.Loader -Version 2022.1.4
dotnet add package Gapotchenko.FX.Reflection.Loader --version 2022.1.4
<PackageReference Include="Gapotchenko.FX.Reflection.Loader" Version="2022.1.4" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Gapotchenko.FX.Reflection.Loader --version 2022.1.4
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: Gapotchenko.FX.Reflection.Loader, 2022.1.4"
#r directive can be used in F# Interactive, C# scripting and .NET Interactive. Copy this into the interactive tool or source code of the script to reference the package.
// Install Gapotchenko.FX.Reflection.Loader as a Cake Addin
#addin nuget:?package=Gapotchenko.FX.Reflection.Loader&version=2022.1.4

// Install Gapotchenko.FX.Reflection.Loader as a Cake Tool
#tool nuget:?package=Gapotchenko.FX.Reflection.Loader&version=2022.1.4
The NuGet Team does not provide support for this client. Please contact its maintainers for support.

Overview

The module provides versatile primitives that can be used to automatically lookup and load assembly dependencies in various dynamic scenarios.

<hr/>

The Assembly Loader

Assembly loading plays a crucial role in .NET apps. Once the app is started, .NET runtime ensures that all required assemblies are gradually loaded.

Whenever the code hits the point where a type from another assembly is used, it raises AppDomain.AssemblyResolve event. The good thing is .NET comes pre-equipped with a default assembly loader, which does a sensible job for most applications.

However, there are situations when having a default assembly loader is just not enough. This is where Gapotchenko.FX.Reflection.Loader module becomes extremely handy.

<hr/>

Scenario #1. Load dependent assemblies from an app's outside folder

Let's take a look on example scenario. Suppose we have ContosoApp installed at C:\Program Files\ContosoApp folder. The folder contains a single ContosoApp.exe assembly which represents the main executable file of the app.

ContosoApp.exe has a dependency on ContosoEngine.dll assembly which is located at C:\Program Files\Common Files\Contoso\Engine folder. It so happens ContosoApp uses a common engine developed by the company.

Now when ContosoApp.exe is run, it bails out with the following exception:

System.IO.FileNotFoundException: Could not load file or assembly 'ContosoEngine, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

It occurs because ContoseEngine.dll assembly is located at the outside folder, and the default .NET assembly loader does not provide an easy way to cover scenarios like this.

In order to cover that scenario, a developer would subscribe to AppDomain.CurrentDomain.AssemblyResolve event. Then he would come up with a custom assembly lookup and loading logic. The thing is: that is not a straightforward thing to do. Even more than that, it is full of gotchas and caveats. And they will painfully bite a developer on subtle occasions, now and then.

That's why Gapotchenko.FX.Reflection.Loader module provides a ready to use AssemblyAutoLoader class that reliably covers the scenarios like that.

Here is the solution for ContosoApp:

using System;
using System.IO;
using Gapotchenko.FX.Reflection;

namespace ContosoApp
{
    class Program
    {
        static void Main()
        {
            // The statement below instructs Gapotchenko.FX assembly loader to use
            // 'C:\Program Files\Common Files\Contoso\Engine' folder as a probing path for
            // dependent assemblies.
            AssemblyAutoLoader.Default.AddProbingPath(
                Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles),
                    @"Contoso\Engine"));
            
            Run();
        }

        static void Run()
        {
            // ...
        }
    }
}

Scenario #2. Load dependent assemblies from an inner folder of an app

ContosoApp continues to evolve and now it has a dependency on Newtonsoft.Json.dll assembly. A straightforward approach would be to put Newtonsoft.Json.dll assembly just besides ContosoApp.exe.

But Mr. Alberto Olivetti from Contoso's Deployment Division decided that an additional file laying near ContosoApp.exe would be an unwanted distraction for command line users of the app. Mr. Olivetti tends to pay a lot of respect to his customers and wants to save their time while they are hanging around ContosoApp.exe. Thus Alberto came up with a respectful solution to put all third-party assemblies to Components subfolder of the app.

Now how can ContosoApp.exe module load the required assemblies from Components folder? Thankfully, the default .NET assembly loader allows to achieve that by specifying a set of private probing paths in application configuration file:

<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="Components" />
    </assemblyBinding>
  </runtime>
</configuration>

The task is solved for ContosoApp (and every other .NET app as well). The default .NET assembly loader can be instructed to load dependent assemblies from inner folders of an app by specifying a set of private probing paths.

Scenario #3. Specifying probing paths for a .DLL assembly

But what if you need to specify probing paths not for a whole app, but for a specific assembly only? Say you created an Autodesk AutoCAD plugin that depends on Newtonsoft.Json.dll and a bunch of other assemblies, and then want to put all those third-party files somewhere else.

Contoso company met the very same challenge. They created an AutoCAD plugin for their ContosoApp product. A straightforward way was to redistribute the dependencies together with plugin but its size then skyrocketed to 1 GB in ZIP file. Bummer.

The substantial contributor to the size was ContosoEngine which was about 3 GB unzipped. Mr. Alberto Olivetti, Contoso's deployment specialist, quickly recognized an opportunity to use a shared setup of ContosoEngine, which was already present at C:\Program Files\Common Files\Contoso\Engine folder.

So the AutoCAD plugin (a .DLL assembly) had to gain an ability to load the dependencies from that folder.

This is what Alberto did. He created AssemblyLoader class in AutoCAD plugin assembly with just one method Activate:

namespace ContosoApp.Integration.AutoCAD
{
    static class AssemblyLoader
    {
        public static void Activate()
        {
        }
    }
}

Alberto then ensured that Activate method is getting called at the early stages of a plugin lifecycle:

namespace ContosoApp.Integration.AutoCAD
{
    public class Plugin : AutodeskPluginBase
    {
        public override void Initialize()
        {
            AssemblyLoader.Activate();

            base.Initialize();

            // ...
        }
    }
}

Now Alberto had a skeleton for a proper assembly loader initialization. The only missing part was the actual implementation which was going to be enormous.

Thanks to the prior experience with custom assembly loading, Alberto was aware about that fancy AssemblyAutoLoader class provided by Gapotchenko.FX.Reflection.Loader package. So he wrote:

using System;
using System.IO;
using Gapotchenko.FX.Reflection;

namespace ContosoApp.Integration.AutoCAD
{
    static class AssemblyLoader
    {
        static AssemblyLoader()
        {
            // The statement below instructs Gapotchenko.FX assembly loader to use
            // 'C:\Program Files\Common Files\Contoso\Engine' folder as a probing path for
            // resolution of 'ContosoApp.Integration.AutoCAD.dll' assembly dependencies.
            AssemblyAutoLoader.Default.AddAssembly(
                typeof(AssemblyLoader).Assembly,
                Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles),
                    @"Contoso\Engine"));
        }

        public static void Activate()
        {
        }
    }
}

Please note how Alberto put the implementation inside a static constructor while leaving Activate method empty. In that way, he was able to achieve a one-shot mode of execution, where the actual assembly loader initialization takes place only once on a first call to Activate method. Smart.

But even if Alberto did not create a singleton, AssemblyAutoLoader is sophisticated enough to do the right job out of the box.

Now why did Alberto call AddAssembly method instead of AddProbingPath? Both would work, actually. There is a subtle but very important difference.

AddProbingPath is a coarse "catch-all" method. It would serve not only the dependencies of a given plugin assembly but would also cover the whole app domain. Sometimes this is a beneficial behavior, like in case with the root ContosoApp.exe assembly.

In contrast, AddAssembly method provides a finer control. It only serves the dependencies of a specified assembly. It turns out to be a much saner choice for plugins where app domain is shared among a lot of things. In this way, assembly loaders from different plugins would not clash with each other, even when they look at a conflicting assembly dependency (it's easy to imagine that a lot of plugins would use the "same" but subtly different version of Newtonsoft.Json).

Scenario #4. Automatic handling of binding redirects for a .DLL assembly

Assembly binding redirects allow to "remap" specific ranges of assembly versions. The redirects are automatically created by build tools, and then being put to corresponding .config files of resulting assemblies. (Learn more)

Assembly binding redirects work well for apps, but get completely broken if you want to employ them for dynamically loaded assemblies like plugins. The default .NET loader simply ignores .config files of .DLL assemblies!

Gapotchenko.FX.Reflection.Loader solves this. Just add the following code to a place which gets executed at the early stage of the assembly lifecycle:

AssemblyLoader.Activate()

AssemblyLoader implementation then goes as follows:

using Gapotchenko.FX.Reflection;

namespace MyPlugin
{
    static class AssemblyLoader
    {
        static AssemblyLoader()
        {
            // The statement below instructs Gapotchenko.FX assembly loader to add a specified
            // assembly to the list of sources to consider during assembly resolution process.
            // The loader automatically handles binding redirects according to a corresponding assembly
            // configuration (.config) file. If configuration file is missing then binding redirects are
            // automatically deducted according to the assembly compatibility heuristics.
            AssemblyAutoLoader.Default.AddAssembly(typeof(AssemblyLoader).Assembly);
        }

        public static void Activate()
        {
        }
    }
}

There are a lot of projects that may need automatic handling of DLL binding redirects: T4 templates, MSBuild tasks, plugins, extensions etc. Basically everything that gets dynamically loaded and depends on one or more NuGet packages with mishmash of versions.

<hr/>

Chicken & Egg Dilemma

Gapotchenko.FX.Reflection.Loader module is distributed as a NuGet package with a single assembly file without dependencies.

This is done to avoid chicken & egg dilemma. In this way, the default .NET assembly loader can always load the assembly despite the possible variety of different NuGet packages that can be used in the given project.

Another point to consider is how to select a point of assembly loader installation that is early enough in the assembly lifecycle. This tends to be trivial for an app: the first few lines of the main entry point are good to go. But it may be hard to do for a class library. Sometimes it gets totally infeasible when public API surface of a library gets wide enough. To overcome that dilemma, assembly loader can be installed at module initializer of a class library.

Fody/ModuleInit is an example of tool that gives access to .NET module initialization functionality from high-level programming languages like C#/VB.NET. Another option is to use more specialized tool like Eazfuscator.NET that provides not only module initialization functionality, but also intellectual property protection.

Please note that some .NET languages provide the out of the box support for module initializers. For example, C# starting with version 9.0 treats all static methods marked with ModuleInitializerAttribute as module initializers.

While ModuleInitializerAttribute is only available in .NET 5.0 and newer, the whole concept is perfectly functional with any .NET version once attribute definition is in place. That's why Gapotchenko.FX module provides a ready to use polyfill for that attribute.

<hr/>

Product Versions
.NET net5.0 net5.0-windows net6.0 net6.0-android net6.0-ios net6.0-maccatalyst net6.0-macos net6.0-tvos net6.0-windows
.NET Core netcoreapp2.0 netcoreapp2.1 netcoreapp2.2 netcoreapp3.0 netcoreapp3.1
.NET Standard netstandard2.0 netstandard2.1
.NET Framework net46 net461 net462 net463 net47 net471 net472 net48
MonoAndroid monoandroid
MonoMac monomac
MonoTouch monotouch
Tizen tizen40 tizen60
Xamarin.iOS xamarinios
Xamarin.Mac xamarinmac
Xamarin.TVOS xamarintvos
Xamarin.WatchOS xamarinwatchos
Compatible target framework(s)
Additional computed target framework(s)
Learn more about Target Frameworks and .NET Standard.
  • .NETCoreApp 2.0

    • No dependencies.
  • .NETCoreApp 3.0

    • No dependencies.
  • .NETFramework 4.6

    • No dependencies.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Gapotchenko.FX.Reflection.Loader:

Package Downloads
Dotnet.Script.Core

A cross platform library allowing you to run C# (CSX) scripts with support for debugging and inline NuGet packages. Based on Roslyn.

GitHub repositories (1)

Showing the top 1 popular GitHub repositories that depend on Gapotchenko.FX.Reflection.Loader:

Repository Stars
filipw/dotnet-script
Run C# scripts from the .NET CLI.
Version Downloads Last updated
2022.2.7 59 5/1/2022
2022.2.5 43 5/1/2022
2022.1.4 78 4/6/2022
2021.2.21 94 1/21/2022
2021.2.20 74 1/17/2022
2021.2.11 1,130 8/22/2021
2021.2.10-beta 117 8/22/2021
2021.2.9-beta 109 8/21/2021
2021.2.8-beta 124 8/20/2021
2021.2.7-beta 110 8/20/2021
2021.2.6-beta 148 8/18/2021
2021.2.5-beta 109 8/18/2021
2021.1.5 193 7/6/2021
2020.2.2-beta 233 11/21/2020
2020.1.15 282 11/5/2020
2020.1.9-beta 249 7/14/2020
2020.1.8-beta 239 7/14/2020
2020.1.7-beta 246 7/14/2020
2020.1.1-beta 330 2/11/2020
2019.3.7 339 11/4/2019
2019.2.20 328 8/13/2019