简体   繁体   中英

Using reflection to instantiate a class from an external assembly

I am currently trying to develop a method of running test classes in external projects programmatically using reflection. Here is a simplified chunk of code that should showcase my problem.

string pathToDLL = @"C:\Path\To\Test\Project\UnitTests.dll";
IEnumerable<Type> testClasses = assembly.GetExportedTypes();
Type testClass = testClasses.First();
object testClassInstance = assembly.CreateInstance(testClass.FullName); 

This code throws the following exception:

'assembly.CreateInstance(testClass.FullName)' threw an exception of type 'System.Reflection.TargetInvocationException'
    Data: {System.Collections.ListDictionaryInternal}
    HResult: -2146232828
    HelpLink: null
    InnerException: {System.IO.FileNotFoundException: Could not load file or assembly 'Project.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. The system cannot find the file specified.
File name: 'Project.Core, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'
   at Project.UnitTests.TestClass..ctor()}
    Message: "Exception has been thrown by the target of an invocation."
    Source: "System.Private.CoreLib"
    StackTrace: "   at System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck)\r\n   at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)\r\n   at System.Activator.CreateInstance(Type type, Boolean nonPublic)\r\n   at System.RuntimeType.CreateInstanceImpl(BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes, StackCrawlMark& stackMark)\r\n   at System.Activator.CreateInstance(Type type, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)\r\n   at System.Reflection.Assembly.CreateInstance(String typeName, Boolean ignoreCase, BindingFlags bindingAttr, Binder binder, Object[] args, CultureInfo culture, Object[] activationAttributes)\r\n   at System.Reflection.Assembly.CreateInstance(String typeName)"

In the stack trace it states that it "Could not load file or assembly 'Project.Core...'".

This project is one that the target DLL references directly (one that it tests). Does anyone know why this won't be able to pick up these DLLs automatically?


I've investigated ways of solving this problem:

  1. It could be the way that the dlls have been compiled - this can be changed as I am in control of this - which is currently by running dotnet build */*/project.json at solution level. This successfully compiles everything, and all of the relevant DLLs seem to be populated in the bin folder. I've also investigated whether or not changing to dotnet publish or dotnet build */*/project.json --configuration Release though neither seem to have helped.

  2. I've also looked into using different methods of compilation like Activator.CreateInstance again no dice.

  3. I don't seem to see a way to load multiple DLLs into the same Assembly class so that I can control the references. Since AppDomains have been removed from .NET Core this doesn't look like it is possible, though I may be mistaken/looking in the wrong area.


If what I'm doing doesn't seem like it will be possible, does anyone know if this kind of functionality can be achieved using a different method? Ie Roslyn?

I just thought that I would update this question with the solution that I managed to find, just in case someone else was having the same problem as I was. Though I would like to thank @Emrah Süngü for pointing me in the right direction.

Emrah drew my attention to the fact that I needed to import the dependencies of the DLL that I wanted to load in order to invoke the classes stored within it. One way to do this is to extend your app.config in order to import those dependencies - however I wanted to do this at runtime (with projects that I didn't know I was going to run prior starting the program) so I needed to look for another solution.

If you aren't using .NET Core this is relatively simple since AppDomains can be used to load all of the dependencies and execute your code. However, since this has been removed from .NET Core I needed to find another solution that would be compatible.

I toyed with the idea of running a separate process (or Powershell), and changing the working directory so that the process was running in the directory that stored all of the dependencies it needed. However, I couldn't find a way of doing this that allowed me to react to the outcome of running the methods.

Later I investigated manipulating the AssemblyLoadContext class, but ( at the time of writing ) there is little to no documentation on how this class. I did find this answer which was able to helped significantly... https://stackoverflow.com/a/37896162/6012159

In order for it to work I did have to make a slight change, instead of creating a new AssemblyLoader every time (which would cause exceptions to be thrown when trying to invoke methods within the Assembly), I reused the AssemblyLoader each time (Which removed this problem).

public class AssemblyLoader : AssemblyLoadContext
{
    private string folderPath;

    public AssemblyLoader(string folderPath)
    {
        this.folderPath = folderPath;
    }

    protected override Assembly Load(AssemblyName assemblyName)
    {
        var deps = DependencyContext.Default;
        var res = deps.CompileLibraries.Where(d => d.Name.Contains(assemblyName.Name)).ToList();
        if (res.Count > 0)
        {
            return Assembly.Load(new AssemblyName(res.First().Name));
        }
        else
        {
            var apiApplicationFileInfo = new FileInfo($"{folderPath}{Path.DirectorySeparatorChar}{assemblyName.Name}.dll");
            if (File.Exists(apiApplicationFileInfo.FullName))
            {
                return this.LoadFromAssemblyPath(apiApplicationFileInfo.FullName);
            }
        }
        return Assembly.Load(assemblyName);
    }
}

Which can be use to load assemblies like this:

string directory = @"C:\Path\To\Project\bin\Debug\netcoreapp1.0\publish\";

string pathToDLL = @"C:\Path\To\Project\bin\Debug\netcoreapp1.0\publish\project.dll";

AssemblyLoader al = new AssemblyLoader(directory);

Assembly assembly = al.LoadFromAssemblyPath(pathToDLL);

I am assuming that "UnitTests.dll" depends on (references) other dll(s) and your program does not know where to look for those referenced dll(s). You should (in fact have to) tell it to where to look for those dll(s) as well. By default is the same directory as your EXE. You can use app.config for telling where else to look. For Load() to succeed dependant dll(s) must be stored in your app's probing path.

That is the reason why you are getting an error.

Here you can find related article. https://msdn.microsoft.com/en-us/library/823z9h8w.aspx

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM