miércoles, 28 de enero de 2015

Plantilla para crear clases Proxy o decorador en C# de forma aútomática

Hola a todos. Hace tiempo que no escribo en mi blog y por ello pido disculpas. Sin embargo, como he comentado otras veces, prefiero no escribir demasiado pero producir contenido que considero que puede ser de calidad.

El trabajo y la vida personal no me deja demasiado tiempo para poder escribir todas las entradas que me gustaría en el blog, sin embargo, sí que escribo ésta por que en el ámbito profesional me he encontrado con un problema que podía resolverse con una sencilla plantilla que nos ayude a crear clases de código de forma automática.

El problema que intento resolver es aquel que surge cuando se dispone de un objeto ya relleno por algún sistema o componente software como puede ser por ejemplo el componente de persistencia Microsoft Entity Framework o cualquier otro componente de terceros. Estos objetos, que ya vienen rellenos, no pueden ser heredados por otra clase que nosotros hagamos puesto que no tenemos el control de poder rellenar dichos objetos. Por ejemplo, cuando recuperamos un registro de base de datos en forma de objetos utilizando Microsoft Entity Framework no podemos rellenar el objeto puesto que este tomará los valores almacenados en base de datos. Para resolver este problema existe un patrón de diseño llamado decorador o proxy.


Implementación:

       

private class ElementoProxy : Elemento
{
    // Incluímos una referencia a otro elemento.
    private Elemento elemento;
 
    // Inyectamos el elemento a través del constructor.
    public ElementoProxy(Elemento elemento)
    {
        this.elemento = elemento;
    }
 
    // El método HttpGet realizará comprobaciones y/o adaptaciones para
    // posteriormente realizar la llamada al método homónimo del objeto real
    public string HttpGet(string uri)
    {
        if (uri.ToLower().Contains("paginaprohibida.com"))
            return null;
        else
            return HttpGet(uri);
    }
};

      
 

Con esta idea también podemos implementar herencia múltiple que no está permitida en los lenguajes de programación orientada a objetos, sin embargo, este patrón nos permitiría dicha herencia múltiple o al menos simular.

En Internet encontramos numerosos ejemplos de cómo implementar un patrón proxy a partir de un objeto ya relleno, sin embargo estos ejemplos encontrados  implementan dicho patrón de forma dinámica en tiempo de ejecución y no en tiempo de diseño de código por lo que perdemos el control a la hora de poder extender nuestras clases proxy que puedan implementar dicho patrón de diseño.

Para poder extender o realizar una clase proxy en la que podamos introducir un objeto ya relleno y extenderlo a nuestras necesidades, la mejor solución que se me ha ocurrido ha sido implementar una plantilla de tipo T4. Dicha plantilla sería capaz de analizar por reflexión el objeto y generar códigos de forma automática que implemente mediante código un objeto proxy para englobar cualquier objeto que necesitemos extender y no podamos, o simplemente, no nos interese instanciar de nuevo. (Objetos definidos en componentes de terceros con constructores privados u objetos que a pesar de tener constructores públicos son rellenados por componentes de los cuales no tenemos control).

La solución que planteo es la siguiente:

Las plantillas de tipo T4 generan un fichero de texto con contenido de código fuente. Este fichero en general es único para cada plantilla, de manera que si nosotros queremos que una misma plantilla T4 genere varios ficheros de código fuente necesitaremos parametrizar este comportamiento e indicar de alguna forma cuántos ficheros de código fuente genera cada uno de éstos. En nuestra solución cada fichero implementará una clase proxy distinta. Debido a que estos ficheros son generados de forma automática, no es recomendable modificar los ya generados. Para complementar estas clases proxy se recomienda crear otro fichero que represente la misma clase generada (el mismo nombre de la clase) indicando que dicha clase es parcial. De hecho, la plantilla genera la clase proxy como clase parcial posibilitando su reimplementación complementaria en cualquier otro fichero evitando así que nuestro código no sea machado o destruido.

Por otro lado, debemos indicar cuál será la clase base o interfaz que queremos extender. Es obligatorio indicar a la plantilla donde está situada dicha clase base o interfaz para que ésta pueda leer su estructura e implementación mediante mecanismos de reflexión. Por supuesto, debemos especificar  el fichero de ensamblado donde se encuentra ya compilada la clase o interfaz a extender.

