Gapotchenko.FX.Reflection.Loader
2024.1.3
Prefix Reserved
dotnet add package Gapotchenko.FX.Reflection.Loader --version 2024.1.3
NuGet\Install-Package Gapotchenko.FX.Reflection.Loader -Version 2024.1.3
<PackageReference Include="Gapotchenko.FX.Reflection.Loader" Version="2024.1.3" />
paket add Gapotchenko.FX.Reflection.Loader --version 2024.1.3
#r "nuget: Gapotchenko.FX.Reflection.Loader, 2024.1.3"
// Install Gapotchenko.FX.Reflection.Loader as a Cake Addin #addin nuget:?package=Gapotchenko.FX.Reflection.Loader&version=2024.1.3 // Install Gapotchenko.FX.Reflection.Loader as a Cake Tool #tool nuget:?package=Gapotchenko.FX.Reflection.Loader&version=2024.1.3
Overview
The module provides versatile primitives that can be used to automatically lookup and load .NET assembly dependencies in various dynamic scenarios.
Gentle Introduction
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.
Scenario #1. Load dependent assemblies from an app's outside directory
Let's take a look on example scenario.
Suppose we have ContosoApp
installed at C:\Program Files\ContosoApp
directory.
The directory 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
directory.
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 directory,
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 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' directory as a probing path for
// dependent assemblies.
AssemblyAutoLoader.Default.AddProbingPath(
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles),
@"Contoso\Engine"));
Run();
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void Run()
{
// ...
}
}
[!NOTE]
Run
method is annotated by[MethodImpl(MethodImplOptions.NoInlining)]
attribute. That attribute instructs .NET Runtime to not inline theRun
method into its calling methods. It is necessary to disable inlining because theRun
method may potentially reference types from not yet loaded assemblies, specifically those fromContosoEngine.dll
. This will lead to inability of .NET Runtime to start executing its calling methods at all (i.e. theMain
method), because those types are resolved before method starts to run. And they cannot be resolved until a proper assembly loader is configured, and it will not be configured due to the presence of unresolvable type references that were inlined fromRun
method. To break that chicken and egg lock, theRun
method inlining should be disabled.
Scenario #2. Load dependent assemblies from an inner directory 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
subdirectory of the app.
Now how can ContosoApp.exe
module load the required assemblies from Components
directory?
Thankfully, the default .NET Framework 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 Framework app as well).
The default .NET Framework assembly loader can be instructed to load dependent assemblies from inner directories of an app by specifying a set of private probing paths.
There is another story for .NET Core and .NET target frameworks.
They do not easily support additional probing paths.
For those target frameworks, using AssemblyAutoLoader
becomes worthy even for inner directories:
using Gapotchenko.FX.Reflection;
namespace ContosoApp;
class Program
{
static void Main()
{
AssemblyAutoLoader.Default.AddProbingPath(
Path.Combine(
Path.GetDirectoryName(typeof(Program).Assembly.Location),
"Components"));
Run();
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void Run()
{
// ...
}
}
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
directory.
So the AutoCAD plugin (a .DLL assembly) had to gain an ability to load the dependencies from that directory.
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
module.
So he wrote:
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' directory 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 .NET 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 variant of Newtonsoft.Json
module).
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.
A 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 Gapotchenko.FX.Reflection.Loader
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 so for a class library due to the sheer breadth of the public API surface. An assembly loader can then be installed at the module initializer of a class library to overcome that dilemma.
A module initializer can be seen as a constructor for an assembly (technically, it is a constructor for a module; each .NET assembly is comprised of one or more modules, typically just one).
Fody/ModuleInit is an example of a tool that gives access to .NET module initialization functionality from high-level programming languages like C#/VB.NET. Another option is to use a 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.
The example of such approach is presented below:
using Gapotchenko.FX.Reflection;
using System.Runtime.CompilerServices;
namespace MyLibrary;
static class AssemblyLoader
{
static AssemblyLoader()
{
AssemblyAutoLoader.Default.AddAssembly(typeof(AssemblyLoader).Assembly);
}
[ModuleInitializer]
public static void Activate()
{
}
}
Commonly Used Types
Gapotchenko.FX.Reflection.AssemblyAutoLoader
Other Modules
Let's continue with a look at some other modules provided by Gapotchenko.FX:
- Gapotchenko.FX
- Gapotchenko.FX.AppModel.Information
- Gapotchenko.FX.Collections
- Gapotchenko.FX.Console
- Gapotchenko.FX.Data
- Gapotchenko.FX.Diagnostics
- Gapotchenko.FX.IO
- Gapotchenko.FX.Linq
- Gapotchenko.FX.Math
- Gapotchenko.FX.Memory
- Gapotchenko.FX.Numerics ✱
- ➴ Gapotchenko.FX.Reflection.Loader ✱
- Gapotchenko.FX.Runtime.InteropServices ✱
- Gapotchenko.FX.Security.Cryptography
- Gapotchenko.FX.Text
- Gapotchenko.FX.Threading
- Gapotchenko.FX.Tuples
Symbol ✱ denotes an advanced module.
Or take a look at the full list of modules.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 is compatible. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 is compatible. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 is compatible. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 is compatible. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETCoreApp 2.0
- No dependencies.
-
.NETCoreApp 3.0
- No dependencies.
-
.NETFramework 4.6.1
- No dependencies.
-
.NETStandard 2.0
- No dependencies.
-
net5.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 |
---|---|
dotnet-script/dotnet-script
Run C# scripts from the .NET CLI.
|
Version | Downloads | Last updated |
---|---|---|
2024.1.3 | 207 | 11/10/2024 |
2022.2.7 | 55,278 | 5/1/2022 |
2022.2.5 | 403 | 5/1/2022 |
2022.1.4 | 451 | 4/6/2022 |
2021.2.21 | 462 | 1/21/2022 |
2021.2.20 | 419 | 1/17/2022 |
2021.2.11 | 5,533 | 8/22/2021 |
2021.2.10-beta | 233 | 8/22/2021 |
2021.2.9-beta | 222 | 8/21/2021 |
2021.2.8-beta | 225 | 8/20/2021 |
2021.2.7-beta | 232 | 8/20/2021 |
2021.2.6-beta | 256 | 8/18/2021 |
2021.2.5-beta | 197 | 8/18/2021 |
2021.1.5 | 353 | 7/6/2021 |
2020.2.2-beta | 331 | 11/21/2020 |
2020.1.15 | 460 | 11/5/2020 |
2020.1.9-beta | 353 | 7/14/2020 |
2020.1.8-beta | 336 | 7/14/2020 |
2020.1.7-beta | 354 | 7/14/2020 |
2020.1.1-beta | 425 | 2/11/2020 |
2019.3.7 | 510 | 11/4/2019 |
2019.2.20 | 504 | 8/13/2019 |