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

2 thoughts on “Resolving Assemblies in .NET Core

  1. This post is closest of what I’m looking for on the internet. However I have a question (posted on your codeproject article also):

    When I’m using this class, the Dispose() method is actually called before any of OnResolving(), causing none of the dependencies to be loaded. Is that the case for you? I’m not sure how this class can make sure all the OnResolving() triggered before it’s disposed

    If I comment out the Dispose() method, everything’s working correctly. However if I want to load multiple dlls using this class, it needs to have Dispose() method

    • Where I’ve used it in the main method, all the code runs while the object is alive. If you’re just having a single instance of the class and you’re happy to treat it as a singleton then there’s no need to dispose it – the dispose is just there to unregister from the event handler so the object can be garbage collected (the object we’re adding to an event handler to actually has a longer lifetime than our class so will keep it alive).

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