La manera más sencilla y eficaz de realizar esta parametrización es mediante un fichero XML el cual nos permitirá indicar de forma clara y sencilla dicha información para que la plantilla T4 pueda tomar estos parámetros y realizar su trabajo.

La estructura de este fichero de parámetros XML es la que propongo en el siguiente fragmento de código.  En éste podemos observar que se indica cuál será el nombre de la clase o interfaz que se quiere extender, el nombre de la clase nueva que se extenderá, así como el fichero de ensamblado que contiene la clase base en la cual se apoyará la plantilla de T4 para realizar su trabajo.


       

            
<?xml version="1.0" encoding="utf-8" ?>
<generador>
  <proxyclass nombreCompletoBase="System.Text.StringBuilder"

 nombre ="MiPaquete.StringBuilderProxy"

 assembly="C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.5.1\mscorlib.dll"></proxyclassu>
</generador>

       
 

Debemos tener en cuenta que la ruta del ensamblado debe ser la ruta completa del mismo.

Ahora toca hablar sobre la implementación de la plantilla.

El siguiente fragmento de código mostrará el código correspondiente a la plantilla T4 en la que se pueden diferenciar varias partes:

  1. En la primera de ellas se hace mención a la importación de ensamblados necesarios para poder tratar con ficheros XML. También se hace la inclusión de los paquetes necesarios para poder leer y escribir fichero de texto, la lectura y tratamiento de ficheros XML. Así mismo, se incluyen los paquetes necesarios para realizar las labores de reflexión.
  2. En la segunda parte del fragmento de código se realiza la lectura del fichero XML al cual le hemos llamado "proxygenerador.xml". Debemos tener en cuenta que cuando Visual Studio ejecuta la plantilla tipo T4, ésta la realiza sobre la carpeta principal de la solución que contiene el proyecto y no donde tengamos alojada la plantilla tipo T4. En mi caso he alojado el fichero de parámetros XML en la misma carpeta de paquetes que la plantilla de tipo T4, pero la ruta del fichero XML podría ser distinta, aunque no lo aconsejo porque cambiar dicha ruta llevaría a reimplementar la plantilla. Al ejecutar la plantilla T4 ésta no encontrará el fichero XML en la misma carpeta donde está alojada la plantilla, por tanto, para resolver este problema necesitamos introducir un mecanismo para que sea capaz de encontrar el fichero XML. Este mecanismo necesita incluir en la cabecera de la plantilla un parámetro "hostspecific" con valor acierto. Posteriormente tendremos que resolver la ruta del fichero XML utilizando el método "ResolvePath" cuyo valor devuelto será pasado a la clase especializada en leer documentos XML.
  3. El siguiente paso será por tanto hacer un "parser" sobre el propio fichero XML y extraer la información que dicho fichero contiene y que se ha visto en el fragmento de código anterior.
    • Por cada etiqueta en la que se especifique los parámetros para la generación de una clase proxy, se creará un fichero de código con la clase proxy deseada.
  4. Por último, en la parte final de la plantilla, es interesante destacar el método "GetFriendlyTypeName" el cual está especializado en la inferencia de los tipos que utilizan los argumentos, los tipos devueltos por las propiedades y tipos devueltos por los métodos de la clase base o interfaz que se quiere extender.

Tengo que reconocer que los métodos para generar e inferir estos tipos, así como el método utilizado para escribir los ficheros de código fuente que se pretenden generar, no son de cosecha propia. Los he localizado en Internet. Sin embargo siento no poder incluir las referencias de dichas localizaciones ya que después de tanto navegar por Internet he perdido dichas referencias. Pido disculpa a sus autores. Si las localizara de nuevo, éstas serán incluidas.

Por tanto el código resultante listo para ser ejecutado es el que muestro en el siguiente fragmento de código.

Plantilla t4

       

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Xml.Linq" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Xml.Linq" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#
 XDocument xdoc = XDocument.Load(this.Host.ResolvePath("ProxyGenerador.xml"));

