Reputation: 2525
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);
Upvotes: 0
Views: 715
Reputation: 8522
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?
Upvotes: 1
Reputation: 87
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).
Upvotes: 2