Resolving Assemblies in .NET Core

.NET Core applications rely heavily on NuGet to resolve their dependencies, which is simplifies development. Unless you’re packaging the application up as a self-contained deployment, at runtime the resolution of assemblies is not as straightforward, however, as all the dependencies are no longer coppied to the output folder.

I needed to reflect over the types defined in an external assembly in order to look for specific attributes. The first problem is how to load an assembly from the disk in .NET Core.

AssemblyLoadContext

The System.Runtime.Loader package contains a type called AssemblyLoadContext that can be used to load assemblies from a specific path, meaning we can write code like this:

public sealed class Program
{
    public static int Main(string[] args)
    {
        Assembly assembly =
            AssemblyLoadContext.Default.LoadFromAssemblyPath(args[0]);

        PrintTypes(assembly);
        return 0;
    }

    private static void PrintTypes(Assembly assembly)
    {
        foreach (TypeInfo type in assembly.DefinedTypes)
        {
            Console.WriteLine(type.Name);
            foreach (PropertyInfo property in type.DeclaredProperties)
            {
                string attributes = string.Join(
                    ", ",
                    property.CustomAttributes.Select(a => a.AttributeType.Name));

                if (!string.IsNullOrEmpty(attributes))
                {
                    Console.WriteLine("    [{0}]", attributes);
                }
                Console.WriteLine("    {0} {1}", property.PropertyType.Name, property.Name);
            }
        }
    }
}

However, as soon as an attribute that is defined in another assembly is found (e.g. from a NuGet package), the above will fail with a System.IO.FileNotFoundException. This is because the AssemblyLoadContext will not load any dependencies – we’ll have to do that ourselves.

Dependencies File

When you build a .NET Core application, the compiler also produces some Runtime Configuration Files, in particular the deps.json file that includes the dependencies for the application. We can hook in to this to allow us to resolve the assemblies at runtime, using additional helper classes from the System.Runtime.Loader package to parse the file for us.

internal sealed class AssemblyResolver : IDisposable
{
    private readonly ICompilationAssemblyResolver assemblyResolver;
    private readonly DependencyContext dependencyContext;
    private readonly AssemblyLoadContext loadContext;

    public AssemblyResolver(string path)
    {
        this.Assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
        this.dependencyContext = DependencyContext.Load(this.Assembly);

        this.assemblyResolver = new CompositeCompilationAssemblyResolver(new ICompilationAssemblyResolver[]
        {
            new AppBaseCompilationAssemblyResolver(Path.GetDirectoryName(path)),
            new ReferenceAssemblyPathResolver(),
            new PackageCompilationAssemblyResolver()
        });

        this.loadContext = AssemblyLoadContext.GetLoadContext(this.Assembly);
        this.loadContext.Resolving += OnResolving;
    }

    public Assembly Assembly { get; }

    public void Dispose()
    {
        this.loadContext.Resolving -= this.OnResolving;
    }

    private Assembly OnResolving(AssemblyLoadContext context, AssemblyName name)
    {
        bool NamesMatch(RuntimeLibrary runtime)
        {
            return string.Equals(runtime.Name, name.Name, StringComparison.OrdinalIgnoreCase);
        }

        RuntimeLibrary library =
            this.dependencyContext.RuntimeLibraries.FirstOrDefault(NamesMatch);
        if (library != null)
        {
            var wrapper = new CompilationLibrary(
                library.Type,
                library.Name,
                library.Version,
                library.Hash,
                library.RuntimeAssemblyGroups.SelectMany(g => g.AssetPaths),
                library.Dependencies,
                library.Serviceable);

            var assemblies = new List<string>();
            this.assemblyResolver.TryResolveAssemblyPaths(wrapper, assemblies);
            if (assemblies.Count > 0)
            {
                return this.loadContext.LoadFromAssemblyPath(assemblies[0]);
            }
        }

        return null;
    }
}

The code works by listening to the Resolving event of the AssemblyLoadContext, which gives us a chance to find the assembly ourselves (the class also implements the IDisposable interface to allow unregistering from the event, as the AssemblyLoadContext is static so will keep the class alive). During resolution, we find the assembly inside the dependency file (note the use of a case insensitive comparer) and wrap this up inside a CompilationLibrary. This can actually be skipped if we set the PreserveCompilationContext option in the csproj file to true, as we can then use the DependencyContext.CompileLibraries property, however, the above code works without that setting being present. Also note that I don’t use the result of TryResolveAssemblyPaths, as it will return true even if it didn’t add any paths to the list.

All that’s left is to modify the main program and the code will now be able to resolve attributes in other assemblies:

public static int Main(string[] args)
{
    using (var dynamicContext = new AssemblyResolver(args[0]))
    {
        PrintTypes(dynamicContext.Assembly);
    }
    return 0;
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s