Снова про загрузку сторонних сборок, чтоб их…

В личном общении не скажу с кем появилась потребность загружать сторонние сборки, раскиданные по подкаталогам основной сборки. Вопрос - что и как грузить, каким образом, и как разрешить сопутствующие проблемы.

На эту тему я уже высказывался, но сегодня начинаю подозревать, что надо слегка все это дело дополнять / расширять / учитывать разные приколы.

И в первых рядах идет разница между LoadFile и LoadFrom для сборки. В MSDN на эту тему написано как-то мутновато, так что мое понимание состоит в следующем:

LoadFile/LoadFrom
Оба метода используются для загрузки сборок, но есть разница в поведении и результатах использования

  • Контекст загрузки:
    1. Assembly.LoadFrom: Этот метод загружает сборку в контексте приложения, что означает, что он учитывает зависимости и разрешает их в соответствии с правилами загрузки. Если сборка уже загружена, LoadFrom не загрузит её повторно, что помогает избежать конфликтов с типами. И все сопустсвующие сборки чисто теоретически должны грузиться автоматом.
    2. Assembly.LoadFile: Этот метод загружает сборку в отдельном контексте, что позволяет загружать несколько копий одной и той же сборки, даже если они имеют одинаковый идентификатор. Это может быть полезно, если нужно работать с разными версиями одной и той же сборки.
  • Разрешение зависимостей:
    1. Assembly.LoadFrom: При загрузке сборки с помощью LoadFrom, .NET будет искать зависимости в том же каталоге, где находится загружаемая сборка, а также в других местах, определённых в конфигурации приложения.
    2. Assembly.LoadFile: Этот метод не учитывает базовый каталог загружаемой сборки при разрешении зависимостей. Это означает, что если есть зависимости, которые находятся в том же каталоге, что и загружаемая сборка, они не будут автоматически загружены.
  • Использование:
    1. Assembly.LoadFrom: Рекомендуется использовать, когда хочется загрузить сборку и её зависимости, и когда важно избежать загрузки нескольких копий одной и той же сборки.
    2. Assembly.LoadFile: Используется, когда нужно загрузить несколько копий одной и той же сборки или когда нужно явно контролировать, какую сборку загружать, независимо от её зависимостей.

Так что, наверное, стоит все же использовать LoadFrom вместо LoadFile. Хотя, возможны варианты…

Теперь по поводу загрузчика. Базовый-то вариант даже дублировать не буду, сразу пропишу вариант "сначала поиск в каталоге основной сборки, и только потом уже, если ничего не найдено - в подкаталогах":

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class ExtensionInitialize : IExtensionApplication
{
    public void Initialize()
    {
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve;
    }

    public void Terminate() { }

    private Assembly CurrentDomainOnAssemblyResolve(object Sender, ResolveEventArgs Args)
    {
        string name = GetAssemblyName(Args);
        string path = Path.Combine(Path.GetDirectoryName(typeof(ExtensionInitialize).Assembly.Location),
            name + ".dll");
        if (File.Exists(path))
        {
            Assembly assembly = Assembly.LoadFrom(path);
            if (assembly.FullName == Args.Name)
            {
                return assembly;
            }
        }
        else
        {
            string subFolderPath = Directory
                .EnumerateFiles(Path.GetDirectoryName(typeof(ExtensionInitialize).Assembly.Location), name + ".dll",
                    SearchOption.AllDirectories)
                .FirstOrDefault();
            if (!string.IsNullOrWhiteSpace(subFolderPath))
            {
                Assembly assembly = Assembly.LoadFrom(subFolderPath);
                if (assembly.FullName == Args.Name)
                {
                    return assembly;
                }
            }
        }
        return null;
    }
    private string GetAssemblyName(ResolveEventArgs args)
    {
        if (args.Name.IndexOf(",") > -1)
        {
            return args.Name.Substring(0, args.Name.IndexOf(","));
        }
        else
        {
            return args.Name;
        }
    }
}

Все, можно выдыхать? Угу, ага, щщас! Проблема в том, что в каталоге основного аддона (или в подкаталогах, не столь суть важно) может лежать практически запрашиваемая библиотека, но слегка более новой версии. И при таких раскладах строка assembly.FullName == Args.Name вернет false, хотя вся разница только в номере версии. А имя, ключ, культура - все остальное совпадает. Так что будет проще всего (наверное) создать отдельный класс

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private class AssemblyInfo
{
    public AssemblyInfo(string FullNameSpec)
    {
        string[] splitted = FullNameSpec.Split(',')
            .Select(o => o.Trim())
            .ToArray();
        Name = splitted[0];
        string version = GetKey(splitted, "Version");
        if (Version.TryParse(version, out Version ver))
        {
            this.Version = ver;
        }
        string key = GetKey(splitted, "PublicKeyToken");
        if (!string.IsNullOrWhiteSpace(key))
        {
            this.KeyToken = key;
        }
        string culture = GetKey(splitted, "Culture");
        if (!string.IsNullOrWhiteSpace(culture))
        {
            this.Culture = culture;
        }
    }
    public string Name { get; }
    public Version Version { get; }
    public string KeyToken { get; }
    public string Culture { get; }
    private string GetKey(string[] SplittedStrings, string Key)
    {
        string value = SplittedStrings.FirstOrDefault(o => o.ToUpper().StartsWith(Key.ToUpper() + "="));
        if (string.IsNullOrWhiteSpace(value))
        {
            return string.Empty;
        }
        string[] splitted = value.Split('=');
        return splitted[1];
    }
}

