Compiler as a Service – Parte 2

Si el compilador de C# pudiera utilizarse como cualquier assembly, podríamos generar código y compilarlo en tiempo de ejecución. Con esto, muchas de las cosas que hoy hacemos mediante CodeDom o Expression trees, las podríamos hacer simplemente concatenando strings (hay maneras mejores).

Por ejemplo, imaginemos un framework para crear clases Mock al que llamaremos RinhaMock. Aplicaremos este framework en el siguiente test unitario:

   1: [TestClass]

   2: public class Tests

   3: {

   4:     public void It_should_send_messages_with_values()

   5:     {

   6:         var sentMessage = string.Empty;

   7:  

   8:         dynamic sender = RinhaMock.Stub<IMessageSender>();

   9:         sender.When_Send_String = new Action<string>(s => sentMessage = s);

  10:  

  11:         var transacionManager = new TransactionManager(sender);

  12:         transacionManager.Deposit(amount: 50m, accountNo: "0087623481743", concept: "Pago cuota"); 

  13:  

  14:         Assert.AreEqual("50;0087623481743;Pago cuota", sentMessage);

  15:     }

  16: }

La idea es crear dinámicamente un tipo que implemente la interface IMessageSender y cuyo comportamiento pueda ser especificado fácilmente. Para que el ejemplo sea bien sencillo, vamos a tener una interface IMessageSender con solo un método y una clase TransactionManager igualmente simple:

   1: public interface IMessageSender

   2: {

   3:     void Send(string message);

   4: }

   5:  

   6: public class TransactionManager

   7: {

   8:     private IMessageSender _messageSender;

   9:  

  10:     public TransactionManager(IMessageSender messageSender)

  11:     {

  12:         this._messageSender = messageSender;

  13:     }

  14:  

  15:     internal void Deposit(decimal amount, string accountNo, string concept)

  16:     {

  17:         // Do the deposit here

  18:         var message = string.Format("{0};{1};{2}", amount, accountNo,concept);

  19:         _messageSender.Send(message);

  20:     }

  21: }

Ahora, la clase stub podría ser escrita por nuestras propias manos, como abajo:

   1: public class Stub__IMessageSender : CSharp5.IMessageSender 

   2: { 

   3:     public Action<System.String> When_Send_String { get; set; }

   4:     public void Send ( System.String message ) 

   5:     {  

   6:         When_Send_String(message); 

   7:     }

   8: }

O bien se podría  creada a partir de la metadata de la interface IMessageSender y compilada en tiempo de ejecución. Esta última opción, que hoy tenemos con CodeDom, podría ser mucho más rápida y sencilla si el compilador de C# estuviera disponible como un assembly.

El código necesario para crear el código de la clase stub es el siguiente:

   1: public class RinhaMock

   2: {

   3:     public static T Stub<T>() where T : class

   4:     {

   5:         var typeOfT = typeof(T);

   6:         string stubName = "Stub__" + typeOfT.Name;

   7:         var sb = new StringBuilder();

   8:  

   9:         var methods = typeOfT

  10:             .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod)

  11:             .Where(m => (m.IsVirtual || m.IsAbstract) && !m.IsSpecialName && !m.IsFinal);

  12:         methods.ForEach(m => sb.AppendLine(GetConfigPropertyCode(m)));

  13:         methods.ForEach(m => sb.AppendLine(GetMethodCode(m)));

  14:  

  15:         var scripRunner = new ScriptRunner(typeOfT);

  16:         var results = new Dictionary<string, object>();

  17:         scripRunner.AddObject<Dictionary<string, object>>("results", results);

  18:         scripRunner.AddUsing("CSharp5");

  19:         scripRunner.AddTypeCode(string.Format("public class {0} : {1} {{ {2} }}", stubName, typeOfT.FullName, sb.ToString()));

  20:         scripRunner.AddCode("results["stub"] = new " + stubName + "();");

  21:         scripRunner.Run();

  22:  

  23:         return (T)results["stub"];

  24:     }

  25:  

  26:     private static string GetMethodCode(MethodInfo method)

  27:     {

  28:         var isProcedure = method.ReturnType == typeof(void);

  29:         var parameters = method.GetParameters();

  30:  

  31:         return string.Format("public {6} {0} {1} ( {2} ) {{ {3} When_{1}{5}({4}); }}",

  32:             isProcedure ? "void" : method.ReturnType.FullName,

  33:             method.Name,

  34:             parameters.Select(p => string.Concat(p.ParameterType.FullName, " ", p.Name)).ToStringList(),

  35:             isProcedure ? string.Empty : "return",

  36:             parameters.Select(p => p.Name).ToStringList(),

  37:             parameters.Any() 

  38:                 ? "_" + parameters.Select(p => p.ParameterType.Name.Replace("[]", "Array")).ToStringList("_") 

  39:                 : string.Empty,

  40:             method.DeclaringType.IsInterface ? "" : "override"

  41:             );

  42:     }

  43:  

  44:     private static string GetConfigPropertyCode(MethodInfo method)

  45:     {

  46:         var isProcedure = method.ReturnType == typeof(void);

  47:         var parameters = method.GetParameters();

  48:  

  49:         var methodReturnedType = isProcedure ? "void" : method.ReturnType.FullName;

  50:         var parametersType = parameters.Select(p => p.ParameterType);

  51:         var returnedSign = isProcedure ? "Action<{0}>" :

  52:             string.Format("Func<{0}{1}>", "{0}", (parametersType.Any() ? "," : "") + methodReturnedType);

  53:         var parametersForSign = parametersType.Select(t => t.Name.Replace("[]", "Array")).ToStringList("_");

  54:  

  55:         return string.Format("public {0} When_{1}{2} {{ get; set; }}",

  56:             isProcedure && !parameters.Any() 

  57:                 ? "Action" 

  58:                 : string.Format(returnedSign, parametersType.Select(t => t.FullName).ToStringList()),

  59:             method.Name,

  60:             parametersType.Any() ? "_" + parametersForSign : string.Empty);

  61:     }

  62: }

  63:  

  64: static class StringListExtensions

  65: {

  66:     public static string ToStringList(this IEnumerable<string> enumerable, string separator = ", ")

  67:     {

  68:         if (!enumerable.Any())

  69:             return string.Empty;

  70:  

  71:         return enumerable.Aggregate((s1, s2) => string.Concat(s1, separator, s2));

  72:     }

  73: }

  74:  

  75: static class IEnumerableExtensions

  76: {

  77:     public static void ForEach<T>(this IEnumerable<T> enumerable, Action<T> action)

  78:     {

  79:         foreach (var t in enumerable) action(t);

  80:     }

  81: }

Como se puede ver, esta podría ser una alternativa viable a todos esos frameworks para la creación de clases proxies como Castle.DynamicProxy (usado por RhinoMocks).

Sin categoría

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *