[Roslyn] Cargar Assembly en memoria en distinto AppDomain

Este post surge a partir de esta interesante pregunta que vi el otro día en MSDN. “Respondí” al usuario indicándole la incapacidad de compilar realmente en memoria con ese approach que estaba trabajando y la dificultad que supone querer cargar dicho ensamblado en un AppDomain diferente. Luego llego el buen Pedro Hurtado con uno de sus loquillos comentarios

Entonces en este post explicaré un poco mas en detalle el approach que expuse en dicho hilo y un nuevo enfoque con Roslyn.

CodeDom NO compila en memoria

Lo primero es aclarar que CodeDom NO hace la compilación en memoria, lo que hace es crear la ilusión de dicha compilación creando archivos temporales (Si! en la carpeta de archivos temporales del usuario) luego, cuando termina la compilación, carga el resultado en un byte[] y ahí si ya tenemos el assembly en memoria para trabajar. Esto quiere decir que el código que el usuario del foro plantea en su pregunta no esta haciendo lo que el quiere hacer 😦 y podemos sacar ventaja de esto para resolver su duda especifica, que esta en esta línea:

nuevoDominio.Load(CompileAssembly(/*CODIGO EJEMPLO*/)); // error, no se como cargar el assembly, ya que se encuentra en la memoria y no tiene un path real.

Como el assembly esta en memoria será imposible pasar un path al método Load() del nuevo AppDomain, entonces, teniendo claro que no hay una ruta que se pueda emplear, podemos recrear la ilusión y recuperar por nuestra propia cuenta los bytes del compilado y terminar borrando dichos archivos. Algo de la forma:

        public static byte[] CompileAssembly(string code)
        {
            CodeDomProvider codeDomProvider = CodeDomProvider.CreateProvider("CSharp");
            var compilerParameters = new CompilerParameters();
            compilerParameters.ReferencedAssemblies.AddRange(References);
            compilerParameters.CompilerOptions = "/t:library";
            compilerParameters.GenerateInMemory = false;
            compilerParameters.TempFiles = new TempFileCollection(Environment.GetEnvironmentVariable("TEMP"), true);
            var compilerResults = codeDomProvider.CompileAssemblyFromSource(compilerParameters, code);
            if (compilerResults.Errors.Count > 0)
            {
                throw new Exception("Hay un error en el codigo");
            }
            var bytes = File.ReadAllBytes(string.Format("{0}.dll", compilerResults.TempFiles.BasePath));
            //Borar Archivos...
            return bytes;
        }

Si nos fijamos, se hace especifica la necesidad de cancelar la ilusión creada por CodeDom  al asignarle false a la propiedad GenerateInMemory del objeto CompilerParameters y le indicamos a la propiedad TempFiles donde queremos ubicar los archivos que se generan en este proceso de compilación. Ahora si podemos usar la sobrecarga del método AppDomain.Load(byte[]). Y listos! lo tenemos… bueno no :(, si ejecutamos el siguiente código recibiremos una excepción “Could not load file or assembly ‘nombregenerado’” :

            var bytes = CompileAssembly(fuentes);
            Assembly assembly = nuevoDominio.Load(bytes);

Ejecutar código a través de AppDomains

Como vemos no se puede cargar nuestro ensamblado, la mejor opción para solucionar este problema es emplear el método CreateInstanceFromAndUnwrap, pero desafortunadamente espera el path del ensamblado, path que no tenemos porque estamos haciendo la ilusión de compilado en memoria y esa ruta ya no existe :(. En este punto tenemos dos opciones, hacer que nuestra ilusión borre los archivos temporales una vez terminado el proceso y todos felices, o implementar alguna técnica para ejecutar el código de forma remota entre los dominios. En mi opinión la primera opción es muy facilista y no merece ser vista :P,  así que veamos a que me refiero con la segunda.

Resulta que es posible comunicar objetos a través de los dominios de aplicación haciendo uso de proxies, esto gracias a la clase MarshalByRefObject. Es decir que ahora podemos hacer un objeto que nos permita comunicarnos con el nuevo AppDomain, ejecutar el código de nuestro ensamblado (que esta en memoria) y obtener un resultado. La implementación de esta clase queda de la forma:

    public class WorkerAssemblyLoader : MarshalByRefObject
    {
        public object Ejecutar(byte[] assemblyByte, string tipo, string miembro, object[] parametros)
        {
            var assembly = Assembly.Load(assemblyByte);
            var type = assembly.GetType(tipo);
            var instancia = Activator.CreateInstance(type);
            var resultado = type.InvokeMember(miembro, BindingFlags.Default | BindingFlags.InvokeMethod, null, instancia, parametros);
            return resultado;
        }
    }

NOTA: Puedes agregar o quitar cuanta flexibilidad se te ocurra a este método en pro de comunicarse mas fácil con los miembros de los tipos del ensamblado que pases.

El código que usé para consumirlo:

        static void Main()
        {
            const string fuentes = @"
                                        namespace Test
                                        {        
                                            public class Operaciones
                                            {
                                                public string ObtenerAppDomain()
                                                {
                                                    return System.AppDomain.CurrentDomain.FriendlyName;
                                                }
                                            }
                                        }";
            AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
            AppDomain nuevoDominio = AppDomain.CreateDomain("nuevoDominio", AppDomain.CurrentDomain.Evidence, setup);
            var bytes = CompileAssembly(fuentes);
            var loader = (WorkerAssemblyLoader)nuevoDominio.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(WorkerAssemblyLoader).FullName);
            var resultado = loader.Ejecutar(bytes, "Test.Operaciones", "ObtenerAppDomain", null);
            Console.WriteLine(resultado);
            Console.ReadKey();
        }

Y voila, hemos ejecutado código de un assembly en memoria (a partir de una ilusión muy sencilla) y en un AppDomain diferente… ahora, haciéndole caso a Pedro y su comentario en twitter, veamos un enfoque mas cool.

Compilando en memoria con Roslyn

Emplear el compilador como servicio nos ayudara a que ese efecto de compilar en memoria se haga real! y no necesitemos de trucos para escribir y borrar archivos en disco duro todo el tiempo. el código con Roslyn queda de la forma:

        public static byte[] CompilarRoslyn(string source)
        {
            byte[] assembly;
            var compiler = Compilation.Create("Test",
                                              new CompilationOptions(outputKind: OutputKind.DynamicallyLinkedLibrary,
                                                                     usings: new[] {"System"}))
                .AddSyntaxTrees(SyntaxTree.ParseText(source))
                .AddReferences(new MetadataFileReference(typeof (object).Assembly.Location));
            
            using (var stream = new MemoryStream())
            {
                var result = compiler.Emit(stream);
                if (!result.Success)
                    throw new Exception("Ay!! error:(");
                assembly = stream.ToArray();
            }
            return assembly;
        }

NOTA: Para trabajar con Roslyn basta con referenciarlo desde Nuget y agregar using de Roslyn.Compilers y Roslyn.Compilers.CSharp

Y voila!! nuevamente consumimos este método de la misma manera que el ejemplo anterior, a través de un MarshallByRefObject.

Conclusión

CodeDom NO COMPILA en memoria, pero es posible crear la ilusión de la misma forma que el lo hace 🙂 y e si es posible comunicarse entre Dominios de aplicación haciendo uso de MarshallByRefObject (para eso esta!). Por ultimo vimos un enfoque con Roslyn que nos facilitó mucho mas el trabajo.

Espero les sea de utilidad.

Hasta el próximo post.

Anuncios
[Roslyn] Cargar Assembly en memoria en distinto AppDomain

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s