Neuere Assemblys laden, ohne Binding-Redirects in der App.Config-Datei

Manchmal ist es in Windows-.NET-Anwendungen (Konsole, WinForms, WPF, usw.) lästig, immer Binding-Redirects in seiner Anwendungs-Konfigurations-Datei mitzugeben.

Abhilfe schafft hier das AssemblyResolve-Ereignis, so wie in dieser Stack-Overflow-Antwort beschrieben.

Meine Code dazu:

static class Program
{
    private static readonly IDictionary<string, Assembly> _additional =
        new Dictionary<string, Assembly>();

    static void Main()
    {
        // --
        // http://stackoverflow.com/a/9180843/107625

        var dir = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
        foreach (var assemblyName in Directory.GetFiles(dir, @"*.dll"))
        {
            var assembly = Assembly.LoadFile(assemblyName);
            _additional.Add(assembly.GetName().Name, assembly);
        }

        AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve +=
            CurrentDomain_ResolveAssembly;
        AppDomain.CurrentDomain.AssemblyResolve += 
            CurrentDomain_ResolveAssembly;

        // --

        // ... Hier kommt der eigentliche Programmcode ...

    }

    // 
    private static Assembly CurrentDomain_ResolveAssembly(
        object sender, 
        ResolveEventArgs e)
    {
        // Hier mache ich quasi mein eigenes, automatisches, Binding-Redirect.
        // (z.B. Newtonsoft.Json 6.0.0.0 nach 9.0.0.0).

        var name = e.Name.Substring(0, e.Name.IndexOf(','));

        _additional.TryGetValue(name, out var res);
        return res;
    }
}

Je nach Programmart, in der obiger Code verwendet wird, kann es nötig sein, das Laden der Assemblies in einen try-catch-Block zu packen.

Also anstatt:

var assembly = Assembly.LoadFile(assemblyName);
_additional.Add(assembly.GetName().Name, assembly);

Dann eher:

try
{
    var assembly = Assembly.LoadFile(assemblyName);
    _additional.Add(assembly.GetName().Name, assembly);
}
catch (BadImageFormatException) // TODO: Ggf. noch weitere Exception-Typen auffangen.
{
    // Ignorieren.
}

Der Grund ist, dass z. B. auch nicht-.NET-DLLs im Programmordner liegen können, und diese dann beim Laden fehlschlagen.

Eine erweiterte Version prüft schon im Vorfeld, ob bestimmte Assemblies schon bekannt sind, dass sie keine managed Assemblies sind, und überspringt diese dann gleich:

public static class BindingRedirectsHelper
{
    private static readonly IDictionary<string, Assembly> _additional =
        new ConcurrentDictionary<string, Assembly>();

    public static void Initialize()
    {
        Console.WriteLine(@"[BRH] Initializing BindingRedirectsHelper.");

        // --
        // http://stackoverflow.com/a/9180843/107625

        var successCount = 0;
        var failureCount = 0;
        var skipCount = 0;

        // Wenn in einer Webanwendung verwendet, stattdessen diese Zeile
        // hier verwenden:
        /*
        var dir = Path.Combine(System.Web.HttpRuntime.AppDomainAppPath, @"bin");
        */

        // Diese Zeile hier für eine Windows-/Konsolen-Anwendung.
        var dir = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);

        if (dir != null)
        {
            Console.WriteLine($@"[BRH] Reading assemblies from '{dir}'.");

            // Ausschließen von bestimmten Assemblies, von denen wir wissen, dass
            // sie keine managed assemblies sind.
            var ignores = new[]
            {
                @"assembly-1-to-ignore.dll",
                @"assembly-2-to-ignore.dll"
            };

            var assemblyNames = Directory.GetFiles(dir, @"*.dll");

            Console.WriteLine($@"[BRH] Processing {assemblyNames.Length} assemblies.");

            foreach (var assemblyName in assemblyNames)
            {
                var fileName = Path.GetFileName(assemblyName);
                if (ignores.Any(i => i.Equals(fileName, StringComparison.OrdinalIgnoreCase)))
                {
                    skipCount++;

                    Console.WriteLine($@"[BRH] Ignoring assembly '{assemblyName}' with file name '{fileName}'.");
                }
                else
                {
                    Console.WriteLine($@"[BRH] Processing assembly '{assemblyName}' with file name '{fileName}'.");

                    try
                    {
                        var assembly = Assembly.LoadFile(assemblyName);
                        _additional.Add(assembly.GetName().Name, assembly);

                        successCount++;

                        Console.WriteLine(
                            $@"[BRH] Successfully loaded '{assemblyName}' with file name '{fileName}'.");
                    }
                    catch (BadImageFormatException x)
                    {
                        // Ignorieren.

                        failureCount++;

                        Console.WriteLine(
                            $@"[BRH] Ignoring exception '{x.Message}' while trying to load '{assemblyName}' with file name '{fileName}'.");
                    }
                    catch (FileLoadException x)
                    {
                        // Ignorieren.

                        failureCount++;

                        Console.WriteLine(
                            $@"[BRH] Ignoring exception '{x.Message}' while trying to load '{assemblyName}' with file name '{fileName}'.");
                    }
                }
            }
        }
        else
        {
            Console.WriteLine(@"[BRH] NOT reading assemblies, because path is NULL.");
        }

        Console.WriteLine(
            $@"[BRH] Successfully loaded {successCount} assemblies, failed loading {failureCount} assemblies, skipped loading {skipCount} assemblies.");

        AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += CurrentDomain_ResolveAssembly;
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_ResolveAssembly;

        Console.WriteLine(@"[BRH] Successfully subscribed to AppDomain events.");
    }

    private static Assembly CurrentDomain_ResolveAssembly(
        object sender,
        ResolveEventArgs e)
    {
        // Hier mache ich quasi mein eigenes, automatisches, Binding-Redirect.
        // (Z. B. Newtonsoft.Json 6.0.0.0 nach 9.0.0.0).

        var i = e.Name.IndexOf(',');
        if (i < 0) return null;

        var name = e.Name.Substring(0, i);

        var success = _additional.TryGetValue(name, out var res);

        Console.WriteLine(success
            ? $@"[BRH] Succeeded getting assembly with name '{name}', full name '{e.Name}'."
            : $@"[BRH] FAILED getting assembly with name '{name}', full name '{e.Name}'.");

        return res;
    }
}

Außerdem habe ich ein ConcurrentDictionary verwendet, damit wir mehr thread-safe sind.

Die obige Zeile

var dir = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);

gilt für eine Windows-/Konsolen-Anwendung.

Um das in einer Web-Anwendung (.NET Framework Full) zu nutzen, stattdessen diese Zeile hier verwenden:

var dir = Path.Combine(System.Web.HttpRuntime.AppDomainAppPath, @"bin");

In einer Webanwendung in .NET Core muss der Pfad anders ermittelt werden:

Ich übergebe in dem Fall aus dem Startup-Konstruktor den Wert von IWebHostEnvironment.ContentRootPath an meine Klasse, ungefähr so:

public static void Initialize(string contentRootPath)
{
    // ...

    var dir = contentRootPath;
    if (Directory.Exists(dir) && 
        Directory.Exists(Path.Combine(dir, @"bin"))) 
    {
        dir = Path.Combine(dir, @"bin");
    }

    // ...
}

Wenn sowohl für eine .NET-Framework-Webanwendung und eine .NET-Konsolenanwendung gelten soll, kann z. B. so ein Code verwenden:

public static void Initialize(string contentRootPath)
{
    // ...

    var dir = (string.IsNullOrEmpty(HttpRuntime.AppDomainAppPath)
        ? null
        : Path.Combine(HttpRuntime.AppDomainAppPath, @"bin")) 
            ?? Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);

    // ...
}