Proposed Surface Area
namespace System.Reflection
{
public abstract class MetadataAssemblyResolver
{
public abstract Assembly Resolve(MetadataLoadContext context, AssemblyName assemblyName = null);
}
public class PathAssemblyResolver : MetadataAssemblyResolver
{
public PathAssemblyResolver(IEnumerable<string> assemblyPaths);
public override Assembly Resolve(MetadataLoadContext context, AssemblyName assemblyName);
}
public sealed partial class MetadataLoadContext : System.IDisposable
{
public MetadataLoadContext(MetadataAssemblyResolver resolver, string coreAssemblyName = null);
public Assembly CoreAssembly { get; }
public void Dispose();
public System.Collections.Generic.IEnumerable<Assembly> GetAssemblies();
public Assembly LoadFromAssemblyName(AssemblyName assemblyName);
public Assembly LoadFromAssemblyName(string assemblyName);
public Assembly LoadFromAssemblyPath(string assemblyPath);
public Assembly LoadFromByteArray(byte[] assembly);
public Assembly LoadFromStream(System.IO.Stream assembly);
}
}
Scenario: Inspecting IL metadata assemblies in custom directories.
var mscorlibpath = @"D:\temp\myselfcontainedapp\mscorlib.dll";
var mydllpath = @"D:\temp\myselfcontainedapp\myap.dll";
var resolver = new PathAssemblyResolver(new string[] {mscorlibpath, mydllpath});
using (var context = new MetadataLoadContext(resolver, "mscorlib"))
{
Assembly a = context.LoadFromAssemblyPath(@"D:\temp\myselfcontainedapp\myapp.dll");
var type = a.GetType("MyApp.MyClass", throwOnError: true);
}
This creates a load context with a policy to probe for external assemblies in a specific directory. An external assembly is loaded on demand when there is a type reference to that assembly.
MetadataLoadContext implements IDisposable. Disposing releases any file locks that may be held on the underlying assembly files. Once disposed, any Assembly objects dispensed by the MetadataLoadContext (and any Reflection objects dispensed by those Assemblies) must no longer be called. Their behavior is undefined. (We'll make a good faith effort to throw ObjectDisposedExceptions but some properties may returned cached data. The primary goal is to avoid the really anti-social behavior such as accessing freed native memory.)
You determine the bind algorithm by selecting a pre-existing resolver (MetadataAssemblyResolver-derived class) or writing you own MetadataAssemblyResolver-derived class and overriding the Resolve method. The Resolve method should return either null or an Assembly object dispensed by one of the Load methods on the MetadataLoadContext that was passed to the Resolve method.
The provided DirectoryAssemblyResolver policy probes each directory in order to find a given assembly by name and stops once the first provided assembly file is found. It does not compare assembly versions or try to find the best matching assembly (if the assembly exists in more than one location). It appends ".dll" to the assembly name in order to find the file. If no assembly is found, a FileNotFoundExcepton is raised. DirectoryAssemblyResolver also sets the context.CoreAssemblyName property to "mscorlib", but that can be changed by the consumer before the first access to the core assembly is requested; the first access to the core assembly occurs by a reference to primitive type such as System.Object or System.Int32.
Scenario: Inspecting IL metadata assemblies using the runtime loader.
In .NET Framework, a common pattern is creating a new AppDomain (using the existing AppDomainSetup) just to load a single assembly, inspect its types and custom attributes, then unload in order to free the memory.
We've looked into the idea of allowing runtime-supplied Assemblies to be injected into the TypeLoader but this is a snake pit. For this idea to work at all, the cooperation needs to work both ways and runtime Assemblies aren't designed to interoperate with externals. In particular, Type.MakeGenericType() is not copacetic with instantiating runtime types on non-runtime type arguments.
A possible midway solution is for TypeLoader (or the calling app) to reload the runtime assembly (from it's Location property) as a TypeLoader-supplied assembly. This allows users to use the binding algorithm of the current runtime if that's what they want. But it does mean "loading the assemblies twice" so there is a perf hit.
Currently there is no MetadataAssemblyResolver that supports this policy, but it would not be difficult to create.
Earlier Proposal and Notes
namespace System.Reflection
{
public sealed class TypeLoader : IDisposable
{
public TypeLoader();
public TypeLoader(string coreAssemblyName);
public Assembly LoadFromAssemblyPath(string assemblyPath);
public Assembly LoadFromByteArray(byte[] assembly);
public Assembly LoadFromStream(Stream assembly);
public Assembly LoadFromAssemblyName(AssemblyName assemblyName);
public Assembly LoadFromAssemblyName(string assemblyName);
public string CoreAssemblyName { get; set; }
public event Func<TypeLoader, AssemblyName, Assembly> Resolving;
public IEnumerable<Assembly> GetAssemblies();
public void Dispose();
}
}
Scenario : Legacy ReflectionOnlyLoad emulation
Assembly a = TypeLoader.Default.Load("Foo, Version=1.2.3.4, PublicKeyToken=xxxxxxxxxx");
The static Default property returns a global TypeLoader that serves as a (compatible within reason) replacement for Assembly.ReflectionOnlyLoad. As with ReflectionOnlyLoad, you must preload all dependencies (other than mscorlib) either in advance or on-demand by subscribing to the AppDomain.ReflectionOnlyAssemblyResolve event.
This approach is not recommended for new code. It's for porting old code only.
Issue - Throw FileLoadException or FileNotFoundException when the Resolving event handler returns null?
ReflectionOnlyLoad (the feature we're officially replacing) throws FileLoadException.
AssemblyLoadContext (the new feature) throws FileNotFoundException.
The former is more compatible, the latter is more logical.
Issue - Semantics of LoadFromAssemblyName() - be like ReflectionOnlyLoad or like AssemblyLoadContext?
On the surface, LoadFromAssemblyName() looks like the equivalent of ReflectionOnlyLoad(AssemblyName). But in fact, it's not.
Assembly.ReflectionOnlyLoad(AssemblyName) only returns an Assembly that had already been loaded via a prior operation. If no assembly matching the name had been loaded, it throws FileNotFoundException (unlike when it fails a dependency load, which throws FileLoadException.) It does not invoke the ReflectionOnlyAssemblyResolve event.
So the name is misleading since it never loads an assembly. It only fetches a previously loaded Assembly. I'm not sure what the use case for it is - if it return null rather than throwing, it could be seen as the TryGet api but given that it throws without giving the resolve event handler a chance to bind, what is the rationale for this behavior?
AssemblyLoadContext.LoadFromAssemblyName(), on the other hand, works much more like I'd expect. It triggers the Load overload and the event handler as needed. It's basically a manual way of triggering the same process that happens when the runtime internally resolves an assembly reference.
I'd rather adopt AssemblyLoadContext's behavior here as we're adopting the api name and the behavior is less surprising. On the other hand, we're supposed to be replacing ReflectionOnlyLoad. I see three choices:
-
Adopt the AssemblyLoadContext behavior.
-
Adopt the ReflectionOnly behavior.
-
Have both Load and LoadFromAssemblyName overloads.
Proposed Surface Area
Scenario: Inspecting IL metadata assemblies in custom directories.
This creates a load context with a policy to probe for external assemblies in a specific directory. An external assembly is loaded on demand when there is a type reference to that assembly.
MetadataLoadContextimplementsIDisposable. Disposing releases any file locks that may be held on the underlying assembly files. Once disposed, any Assembly objects dispensed by the MetadataLoadContext (and any Reflection objects dispensed by those Assemblies) must no longer be called. Their behavior is undefined. (We'll make a good faith effort to throwObjectDisposedExceptionsbut some properties may returned cached data. The primary goal is to avoid the really anti-social behavior such as accessing freed native memory.)You determine the bind algorithm by selecting a pre-existing resolver (MetadataAssemblyResolver-derived class) or writing you own MetadataAssemblyResolver-derived class and overriding the
Resolvemethod. The Resolve method should return eithernullor anAssemblyobject dispensed by one of theLoadmethods on theMetadataLoadContextthat was passed to the Resolve method.The provided DirectoryAssemblyResolver policy probes each directory in order to find a given assembly by name and stops once the first provided assembly file is found. It does not compare assembly versions or try to find the best matching assembly (if the assembly exists in more than one location). It appends ".dll" to the assembly name in order to find the file. If no assembly is found, a FileNotFoundExcepton is raised. DirectoryAssemblyResolver also sets the context.CoreAssemblyName property to "mscorlib", but that can be changed by the consumer before the first access to the core assembly is requested; the first access to the core assembly occurs by a reference to primitive type such as System.Object or System.Int32.
Scenario: Inspecting IL metadata assemblies using the runtime loader.
In .NET Framework, a common pattern is creating a new AppDomain (using the existing AppDomainSetup) just to load a single assembly, inspect its types and custom attributes, then unload in order to free the memory.
We've looked into the idea of allowing runtime-supplied Assemblies to be injected into the TypeLoader but this is a snake pit. For this idea to work at all, the cooperation needs to work both ways and runtime Assemblies aren't designed to interoperate with externals. In particular, Type.MakeGenericType() is not copacetic with instantiating runtime types on non-runtime type arguments.
A possible midway solution is for TypeLoader (or the calling app) to reload the runtime assembly (from it's Location property) as a TypeLoader-supplied assembly. This allows users to use the binding algorithm of the current runtime if that's what they want. But it does mean "loading the assemblies twice" so there is a perf hit.
Currently there is no MetadataAssemblyResolver that supports this policy, but it would not be difficult to create.
Earlier Proposal and Notes
Scenario : Legacy ReflectionOnlyLoad emulation
The static
Defaultproperty returns a globalTypeLoaderthat serves as a (compatible within reason) replacement forAssembly.ReflectionOnlyLoad. As withReflectionOnlyLoad, you must preload all dependencies (other than mscorlib) either in advance or on-demand by subscribing to theAppDomain.ReflectionOnlyAssemblyResolveevent.This approach is not recommended for new code. It's for porting old code only.
Issue - Throw FileLoadException or FileNotFoundException when the Resolving event handler returns null?
ReflectionOnlyLoad (the feature we're officially replacing) throws
FileLoadException.AssemblyLoadContext (the new feature) throws
FileNotFoundException.The former is more compatible, the latter is more logical.
Issue - Semantics of LoadFromAssemblyName() - be like ReflectionOnlyLoad or like AssemblyLoadContext?
On the surface,
LoadFromAssemblyName()looks like the equivalent ofReflectionOnlyLoad(AssemblyName). But in fact, it's not.Assembly.ReflectionOnlyLoad(AssemblyName)only returns an Assembly that had already been loaded via a prior operation. If no assembly matching the name had been loaded, it throwsFileNotFoundException(unlike when it fails a dependency load, which throwsFileLoadException.) It does not invoke theReflectionOnlyAssemblyResolveevent.So the name is misleading since it never loads an assembly. It only fetches a previously loaded Assembly. I'm not sure what the use case for it is - if it return
nullrather than throwing, it could be seen as theTryGetapi but given that it throws without giving the resolve event handler a chance to bind, what is the rationale for this behavior?AssemblyLoadContext.LoadFromAssemblyName(), on the other hand, works much more like I'd expect. It triggers theLoadoverload and the event handler as needed. It's basically a manual way of triggering the same process that happens when the runtime internally resolves an assembly reference.I'd rather adopt
AssemblyLoadContext's behavior here as we're adopting the api name and the behavior is less surprising. On the other hand, we're supposed to be replacingReflectionOnlyLoad. I see three choices:Adopt the
AssemblyLoadContextbehavior.Adopt the
ReflectionOnlybehavior.Have both
LoadandLoadFromAssemblyNameoverloads.