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

Compiler as a Service – Parte 1

Pensaba acerca de las cosas que podríamos hacer si el compilador de C# fuese un assembly reutilizable (Compilar as a service). Lo primero, y menos original, que se me ocurrió es que podríamos tener algo como JSON pero para C# en lugar de Java Script.

   1: class Program

   2: {

   3:     static void Main(string[] args)

   4:     {

   5:         const string data = @"results[""professional""] = 

   6:         new

   7:         {

   8:             FirstName = ""Juan Pablo"",

   9:             LastName = ""Ibañez"",

  10:             Certifications = new[]

  11:             {

  12:                 new { Provider=""MSFT"", ExamCode = ""70-536"", Title = ""Application Development Foundation"" },

  13:                 new { Provider=""MSFT"", ExamCode = ""70-505"", Title = ""Application Development Foundation"" },

  14:                 new { Provider=""MSFT"", ExamCode = ""70-563"", Title = ""Application Development Foundation"" }

  15:             },

  16:             

  17:             SayHi = new System.Action(()=> System.Console.WriteLine(""Hi""))

  18:         };";

  19:  

  20:         dynamic professional = CSharpEvaluator.Evaluate(data)["professional"];

  21:         

  22:         Console.WriteLine("First Name: {0}", professional.FirstName);

  23:         Console.WriteLine("Last Name : {0}", professional.LastName);

  24:         foreach (var certification in professional.Certifications)

  25:         {

  26:             Console.WriteLine("tCertification: {0}", certification.Title);

  27:         }

  28:         professional.SayHi();

  29:     }

  30: }

image

Esto abriría realmente muchas posibilidades. Una de las que me atrae más es el sustituir los XMLs. Quizás para guardarlos en una base de datos como entidades completas y hacerlas viajar por la red.