Una de las cosas que más me gusta de estas fechas es poder pasear con un cucurucho de castañas. Por tres cosas principalmente
-
Se me calientan las manos.
-
Se me llena la barriga.
-
Me sale barato y en tiempos de crisis no hay nada como algo barato y bueno.
En contra tiene una pega y es que alguna puede llevar premio en forma de gusano.
Sí a estás alturas no has cerrado el post, vas a ver que es lo que para mí es una “castaña con gusanos”.
Si señores las routas de mvc y WebApi por lo menos con la versión con la cual yo estoy trabajando(4.0).
Mientras que los métodos en WebApi se llamán Get,Post,Put,Delete, todo es perfecto, ahora cuando en un mismo controlador necesitas otra cosa, ponte a llorar.
Bueno pues dicho esto ya sabéis de que va el post. Los otros días tras esta entrada en twitter.
Para los que quieran la conversación completa os paso la url
https://twitter.com/_PedroHurtado/status/410525801314938881.
Mi buen amigo @nicolocodev me recomendó que utilizase Attribute Routing in Web API 2. Aunque ya había leído sobre esto se me planteaba algún que otro problema y es que yo no tenía mi proyecto en 2.0. Lo típico te vas a Nuget, buscas y te lleva a este sitio http://attributerouting.net/ la verdad que es un proyecto de lo más interesante y se puede ejecutar en mi escenario, pero…. Que veo!!!
Un símbolo rojo en WebApi y este cartel más bien llamativo.
Con lo cual al leer algo de performance como que se me activan las neuronas y de una conversación de las muchas que mantengo por twitter nace todo lo que vais a ver.
Si hablamos de performance, porque no creamos 1000 controladores con un método “GET” que simplemente devuelva una cadena y vemos si es verdad lo que dicen.
Manos a la obra.
1. Vía Nuget o bien desde el Pakage Manager Console instalas AttributeRouting.
2. Una vez que se instala se crea una clase en la carpeta App_Start con nombre AttributeRoutingHttpConfig. Donde rápidamente puedes ver que cuando la web se activa se ejecuta el método Start.
[assembly: WebActivator.PreApplicationStartMethod(typeof(MvcApplication18.AttributeRoutingHttpConfig), "Start")]
3. Pero como creo yo 1000 controladores sin T4 y sin copiar ni pegar. Sencillo pero misterioso. De esas cosas que dan miedo hasta que no las pruebas. Os presento a …. Don Reflection Emit.
En esa misma clase voy a crear un método llamado CreateManyControladores con el siguiente código.
1: private static Assembly CreateManyControladores()
2: {
3: AppDomain ad = AppDomain.CurrentDomain;
4: AssemblyName am = new AssemblyName();
5: am.Name = "TestAttributeRouting";
6: AssemblyBuilder ab = ad.DefineDynamicAssembly(am, AssemblyBuilderAccess.RunAndSave,"c:\mvc");
7: ModuleBuilder mb = ab.DefineDynamicModule("testmod", "TestAttributeRouting.dll");
8: Type t = null;
9: for (int i = 0; i < 1000; i++)
10: {
11: var controllerName = string.Format("Type{0}Controller", i);
12: var routeUrl = string.Format("/Hello{0}", i);
13: var returnMessage = string.Format("This is a string from method {0}", i);
14:
15: var typeBuilder = mb.DefineType(controllerName, TypeAttributes.Public, typeof(ApiController));
16:
17: var methodBuilDer = typeBuilder.DefineMethod("Get", MethodAttributes.Public, typeof(string), null);
18: var constructor = typeof(GETAttribute).GetConstructor(new Type[] { typeof(string) });
19: methodBuilDer.SetCustomAttribute(new CustomAttributeBuilder(constructor, new object[] { routeUrl }));
20:
21: ILGenerator il = methodBuilDer.GetILGenerator();
22: il.Emit(OpCodes.Ldstr, returnMessage);
23: il.Emit(OpCodes.Ret);
24: t = typeBuilder.CreateType();
25: }
26:
27: ab.Save("TestAttributeRouting.dll");
28: return t.Assembly;
29: }
No te asustes que te lo explico.
-
Crear un AssemblyBuilder y le dices que es para ejecutar y guardar e indicas una ruta donde guardar la dll, en mi caso c:mvc.
-
Creamos un modulo que es donde vamos a compilar todos nuestros tipos.
-
Creamos 1000 controladores con nombre Type000Controller, 001,002,3 etc,etc con el Atributo “GET” y con la ruta “Hello0,1,2,3”
-
Devolvemos una cadena que nos indica que método se está ejecutando.
-
Por último guardamos la dll “TestAttributeRouting.dll” y devolvemos el Assembly.
¿Que no te lo crees?
Pues mira si existe el archivo y con ILSpy descompílalo y vas a obtener esto.
Hay Dios, “dichoso del que cree sin ver” .
Y ahora vamos con la confirmación de una mala “performance”.
Para ello en el método RegisterRoutes de la clase AttributeRoutingHttpConfig vamos a agregar el siguiente código y a tomar tiempos.
1: public static void RegisterRoutes(HttpRouteCollection routes)
2: {
3: // See http://github.com/mccalltd/AttributeRouting/wiki for more options.
4: // To debug routes locally using the built in ASP.NET development server, go to /routes.axd
5:
6:
7: routes.MapHttpAttributeRoutes();
8:
9: //Create assembly
10: var stopwatch = Stopwatch.StartNew();
11: var assembly = CreateManyControladores();
12: stopwatch.Stop();
13: var miliseconds = stopwatch.ElapsedMilliseconds;
14:
15: //Compile routes
16: stopwatch.Start();
17: routes.MapHttpAttributeRoutes(c => c.AddRoutesFromAssembly(assembly));
18: stopwatch.Stop();
19: miliseconds = stopwatch.ElapsedMilliseconds;
20:
21:
22: }
Creación de la dll dinámica.
Compilación de rutas.
Efectivamente hemos confirmado que existe en esta dll un serio problema de rendimiento. No os voy a explicar más de esto puesto que tenemos que pasar a otra cosa, pero yo me he bajado el código fuente de AttributeRouting y si que tiene alguna que otra sentencia rara, yo creo que desconocimiento de Reflectión como el siguiente de los protagonista.
Debería decir lo que es, pero lo dejo para otro capítulo o mejor para que investigues tú .
Si ya tenemos todo esto. Es el momento de ejecutar desde nuestro explorador cada una de las rutas y ver lo que pasa.
Pín,Pín,Pum!!! algo raro está pasado. Venga ánimo que vamos a ver lo que es.
Dentro de todo lo que pasa en WebApi hay una serie de Servicios que se encargan de algunas cosas y concretamente el problema lo tenemos en el servicio DefaultHttpControllerTypeResolver.
En este servicio hay bajo mi punto de vista un problema y es que una dll dynamica no devuelve los tipos con esta instrucción.
assembly.GetExportedTypes();
Y claro el señor que escribió esta clase por lo visto no conocía,no recuerda o tuvo un lapsus para saber como obtener los tipos públicos de un ensamblado dinámico.
assembly.GetTypes();
Es por eso por lo que escribió el siguiente código.
O lo mismo estaba en la especificación y entonces el no es el culpable sino otro.
No permitir definir 1000 controladores al vuelo y tampoco ejecutar dll’s dinámicas de serie cosa que se hace una y otra vez con las vistas. Pero bueno… Vamos con la solución.
Crea una clase con el siguiente código.
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Reflection;
5: using System.Web;
6: using System.Web.Http.Dispatcher;
7:
8: namespace MvcApplication18.Handler
9: {
10: public class MyDefaultTypeResolver : DefaultHttpControllerTypeResolver
11: {
12: public MyDefaultTypeResolver()
13: :base()
14: {
15: }
16: public override ICollection<Type> GetControllerTypes(IAssembliesResolver assembliesResolver)
17: {
18: List<Type> result = new List<Type>();
19: var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
20:
21: foreach (var assembly in assemblies)
22: {
23:
24: Type[] exportedTypes = null;
25: if (assembly == null)
26: {
27: // can't call GetExportedTypes on a dynamic assembly
28: continue;
29: }
30:
31: try
32: {
33: if (assembly.IsDynamic)
34: {
35: exportedTypes = assembly.GetTypes();
36: }
37: else
38: {
39: exportedTypes = assembly.GetExportedTypes();
40: }
41: }
42: catch (ReflectionTypeLoadException ex)
43: {
44: exportedTypes = ex.Types;
45: }
46: catch
47: {
48: // We deliberately ignore all exceptions when building the cache. If
49: // a controller type is not found then we will respond later with a 404.
50: // However, until then we don't know whether an exception at all will
51: // have an impact on finding a controller.
52: continue;
53: }
54:
55: if (exportedTypes != null)
56: {
57: result.AddRange(exportedTypes.Where(x => IsControllerTypePredicate(x)));
58: }
59: }
60:
61: return result;
62:
63: }
64:
65:
66: }
67: }
Y en el archivo Global.asax en el método Application_Start incluir esta línea de código.
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerTypeResolver),
new MyDefaultTypeResolver());
Como podéis ver en el código, yo si permito ensamblados dinámicos y luego cargo los tipos correctos, queréis que todos vuestros controladores sean privados, pues otra cosa para investigar.
Ves que sencillo.
Ahora puedo ejecutar desde el explorador estás dos rutas, pero no le des que falla.
http://localhost:50150/Hello999
http://localhost:50150/api/type999
Claro esto pone en favor de “AttributeRouting” otro problema y es que nuestra api ahora responde a dos rutas con lo que plantéate problemas de auditoría y me dices por donde te están llamando.
Que es? un todo o nada. Me obliga a decorar todos mis métodos con un atributo.
Hemos vuelto a
mvc restfull webservices o no hemos salido aún
Conclusiones.
1. Si esto pasa en WebApi20 es para llorar, no lo he probado.
2. Ten claro que hasta que no llega la llamada a tu controlador todo es Reflection que puede ser optimo si lo haces bien o poco optimo si lo haces mal y en este caso esta mal hecho.
3. Que no inventes lo ya inventado que todo el mercado funciona con una convención de rutas y que se ha demostrado que funciona.
4. Que no os hago una demo en #nodejs y en #go porque se está haciendo pesado.
Pero repito menos papas, guardianes ,que funcione bien y se testee para escenarios de este tipo que con dos controladores todo funciona y tener 1000 controladores es fácil (200 entidades x 5 Métodos).
Hasta la próxima, que dada las fechas será para 2014.
FELIZ NAVIDAD Y PROSPERO AÑO NUEVO!!!
P.D. Me despido desde el blog porque desde el twitter aún os quedan aprox. unos 15 de días de guerra.
Anda que no estoy buscando pececillos. Que estoy buscando puertos y misterios . Los puertos es sencillo