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