Programación orientada a aspectos (AOP) en .NET Core con MacOS

Carlos Blanco | 22 de febrero, 2021

Para los desarrolladores de .NET es posible “transferir” sus conocimientos de programación a todas las plataformas.

Aquí se explica cómo aplicar en Mac las mismas técnicas utilizadas para codificar la Programación Orientada a Aspectos (AOP) en .NET Core 2 en Microsoft Windows .

Programación orientada a aspectos

AOP es un paradigma de programación que tiene como objetivo aumentar la modularidad al permitir la separación de los efectos secundarios de los cambios en el propio código. Esto es algo que una aplicación necesita hacer en muchos lugares diferentes, como el registro, el almacenamiento en caché, etc.

Estos comportamientos que no son centrales para la lógica del negocio pueden ser añadidos sin abarrotar el código con Autofac y DynamicProxy.

Guía de .NET en MS Windows

.NET Core framework y Visual Studio (VS) son necesarios para esta guía. Se pueden instalar con homebrew y cask.

Usted puede utilizar ambos para instalar software en un Mac. Ambas funcionan muy bien.

 


macbook:aop cblanco$ brew cask install dotnet-sdk
macbook:aop cblanco$ brew cask install visual-studio

 

Creación de un nuevo proyecto

Crea un nuevo proyecto de consola. Abra una ventana de terminal y ejecute el siguiente comando,


macbook:dotnet cblanco$ dotnet new console -n aop

 

En esta guía se utilizará Visual Studio para Mac. Abra la solución en VS para Mac.

 

Cd en la carpeta del proyecto, ejecute la solución y aparecerá “¡Hola Mundo!” en su consola:


macbook:aop cblanco$ dotnet run
Hello World!

 

Uso de Autofac y DynamicProxy

Aquí se explica cómo implementar AOP usando Autofac y DynamicProxy.

Añada el paquete nuget Autofac.Extras.DynamicProxy a su solución. Este paquete también añade los paquetes Autofac y Castle.Core como dependencias:

Autofac.Extras.DynamicProxy permite interceptar las llamadas a métodos de los componentes de Autofac. Esta definición coincide prácticamente con el objetivo de la AOP. Algunos casos de uso comunes son el registro, la gestión de transacciones y el almacenamiento en caché.

Hay cuatro pasos para implementar la interceptación usando DynamicProxy:

  1. Crear interceptores.
  2. Registrar los interceptores con Autofac.
  3. Activar la interceptación en los Tipos.
  4. Asociar los interceptores con los Tipos a interceptar.

Usted va a implementar dos intercepciones. Uno para el registro y otro para el almacenamiento en caché. Luego, los combinará para que pueda tener una mejor idea de cómo funciona el conjunto.

1. Implementación de un Logger

Lo primero que hay que hacer es implementar la interfaz Castle.DynamicProxy.IInterceptor para crear un nuevo interceptor. Esto registrará qué método se está ejecutando y qué valores de parámetros se están alimentando. También dirá cuánto tiempo ha tardado en ejecutarse.

 


public class Logger: IInterceptor
{
  TextWriter writer;
  public Logger(TextWriter writer
  {
    if(writer == null){
    	throw new ArgumentNullException(nameof(writer));
  	}
  	this.writer = writer
  }

  public void Intercept(IInvocation invocation)
  {
    var name = $"{invocation.Method.DeclaringType}.{invocation.Method.Name}";
    var args = string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()));

    writer.WriteLine($"Calling: {name}");
    writer.WriteLine($"Args: {args}");

    var watch = System.Diagnostics.Stopwatch.StartNew();
    invocation.Proceed(); //Intercepted method is executed here.
    watch.Stop();
    var executionTime = watch.ElapsedMilliseconds;

    writer.WriteLine($"Done: result was {invocation.ReturnValue}");
    writer.WriteLine($"Execution Time: {executionTime} ms.");
    writer.WriteLine();
  }
}

2. Autofac

