Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Missing default AssemblyResolver on .NET Core #306

Closed
uhrm opened this issue Nov 27, 2016 · 14 comments
Closed

Missing default AssemblyResolver on .NET Core #306

uhrm opened this issue Nov 27, 2016 · 14 comments
Assignees
Milestone

Comments

@uhrm
Copy link

uhrm commented Nov 27, 2016

I have essentially the following program:

var module = ModuleDefinition.ReadModule(moduleName);
var type = module.Types.Single(ti => ti.FullName == typeName);
var method = type.Methods.Single(mi => mi.Name == methodName);
foreach (var instr in method.Body.Instructions) {
    switch (instr.OpCode.Code) {
        case Code.Call: {
            var mref = instr.Operand as MethodReference;
            var mdef = mref.Resolve();
        }
    }
}

This works fine in Mono, but gives an ArgumentNullException running in .NET Core (Linux) when calling mref.Resolve(). Stacktrace is

System.ArgumentNullException : Value cannot be null.
Parameter name: assemblyResolver
   at Mono.Cecil.MetadataResolver..ctor(IAssemblyResolver assemblyResolver)
   at Mono.Cecil.ModuleDefinition.get_MetadataResolver()
   at Mono.Cecil.ModuleDefinition.Resolve(MethodReference method)
   at ...
@jbevain jbevain self-assigned this Nov 27, 2016
@jbevain
Copy link
Owner

jbevain commented Nov 27, 2016

Good catch, we do not have a default assembly resolver for .NET Core. That's something we need to fix.
You can workaround it by implementing an IAssemblyResolver and passing an instance to:

ReadModule(moduleName, new ReaderParameters { AssemblyResolver = yourAssemblyResolver });

Thanks for reporting this!

@jbevain jbevain changed the title ArgumentNullException in MethodReference.Resolve() using .NET Core Missing default AssemblyResolver on .NET Core Nov 27, 2016
@jbevain
Copy link
Owner

jbevain commented Nov 27, 2016

It should be relatively easy to create a IAssemblyResolver implementation for when the .NET Core application has been published (all .dll will be in the output folder).
It will be harder to create an implementation that works in the development cycle and has to resolve to the different libraries in the NuGets that the app references.

@jbevain
Copy link
Owner

jbevain commented Nov 28, 2016

The following works during the development. We would need a way to switch to a simple one when running published.

  1. Change your project json to include "preserveCompilationContext" : true
{
  "buildOptions": {
    "preserveCompilationContext": true
  }
}
  1. Reference the package Microsoft.Extensions.DependencyModel:
{
  "frameworks": {
    "netcoreapp1.1": {
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.1.0"
        },
        "Mono.Cecil": "0.10.0-beta1-v2",
        "Microsoft.Extensions.DependencyModel": "1.1.0-preview1-001100"
      },
    }
  }
}
  1. Add this code to your project:
using System;
using System.Collections.Generic;
using System.Linq;

namespace Mono.Cecil
{
    public sealed class AssemblyResolutionException : Exception {

        readonly AssemblyNameReference reference;

        public AssemblyNameReference AssemblyReference {
            get { return reference; }
        }

        public AssemblyResolutionException (AssemblyNameReference reference)
            : base (string.Format ("Failed to resolve assembly: '{0}'", reference))
        {
            this.reference = reference;
        }
    }

    class DotNetCoreAssemblyResolver : IAssemblyResolver
    {
        Dictionary<string, Lazy<AssemblyDefinition>> _libraries;

        public DotNetCoreAssemblyResolver()
        {
             _libraries = new Dictionary<string, Lazy<AssemblyDefinition>>();

             var compileLibraries = DependencyContext.Default.CompileLibraries;
             foreach (var library in compileLibraries)
             {
                 var path = library.ResolveReferencePaths().FirstOrDefault();
                 if (string.IsNullOrEmpty(path))
                    continue;
                
                _libraries.Add(library.Name, new Lazy<AssemblyDefinition>(() => AssemblyDefinition.ReadAssembly(path, new ReaderParameters() { AssemblyResolver = this })));
             }
        }

