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


#1

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.


#2

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;

        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 name = e.Name.Substring(0, e.Name.IndexOf(','));

        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.