Click here to Skip to main content
14,876,655 members
Articles / .NET
Technical Blog
Posted 30 Jun 2017

Tagged as

Stats

26K views
6 bookmarked

Resolving Assemblies in .NET Core

Rate me:
Please Sign up or sign in to vote.
5.00/5 (11 votes)
30 Jun 2017CPOL2 min read
How to resolve assemblies in .NET Core

.NET Core applications rely heavily on NuGet to resolve their dependencies, which 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 copied 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:

C#
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.

C#
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:

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

Filed under: CodeProject

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Samuel Cragg
United Kingdom United Kingdom
No Biography provided

Comments and Discussions

 
QuestionGreat post man!! Pin
mfvilella22-Aug-18 10:11
Membermfvilella22-Aug-18 10:11 
QuestionQuestion about resolving nuget dependencies Pin
Member 470570121-Feb-18 5:23
MemberMember 470570121-Feb-18 5:23 
GeneralMy vote of 5 Pin
Eugene Sadovoi27-Jan-18 14:02
MemberEugene Sadovoi27-Jan-18 14:02 
QuestionA question Pin
David Nguyen Hung Phuong2-Nov-17 4:17
professionalDavid Nguyen Hung Phuong2-Nov-17 4:17 
AnswerRe: A question Pin
Samuel Cragg3-Nov-17 22:19
MemberSamuel Cragg3-Nov-17 22:19 
GeneralRe: A question Pin
David Nguyen Hung Phuong5-Nov-17 2:04
professionalDavid Nguyen Hung Phuong5-Nov-17 2:04 
GeneralMy vote of 5 Pin
Callum7-Jul-17 6:30
MemberCallum7-Jul-17 6:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.