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 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.

Cómo crear un servicio Net Core multiplataforma

Imagen para el post de crear servicios para .Net Core

Después de varias semanas hablando sobre las maravillas de Terraform o de Integración y Despliegue continuos (CI/CD), hoy vengo a hablaros de un problema con el que me he encontrado con un proyecto que tengo entre manos.

Por necesidades del proyecto, quiero hacer un servicio .Net Core, y poder instalarlo en cualquier plataforma. Esto no es ningún problema en Linux por ejemplo, pero para poder correr un servicio en Windows hay que cumplir con ciertos requisitos. Para .Net Framework existen múltiples herramientas que nos lo permiten, como puede ser un proyecto de instalador de servicio o utilizar Topself, pero para .Net Core ya es otra cosa…

Para que un servicio se pueda instalar y funcione en Windows, tiene que heredar de «ServiceBase«, lo que difiere un poco de cómo funciona en otras plataformas, y hace que crear un servicio multiplataforma no sea algo directo. Esto se puede solucionar fácilmente con un poco de código.

Creando nuestro servicio Net Core

Para empezar, vamos a crear una aplicación de consola .Net Core:

La imagen muestra la creación de un proyecto de consola net core

Para poder hospedar un servicio cualquiera, vamos a necesitar añadir el paquete NuGet «Microsoft.Extensions.Hosting«, y además, para poder correrlo cuando funcione en Windows, vamos a necesitar también el paquete NuGet «System.ServiceProcess.ServiceController«. Una vez que los hemos añadido, vamos a crear nuestro IHost:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace PostServicioNetCore
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var host = new HostBuilder()
                 .ConfigureHostConfiguration(configHost =>
                 {
                     //Configuración del host
                 })
                 .ConfigureAppConfiguration((hostContext, configApp) =>
                 {
                     //Configuración de la aplicacion

                 })
                .ConfigureServices((hostContext, services) =>
                {
                    //Configuración de los servicios
                    services.AddHostedService<LifetimeHostedService>();
                });
            if (!Debugger.IsAttached) //En caso de no haber debugger, lanzamos como servicio
            {
                await host.RunServiceAsync();
            }
            else
            {
                await host.RunConsoleAsync();
            }
        }
    }
}

Al igual que haríamos en un proyecto ASP NET Core en el Startup.cs y en Program.cs, podemos configurar las diferentes partes de nuestro servicio Net Core mediante los métodos:

  • «ConfigureHostConfiguration» (en Program.cs)
  • «ConfigureAppConfiguration» («Configure» en Startup.cs)
  • «ConfigureServices» (en Startup.cs)

En este caso, estamos levantando un IHost genérico al cual le vamos a inyectar la dependencia del servicio/servicios que queremos ejecutar registrándolos mediante «AddHostedService».