A continuación, debe registrar el interceptor en Autofac. Esto le permite asociarlo a los tipos. Para configurar su Logger para que registre en Console.Out, pase Console.Out como parámetro a su constructor.

Hágalo en la raíz de la composición así:


var b = new ContainerBuilder();
b.Register(i=> new Logger(Console.Out));
var container = b.Build();

 

3. Habilitación de interceptores

El tercer paso es habilitar los interceptores en los tipos, lo que se hace llamando al método EnableInterfaceInterceptors cuando se registran los tipos propios. Modifique su raíz de composición para registrar un tipo y habilitar las intercepciones en él:


var b = new ContainerBuilder();

b.Register(i=> new Logger(Console.Out));
b.RegisterType()
	.As()
    .EnableInterfaceInterceptors();
    
 var container = b.Build();   

La calculadora y la interfaz ICalculator que usará como tipo interceptado contienen una implementación muy simple de un método de suma que funciona para este ejemplo.

Se ven así:


public interface ICalculator {
	int add(int a, int b);
}

public class Calculator : ICalculator
{
	public int add(int a, int b)
    {
    	return a + b;
    }
}

 

4. Asociar interceptores

Por último, hay que asociar los interceptores a los tipos. Puede hacerlo mientras registra el tipo, llamando al método InterceptedBy, como sigue:


b.RegisterType()
	.As()
    .EnableInterfaceInterceptors()
    .InterceptedBy(typeof(Logger));

 

Una vez que hayas configurado esto, los métodos del tipo Calculadora serán interceptados por el interceptor Logger que luego registrará en consecuencia:


macbook:aop cblanco$ dotnet run
Calling: aop.Domain.ICalculator.add
Args: 5, 8
Done: result was 13
Execution Time: 0 ms.
macbook:aop cblanco$

 

Como verá, se ejecutan con bastante rapidez.

Ha creado un interceptor que añade registro a su aplicación y envuelve su lógica de negocio sin mezclar código. Pero eso no es todo.

Esta técnica de AOP le permite colocar interceptores en capas para lograr más funcionalidad.

En el siguiente ejemplo, se implementará una memoria caché muy sencilla. No será una caché lista para producción, pero le enseñará cómo combinar interceptores y cómo empezar a escribir una memoria caché.

Implementación de una memoria caché

Sobre la base de lo que has hecho, ahora puedes implementar la interfaz IInterceptor para crear un interceptor MemoryCaching. Necesitará memoria para mantener el valor de retorno de los métodos interceptados para poder dar el valor de retorno desde el almacenamiento la próxima vez que se ejecute el mismo método, en lugar de ejecutar realmente el método por segunda vez.

También tiene que tener en cuenta el valor de los argumentos introducidos en el método interceptado para que no devuelva valores incorrectos en las siguientes llamadas.

La puesta en práctica tiene este aspecto:


public class MemoryCaching : IInterceptor
{
	private Dictionary<string, object> cache = new Dictionary<string, object>();
    
    public void Intercept(IInvocation invocation)
    {
    	var name = $"{invocation.Method.DeclaringType}_{invocation.Method.Name}";
        var args = string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()));
        var cacheKey = $"{name}|{args}";
        
        cache.TryGetValue(cacheKey, out object returnValue);
        
        if (returnValue == null)
        {
        	invocation.Proceed();
            returnValue = invocation.ReturnValue;
            cache.Add(cacheKey, returnValue);
        }
        else
        {
        	invocation.ReturnValue = returnValue;
        }
    }
}

 

Esta implementación se puede ampliar con cosas como la serialización de argumentos no primitivos y la creación de un hash de la cadena como clave de caché. Para ello se pueden utilizar Json.NET y xxHash.

Interceptores por capas

Registre ambos interceptores en la raíz de su composición. Combínelos para interceptar el tipo de calculadora. Por ejemplo:


var b = new ContainerBuilder();

b.Register(i => new Logger(Console.Out));
b.Register(i => new MemoryCaching());

b.RegisterType()
	.As()
    .EnableInterfaceInterceptors()
    .InterceptedBy(typeof(Logger))
    .InterceptedBy(typeof(MemoryCaching));
var container = b.Build();

Ejecute el método de adición en su Calculadora. Escriba unas cuantas veces y compruebe los resultados.


var calc = container.Resolve();

calc.add(5, 8);
calc.add(5, 8);
calc.add(6, 8);
calc.add(6, 8);
calc.add(5, 8);

La salida mostrará la primera vez que se ejecute el método. Lleva algo de tiempo. Sin embargo, las llamadas posteriores con los mismos valores son instantáneas porque se recuperan de la caché.

Obsérvese que el hilo de ejecución se ha detenido durante 1000ms para que el tiempo de ejecución sea más evidente.


macbook:aop cblanco$ dotnet run
Calling: aop.Domain.ICalculator.add
Args: 5, 8
Done: result was 13
Execution Time: 1006 ms.

Calling: aop.Domain.ICalculator.add
Args: 5, 8
Done: result was 13
Execution Time: 0 ms.

Calling: aop.Domain.ICalculator.add
Args: 6, 8
Done: result was 14
Execution Time: 1004 ms.

Calling: aop.Domain.ICalculator.add
Args: 6, 8
Done: result was 14
Execution Time: 0 ms.

Calling: aop.Domain.ICalculator.add
Args: 5, 8
Done: result was 13
Execution Time: 0 ms.

macbook:aop cblanco$

Ahora ha combinado dos interceptores para añadir funcionalidad a la aplicación sin modificar el código dentro del tipo Calculadora.

Su aplicación puede ser ampliada sin que su código sea modificado directamente. La nueva funcionalidad se añade al código existente sin tocarlo. Eso facilita la codificación.

Puntos clave para tener en cuenta:

  • Los desarrolladores de .NET no están atados a un sistema operativo específico ahora que Microsoft ha adaptado sus tecnologías a otras plataformas.
  • La AOP pretende aumentar la modularidad al permitir la separación de los efectos secundarios de los cambios en el código base.
  •  El registro y el almacenamiento en caché pueden añadirse a una aplicación sin saturar el código con Autofac y DynamicProxy.

Conclusión

Esta guía ofrece la oportunidad de trabajar con la última versión de .NET y C#. Ahora que Microsoft ha adaptado sus tecnologías a otras plataformas, .NET puede utilizarse sin estar atado a una plataforma específica. Tenga en cuenta que Visual Studio para MS Windows sigue siendo el mejor IDE.

Si quiere saber más sobre nuestros servicios de desarrollo de aplicaciones en Encora, 

Contáctenos

 

Acerca de Encora

En Encora, creamos ventajas competitivas para los clientes asociados a través de la innovación tecnológica acelerada. Estaremos encantados de acompañarlo en su viaje.

Por qué Encora 

  • Somos globales: Con más de 20 oficinas y laboratorios de innovación en 12 países, Encora está dispersa por todo el mundo. Como operamos en diferentes zonas horarias, siempre hay alguien dispuesto a ayudarlo.
  • Somos un servicio completo: La innovación tecnológica abarca un enorme rango de temas, habilidades y estrategias. Ofrecemos un concepto de servicio completo, cada componente adaptado según sus necesidades, para presentar una solución completa. 
  • Nos dedicamos a ello: Nuestro creciente equipo de más de 4.000 programadores, desarrolladores, ingenieros y pensadores estratégicos cualificados y dedicados es la razón por la que Encora se ha convertido en una empresa tecnológica galardonada con una reputación envidiable.
  • Tenemos experiencia: Nos asociamos principalmente con empresas tecnológicas de rápido crecimiento que impulsan la innovación y el crecimiento en sus sectores. Nuestro modelo de entrega único, nuestra metodología ágil y nuestra calidad inigualable han contribuido a nuestro crecimiento constante.

Contenido

Compartir Artículo

Artículos Destacados