        public virtual AssemblyDefinition Resolve(string fullName)
        {
            return Resolve(fullName, new ReaderParameters());
        }

        public virtual AssemblyDefinition Resolve(string fullName, ReaderParameters parameters)
        {
            if (fullName == null)
                throw new ArgumentNullException("fullName");

            return Resolve(AssemblyNameReference.Parse(fullName), parameters);
        }

        public AssemblyDefinition Resolve(AssemblyNameReference name)
        {
            return Resolve(name, new ReaderParameters());
        }

        public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
        {
            if (name == null)
                throw new ArgumentNullException("name");

            Lazy<AssemblyDefinition> asm;
            if (_libraries.TryGetValue(name.Name, out asm))
                return asm.Value;

            throw new AssemblyResolutionException(name);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!disposing)
                return;
 
            foreach (var lazy in _libraries.Values)
            {
                if (!lazy.IsValueCreated)
                    continue;

                lazy.Value.Dispose();
            }
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
}
  1. Use the new resolver
using (var module = ModuleDefinition.ReadModule(fileName", new ReaderParameters { AssemblyResolver = new DotNetCoreAssemblyResolver() }))

@Ziflin
Copy link

Ziflin commented Mar 20, 2017

I've got a similar problem with resolving assemblies in .NET Core and the above does not seem to work (in VS 2017 release). I have a 'netstandard1.6.1' library "A" and "B". "B" depends on "A". I have a 'netcoreapp1.1' application that loads "B" and then attempts to resolve a parameter from a method in "B" whose parameter type is from "A".

The problem seems to be that for all the libraries in DependencyContext.Default.CompileLibraries the var path = library.ResolveReferencePaths().FirstOrDefault(); returns null. So when the Resolve() call comes through, the TryGetValue() call fails and it throws an exception.

Since the above example was written based on the .json project framework, I tried adding what I think is the correct line to my .csproj:

<PreserveCompilationContext>true</PreserveCompilationContext>

But that did not seem to help. This was based on this bug report which implies it was fixed for 'publish' (?) but I'm trying to get it to work during development currently (and in unit tests).

@jbevain
Copy link
Owner

jbevain commented Mar 20, 2017

@Ziflin could you put together a small repro that shows your issue and link it here, I'll have a look with the newer toolkit. Thanks!

@uhrm
Copy link
Author

uhrm commented Sep 11, 2017

I've finally found some time to investigate the issue further. I've migrated to .NET Core 2.0 in the meantime. The following assembly resolver seems to work fine from the command line. I guess it should also work for published applications.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

using Mono.Cecil;

namespace Some.Namespace
{
    class DotNetCoreAssemblyResolver : IAssemblyResolver
    {
        private static readonly string BaseDirectory = System.AppContext.BaseDirectory;
        private static readonly string RuntimeDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location);

        private readonly Dictionary<string,AssemblyDefinition> libraries;

        public DotNetCoreAssemblyResolver()
        {
             this.libraries = new Dictionary<string,AssemblyDefinition>();
        }

        public virtual AssemblyDefinition Resolve(string fullName)
        {
            return Resolve(fullName, new ReaderParameters() { AssemblyResolver = this });
        }

        public virtual AssemblyDefinition Resolve(string fullName, ReaderParameters parameters)
        {
            if (fullName == null) {
                throw new ArgumentNullException("fullName");
            }
            return Resolve(AssemblyNameReference.Parse(fullName), parameters);
        }

        // IAssemblyResolver API

        public AssemblyDefinition Resolve(AssemblyNameReference name)
        {
            return Resolve(name, new ReaderParameters() { AssemblyResolver = this });
        }

        public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
        {
            if (name == null) {
                throw new ArgumentNullException("name");
            }
            AssemblyDefinition def;
            if (!this.libraries.TryGetValue(name.Name, out def)) {
                var path = Path.Combine(BaseDirectory, $"{name.Name}.dll");
                if (File.Exists(path)) {
                    def = AssemblyDefinition.ReadAssembly(path, parameters);
                    this.libraries.Add(name.Name, def);
                }
                else {
                    path = Path.Combine(RuntimeDirectory, $"{name.Name}.dll");
                    if (File.Exists(path)) {
                        def = AssemblyDefinition.ReadAssembly(path, parameters);
                        this.libraries.Add(name.Name, def);
                    }
                    else {
                        path = $"{name.Name}.dll";
                        if (File.Exists(path)) {
                            def = AssemblyDefinition.ReadAssembly(path, parameters);
                            this.libraries.Add(name.Name, def);
                        }
                    }
                }
            }
            return def;
        }

        // IDisposable API

        protected virtual void Dispose(bool disposing)
        {
            if (!disposing) {
                return;
            }
            foreach (var def in this.libraries.Values) {
                def.Dispose();
            }
        }

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
}

The basic strategy to find assemblies is as follows:

  • Use the System.AppContext.BaseDirectory property for finding the path of project assemblies.
  • Use reflection on the object assembly for finding the path of runtime assemblies.
  • Also scan the current working directory if the assembly is not found in the other two locations.
  • The module name of the assembly is constructed by appending .dll to the assembly name (.NET Core never uses .exe).

Hopefully, this might be of some help to others.

@bording
Copy link

bording commented Sep 12, 2017

@uhrm What about when an assembly is referenced directly from the global NuGet package folder? I'm not seeing anything that would handle that case.

@uhrm
Copy link
Author

uhrm commented Sep 12, 2017

@bording Unfortunately, I don't understand what that means... My (limited and incomplete) understanding of package management under .NET Core 2.0 goes only so far:

  1. Assemblies from the .NET SDK are implicitly referenced and reside in the runtime directory.
  2. NuGet packages are referenced using <PackageReference Include="..." Version="..." /> and get copied into the base directory during build.
  3. Project references are specified as <ProjectReference Include="..." />. The project artifacts (with their transitive dependencies) are also copied into the base directory during build.

I don't know of other options to specify dependencies.

@uhrm uhrm closed this as completed Sep 12, 2017
@uhrm uhrm reopened this Sep 12, 2017
@bording
Copy link

bording commented Sep 12, 2017

NuGet packages are referenced using and get copied into the base directory during build.

@uhrm This is the step I'm talking about. Assemblies from package references do not get copied into the base directory, though there is a setting you can add to your project to make this happen.

By default, those assemblies are directly referenced from their location in the global NuGet package folder.

If you look at the *..runtimeconfig.json file in the bin folder, you'll see that additional probing paths are listed, and those are all places .NET Core will look for assemblies.

You are correct about project references. Those will be copied into the bin folder.

@erozenfeld
Copy link
Contributor

@sbomer FYI

@per-samuelsson
Copy link

So can anyone kind make this more concrete for me.

If...
Under .NET Core 2.0, we have a simple application (i.e "myapp.exe") that reference a NuGet package (like... well, whichever), and we do not have some non-standard setting set ("...though there is a setting you can add to your project to make this happen").

...then:
We use Cecil to read "myapp.exe" from build output and want to do something as trivial as walk the dependency tree (recursively) for that module, we'll see it fail because Cecil won't be able to resolve the reference to that NuGet-packages assemblies? Not even with any known, custom resolver?

If so, that's quite a severe limitation, no?

@per-samuelsson
Copy link

Nevermind my previous question, I had to dig into it myself already to find out. I see how things fit together now. I'll do some experimenting with a custom assembly resolver for .NET Core (2.0). I'll update here if I find anything of value I guess.

@per-samuelsson
Copy link

@jbevain

I might be missing something essential here, but I just can't see how the custom resolver posted earlier could have ever worked.

Basing it on DependencyContext.Default.CompileLibraries, will that not resolve to libraries of the current application? And if so, how often would you want to resolve references based on that?

I'm guessing "all" Cecil consumers would work on a given input assembly, and for that to work, resolving must be based on that, no?

If you'd ever find time to reflect on that, could you also comment on the status of this:

Good catch, we do not have a default assembly resolver for .NET Core. That's something we need to fix.

(And just to make sure I'm not only coming here bringing pain: I really love Cecil; its code-base, the design, and the straightforwardness in using it - big props! 💘)

@jbevain
Copy link
Owner

jbevain commented Oct 25, 2017

Fixed in #444.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants