Skip to content

Assembly.ReflectionOnlyLoad replacement - TypeInfo and family implementation over System.Reflection.Metadata #15033

Description

@davidfowl

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:

  1. Adopt the AssemblyLoadContext behavior.

  2. Adopt the ReflectionOnly behavior.

  3. Have both Load and LoadFromAssemblyName overloads.

Metadata

Metadata

Assignees

Labels

api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.ReflectionenhancementProduct code improvement that does NOT require public API changes/additions

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions