0

I am working with the following class:

public class Person
{

    public string Name { get; set; }

    public int Age { get; set; }
}

And I have a string containing following:

public class PersonActions 
{
    public static void Greet(Person p)
    {
        string test = p.Name;
    } 
}

In my client application developped in WPF (.NET 4.7) I am compiling this string at runtime and invoke the Greet method like this:

        //Person x = new Person();
        //x.Name = "Albert";
        //x.Age = 76;

        var assembly = Assembly.LoadFile(pathToAsseblyContainingPersonClass);
        Type t = assembly.GetType("Person");
        var x = Activator.CreateInstance(t);

        CSharpCodeProvider provider = new CSharpCodeProvider();
        CompilerParameters parameters = new CompilerParameters();

        parameters.ReferencedAssemblies.Add(pathToAsseblyContainingPersonClass);

        //code being the code from abrom above (PersonActions)
        CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
        Assembly importassembly = results.CompiledAssembly;

        Type assemblytype = importassembly.GetType("PersonActions");
        ConstructorInfo constructor = assemblytype.GetConstructor(Type.EmptyTypes);
        object classObject = constructor.Invoke(new object[] { });// not used for anything

        MethodInfo main = assemblytype.GetMethod("Greet");
        main.Invoke(classObject, new object[] { x });

Unfotunately this always crashes because somehow it cannot find the method with the same parameter type even if the types come from the same assembly.

The error thrown is a "System.IO.FileNotFoundException" although this makes not much sense. It's not a file that can't be found it's the method overload.

Somehow it is just looking for: public static void Greet(object p) Using just 'object' as parameter type works, but is not a possibility in my case.

Is there a way to recieve the object in the type that it is? Or maby to tell the Invocation method that the types match?

EDIT:

Guess I made both an error in my code above and my tests: Declareing the Person as mentioned before (now commented above) works properly:

Person x = new Person();
x.Name = "Albert";
x.Age = 76;

Using Activator.Createinstance (now correct above) to create the Person x dynamically form the assebly does not work. It seems like var x = Activator.CreateInstance(t); causes x still to be an "object" and not a "Person".

EDIT 2: Here a minimal working example of the problem:

Having a solution containing one WPF application. MainWindow.cs containing:

using Microsoft.CSharp;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Example
{
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Window_Loaded(object sender, RoutedEventArgs e)
    {

        string code = @"public class PersonActions 
                        {
                        public static void Greet(Person p)
                        {
                        }
                        }";
        //Change to an absolute path if there is an exception 
        string pathToAsseblyContainingPersonClass = System.IO.Path.GetFullPath(@"..\..\..\Person\bin\Debug\Person.dll");

        var assembly = Assembly.LoadFile(pathToAsseblyContainingPersonClass);
        Type t = assembly.GetType("Person");
        var x = Activator.CreateInstance(t);

        CSharpCodeProvider provider = new CSharpCodeProvider();
        CompilerParameters parameters = new CompilerParameters();

        parameters.ReferencedAssemblies.Add(pathToAsseblyContainingPersonClass);

        //code being the code from abrom above (PersonActions)
        CompilerResults results = provider.CompileAssemblyFromSource(parameters, code);
        Assembly importassembly = results.CompiledAssembly;

        Type assemblytype = importassembly.GetType("PersonActions");
        ConstructorInfo constructor = assemblytype.GetConstructor(Type.EmptyTypes);
        object classObject = constructor.Invoke(new object[] { });// not used for anything

        MethodInfo main = assemblytype.GetMethod("Greet");
        main.Invoke(classObject, new object[] { x });
    }
}
}

And containing one class Library Project calles "Person" containing: (note that there is no namespace)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

EDIT 3: What I ended up with

Thanks to @Adam Benson I could identify the whole problem. The overall problem is, that the current appdomain does not allow to directly load load assemblies from other appdomains. Like Adam pointed out there are three solutions for that (in the linked Microsoft article). The third and definitely also the easiest solution to implement is using the AssemblyResolve event. Although this is a good solution it pains my heart and bones to let my application run into exceptions to resolve this problem.

Like Adam also pointed out is that you get another exception if you put the dll directly into the folder where the exe is located. This is only partly true since the evil twin error only appears if you compare the Person from the original Debug folder assembly and the Person loaded from the appdomain assembly (basically if you have the dll in both directories)

Loading the assembly only from the folder where there exe is located resolves both the FileNotFound and the evil twin error:

old: System.IO.Path.GetFullPath(@"..\..\..\Person\bin\Debug\Person.dll");

new:System.IO.Path.GetFullPath(@"Person.dll");

So what I ended up doing was copying the necessary assembly into the current working directory first:

File.Copy(pathToAsseblyContainingPersonClass, currentDir + @"\\Person.dll" , true);
colosso
  • 2,525
  • 3
  • 25
  • 49
  • 2
    _"...Unfotunately this always crashes..."_ - you should post the actual exception message. Also, mentioning `WPF` is not really relevant in this case –  May 10 '19 at 12:42
  • It's also unclear why you need classObject, since you are calling a static method. The first argument to `Invoke` is ignored. – Clemens May 10 '19 at 12:43
  • The GetMethod() overload you use can only find instance methods. You need BindingFlags.Public | BindingFlags.Static. And pass null as the first argument to Invoke() since it is static. – Hans Passant May 10 '19 at 13:37
  • https://stackoverflow.com/q/11908156/17034 – Hans Passant May 10 '19 at 13:38
  • @MickyD its a "System.IO.FileNotFoundException" – colosso May 13 '19 at 07:23
  • @Clemens thanks for pointing that out, its not used for anything – colosso May 13 '19 at 07:24
  • @HansPassant I dont think so, Changing the the parameter to a string both inside the call and the method causes everything to work perfectly as it shoud without any binding flags – colosso May 13 '19 at 07:26
  • 1
    The simple explanation is the one you forgot to document well in your question: these two classes live in different assemblies. And the just-in-time compiler cannot find the one that contains "Person", thus producing the FileNotFoundException. Complete guess, not at all backed-up by either the question or the code snippet, but the problem fits the mishap. Use Fuslogvw.exe to troubleshoot assembly resolution problems. – Hans Passant May 13 '19 at 08:05
  • @HansPassant I made a slight mistake in the code above. Unfortunately this rules out you mentioned cause. – colosso May 13 '19 at 09:14
  • Maybe the class PersonActions should also be also public? – Menahem May 13 '19 at 09:39
  • @Menahem thanks for pointing that out, althou that changes nothing :) – colosso May 13 '19 at 09:43

2 Answers2

2

results.CompiledAssembly throws FileNotFoundException because the assembly is not being generated due to an error occurring during the generation process. You can see the actual compilation error by checking Errors property of CompilerResults.

In this case, the error is that code provided to CompileAssemblyFromSource does not know what Person class is.

You can fix this by adding a reference to the assembly containing Person class:

parameters.ReferencedAssemblies.Add("some_dll");

Edit: I missed the comment saying that parameters contain the reference to assembly containing Person class. That probably means that there is a different error in the results.Error collection. Check it and I will update the answer (I cannot comment yet due to not having 50 rep).

kra
  • 87
  • 9
  • Unfortunately there are no other Exceptions anywhere. Although there was a slight mistake in the code above. Have a look at my edits. – colosso May 13 '19 at 09:08
  • It's currently unclear to me what the error was previously and what the problem is now. Can you elaborate? – kra May 13 '19 at 09:18
  • Same error as before, nothing changed. By adding the assebly to the solution and declaring Person directly the code works. The Problem is that it doesent work by using Activator.CreateInstance(t); – colosso May 13 '19 at 09:44
  • @colosso I don't understand why you need to use Activator.CreateInstance. Can you explain? Also, can you post a minimal working example illustrating your problem? – kra May 13 '19 at 10:50
  • Added an example above – colosso May 13 '19 at 11:13
  • 1
    Thanks. You can now use AssemblyResolve as stated in the other answer and it works. But do you have to load this assembly dynamically in your code? If you add a regular reference and initialize a Person normally instead, it will work without AssemblyResolve trick. – kra May 13 '19 at 11:43
  • I added above what I ended up doing. Unfortunately adding regular references is not a possibility since the "Person" object will change during runtime like the content of the PersonActions class aswell. – colosso May 13 '19 at 13:43
1

This works (at least it doesn't exception):

object classObject = constructor.Invoke(new object[] { });// not used for anything

//////////////////////////////////////////

AppDomain.CurrentDomain.AssemblyResolve +=
    (object sender, ResolveEventArgs resolve_args) =>
    {
        if (resolve_args.Name == assembly.FullName)
            return assembly;
        return null;
    };

//////////////////////////////////////////

MethodInfo main = assemblytype.GetMethod("Greet");

Based on https://support.microsoft.com/en-gb/help/837908/how-to-load-an-assembly-at-runtime-that-is-located-in-a-folder-that-is method 3 (use the AssemblyResolve event).

I must confess to being mystified as to why it doesn't just work since you have added a ref to the assembly.

I should add that copying the extra dll that defines Person into your exe directory will not work as you then run into the "evil twin" issue where a type created in one assembly cannot be used by another instance of that assembly. (The error you get is the mind-bending "System.ArgumentException: 'Object of type 'Person' cannot be converted to type 'Person'."!!)

Edit: Just discovered that LoadFrom avoids loading the same assembly twice. See Difference between LoadFile and LoadFrom with .NET Assemblies?

Adam Benson
  • 7,480
  • 4
  • 22
  • 45
  • Thank you very much for your help. Have a look what I ended up doing above, if you are interested! The bounty will follow. :) – colosso May 13 '19 at 13:41
  • @colosso - Totally agree that loading from the exe dir was the nicest way of going about it. We hit the "evil twin" issue a while ago when we were loading dlls from more than 1 folder. Oh the fun we had ... Anyway, glad I could give a bit of a nudge in the right direction :-) – Adam Benson May 13 '19 at 14:02