И для него отдельный метод сравнения:

1
2
3
4
5
6
7
8
9
private bool EqualOrGreater(string AssemblyFullName, string ArgName)
{
    AssemblyInfo fullNameInfo = new AssemblyInfo(AssemblyFullName);
    AssemblyInfo argInfo = new AssemblyInfo(ArgName);
    return fullNameInfo.Name == argInfo.Name &&
           fullNameInfo.Version >= argInfo.Version &&
           fullNameInfo.Culture == argInfo.Culture &&
           fullNameInfo.KeyToken == argInfo.KeyToken;
}

И сравнение уже становится слегка посложнее: if (assembly.FullName == Args.Name) трансформируется в if (assembly.FullName == Args.Name || EqualOrGreater(assembly.FullName, Args.Name)). Ну и общий код, чтоб голову особо не ломать:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
public class ExtensionInitialize : IExtensionApplication
{
    public void Initialize()
    {
        AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve;
    }
    public void Terminate() { }
    private Assembly CurrentDomainOnAssemblyResolve(object Sender, ResolveEventArgs Args)
    {
        string name = GetAssemblyName(Args);
        string path = Path.Combine(Path.GetDirectoryName(typeof(ExtensionInitialize).Assembly.Location),
            name + ".dll");
        if (File.Exists(path))
        {
            Assembly assembly = Assembly.LoadFrom(path);
            if (assembly.FullName == Args.Name || EqualOrGreater(assembly.FullName, Args.Name))
            {
                return assembly;
            }
        }
        else
        {
            string subFolderPath = Directory
                .EnumerateFiles(Path.GetDirectoryName(typeof(ExtensionInitialize).Assembly.Location), name + ".dll",
                    SearchOption.AllDirectories)
                .FirstOrDefault();
            if (!string.IsNullOrWhiteSpace(subFolderPath))
            {
                Assembly assembly = Assembly.LoadFile(subFolderPath);
                if (assembly.FullName == Args.Name || EqualOrGreater(assembly.FullName, Args.Name))
                {
                    return assembly;
                }
            }
        }
        return null;
    }
    private string GetAssemblyName(ResolveEventArgs args)
    {
        if (args.Name.IndexOf(",") > -1)
        {
            return args.Name.Substring(0, args.Name.IndexOf(","));
        }
        else
        {
            return args.Name;
        }
    }
    private bool EqualOrGreater(string AssemblyFullName, string ArgName)
    {
        AssemblyInfo fullNameInfo = new AssemblyInfo(AssemblyFullName);
        AssemblyInfo argInfo = new AssemblyInfo(ArgName);
        return fullNameInfo.Name == argInfo.Name &&
               fullNameInfo.Version >= argInfo.Version &&
               fullNameInfo.Culture == argInfo.Culture &&
               fullNameInfo.KeyToken == argInfo.KeyToken;
    }
    private class AssemblyInfo
    {
        public AssemblyInfo(string FullNameSpec)
        {
            string[] splitted = FullNameSpec.Split(',')
                .Select(o => o.Trim())
                .ToArray();
            Name = splitted[0];
            string version = GetKey(splitted, "Version");
            if (Version.TryParse(version, out Version ver))
            {
                this.Version = ver;
            }
            string key = GetKey(splitted, "PublicKeyToken");
            if (!string.IsNullOrWhiteSpace(key))
            {
                this.KeyToken = key;
            }
            string culture = GetKey(splitted, "Culture");
            if (!string.IsNullOrWhiteSpace(culture))
            {
                this.Culture = culture;
            }
        }
        public string Name { get; }
        public Version Version { get; }
        public string KeyToken { get; }
        public string Culture { get; }
        private string GetKey(string[] SplittedStrings, string Key)
        {
            string value = SplittedStrings.FirstOrDefault(o => o.ToUpper().StartsWith(Key.ToUpper() + "="));
            if (string.IsNullOrWhiteSpace(value))
            {
                return string.Empty;
            }
            string[] splitted = value.Split('=');
            return splitted[1];
        }
    }
}

Как отдельный реп оформлять не стану, смысла особого не вижу.

ЗЫ У меня сработало корректно, если что - вместо версии 4.0.0.0 без вопросов загрузилась сборка 8.0.0.0 (в остальном сборки были одинаковы).

Размещено в .NET, AutoCAD, nanoCAD · Метки: , , , ,



Поделитесь своим мнением


Я не робот.