Para poder probar, el servicio que hemos registrado es este (tiene que implementar IHostedService):

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace PostServicioNetCore
{
    public class LifetimeHostedService : IHostedService, IDisposable
    {
        ILogger<LifetimeHostedService> logger;
        private Timer _timer;
        private string _path;
        public LifetimeHostedService(ILogger<LifetimeHostedService> logger, IHostingEnvironment hostingEnvironment)
        {
            this.logger = logger;
            _path = $"{hostingEnvironment.ContentRootPath}PostServicioCore.txt";
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            try
            {
                WriteToFile("Inicia el servicio");
                _timer = new Timer((e) => WriteTimeToFile(), null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
            }
            catch (Exception ex)
            {
                logger.LogError($"{ex.Message},{ex.StackTrace}");
            }
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _timer?.Change(Timeout.Infinite, 0);
            WriteToFile("Finaliza el servicio");
            return Task.CompletedTask;
        }

        private void WriteTimeToFile()
        {
            WriteToFile(DateTime.Now.ToString());
        }

        private void WriteToFile(string message)
        {
            if (!File.Exists(_path))
            {
                using (var sw = File.CreateText(_path))
                {
                    sw.WriteLine(message);
                }
            }
            else
            {
                using (var sw = File.AppendText(_path))
                {
                    sw.WriteLine(message);
                }
            }
        }

        public void Dispose()
        {
            _timer?.Dispose();
        }
    }
}

En él, simplemente se añaden mensajes a un fichero que se encuentra junto al binario para indicar cuando empieza, cuando finaliza y cada minuto mientras esté activo, pero aquí sería donde vamos a poner que es lo que hace nuestro servicio (consultar una cola de mensajes, lecturas a una Db, etc).

Hasta aquí, este es el mismo concepto que utilizamos en ASP NET Core para crear servicios que corran en segundo plano en una web. Creamos el servicio heredando de «IHostedService» y lo registramos desde «ConfigureServices». El problema como decía al principio, es que los servicios en Windows no funcionan así, tienen que heredar de «ServiceBase».

Preparando nuestro servicio para Windows

En primer lugar, vamos a necesitar crear nuestra clase heredada:

using Microsoft.Extensions.Hosting;
using System;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;

namespace PostServicioNetCore
{
    public class ServiceBaseLifetime : ServiceBase, IHostLifetime
    {
        private readonly TaskCompletionSource<object> _delayStart = new TaskCompletionSource<object>();

        public ServiceBaseLifetime(IApplicationLifetime applicationLifetime)
        {
            ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
        }

        private IApplicationLifetime ApplicationLifetime { get; }

        public Task WaitForStartAsync(CancellationToken cancellationToken)
        {
            cancellationToken.Register(() => _delayStart.TrySetCanceled());
            ApplicationLifetime.ApplicationStopping.Register(Stop);

            new Thread(Run).Start(); //Ejecutamos la tarea en un hilo para bloquear y prevenir que IHost.StartAsync termine.
            return _delayStart.Task;
        }

        private void Run()
        {
            try
            {
                Run(this); // Bloqueamos la ejecución hasta que el servicio termine.
                _delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
            }
            catch (Exception ex)
            {
                _delayStart.TrySetException(ex);
            }
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            Stop();
            return Task.CompletedTask;
        }

        // Se llama desde base.Run() cuando el servicio esta listo para funcionar.
        protected override void OnStart(string[] args)
        {
            _delayStart.TrySetResult(null);
            base.OnStart(args);
        }

        protected override void OnStop()
        {
            ApplicationLifetime.StopApplication();
            base.OnStop();
        }
    }
}

Sin ánimo de entrar muy en profundidad, ya que esta clase es una copia del ejemplo que ofrece Microsoft navegando entre los repositorios en GitHub, en ella básicamente se implementan los métodos propios de un servicio Windows y de la interfaz «IHostLifetime«. De este modo, la aplicación puede correr como un servicio en Windows y ser usada desde «IHost».

Esta clase tiene que estar en el contenedor de inyección de dependencias siempre que el servicio corra sobre Windows, así que vamos a añadir unos métodos de extensión que nos facilite incluirla si es necesario:

using System.Runtime.InteropServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;

namespace PostServicioNetCore
{
    public static class ServiceBaseLifetimeHostExtensions
    {
        public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder)
        {
            return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>());
        }

        public static Task RunServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
        {
            //Si es Windows, añadimos el ServiceBaseLifetime a la inyeccion de dependencias
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
            }
            else //Sino, ejecutamos normalmente
            {
                return hostBuilder.Build().RunAsync(cancellationToken);
            }
        }
    }
}

Gracias a la condición que pusimos al arrancar el IHost para detectar si estábamos en sin el debugger, y con la condición que añadimos dentro de la extensión RunAsServiceAsync, vamos a poder ejecutar nuestro programa como un servicio para Windows, para Linux o como un programa de consola (que es ideal para depurarlo).

Por último, si queremos instalar el servicio en Windows, primero vamos a publicarlo de manera que generemos un .exe indicándole al publicar que queremos que el «Tiempo de ejecución de destino» sea una versión de Windows»

La imagen muestra como indicarle el destino de la publicación

Una vez que hemos publicado nuestro .exe, vamos a tirar de «Service Controller Commands» para instalarlo. Desde la terminal:

sc create "Servicio Core" binpath= "C:\Users\jorge\source\repos\PostServicioNetCore\PostServicioNetCore\bin\Release\netcoreapp2.2\publish\PostServicioNetCore.exe"

Por el contrario, si queremos que el servicio sea para Linux, basta con que no generemos un .exe al publicar y lo registremos normalmente (como veremos en la próxima entrada aprovechando este código).

Con esto, ya tenemos nuestro servicio Net Core multiplataforma listo para ser instalado y ejecutado tanto en Windows como en Linux, y al que solo tenemos que ir añadiéndole la funcionalidad que queramos mediante nuevas implementaciones de «IHostedService».

Como siempre, dejo el código fuente en GitHub para que puedas descargártelo y probar.