Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
miércoles, 1 de julio de 2009

Piezas de Lego Hace unas semanas, en el post Cambiar la ubicación de las vistas en ASP.NET MVC estuvimos viendo cómo era posible aprovechar la flexibilidad del framework para saltarse las convenciones respecto a la ubicación de los archivos de vistas (.aspx, .ascx y .master).

En esta ocasión vamos a seguir profundizando en los puntos de extensión del framework, centrándonos en la facilidad con la que podemos modificar el ControllerFactory, que es el componente encargado de localizar y crear las instancias de los controladores que deben procesar las peticiones.

Y vamos a ilustrarlo con un ejemplo, en el que desmontaremos otra de las convenciones del framework MVC: el nombre de los controladores. Como sabemos, cuando se recibe una petición que según su ruta debe ser procesada por un controlador llamado XYZ, el motor por defecto intentará instanciar una clase denominada XYZController.  Lo que vamos a hacer es retocar este comportamiento para que nuestros controladores puedan denominarse “ControladorDeXYZ”, un nombre mucho más acertado en nuestro idioma.

En este post veremos cómo podemos modificar la forma en que este componente localiza las clases de controladores y adaptarla a nuestras preferencias.

Pero antes, recuerda:

Saltarse las convenciones = malo

Hazlo sólo cuando realmente esté justificado... o por diversión, como es el caso ;-)

Lo que vamos a hacer en el post no es una implementación eficiente, y por supuesto no es válida para un entorno de producción; se trata simplemente de un experimento que demuestra la facilidad para sustituir ciertos componentes del framework, y la flexibilidad que nos aporta. Los conceptos que vamos a tratar pueden ser utilizados con otros fines, como facilitar la inyección de dependencias en controladores, que probablemente veamos en un futuro post.

Y ahora, al tema…

1. El proceso básico de la petición

Antes de empezar la faena, es conveniente estudiar un poco el funcionamiento del framework; así podremos entender mejor qué estamos tocando.

Cuando se recibe una petición, su tratamiento pasa a través de una serie de componentes que se encargan de procesarla a distintos niveles siguiendo el ciclo de vida definido para el framework. Así, una vez que el sistema de enrutamiento ha terminado su tarea detectando la información presente en la URL y determinando, por tanto, su destino, utilizará un manejador MvcHandler para continuar procesando la petición desde el método ProcessRequest().

La porción de código de ASP.NET MVC framework donde se define este método es la siguiente (en el archivo MvcHandler.cs, recordad que el código fuente está disponible en Codeplex):

   1: protected internal virtual void ProcessRequest(HttpContextBase httpContext) 
   2: {
   3:     [...]
   4:     
   5:     // Get the controller type
   6:     string controllerName = 
   7:         RequestContext.RouteData.GetRequiredString("controller");
   8:     
   9:     // Instantiate the controller and call Execute
  10:     IControllerFactory factory = 
  11:         ControllerBuilder.GetControllerFactory();
  12:             
  13:     IController controller = 
  14:         factory.CreateController(RequestContext, controllerName);
  15:             
  16:     if (controller == null) 
  17:     {
  18:         throw new InvalidOperationException(...); // Simplificado
  19:     }
  20:     try 
  21:     {
  22:         controller.Execute(RequestContext);
  23:     }
  24:     finally 
  25:     {
  26:         factory.ReleaseController(controller);
  27:     }
  28: }

Resumidamente, podríamos decir que el sistema procesa las peticiones siguiendo estos pasos:

  • en primer lugar, obtiene el nombre del controlador desde el contexto de la petición (normalmente desde la ruta). En el código anterior, se guarda en la variable controllerName.
  • a continuación obtiene una instancia de la factoría de controladores desde la clase ControllerBuilder.
  • una vez obtenida la factoría según el paso anterior, se le pide mediante una llamada a su método CreateController() que cree el controlador que procesará la petición, enviándole como parámetro el nombre obtenido anteriormente.
  • y por último, se invoca al método Execute del controlador para que realice las acciones oportunas, liberando al final los recursos utilizados mediante una llamada a ReleaseController.

Como en otras muchas partes del framework, se puede observar en el código anterior la utilización de interfaces en lugar de clases para los elementos que intervienen en el proceso, lo que da pistas sobre la flexibilidad de ASP.NET MVC: casi cualquier elemento puede ser sustituido por otro siempre que se cumpla el contrato definido.

2. La factoría de controladores, a corazón abierto

Visto el procedimiento anteriormente descrito, está claro que la lógica de localización e instanciación del controlador es responsabilidad de las factorías de controladores, y más exactamente del método CreateController() que éstas deben implementar.

ASP.NET MVC utiliza como factoría la clase DefaultControllerFactory  de forma predeterminada, sin embargo, nada en el código anterior obliga a que esto sea así. Si logramos crear una nueva factoría que implemente el interfaz IControllerFactory e indicamos al framework que la utilice, ya casi tendremos el trabajo hecho.

En el código de la clase DefaultControllerFactory (la factoría predeterminada), se puede observar que el proceso de creación está compuesto por dos pasos; en el primero de ellos se obtiene el tipo de controlador mediante una llamada a GetControllerType(), mientras que en el segundo se obtiene una instancia de dicho tipo llamando a GetControllerInstance(), que es la que retorna el método:


   1: public virtual IController 
   2:     CreateController(RequestContext requestContext, string controllerName) 
   3: {
   4:     [...] // Simplificado
   5:     Type controllerType = GetControllerType(controllerName);
   6:     IController controller = GetControllerInstance(controllerType);
   7:     return controller;
   8: }

