Когда JIT компилирует метод, ему необходимо получить информацию об используемых в методе типах, например:
Тогда он идет в TypeRef сборки(служебная таблица в метаданных), и находит там такую информацию:
Это определение типа, которое добавляется в метаданные только если в коде идет работа с типом, а потом в AssemblyRef, где уже лежит вся информация о типе. Таким образом он собирает всю информацию вместе: полное имя типа и полное имя сборки, далее версия, культура и публичный токен. Потом пробует загрузить сборку в AppDomain, если она еще не была загружена, если сборка не имеет strong name, то сборка будет идентифицироваться только по имени(без версии, culture и publictoken'a).
Внутри CLR при загрузке сборки вызывает Assembly.Load
. Этот статический метод, он открыт и можно самостоятельно вызвать его из кода. Рихтер пишет, что если у сборки нет strong name, то биндинг редиректов применяться не будет, а если есть - будет. Если же сборка загружается автоматически, и сборки не подписаны, то биндинг в рантайме будет применять. Если подписаны, то будет применяться только в момент компиляции солюшена и записываться в AssemblyRef правильный номер сборки, но в рантайме она не сможет быть подтянута, потому что если сборка подписана strong name, то redirect в рантайме будет работать только при наличии прописанного биндинг редиректа.Например так:
<dependentAssembly>
<assemblyIdentity name="someAssembly"
publicKeyToken="32ab4ba45e0a69a1"
culture="en-us" />
<bindingRedirect oldVersion="7.0.0.0" newVersion="8.0.0.0" />
</dependentAssembly>
Это пример отсюда. В таком случае в рантайме при попытке разрешить сборку someAssembly с public token "32ab4ba45e0a69a1" и culture "en-us" и version "7.0.0.0" runtime будет разрешать такую же сборку но версии "8.0.0.0".
Далее происходит поиск сборки, в GAC, application base directory, private path subdirectories, codebase locations(это также задается в настройках csproj msdn).
также если сборка не подписана, то CLR не будет пытаться найди ее в gac, потому что в gac только подписанные сборки. Если в итоге сборку никак не получится обнаружить то будет FileNotFoundException
.
В чем отличие Assembly.Load от AppDomain.Load и почему второй лучше не применять(какие есть минусы):
AppDomain.Load
это уже не статический метод, а экземплярный. Его отличие от Assembly.Load в том, сборка будет искаться в вызываемом AppDomain, по его политикам доступа и app base directory. Далее из этого вызываемого AppDomain'a будет она будет маршаллиться в текущий как тип значения(сериализоваться и десериалищовываться). Из-за этого возникает ситуация, что у AppDomain'a из которого вызывался метод (AppDomain.Load) может не быть прав на загрузку этой сборки, и будет сгенерирован FileNotFoundException
В случае Assembly.LoadFile(string path)
, сначала вызывается метод AssemblyName.GetAssemblyName(string assemblyFile). Этот метод загружает файл, находит в нем AssemblyDef таблицу и находит там AssemblyName
. После того, как мы получим AssemblyName вызывается метод Load, которому передается AssemblyName(это вторая перегрузка метода Assembly.Load). В этом случае CLR всегда будет применять биндинг редиректов. Далее если метод Load смог найти сборку(пока путь переданный в LoadFile не используется), то сборка загружается и возвращается методов LoadFile, если нет, то сборка загружается по пути, который был передан в метод LoadFile
.
Также LoadFrom, вместе с загружаемой сборкой пытается загрузить все ее зависимости(дописать как именно)
Как ищутся сборки в .net core и .net framework (дописать сюда)[https://gist.github.com/brager17/404621e102bc573acc103a762253c856#%D0%B2-%D1%87%D0%B5%D0%BC-
%D0%BE%D1%82%D0%BB%D0%B8%D1%87%D0%B8%D0%B5-%D0%BF%D0%BE%D0%B8%D1%81%D0%BA%D0%B0-%D1%81%D0%B1%D0%BE%D1%80%D0%BE%D0%BA-%D0%B2-net-core-%D0%B8-net-framework]
В .net core эти методы генерируют PlatformNotSupportedException. При загрузке сборок этими методами CLR следит, чтобы никакой код в этих сборках не мог вызываться. Если он вызывается, то генерируется:
Также оба этих метода не применяют политики при загрузке сборок. Т.E вы всегда получите точно ту версию сборки которая была указана в пути. Если бы вместо этих методов использовались бы Assembly.LoadFrom/Assemby.Load, то CLR бы посмотрел есть ли уже загруженная сборка с таким именем(даже если у нее другая версия) и вернул бы загруженную. Например:
var pathToAssemblyVersion2_0_0_0 =
@"C:\Users\evgeniy\RiderProjects\frameworkapp\frameworkapp1\bin\Debug\frameworkapp12.exe";
var pathToSomeAssemblyVersion1_0_0_0 =
@"C:\Users\evgeniy\RiderProjects\frameworkapp\frameworkapp1\bin\Debug\frameworkapp1.exe";
var a = Assembly.LoadFrom(pathToAssemblyVersion2_0_0_0); // loaded 2.0.0.0
var aa = Assembly.LoadFrom(pathToSomeAssemblyVersion1_0_0_0); // returned already loaded 2.0.0.0
a = Assembly.LoadFrom(pathToAssemblyVersion2_0_0_0); // loaded 2.0.0.0
aa = Assembly.ReflectionOnlyLoadFrom(pathToSomeAssemblyVersion1_0_0_0); // loaded 1.0.0.0
ReflectionOnlyLoad будет искать сборку в BaseDirectory,private paths,codebases, ReflectionOnlyLoadFrom - загрузит именну ту сборку которая указана в пути. Также AppDomain предлагает ReflectionOnlyAssemblyResolve
event, который позволяет разрешать все зависимости.
Также отличие LoadFrom от ReflectionOnlyLoadFrom - LoadFrom загружает еще все зависимости, а ReflectionOnlyLoadFrom загружает только текущую сборку, а все зависимости нужно решать вручную используя ReflectionOnlyAssemblyResolve
event.
CLR не поддерживает выгрузку сборок, можно выгрузить только домен.
Также Рихтер показал способ, как можно загрузить сборки, если не хочется тащить много .dll файлов, можно добавить их всех в ресурсы:
<ItemGroup>
<EmbeddedResource Include="C:\Users\evgeniy\RiderProjects\frameworkapp\frameworkapp3\bin\Debug\frameworkapp3.exe"/>
<EmbeddedResource Include="C:\Users\evgeniy\RiderProjects\frameworkapp\frameworkapp1\bin\Debug\frameworkapp1.exe"/>
<EmbeddedResource Include="C:\Users\evgeniy\RiderProjects\frameworkapp\frameworkapp\bin\Debug\frameworkapp.exe"/>
</ItemGroup>
Все эти файлы будут добавлены в ресурсы в сборке:
Оттуда их можно загрузить(псевдокод, не тестировал):
AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) =>
{
var dll = new AssemblyName(eventArgs.Name).Name + ".exe"; // got assembly name
var dllFromResources = Assembly.GetExecutingAssembly().GetManifestResourceNames()
.FirstOrDefault(x => x.EndsWith(dll)); // found resource name
if (dllFromResources == null) return null;
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(dllFromResources)) // got resource as stream
{
var assemblyData = new byte[stream.Length];
stream.Read(assemblyData, 0, assemblyData.Length);
return Assembly.Load(assemblyData); // load it
}
};
Интересно, почему этот код не сможет загрузить сборки в runtime(точнее почему не вызовется AssemblyResolve, когда будет попытка создать NotExport):
internal class Program
{
public static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) =>
{
Console.WriteLine("AssemblyResolve");
var dllFromResources = Assembly.GetExecutingAssembly().GetManifestResourceNames()
.First(); // found resource name
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(dllFromResources)
) // got resource as stream
{
Console.WriteLine("using");
var assemblyData = new byte[stream.Length];
stream.Read(assemblyData, 0, assemblyData.Length);
return Assembly.Load(assemblyData); // load it
}
};
var export = new NotExport(); // The NotExport is class in other assembly, we don't have reference on it in runtime
}
}
А этот работает (в данном случае callback AssemblyResolve будет вызван):
internal class Program
{
public static void Main(string[] args)
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, eventArgs) =>
{
Console.WriteLine("AssemblyResolve");
var dllFromResources = Assembly.GetExecutingAssembly().GetManifestResourceNames()
.First(); // found resource name
using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(dllFromResources)
) // got resource as stream
{
Console.WriteLine("using");
var assemblyData = new byte[stream.Length];
stream.Read(assemblyData, 0, assemblyData.Length);
return Assembly.Load(assemblyData); // load it
}
};
NewMain();
}
public static void NewMain()
{
var export = new NotExport(); // The NotExport is class in other assembly, we don't have reference on it in runtime
}
}
Дело в jit компиляторе, Он сразу компилирует весь метод, и если не может найти какой-то тип, кидает FileNotFoundException
, получается, что в первом случае ошибка будет еще до вызова метода Main. А во втором метод Main вызовется, мы подпишемся на event, и при компиляции метода NewMain удачно загрузим сборку.
Рихтер пишет, что Type - это только ссылка на метаданные, и работать с Type можно не загружая сборку, я попробовал и у меня при любом обращении к type, загружается сборка в которой он определен(кроме загрузки сборки как ReflfectionOnly
var loaded = AppDomain.CurrentDomain.GetAssemblies(); // assembly is not loaded
var a = Assembly.ReflectionOnlyLoadFrom( @"C:\Users\evgeniy\RiderProjects\frameworkapp\Core\bin\Debug\Core.dll");
var type = a.GetExportedTypes().Single(x => x.Name == "Base").GetTypeInfo();
var afterLoaded = AppDomain.CurrentDomain.GetAssemblies(); // assembly is still not loaded
А метод GetTypeInfo(как пишет Рихтер), загружает сборку. В моем случае сборка загружается еще до попытки обращения к type.GetTypeInfo. Поэтому я просто считаю, что GetTypeInfo - это более удобный способ работы с типом, потому что в этом классе есть свойства посредники, предоставляющие более удобный api, чем Type.
Что интересно, это как можно располагать в памяти без кеширования объекты получаемые reflection'ом. Когда CLR отдает нам reflection объект(например Type), среде приходится создавать его. Но самой среде этот объект не нужен, потому что существует более простой способ для хранения этой информации, это хэндлы: RuntimeTypeHandle, RuntimeFieldHandle и RuntimeMethodHandle. Все эти типы содержат единственное поле - IntPtr, которое является ссылкой на объект в Loader Head App Domain'a. Например, вот так можно оптимизировать хранение Type инстансов.
var list = new object[] {new RedBlackTree.Node(), new Random(), new RedBlackTree()};
var handles = list.Select(Type.GetTypeHandle).ToArray(); // storage RunTypeHandles
foreach (var runtimeTypeHandle in handles)
{
_testOutputHelper.WriteLine(Type.GetTypeFromHandle(runtimeTypeHandle).FullName);
}
Получение происходит с помощью метода Type.GetTypeFromHandle. Также хэндлы можно получаться MethodBase, и для FieldInfo:
MethodBase methodBase = ...;
var methodHandle = methodBase.MethodHandle;
var mb = MethodHandle.FromMethodHandle(methodHandle);
FieldInfo field = ...
var fieldHandle = field.FieldHandle;
var fi = FieldInfo.GetFieldFromHandle(fieldHandle);