Manera correcta de cargar ensamblaje, buscar clase y llamar al método Run ()

81

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. ¿Debo intentar transmitir la clase TestRunner a algo? No estoy seguro de cómo los tipos en un ensamblado (código dinámico) sabrían acerca de 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

BuddyJoe
fuente
Hablando desde el futuro ... ¿has trabajado con MEF? Vamos a ti exporty importclases en ensamblajes separados que se derivan de una interfaz conocida
RJB

Respuestas:

78

Utilice un dominio de aplicación

Es más seguro y flexible cargar AppDomainprimero el conjunto en sí mismo .

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 puede 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 mirar el marco de complementos administrados (es decir, el System.AddInespacio de nombres). Para obtener más información, consulte este artículo sobre complementos y extensibilidad en MSDN .

cdiggins
fuente
1
¿Qué pasa si TypeIWantToLoad es una cadena? ¿Tiene una alternativa al asm.GetType ("type string") de la respuesta anterior?
paz
2
Creo que CreateInstanceFromAndUnwraprequiere AssemblyName en lugar de una ruta; ¿te refieres CreateFrom(path, fullname).Unwrap()? También me quemé por el MarshalByRefObjectrequisito
drzaus
1
CreateInstanceAndUnwrap(typeof(TypeIWantToLoad).Assembly.FullName, typeof(TypeIWantToLoad).FullName)¿ Quizás ?
Fadden
1
Hola amigos, creo que está confundiendo CreateInstanceAndUnwrap con CreateInstanceFromAndUnwrap.
cdiggins
48

Si no tiene acceso a la TestRunnerinformación de tipo en el ensamblado de llamada (parece que no puede), 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 IRunnabletipo de interfaz, puede convertir su instancia a eso (en lugar del TestRunnertipo, que se implementa en el ensamblaje 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();
Jeff Sternal
fuente
+1 Funcionó usando la línea type.invokeMember. ¿Debería usar ese método o seguir intentando hacer algo con la interfaz? Preferiría ni siquiera tener que preocuparme por poner eso en el código construido dinámicamente.
BuddyJoe
Hmm, ¿no te funciona el segundo bloque de código? ¿Su ensamblaje de llamada tiene acceso al tipo IRunnable?
Jeff Sternal
El segundo bloque funciona. Llamar a la asamblea realmente no sabe acerca de IRunnable. Así que supongo que me quedaré con el segundo método. Ligero seguimiento. Cuando recupero el código y luego rehago dyn.dll, parece que no puedo reemplazarlo porque está en uso. ¿Algo como un Assembly.UnloadType o algo que me permita reemplazar el .dll? ¿O debería hacerlo "en la memoria"? pensamientos? gracias
BuddyJoe
Supongo que no conozco la forma correcta de hacer lo "en memoria" si esa es la mejor solución.
BuddyJoe
No recuerdo los detalles (y me alejo de mi computadora por un tiempo), pero creo que un ensamblado solo se puede cargar una vez por dominio de aplicación, por lo que tendrá que crear nuevos dominios de aplicación para cada instancia de ensamblaje ( y cargue los ensamblados en esos) o tendrá que reiniciar su aplicación antes de poder compilar una nueva versión del ensamblaje.
Jeff Sternal
12

Estoy haciendo exactamente lo que está buscando en mi motor de reglas, que usa CS-Script para compilar, cargar y ejecutar dinámicamente C #. Debería ser fácilmente traducible a lo que está buscando y le 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, instanciará una clase de un tipo dado y alineará esa clase instanciada con la interfaz T. Básicamente, solo debe asegurarse 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 (), así:

public interface ITestRunner
{
    void Run();
}

Luego crea una clase que lo implemente, así:

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 que desee llamarlo, puede 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 pasar el archivo de tipo / fuente para cada uno que quisiera ejecutar. Solo tendría que mover el código del captador a un método que tomara esos dos parámetros.

Además, use su IRunnable en lugar de ITestRunner.

Chris Doggett
fuente
¿Qué es @Interface? ideas muy interesantes aquí. Necesito digerir completamente esto. +1
BuddyJoe
muy interesante No me di cuenta de que el analizador de C # tenía que mirar un carácter para pasar la @ para ver si era parte de un nombre de variable o una cadena @ "".
BuddyJoe
Gracias. La @ antes del nombre de la variable se usa cuando el nombre de la variable es una palabra clave. No puede nombrar una variable "clase", "interfaz", "nueva", etc. Pero puede hacerlo si antepone una @. Probablemente no importe en mi caso con una "I" mayúscula, pero originalmente era una variable interna con un getter y un setter antes de convertirla en una propiedad automática.
Chris Doggett
Así es. Me olvidé de la cosa @. ¿Cómo manejaría la pregunta que le hice a Jeff Sternal sobre "lo que está en la memoria"? Supongo que mi gran problema ahora es que puedo compilar el .dll dinámico y cargarlo, pero solo puedo hacerlo una vez. No sé cómo "descargar" el conjunto. ¿Es posible crear otro AppDomain? Cargue el ensamblado en ese espacio, úselo y luego elimine este segundo AppDomain. Enjuague. Repetir.?
BuddyJoe
1
No hay forma de descargar el ensamblado a menos que use un segundo AppDomain. No estoy seguro de cómo lo hace CS-Script internamente, pero la parte de mi motor de reglas que eliminé es un FileSystemWatcher que ejecuta automáticamente LoadRules () nuevamente cada vez que cambia el archivo. Editamos las reglas, las enviamos a los usuarios, cuyo cliente sobrescribe ese archivo, FileSystemWatcher nota los cambios y recompila y recarga la DLL escribiendo otro archivo en el directorio temporal. Cuando el cliente se inicia, borra ese directorio antes de la primera compilación dinámica, por lo que no tenemos un montón de sobras.
Chris Doggett
6

Deberá utilizar la reflexión para obtener el tipo "TestRunner". Utilice el método Assembly.GetType.

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();
    }
}
William Edmondson
fuente
¿No es esto faltar un paso, en el que obtienes lo apropiado MethodInfodel tipo y llamas Invoke? (Entendí que la pregunta original especificaba que la persona que llama no sabía nada sobre el tipo en cuestión)
Jeff Sternal
Te estás perdiendo una cosa. Tienes que lanzar obj para escribir TestRunner. var obj = (TestRunner) Activator.CreateInstance (tipo);
BFree
Parece que Tyndall está construyendo este dll en un paso anterior. Esta implementación asume que él sabe que el método Run () ya existe y sabe que no tiene parámetros. Si estos son realmente desconocidos, entonces necesitaría hacer una reflexión un poco más profunda
William Edmondson
hmmm. TestRunner es una clase dentro de mi código escrito dinámico. Entonces, este código estático en su ejemplo no puede resolver TestRunner. No tiene idea de qué es.
BuddyJoe
@WilliamEdmondson ¿cómo se puede usar "(TestRunner)" en el código si no se hace referencia aquí?
Antoops
2

Cuando construya su conjunto, puede llamar AssemblyBuilder.SetEntryPointy luego recuperarlo de la Assembly.EntryPointpropiedad para invocarlo.

Tenga en cuenta que querrá usar esta firma y tenga en cuenta que no es necesario que tenga un nombre Main:

static void Run(string[] args)
Sam Harwell
fuente
¿Qué es AssemblyBuilder? Estaba probando CodeDomProvider y luego "provider.CompileAssemblyFromSource"
BuddyJoe