Dado que lo único que queremos sustituir es el mecanismo de localización, lo más sencillo es crear nuestra nueva factoría, a la que llamaremos AcmeControllerFactory, heredando de DefaultControllerFactory y sobrescribir el método GetControllerType, que afortunadamente es virtual.

El código completo para esta nueva clase es el siguiente:


   1: public class AcmeControllerFactory: DefaultControllerFactory
   2: {
   3:   protected override Type GetControllerType(string controllerName)
   4:   {
   5:       Type r = base.GetControllerType(controllerName);
   6:       if (r == null)
   7:       {
   8:           Assembly current = Assembly.GetExecutingAssembly();
   9:           r = (from t in current.GetTypes()
  10:                where
  11:                (
  12:                        t.Name.Equals("ControladorDe" + controllerName,
  13:                                       StringComparison.InvariantCultureIgnoreCase)
  14:                   && t.IsPublic
  15:                   && !t.IsAbstract 
  16:                   && typeof(IController).IsAssignableFrom(t)
  17:                )
  18:                select t).FirstOrDefault();
  19:       }
  20:       return r;
  21:   }
  22: }

Como podemos observar, lo primero que hacemos es intentar localizar el controlador con el nombre que nos llega en el parámetro, pero utilizando el método propuesto por la factoría por defecto, es decir, llamando al método GetControllerType() original. Es decir, vamos a ser al menos algo respetuosos con las convenciones de nombrado existentes ;-) … primero intentaremos localizar el controlador ciñéndonos a ellas, y sólo si no tenemos éxito aplicaremos nuestro propio criterio de nombrado.

Así, ante una petición rutada hacia el controlador Cliente, primero buscaremos la clase ClienteController (según la convención), y si no existe es cuando intentaremos localizar un tipo llamado ControladorDeCliente. Obviamente, podríamos hacerlo al revés, es decir, buscar primero según nuestro criterio de nombrado y si no tenemos éxito buscar utilizando el estándar, o bien ni siquiera intentar esta última opción.

Por tanto, si el método por defecto no es capaz de encontrar un controlador correspondiente a dicha denominación es cuando ejecutamos nuestra lógica adicional, que consiste en buscar en el ensamblado actual, mediante una consulta LINQ, aquellas clases cuyo nombre corresponda con el patrón que pretendemos emplear (“ControladorDe”+controllerName), y que sean públicas, instanciables, e implementen el interfaz IController.

No se han implementado optimizaciones, como la inclusión de un caché, que permitan mejorar el rendimiento en esta consulta. Cada petición deberá recorrer, en el peor de los casos, todo el ensamblado en busca de un controlador apropiado.

Tampoco se ha implementado ningún tipo de resolución de conflictos; si existe más de un controlador con el mismo nombre en distintos namespaces, se retornará el primero que se encuentre.

Eso os lo dejo de deberes ;-)

3. Sustituir la factoría de controladores

Ya tenemos creada la clase AcmeControllerFactory, nuestra propia factoría de controladores, y está lista para entrar en acción. Pero, ¿cómo le decimos al framework que queremos utilizar una factoría de controladores distinta a la predeterminada?

Volvamos a estudiar el código de procesado de las peticiones que vimos anteriormente. Como se puede observar, el IControllerFactory a utilizar es obtenido desde la clase ControllerBuilder:


   1: protected internal virtual void ProcessRequest(HttpContextBase httpContext) 
   2: {
   3:   [...]
   4:   // Instantiate the controller and call Execute
   5:   IControllerFactory factory = 
   6:             ControllerBuilder.GetControllerFactory();
   7:             
   8:   IController controller = 
   9:             factory.CreateController(RequestContext, controllerName);
  10:     [...]
  11: }

Y aquí de nuevo entra en juego la flexibilidad del diseño de ASP.NET MVC: ControllerBuilder viene preparada para que podamos sustituir muy fácilmente la factoría a instanciar, por lo que podemos indicarle el tipo concreto que queremos que utilice.

En el siguiente código estamos indicando al ControllerBuilder que nuestra factoría por defecto, es decir, la clase encargada de buscar e instanciar los controladores, será AcmeControllerFactory. Como  se define en la inicialización de la aplicación (Application_Start del global.asax), todas las peticiones utilizarán esta factoría para crear los controladores:


   1: protected void Application_Start()
   2: {
   3:     RegisterRoutes(RouteTable.Routes);
   4:     ControllerBuilder
   5:         .Current
   6:         .SetControllerFactory(typeof(AcmeControllerFactory));
   7: }

4. Probar que realmente funciona

Podemos comprobar el funcionamiento de todo este invento de forma muy sencilla. Una vez seguidos los pasos anteriores, basta con crear un nuevo controlador que ya atienda a nuestra propia convención de nombrado, como el siguiente:


   1: [HandleError]
   2: public class ControladorDeCliente : Controller
   3: {
   4:   public ActionResult Listar()
   5:   {
   6:       return View();
   7:   }
   8:  
   9:   public ActionResult Editar()
  10:   {
  11:       return View();
  12:   }
  13: }

Creamos, asimismo, una vista “Listar.aspx” y otra “Editar.aspx” en la carpeta “Views\Cliente” para que el motor pueda localizarlas y podamos ver algún resultado en pantalla. A partir de este momento, ejecutando el proyecto, podremos comprobar cómo si introducimos la URL  http://localhost:tupuerto/Cliente/Listar el flujo de ejecución pasará por el método Listar() visto anteriormente. También podemos invocar a la acción Editar(), y el resultado será el esperado.

Y para los perezosos, ahí va el código fuente del proyecto (requiere Visual Studio 2008 o Web Developer Express con SP1 y ASP.NET MVC 1.0):



Publicado en: Variable not found.

Aún no hay comentarios, ¡sé el primero!