I am stuck for 3rd day with this problem and I have no freaking idea why this doesn't work. I just want to load external .dll to read some info using reflection and delete the file after all. The problem is that read files are locked. The most strange thing is that only two files are locked while i read 5 of them succesfully. I've tried ShadowCopy with no result. I got no clue right now.
This is my appdomain class:
public class AppDomainExpander
{
private Type[] _types;
public Type[] Types
{
get { return _types; }
set { _types = value; }
}
public void Create(string domainName, string path)
{
AppDomainSetup aps = new AppDomainSetup();
aps.ShadowCopyFiles = "true";
AppDomain dmn = AppDomain.CreateDomain(domainName);
string typename = typeof(DomainCommunicator).FullName;
string assemblyName = typeof(DomainCommunicator).Assembly.FullName;
var inner = (DomainCommunicator)dmn.CreateInstanceAndUnwrap(assemblyName, typename);
inner.Create();
Assembly assembly = Assembly.LoadFrom(path);
Types = assembly.GetTypes();
AppDomain.Unload(dmn); //it's strange that the code even work because i try to unload domain before i get Types[]
}
public class DomainCommunicator : MarshalByRefObject
{
public void Create()
{
AppDomain.CurrentDomain.DomainUnload += new EventHandler(OnDomainUnload);
}
void OnDomainUnload(object sender, EventArgs e)
{
AppDomain.CurrentDomain.DomainUnload -= new EventHandler(OnDomainUnload);
}
}
}
And this is how i try to use it:
var expander = new AppDomainExpander();
expander.Create("MyDomain", file.Path);
foreach (var type in expander.Types)
The types are loaded to your main AppDomain which does not have the ShadowCopy feature enabled. This is why the files are locked.
You will need to load the assembly in the DomainCommunicator.Create method instead. Note though that you cannot keep the Types property. This will lead to types leaking from the child AppDomain to the main one and the file locking problems that you currently face.
I just noticed that only interfaces are locked. What is more when I load two classes and then two interfaces it's OK. But when I add interface and implementing class at same time it locks
Related
I have type A implementing interface IA from assembly aA.dll. It creates K instances of type B from aB.dll. I want to get A from aA.dll to use type B from bB.dll which is same in its name and version to aB.dll yet with minor code differences. So I try:
public class CollectibleAssemblyLoadContext
: AssemblyLoadContext
{
public CollectibleAssemblyLoadContext() : base(isCollectible: true)
{ }
protected override Assembly Load(AssemblyName assemblyName)
{
string path = "";
if (AssemblyNameToPath.TryGetValue(assemblyName.FullName, out path))
{
return Assembly.LoadFile(path);
}
return null;
}
}
Yet when I try to create a A as IA using:
public static object GetRaw<T>() where T : class
{
AssemblyLoadContext context = new CollectibleAssemblyLoadContext();
var type = typeof(T);
Assembly assembly = context.LoadFromAssemblyName(type.Assembly.GetName());
Type programType = assembly.GetType(type.FullName);
object result = Activator.CreateInstance(programType);
return result;
}
Generally X is what I get while V is what I want from this picture:
Type B is used from preloaded in general context aB.dll. How to make it load if from bB.dll? How to make sure AssemblyLoadContext would use Load to get all the assemblies from scratch, not only one?
A small demo project we tried to test it with, yet it fails to load more than one assembly deep no matter what...
To my knowledge different versions of DLLs / dependencies are not allowed in the same AppDomain. So a type can only resolve to one DLL in the same ApPDomain. Spinning up a new AppDomain might be what you can try.
I need to create new AppDomain and pass executing assembly to it, without any access to the assembly file itself.
I have tried to use a binary serializer to transfer the loaded assembly, but it can't be done with the assembly existing only in memory.
The problem is that new AppDomain throws assembly load exception because there is no such a file in current directory.
Update:
Even if I found a way to get the actual assembly byte array, saved it to disk and forced it to be loaded into new AppDomain - there is a casting error - I can't cast it from proxy type to actual class or interface.
Example project:
https://drive.google.com/open?id=16z9nr8W5D8HjkBABgOCe5MIZKJSpFOul
Update 2:
The example below is working when executed from assembly on the hard drive - there is no need to search the assembly file, and assembly FullName is enough in CreateInstanceFromAndUnwrap method. All errors occur when the assembly containing this code is loaded from byte array.
Usage:
public sealed class LoadDomain: IDisposable
{
private AppDomain _domain;
private IFacade _value;
public IFacade Value
{
get
{
return _value;
}
}
public void Dispose()
{
if (_domain != null)
{
AppDomain.Unload(_domain);
_domain = null;
}
}
public void Load(byte[] assemblyBytes,string assemblyPath,string assemmblyDir)
{
var domainSetup = new AppDomainSetup()
{
ShadowCopyDirectories = "true",
ShadowCopyFiles = "true",
ApplicationName = Assembly.GetExecutingAssembly().ManifestModule.ScopeName,
DynamicBase = assemmblyDir,
LoaderOptimization = LoaderOptimization.MultiDomainHost,
};
_domain = AppDomain.CreateDomain("Isolated:" + Guid.NewGuid(), null, domainSetup);
// _domain.Load(assemblyBytes); not working
// _domain.AssemblyResolve+=.. not working
Type type = typeof(Facade);
_value = (IFacade)_domain.CreateInstanceFromAndUnwrap(assemblyPath, type.FullName);
//assemlby path working, but casting error : InvalidCastException
//assembly FullName not working: FileNotFoundException
}
}
public class Facade : MarshalByRefObject, IFacade
{
public void DoSomething()
{
MessageBox.Show("new domain");
}
}
This is not a duplicate - I have reviewed this related StackOverflow question with no luck: How to Load an Assembly to AppDomain with all references recursively?
I have two console applications. AssemblyLoaderTest.exe and testapp.exe
I am trying to use AssemblyLoaderTest.exe to dynamically load testapp.exe and call a method from a class within testapp.exe
So far the code works - the method "TestWrite()" in testapp.exe is executed correctly (and outputsuccess.txt is written), however, testapp.exe is loaded in the same AppDomain, which is proven because "CallMethodFromDllInNewAppDomain" always returns false. I am trying to load testapp.exe in a new AppDomain.
My question: how can I modify the below code so that testapp.exe is loaded in a new AppDomain, and as a result, "CallMethodFromDllInNewAppDomain" returns true? Thank you!
Code below. Both can be simply copied into new Console applications in VS and executed/compiled.
Console application #1:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Policy;
namespace AssemblyLoaderTest
{
class Program
{
static void Main(string[] args)
{
List<object> parameters = new List<object>();
parameters.Add("Test from console app");
bool loadedInNewAppDomain = DynamicAssemblyLoader.CallMethodFromDllInNewAppDomain(#"c:\temp\testapp.exe", "testapp.TestClass", "TestWrite", parameters);
}
}
public static class DynamicAssemblyLoader
{
public static string ExeLoc = "";
public static bool CallMethodFromDllInNewAppDomain(string exePath, string fullyQualifiedClassName, string methodName, List<object> parameters)
{
ExeLoc = exePath;
List<Assembly> assembliesLoadedBefore = AppDomain.CurrentDomain.GetAssemblies().ToList<Assembly>();
int assemblyCountBefore = assembliesLoadedBefore.Count;
AppDomainSetup domaininfo = new AppDomainSetup();
Evidence adevidence = AppDomain.CurrentDomain.Evidence;
AppDomain domain = AppDomain.CreateDomain("testDomain", adevidence, domaininfo);
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
domain.CreateInstanceFromAndUnwrap(exePath, fullyQualifiedClassName);
List<Assembly> assemblies = domain.GetAssemblies().ToList<Assembly>();
string mainExeName = System.IO.Path.GetFileNameWithoutExtension(exePath);
Assembly assembly = assemblies.FirstOrDefault(c => c.FullName.StartsWith(mainExeName));
Type type2 = assembly.GetType(fullyQualifiedClassName);
List<Type> parameterTypes = new List<Type>();
foreach (var parameter in parameters)
{
parameterTypes.Add(parameter.GetType());
}
var methodInfo = type2.GetMethod(methodName, parameterTypes.ToArray());
var testClass = Activator.CreateInstance(type2);
object returnValue = methodInfo.Invoke(testClass, parameters.ToArray());
List<Assembly> assembliesLoadedAfter = AppDomain.CurrentDomain.GetAssemblies().ToList<Assembly>();
int assemblyCountAfter = assembliesLoadedAfter.Count;
if (assemblyCountAfter > assemblyCountBefore)
{
// Code always comes here
return false;
}
else
{
// This would prove the assembly was loaded in a NEW domain. Never gets here.
return true;
}
}
public static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
// This is required I've found
return System.Reflection.Assembly.LoadFrom(ExeLoc);
}
}
}
Console application #2:
using System;
namespace testapp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello from console");
}
}
[Serializable]
public class TestClass : MarshalByRefObject
{
public void TestWrite(string message)
{
System.IO.File.WriteAllText(#"outputsuccess.txt", message);
}
}
}
Use this class. Here are some notes:
This class explicitly sets the current directory of the process and the app base path of the isolated app domain. This isn't entirely necessary, but it will make your life a whole lot easier.
If you don't set the app base path to the directory containing the secondary assembly, then the runtime will attempt to resolve any dependencies of the secondary assembly against the same app base path as the primary assembly, which probably doesn't have the secondary assembly's dependencies. You could use the AssemblyResolve event to manually resolve the dependencies correctly, but settings the app base path is a much simpler and less error-prone way to do this.
If you don't set Environment.CurrentDirectory, then file operations such as File.WriteAllText("myfile.txt", "blah") will resolve paths against the current directory, which is probably not what the secondary assembly's author intended. (ASIDE: Always resolve paths manually for this reason.)
I believe simple reflection operations like GetMethod won't work on a MarshalByRefObject proxy such as returned by CreateInstanceFromAndUnwrap. So you need to do a little more to invoke.
If you are the owner of both the primary and secondary assemblies, you could create an interface for the invocation -- put the interface in a shared assembly, define the cross-domain call in the interface, implement the interface in the target class, do a domain.CreateInstanceFromAndUnwrap on the target type and cast the result as the interface, which you can then use to call across the domain boundary.
The solution below provides an alternative means that is less invasive -- you don't have to own the secondary assembly for this technique to work. The idea is that the primary domain creates a well-known intermediary object (InvokerHelper) in the secondary domain, and that intermediary performs the necessary reflection from inside the secondary domain.
Here's a complete implementation:
// Provides a means of invoking an assembly in an isolated appdomain
public static class IsolatedInvoker
{
// main Invoke method
public static void Invoke(string assemblyFile, string typeName, string methodName, object[] parameters)
{
// resolve path
assemblyFile = Path.Combine(Environment.CurrentDirectory, assemblyFile);
Debug.Assert(assemblyFile != null);
// get base path
var appBasePath = Path.GetDirectoryName(assemblyFile);
Debug.Assert(appBasePath != null);
// change current directory
var oldDirectory = Environment.CurrentDirectory;
Environment.CurrentDirectory = appBasePath;
try
{
// create new app domain
var domain = AppDomain.CreateDomain(Guid.NewGuid().ToString(), null, appBasePath, null, false);
try
{
// create instance
var invoker = (InvokerHelper) domain.CreateInstanceFromAndUnwrap(Assembly.GetExecutingAssembly().Location, typeof(InvokerHelper).FullName);
// invoke method
var result = invoker.InvokeHelper(assemblyFile, typeName, methodName, parameters);
// process result
Debug.WriteLine(result);
}
finally
{
// unload app domain
AppDomain.Unload(domain);
}
}
finally
{
// revert current directory
Environment.CurrentDirectory = oldDirectory;
}
}
// This helper class is instantiated in an isolated app domain
private class InvokerHelper : MarshalByRefObject
{
// This helper function is executed in an isolated app domain
public object InvokeHelper(string assemblyFile, string typeName, string methodName, object[] parameters)
{
// create an instance of the target object
var handle = Activator.CreateInstanceFrom(assemblyFile, typeName);
// get the instance of the target object
var instance = handle.Unwrap();
// get the type of the target object
var type = instance.GetType();
// invoke the method
var result = type.InvokeMember(methodName, BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance, null, instance, parameters);
// success
return result;
}
}
}
I have a service that creates an app domain and starts it:
this._appDomain = AppDomain.CreateDomain(this._appName, AppDomain.CurrentDomain.Evidence, appDomainSetup);
this._startStopControllerToRun = (IStartStop)this._appDomain.CreateInstanceFromAndUnwrap(assemblyName, this._fullyQualifiedClassName);
this._startStopControllerToRun.Start();
That has been running well for a long time now. The issue is when the controller, started within this app domain, calls a framework logging class. The logger gets the entry assembly name and records that as the source in the event log. This is how the logger gets the source (caller) name:
private static string GetSource()
{
try
{
var assembly = Assembly.GetEntryAssembly();
// GetEntryAssembly() can return null when called in the context of a unit test project.
// That can also happen when called from an app hosted in IIS, or even a windows service.
if (assembly == null)
{
// From http://stackoverflow.com/a/14165787/279516:
assembly = new StackTrace().GetFrames().Last().GetMethod().Module.Assembly;
}
return assembly.GetName().Name;
}
catch
{
return "Unknown";
}
}
Before adding that if check, the logger would record "Unknown" for the source. After some research, I added that attempt in the if block. Now the logger records "mscorlib" as the source (entry assembly name).
This is the overview:
Host -> Controller (running within app domain)
How can I get the name of the assembly (that has the controller) running within the domain?
Note: I also tried this (below), but it gives me the name of the framework where the logging class exists (not the name of the assembly in which the controller is running within the app domain):
assembly = Assembly.GetExecutingAssembly();
This is perhaps one way to do what you want. What I'm demonstrating here is passing and receiving metadata to the created AppDomain via SetData and GetData methods so disregard how I am creating the actual remote type.
using System;
using System.Reflection;
namespace ConsoleApplication13
{
class Program
{
static void Main(string[] args)
{
AppDomain appDomain = AppDomain.CreateDomain("foo");
appDomain.SetData(FooUtility.SourceKey, FooUtility.SourceValue);
IFoo foo = (IFoo)appDomain.CreateInstanceFromAndUnwrap(Assembly.GetEntryAssembly().Location, typeof(Foo).FullName);
foo.DoSomething();
}
}
public static class FooUtility
{
public const string SourceKey = "Source";
public const string SourceValue = "Foo Host";
}
public interface IFoo
{
void DoSomething();
}
public class Foo : MarshalByRefObject, IFoo
{
public void DoSomething()
{
string source = AppDomain.CurrentDomain.GetData(FooUtility.SourceKey) as string;
if (String.IsNullOrWhiteSpace(source))
source = "some default";
Console.WriteLine(source);
}
}
}
Which outputs:
Foo Host
Press any key to continue ...
So in your case, you could pass whatever source metadata to the AppDomain:
this._appDomain = AppDomain.CreateDomain(this._appName, AppDomain.CurrentDomain.Evidence, appDomainSetup);
this._appDomain.SetData("Source", "MyController");
this._startStopControllerToRun = (IStartStop)this._appDomain.CreateInstanceFromAndUnwrap(assemblyName, this._fullyQualifiedClassName);
this._startStopControllerToRun.Start();
and in your GetSource method check for its existence.
private static string GetSource()
{
try
{
string source = AppDomain.CurrentDomain.GetData("Source") as string;
if (!String.IsNullOrWhiteSpace(source))
return source;
var assembly = Assembly.GetEntryAssembly();
// GetEntryAssembly() can return null when called in the context of a unit test project.
// That can also happen when called from an app hosted in IIS, or even a windows service.
if (assembly == null)
{
// From http://stackoverflow.com/a/14165787/279516:
assembly = new StackTrace().GetFrames().Last().GetMethod().Module.Assembly;
}
return assembly.GetName().Name;
}
catch
{
return "Unknown";
}
}
UPDATE ALTERNATIVE
You could also declare a public interface method for setting the source on a static location in the target domain as well.
using System;
using System.Reflection;
namespace ConsoleApplication13
{
class Program
{
static void Main(string[] args)
{
AppDomain appDomain = AppDomain.CreateDomain("foo");
IFoo foo = (IFoo)appDomain.CreateInstanceFromAndUnwrap(Assembly.GetEntryAssembly().Location, typeof(Foo).FullName);
foo.SetSource("Foo Host");
foo.DoSomething();
}
}
public interface IFoo
{
void DoSomething();
void SetSource(string source);
}
public class Foo : MarshalByRefObject, IFoo
{
public void DoSomething()
{
string source = Foo.Source;
if (String.IsNullOrWhiteSpace(source))
source = "some default";
Console.WriteLine(source);
}
public static string Source{get; private set;}
public void SetSource(string source)
{
Foo.Source = source;
}
}
}
I ran into a situation where somewhere buried in the .net code, it was relying on Assembly.GetEntryAssembly(). It would take the returned assembly and inspect it for an assembly level attribute. Which is going to fail if code is in an app domain.
Long story short, I had to work around this same issue. The solution is ugly, I hate that I needed to do this but it worked...
If you read the docs here - Assembly.GetEntryAssembly() Method
It contains this section:
Return Value
Type: System.Reflection.Assembly
The assembly that is the process executable in the default application domain, or the
first executable that was executed by AppDomain.ExecuteAssembly. Can
return null when called from unmanaged code.
To get around this, I added some code to my exe which makes the process exit if "/initializingappdomain" is passed as an argument.
Here is some code to do this...
// 1. Create your new app domain...
var newDomain = AppDomain.CreateDomain(...);
// 2. call domain.ExecuteAssembly, passing in this process and the "/initializingappdomain" argument which will cause the process to exit right away
newDomain.ExecuteAssembly(GetProcessName(), new[] { "/initializingappdomain" });
private static string GetProcessName()
{
return System.IO.Path.GetFileName(Process.GetCurrentProcess().MainModule.FileName.Replace(".vshost", ""));
}
// 3. Use your app domain as you see fit, Assembly.GetEntryAssembly will now return this hosting .net exe.
Again, this is far from ideal. There are better solutions if you can avoid this but if you find yourself in a situation where you don't own the code relying on Assembly.GetEntryAssembly(), this will get you by in a pinch.
Sample console program.
class Program
{
static void Main(string[] args)
{
// ... code to build dll ... not written yet ...
Assembly assembly = Assembly.LoadFile(#"C:\dyn.dll");
// don't know what or how to cast here
// looking for a better way to do next 3 lines
IRunnable r = assembly.CreateInstance("TestRunner");
if (r == null) throw new Exception("broke");
r.Run();
}
}
I want to dynamically build an assembly (.dll), and then load the assembly, instantiate a class, and call the Run() method of that class. Should I try casting the TestRunner class to something? Not sure how the types in one assembly (dynamic code) would know about my types in my (static assembly / shell app). Is it better to just use a few lines of reflection code to call Run() on just an object? What should that code look like?
UPDATE:
William Edmondson - see comment
Use an AppDomain
It is safer and more flexible to load the assembly into its own AppDomain first.
So instead of the answer given previously:
var asm = Assembly.LoadFile(#"C:\myDll.dll");
var type = asm.GetType("TestRunner");
var runnable = Activator.CreateInstance(type) as IRunnable;
if (runnable == null) throw new Exception("broke");
runnable.Run();
I would suggest the following (adapted from this answer to a related question):
var domain = AppDomain.CreateDomain("NewDomainName");
var t = typeof(TypeIWantToLoad);
var runnable = domain.CreateInstanceFromAndUnwrap(#"C:\myDll.dll", t.Name) as IRunnable;
if (runnable == null) throw new Exception("broke");
runnable.Run();
Now you can unload the assembly and have different security settings.
If you want even more flexibility and power for dynamic loading and unloading of assemblies, you should look at the Managed Add-ins Framework (i.e. the System.AddIn namespace). For more information, see this article on Add-ins and Extensibility on MSDN.
If you do not have access to the TestRunner type information in the calling assembly (it sounds like you may not), you can call the method like this:
Assembly assembly = Assembly.LoadFile(#"C:\dyn.dll");
Type type = assembly.GetType("TestRunner");
var obj = Activator.CreateInstance(type);
// Alternately you could get the MethodInfo for the TestRunner.Run method
type.InvokeMember("Run",
BindingFlags.Default | BindingFlags.InvokeMethod,
null,
obj,
null);
If you have access to the IRunnable interface type, you can cast your instance to that (rather than the TestRunner type, which is implemented in the dynamically created or loaded assembly, right?):
Assembly assembly = Assembly.LoadFile(#"C:\dyn.dll");
Type type = assembly.GetType("TestRunner");
IRunnable runnable = Activator.CreateInstance(type) as IRunnable;
if (runnable == null) throw new Exception("broke");
runnable.Run();
I'm doing exactly what you're looking for in my rules engine, which uses CS-Script for dynamically compiling, loading, and running C#. It should be easily translatable into what you're looking for, and I'll give an example. First, the code (stripped-down):
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using CSScriptLibrary;
namespace RulesEngine
{
/// <summary>
/// Make sure <typeparamref name="T"/> is an interface, not just any type of class.
///
/// Should be enforced by the compiler, but just in case it's not, here's your warning.
/// </summary>
/// <typeparam name="T"></typeparam>
public class RulesEngine<T> where T : class
{
public RulesEngine(string rulesScriptFileName, string classToInstantiate)
: this()
{
if (rulesScriptFileName == null) throw new ArgumentNullException("rulesScriptFileName");
if (classToInstantiate == null) throw new ArgumentNullException("classToInstantiate");
if (!File.Exists(rulesScriptFileName))
{
throw new FileNotFoundException("Unable to find rules script", rulesScriptFileName);
}
RulesScriptFileName = rulesScriptFileName;
ClassToInstantiate = classToInstantiate;
LoadRules();
}
public T #Interface;
public string RulesScriptFileName { get; private set; }
public string ClassToInstantiate { get; private set; }
public DateTime RulesLastModified { get; private set; }
private RulesEngine()
{
#Interface = null;
}
private void LoadRules()
{
if (!File.Exists(RulesScriptFileName))
{
throw new FileNotFoundException("Unable to find rules script", RulesScriptFileName);
}
FileInfo file = new FileInfo(RulesScriptFileName);
DateTime lastModified = file.LastWriteTime;
if (lastModified == RulesLastModified)
{
// No need to load the same rules twice.
return;
}
string rulesScript = File.ReadAllText(RulesScriptFileName);
Assembly compiledAssembly = CSScript.LoadCode(rulesScript, null, true);
#Interface = compiledAssembly.CreateInstance(ClassToInstantiate).AlignToInterface<T>();
RulesLastModified = lastModified;
}
}
}
This will take an interface of type T, compile a .cs file into an assembly, instantiate a class of a given type, and align that instantiated class to the T interface. Basically, you just have to make sure the instantiated class implements that interface. I use properties to setup and access everything, like so:
private RulesEngine<IRulesEngine> rulesEngine;
public RulesEngine<IRulesEngine> RulesEngine
{
get
{
if (null == rulesEngine)
{
string rulesPath = Path.Combine(Application.StartupPath, "Rules.cs");
rulesEngine = new RulesEngine<IRulesEngine>(rulesPath, typeof(Rules).FullName);
}
return rulesEngine;
}
}
public IRulesEngine RulesEngineInterface
{
get { return RulesEngine.Interface; }
}
For your example, you want to call Run(), so I'd make an interface that defines the Run() method, like this:
public interface ITestRunner
{
void Run();
}
Then make a class that implements it, like this:
public class TestRunner : ITestRunner
{
public void Run()
{
// implementation goes here
}
}
Change the name of RulesEngine to something like TestHarness, and set your properties:
private TestHarness<ITestRunner> testHarness;
public TestHarness<ITestRunner> TestHarness
{
get
{
if (null == testHarness)
{
string sourcePath = Path.Combine(Application.StartupPath, "TestRunner.cs");
testHarness = new TestHarness<ITestRunner>(sourcePath , typeof(TestRunner).FullName);
}
return testHarness;
}
}
public ITestRunner TestHarnessInterface
{
get { return TestHarness.Interface; }
}
Then, anywhere you want to call it, you can just run:
ITestRunner testRunner = TestHarnessInterface;
if (null != testRunner)
{
testRunner.Run();
}
It would probably work great for a plugin system, but my code as-is is limited to loading and running one file, since all of our rules are in one C# source file. I would think it'd be pretty easy to modify it to just pass in the type/source file for each one you wanted to run, though. You'd just have to move the code from the getter into a method that took those two parameters.
Also, use your IRunnable in place of ITestRunner.
You will need to use reflection to get the type "TestRunner". Use the Assembly.GetType method.
class Program
{
static void Main(string[] args)
{
Assembly assembly = Assembly.LoadFile(#"C:\dyn.dll");
Type type = assembly.GetType("TestRunner");
var obj = (TestRunner)Activator.CreateInstance(type);
obj.Run();
}
}
When you build your assembly, you can call AssemblyBuilder.SetEntryPoint, and then get it back from the Assembly.EntryPoint property to invoke it.
Keep in mind you'll want to use this signature, and note that it doesn't have to be named Main:
static void Run(string[] args)