La potencia de la Reflexión en C# (Parte 2: Ensamblados)

La imagen muestra el logo de C# para Reflexión Ensamblados

En la última entrada hablábamos sobre un problema que me había encontrado en el trabajo y como la reflexión me había permitido resolverlo de una manera elegante y con poco código. Al menos, muchísimo menos código del que habría necesitado sin haberla usado.

Pensaba hacer simplemente un par de entradas hablando sobre el tema, pero creo que tiene bastante miga, así que lo mejor va a ser hacer una pequeña serie hablando de todas las posibilidades que nos ofrece esta poderosa herramienta. Así que hoy vamos a empezar con la primera de la lista y la que a su vez las contiene a todas: «Assembly».

Assembly (ensamblado): Los ensamblados son los bloques de creación de las aplicaciones .NET Framework; constituyen la unidad fundamental de implementación, control de versiones, reutilización, ámbitos de activación y permisos de seguridad. Un ensamblado es una colección de tipos y recursos compilados para funcionar en conjunto y formar una unidad lógica de funcionalidad. Los ensamblados proporcionan a Common Language Runtime la información necesaria para conocer las implementaciones de tipos. Para la ejecución, un tipo no existe fuera del contexto de un ensamblado.

Fuente: MSDN

¿Y que es un assembly? Pues dicho con palabras sencillas, es la unidad mínima que el CLR puede ejecutar. Contiene las los módulos, las clases, los tipos… y lo más importante, el manifiesto, que es donde se registran todos los metadatos. (Cuando hablemos de los módulos veremos que la principal diferencia es esta última). Cuando una aplicación arranca, el CLR consulta los metadatos del ensamblado para conocer el punto de entrada a este. Él

static void Main(string[] args)

de toda la vida. Mediante reflexión, podemos obtener esos metadatos de el/los ensamblados cargados en la aplicación, pudiendo saber que versión de fichero estamos ejecutando, obtener una lista detallada de todas las clases y métodos que están disponibles en nuestra aplicación, o incluso cargar nuevos ensamblados en nuestra aplicación y permitir que estén disponibles de manera dinámica.

Ejemplos de uso para metadatos

Por ejemplo, un código como este nos permitiría saber la versión que estamos ejecutando:

using System;
using System.Reflection;

namespace PostReflexion
{
    class Program
    {
        static void Main(string[] args)
        {
            var assembly = Assembly.GetAssembly(typeof(Program));
            Console.WriteLine($"Versión: {assembly.GetName().Version}");
        }
    }
}

U obtener todos los tipos (clases, interfaces, enumeraciones….):

using System;
using System.Reflection;

namespace PostReflexion
{
    class Program
    {
        static void Main(string[] args)
        {
            var assembly = Assembly.GetAssembly(typeof(Program));
            foreach (var type in assembly.DefinedTypes)
            {
                Console.WriteLine(type);
            }
        }
    }
}

-Vale, ¿y eso para qué vale? ¡Tampoco es una grandísima utilidad…!

En si mismo, lo visto anteriormente no es de una gran utilidad, ya que para obtener la versión podríamos utilizar «FileVersionInfo» y saber el contenido de un ensamblado en tiempo de ejecución no nos vale de mucho… ¿O sí?

Una herramienta muy potente que nos da la clase Assembly, es que nos permite crear objetos de manera dinámica, es decir, durante el la ejecución del programa, y sin estar explícitamente escrito en nuestro código.

Imagina que tienes una aplicación que sirve de punto de lanzador de otra serie de aplicaciones contenidas en librerías. A simple vista, tienes 3 opciones:

  • Distribuir la aplicación con todas su dll referenciadas, haciendo que gente que no ha pagado por todas las aplicaciones se tenga que descargar un montón de ficheros que no va a poder usar. (Mayor tamaño de archivos)
  • Generar diferentes soluciones que utilicen unas u otras librerías para generar diferentes paquetes de aplicaciones, pero esto crece de manera exponencial con el número de librerías y tipos de «paquete». (Mayor trabajo de mantenimiento)
  • Listar las librerías disponibles dentro del directorio, obtener de ahí las clases que inician las diferentes aplicaciones y ejecutarlas de manera dinámica.

