Forma correcta de cargar el ensamblaje, buscar clase y llamar al método Run()
Programa de consola de muestra.
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();
}
}
Quiero construir dinámicamente un ensamblado (.dll) y luego cargar el ensamblado, crear una instancia de una clase y llamar al método Run() de esa clase. ¿Debería intentar convertir la clase TestRunner en algo? No estoy seguro de cómo los tipos en un ensamblado (código dinámico) conocerían mis tipos en mi (ensamblaje estático/aplicación de shell). ¿Es mejor usar solo unas pocas líneas de código de reflexión para llamar a Run() solo en un objeto? ¿Cómo debería verse ese código?
ACTUALIZACIÓN: William Edmondson - ver comentario
Utilice un dominio de aplicación
Es más seguro y flexible cargar el conjunto por su cuenta AppDomain
primero.
Entonces, en lugar de la respuesta dada anteriormente :
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();
Sugeriría lo siguiente (adaptado de esta respuesta a una pregunta relacionada ):
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();
Ahora puedes descargar el ensamblaje y tener diferentes configuraciones de seguridad.
Si desea aún más flexibilidad y potencia para la carga y descarga dinámica de ensamblados, debe consultar el marco de complementos administrados (es decir, el System.AddIn
espacio de nombres). Para obtener más información, consulte este artículo sobre complementos y extensibilidad en MSDN .
Si no tiene acceso a la TestRunner
información de tipo en el ensamblado que realiza la llamada (parece que es posible que no lo tenga), puede llamar al método de esta manera:
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);
Si tiene acceso al IRunnable
tipo de interfaz, puede convertir su instancia a ese (en lugar del TestRunner
tipo, que se implementa en el ensamblado creado o cargado dinámicamente, ¿verdad?):
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();
Estoy haciendo exactamente lo que buscas en mi motor de reglas, que utiliza CS-Script para compilar, cargar y ejecutar C# dinámicamente. Debería ser fácilmente traducible a lo que estás buscando y te daré un ejemplo. Primero, el código (simplificado):
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;
}
}
}
Esto tomará una interfaz de tipo T, compilará un archivo .cs en un ensamblado, creará una instancia de una clase de un tipo determinado y alineará esa clase instanciada con la interfaz T. Básicamente, sólo tienes que asegurarte de que la clase instanciada implemente esa interfaz. Utilizo propiedades para configurar y acceder a todo, así:
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; }
}
Para su ejemplo, desea llamar a Run(), por lo que crearía una interfaz que defina el método Run(), como esta:
public interface ITestRunner
{
void Run();
}
Luego crea una clase que lo implemente, como esta:
public class TestRunner : ITestRunner
{
public void Run()
{
// implementation goes here
}
}
Cambie el nombre de RulesEngine a algo como TestHarness y establezca sus propiedades:
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; }
}
Luego, en cualquier lugar donde quieras llamarlo, puedes simplemente ejecutar:
ITestRunner testRunner = TestHarnessInterface;
if (null != testRunner)
{
testRunner.Run();
}
Probablemente funcionaría muy bien para un sistema de complementos, pero mi código tal como está se limita a cargar y ejecutar un archivo, ya que todas nuestras reglas están en un archivo fuente de C#. Sin embargo, creo que sería bastante fácil modificarlo para simplemente pasar el archivo de tipo/fuente para cada uno que quisieras ejecutar. Sólo tendrías que mover el código del captador a un método que tomara esos dos parámetros.
Además, utilice su IRunnable en lugar de ITestRunner.