Снова про загрузку сторонних сборок, чтоб их…
В личном общении не скажу с кем появилась потребность загружать сторонние сборки, раскиданные по подкаталогам основной сборки. Вопрос - что и как грузить, каким образом, и как разрешить сопутствующие проблемы.
На эту тему я уже высказывался, но сегодня начинаю подозревать, что надо слегка все это дело дополнять / расширять / учитывать разные приколы.
И в первых рядах идет разница между LoadFile и LoadFrom для сборки. В MSDN на эту тему написано как-то мутновато, так что мое понимание состоит в следующем:
Теперь по поводу загрузчика. Базовый-то вариант даже дублировать не буду, сразу пропишу вариант "сначала поиск в каталоге основной сборки, и только потом уже, если ничего не найдено - в подкаталогах":
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 (в остальном сборки были одинаковы).