Nota: Cualquiera de las 3 opciones debería contar con un sistema de autenticación que acredite que realmente tiene derecho a utilizar la aplicación.

Creando instancias de manera dinámica

Para ello, aprovechando la opción que nos da la reflexión de instanciar clases, podríamos hacer algo como esto:

var assembly = Assembly.LoadFile("ruta a la librería");
            
//Creamos el objeto de manera dinámica
var formDinamico = assembly.CreateInstance("Nombre completo de la clase") as Form;

//Si hemos podido crear la instancia, abrimos el formulario
formDinamico?.ShowDialog();

Con este código y gracias a assembly, solo cambiando las rutas de cargar la librería y el nombre de la clase, podemos abrir cualquier formulario que herede de Form (WinForms). Esto mismo se puede aplicar a WPF sin ninguna dificultad.

Además, también es posible crear instancias de clases que tienen un constructor con argumentos, pasándole esos argumentos:

//Código de la clase
//=============================================
public class ClaseEjemplo
{
    private int _valor;
    public ClaseEjemplo(int valor)
    {
        _valor = valor;
    }
    public int Multiplicar(int por)
    {
        Console.WriteLine($"Llamada a {nameof(Multiplicar)} con parámetro {por}");
        return _valor * por;
    }
}

//Código para instanciar y ejecutar sus métodos
//=============================================
var assembly = Assembly.LoadFile("ruta a la librería");
//Creamos el objeto de manera dinámica
var objetoDinamico = assembly.CreateInstance("PostReflexion.ClaseEjemplo"
                               ,false
                               ,BindingFlags.ExactBinding
                               ,null
                               ,new object[]{2} //Contructor
                               ,null  
                               ,null);

// Creamos una referencia al método   
var m = objetoDinamico.GetType().GetMethod("Multiplicar");

//Llamamos al método pasandole el objeto creado dinámicamente y los argumentos dentro de un object[]
var ret = m.Invoke(objetoDinamico, new object[] { 3 });
Console.WriteLine($"El retorno de la función es: {ret}");
Console.WriteLine();

No vamos a entrar a ver como hemos llamado a su método, ya que eso lo veremos cuando lleguemos a esa parte, pero el código es funcional y podemos comprobar como hemos instanciado de manera dinámica la clase y le hemos pasado un valor a su constructor, pudiéndolo comprobar al llamar al método.

Estas son solo alguna de las muchísimas opciones que nos da la reflexión en C# a través de la clase Assembly. Combinándola con las demás que vamos a ver, nos permite ejecutar cualquier código de manera dinámica en tiempo de ejecución, lo cual es una herramienta muy potente y que abre un gran abanico de posibilidades.

He actualizado el repositorio de GitHub para añadir los ejemplos de obtención de metadatos y creación de clases de manera dinámica para poder probar esos conceptos.

La potencia de la Reflexión (Parte 1)

      4 comentarios en La potencia de la Reflexión (Parte 1)

¡Ya estamos de vuelta por estos lares! Hoy vengo a contaros un problema que he tenido en el trabajo y como lo he solucionado con una herramienta no muy visible pero muy potente que nos ofrece .Net, la reflexión. Antes de nada, ¿qué es eso de la reflexión y como puedo usarla en C#?

Si nos fijamos en la definición:

La reflexión proporciona objetos (de tipo Type) que describen los ensamblados, módulos y tipos. Puede usar la reflexión para crear dinámicamente una instancia de un tipo, enlazar el tipo a un objeto existente u obtener el tipo desde un objeto existente e invocar sus métodos, o acceder a sus campos y propiedades. Fuente: Reflexión (C#)

Dicho con otras palabras más sencillas, la reflexión en C# es la capacidad que tenemos para desde el código, conseguir información sobre tipos (clases, librerías, objetos, etc…) y usar esa información para crear objetos dinámicamente y/o acceder a sus miembros sin crear una instancia tipada.

Vale, ¿con lo que he dicho no he conseguido aclararlo mucho más verdad? La verdad es que personalmente creo que es un concepto avanzado del lenguaje y que eso es lo que echa para atrás a muchos programadores. De hecho, si vemos su página de MSDN, vamos que tiene varias opciones:

Seguramente después de esto te sigas preguntando en que me ha podido valer la reflexión de c#. Vale, en mi caso, yo tenia una serie de clases que heredaban todas de una clase base, pero cada una tenia sus particularidades, algo como por ejemplo esto:

public class BaseClass
{
    public string ModeloMotor { get; set; }
}

public class Coche : BaseClass
{
    public bool Descapotable { get; set; }
}

public class Moto : BaseClass
{
    public bool Motor2Tiempos { get; set; }
}

public class Camion : BaseClass
{
    public bool EsVehiculoLargo{ get; set; }
}

Todo ello son posibles clases que recibimos de un método o una api, la cual nos devuelve un «BaseClass», si nos fijamos, todas están relacionadas entre ellas, pero cada una de las clases tiene sus particularidades. Aquí, tendríamos una opción clara, aprovecharnos del poliformismo y tener una clase base que lo contenga todo:

public class BaseClass
{
    public string ModeloMotor { get; set; }
    public bool Descapotable { get; set; }
    public bool Motor2Tiempos { get; set; }
    public bool EsVehiculoLargo{ get; set; }
}

public class Coche : BaseClass
{
    //...
}

public class Moto : BaseClass
{
    //...
}

public class Camion : BaseClass
{
    //...
}

De este modo, desde cada una de las clases hijas tendremos acceso a todas las propiedades. Pero claro, tampoco tiene mucho sentido hablar de un camión con un motor de dos tiempos o una moto que sea un vehículo largo.

Otra opción posible, es hacer casting a absolutamente todas las posibles clases hijas para ver si alguna coincide:

var moto = claseBase as Moto;
if (!(moto is null))
    Console.WriteLine(moto.Motor2Tiempos);
var coche = claseBase as Coche;
if (!(coche is null))
    Console.WriteLine(coche.Descapotable);
var camion = claseBase as Camion;
if (!(camion is null))
    Console.WriteLine(camion.Tara);
//.....

¿Os imagináis lo larga que se puede hacer la lista si tengo 20 clases diferente que heredan de clase base?

Aquí es donde entra en juego la reflexión, desde un objeto de tipo «BaseClass», mediante la reflexión de C#, podemos iterar las propiedades del objeto encapsulado y obtener su valor, por ejemplo, vamos a crear un método de extensión que nos permita obtener desde la clase base si es un vehículo largo:

static class Extensiones
{
    public static bool? EsVehiculoLargo(this BaseClass clase)
    {
        //Obtenemos todas las propiedades de la clase que nos pasan
        var properties = clase.GetType().GetProperties();
        //Iteramos las propiedades
        foreach (var propertyInfo in properties)
        {
            //Si alguna se llama 
            if (propertyInfo.Name == "EsVehiculoLargo")
            {
                //Retornamos el valor
                return Convert.ToBoolean(propertyInfo.GetValue(clase));
            }
        }
        //Si ninguna coincide, retornamos null
        return null;
    }
}

Si nos fijamos en el código, estamos creando un método extensor para «BaseClass». En él, lo que vamos a hacer es obtener todas las propiedades, pero no de «BaseClass», sino de la clase hija encapsulada dentro. Dentro de las propiedades, vamos a buscar la que tenga el nombre que nos interesa, y si hay alguna, vamos a obtener el valor pasándole al objeto PropertyInfo la instancia de donde tiene que obtener el valor.

Con esto, no solo podemos leer la propiedad, también podríamos escribirla llamando al método «SetValue»:

propertyInfo.SetValue(clase, true);
propertyInfo.SetValue(clase, false);

Pero eso no es todo lo que podemos conseguir, también podemos obtener información sobre su tipo, obtener el método get o set, los atributos que tiene, si es de lectura y/o escritura …

De este modo tan elegante, podemos acceder a la información de la clase hija sin tener que conocer el tipo exacto de la clase hija. En mi caso, pude conseguir consumir la api sin tener que hacer casting individuales (en mi caso yo tenía más de 60 clases hijas).

Pero no se queda ahí, ¡la reflexión en C# da para mucho más! Si con este ejemplo te ha picado el gusanillo sobre lo que puede ofrecer la reflexión, en las próximas entradas vamos a profundizar en diferentes casos, como crear un objeto de una clase de manera dinámica, buscar entre los atributos de un objeto, o llamar a métodos de objetos dinámicamente. De momento, dejo un enlace al código para poder probarlo y lo iremos ampliando.

Como instalar un servicio Net Core en Linux

Banner de la entrada "Como instalar un servicio NetCore en Linux"

Hace un par de semanas hablamos de como poder crear un servicio Net Core que fuese multiplataforma, y probamos a instalarlo en Windows. Hoy vamos a continuar con el proceso y vamos a instalarlo en Linux para comprobar que realmente es multiplataforma (y de paso ver lo realmente fácil que es).

Para poder hacerlo, solo necesitamos 2 cosas:

  • Una máquina Linux con .Net Core.
  • Una aplicación .Net Core programada como un servicio.

Para la primera de las dos, te dejo un enlace a mi entrada hablando sobre «Como instalar .Net Core en Linux» , para la segunda, vamos a reutilizar el código que vimos en la última entrada. Para ello, desde nuestra terminal (asumo que no todo el mundo usa git, por lo que voy a utilizar la descarga http), vamos a escribir los comandos:

wget https://codeload.github.com/FixedBuffer/ServicioNetCore/zip/master
unzip -a master

Una vez descomprimido, vamos a navegar hasta el .csproj con:

cd ServicioNetCore-master
cd PostServicioNetCore

Y una vez dentro, simplemente vamos a publicarlo:

dotnet publish --configuration Release

Con estos pasos, ya tenemos un binario listo para registrar como servicio. ¡Vamos a ello!

Instalando nuestro servicio Net Core en Linux

En primer lugar, la ruta donde hemos generado los binarios es poco amigable (además de ser poco práctica), así que vamos a mover la carpeta a una ruta más apropiada:

sudo mv /home/jorturfer/ServicioNetCore-master/PostServicioNetCore/bin/Release//netcoreapp2.2/publish /usr/local/NetCoreService

*Estamos moviendo la carpeta publish entera a la ruta «/usr/local/», que es una ruta mucho más apropiada para nuestro servicio, aunque no es obligatorio.

Una vez que tenemos todo puesto en su sitio vamos a instalar un servicio NetCore en Linux, para ello, solo hay que crear su fichero de definición en «/etc/systemd/system/» y registrarlo. Para crear la definición, vamos a escribir:

sudo nano /etc/systemd/system/netcoreservice.service

Y dentro del fichero, vamos a rellenar el template:

[Unit]
Description=DESCRIPCION DEL SERVICIO

[Service]
WorkingDirectory= RUTA DONDE ESTAN LOS BINARIOS
ExecStart=/usr/bin/dotnet RUTA HASTA LA DLL
Restart=always
RestartSec=10
SyslogIdentifier=IDENTIFICADOR DEL SISTEMA
User=USUARIO QUE LO VA A EJECUTAR
Environment=VARIABLES DE ENTORNO QUE VA A RECIBIR

[Install]
WantedBy=multi-user.target

Después de rellenar los datos, nos quedará algo como esto:

[Unit]
Description=Servicio para la entrada de "Como instalar un servicio NetCore en Linux"
[Service]
WorkingDirectory= /usr/local/NetCoreService
ExecStart=/usr/bin/dotnet /usr/local/NetCoreService/PostServicioNetCore.dll
Restart=always
RestartSec=10
SyslogIdentifier=dotnet-postservice
User=jorturfer
Environment=ASPNETCORE_ENVIRONMENT=Production

[Install]
WantedBy=multi-user.target

Por último, vamos a registrarlo con el comando:

sudo systemctl enable netcoreservice.service

Después de esto, ya podemos manejar nuestro servicio Net Core en Linux como cualquier otro servicio con los comandos:

sudo service nombreservicio start/stop/status/restart

Vamos a comprobar que funciona bien, ejecutando:

sudo service netcoreservice start

Después de eso, deberíamos encontrar un fichero llamado «PostServicioCore.txt» junto al binario con un registro por cada minuto que ha estado arrancado. Además, gracias a SSH, vamos a poder asociarnos al proceso y depurarlo como si estuviese en nuestra maquina. (Incluso con Visual Studio Code gracias al último anuncio de Microsoft sobre depuración remota en la MSBuild2019)

Como hemos podido comprobar, el servicio que hicimos multiplataforma, realmente lo es, e instalar un servicio Net Core en Linux no tiene ningún misterio.