foreach (XElement xElement in xdoc.Descendants("ProxyClass"))
{
    Assembly assem = Assembly.LoadFile(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(this.Host.ResolvePath("ProxyGenerador.xml")), xElement.Attribute("assembly").Value)));
    Type baseType = assem.GetType(xElement.Attribute("nombreCompletoBase").Value);

 string nombreClaseProxy=xElement.Attribute("nombre").Value;

 #>
using System;

// ReSharper disable CheckNamespace
namespace <#= baseType.Namespace #>
// ReSharper restore CheckNamespace
{
 public partial class <#= nombreClaseProxy #> : <#= GetFriendlyTypeName(baseType) #>
 {
  #region Atributos

  private readonly <#= GetFriendlyTypeName(baseType) #> _component;

  #endregion

  #region Propiedades

<#
  foreach (PropertyInfo propertyInfo in baseType.GetProperties())
  {
#>
  public <#= GetFriendlyTypeName(propertyInfo.PropertyType) #> <#= propertyInfo.Name #>
  {
<#
 if (propertyInfo.CanRead)
 {
#>
   get { return _component.<#= propertyInfo.Name #>; }
<#   
 }
#>
<#
 if (propertyInfo.CanWrite)
 {
#>
   set { _component.<#= propertyInfo.Name #> = value; }
<#  
 }
#>
  } 
<#
  }
  
#>

  #endregion

  #region Contructores

  public <#= nombreClaseProxy #>(<#= baseType.Name #> component)
        {
            if (component == null)
                throw new ArgumentNullException("component");
            _component = component;
        }

  #endregion

  #region Metodos

<#
  foreach (MethodInfo methodInfo in baseType.GetMethods())
            {
   if (methodInfo.IsSpecialName)
   {
    continue;
   }
#>
  public <#= methodInfo.ReturnType.FullName #> <#= methodInfo.Name #>(<#
for (int index = 0; index < methodInfo.GetParameters().Length; index++)
{
    ParameterInfo parameterInfo = methodInfo.GetParameters()[index];

     if (index>0)
                    {                       #>,<# 
                    }
     if (parameterInfo.ParameterType.IsByRef)
                    {  
     if (parameterInfo.IsOut)
                    {
                        #>out <# 
                    }
                    else if (parameterInfo.IsIn)
                    {
     #>in <# 
                    }
                    else
                    {
                        #>ref <# 
                    } 
                    }
#><#= GetFriendlyTypeName(parameterInfo.ParameterType).Replace("&","") #> <#= parameterInfo.Name #>
<#

}#>)
  {
   <#

if (!"System.Void".Equals(methodInfo.ReturnType.FullName))
                {
      #>return <# 
                }
#>_component.<#= methodInfo.Name #>(<#
    for (int index = 0; index < methodInfo.GetParameters().Length; index++)
    {
     ParameterInfo parameterInfo = methodInfo.GetParameters()[index];

          if (index>0)
         {                       #>,<# 
         }

         if (parameterInfo.ParameterType.IsByRef)
                    {     
     if (parameterInfo.IsOut)
                    {
                        #>out <# 
                    }
                    else if (parameterInfo.IsIn)
                    {
     #>in <# 
                    }
                    else
                    {
                        #>ref <# 
                    }                  
                    }
    #><#= parameterInfo.Name #>
    <#

    }#>);
  }
<#

                
            }
#>

  #endregion
 }
}
<#
 SaveOutput(string.Format("{0}.cs",nombreClaseProxy));
}


#>
<#+
  void SaveOutput(string outputFileName)
  {
      string templateDirectory = Path.GetDirectoryName(Host.TemplateFile);
      string outputFilePath = Path.Combine(templateDirectory, outputFileName);
      File.WriteAllText(outputFilePath, this.GenerationEnvironment.ToString()); 

      this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length);
  }
 public static string GetFriendlyTypeName(Type t)
 {
 string typeName = t.FullName.Split("`".ToCharArray())[0];
 var genericArgs = t.GetGenericArguments();
 if (genericArgs.Length > 0)
 {
  typeName += "<";
  foreach (var genericArg in genericArgs)
  {
   typeName += GetFriendlyTypeName(genericArg) + ", ";
  }
  typeName = typeName.TrimEnd(',', ' ') + ">";
 }
 return typeName;
 }
#>





       



No hay comentarios:

Publicar un comentario en la entrada