El Controlador en ASP.NET MVC 4 (III): Action Filters


Un Action Filter o filtro de acción es un atributo que puede asociarse a una acción de un controlador (o al controlador en su totalidad), modificando la forma en la que la acción se ejecuta. El Framework ASP.NET MVC proporciona unos cuantos, entre ellos:

Nombre Descripción
Authorize Restringe una acción para los roles o usuarios especificados.
HandleError Permite especificar una vista que se mostrará en caso de excepción no controlada.
OutputCache Almacena en caché la salida de un controlador
ValidateAntiForgeryToken Ayuda a evitar peticiones de falsificación de petición (pishing)
ValidateInput Desactiva las validaciones de petición. No es aconsejable hacer uso de este filtro salvo en ocasiones muy concretas, ya que pone en peligro la seguridad de la aplicación.

Además de los existentes, es posible crear filtros personalizados para fines definidos por el negocio (como por ejemplo, un sistema de autenticación personalizado).

Tal y como vimos brevemente en el artículo anterior con [HttpGet] y [HttpPost], las acciones pueden “condimentarse” con atributos que afectan a su funcionalidad. Los filtros se añaden a modo de atributo a las acciones, haciendo que su comportamiento varíe. Veamos un par de ejemplos.

Aplicando un filtro a una acción

Cachear información que sabemos que no va a cambiar en un período determinado es una técnica que incrementa dramáticamente el rendimiento de una aplicación, especialmente si ésta es de cierto tamaño. Para realizare esta operación, bastará con utilizar el filtro [OutputCache] sobre la acción cuya respuesta (Response) queremos cachear. Veremos como aplicar esta funcionalidad a un controlador completo o a toda la aplicación más adelante. Por ahora, basta con fijarnos en el siguiente código:


        [OutputCache(Duration = 20)]
        public ActionResult CachearInformacion()
        {
            string respuesta = string.Format("La hora se ha cacheado en el instante {0}", DateTime.Now);
            return Content(respuesta);
        }

Si nos fijamos en el Intellisense del filtro, vemos que podemos obtener información muy valiosa acerca de los elementos del filtro que podemos configurar.


En nuestro caso utilizaremos el parámetro Duration, al que le pasaremos un valor entero que simbolizará el número de segundos que permanecerá la respuesta (en este caso de tipo ContentResponse) en caché sin actualizar su valor. Ejecutaremos la aplicación un par de veces para comprobar su funcionamiento:

    

Vemos que el resultado no parece variar. Si esperamos al menos veinte segundos y volvemos a ejecutar, veamos lo que ocurre:


Aplicando un filtro a un controlador completo

La filosofía para asociar un filtro a un controlador completo es similar a la que hemos visto hasta ahora. La única diferencia radica en que el atributo debe especificarse sobre la clase que implementa el controlador en lugar de hacerlo sobre el método que implementa la acción. Si por ejemplo queremos que sea obligatorio que el usuario esté registrado para que pueda acceder a la funcionalidad de nuestro controlador, basta con añadir el atributo [Authorize] sobre la declaración del controlador:


namespace MvcApp.Controllers
{
    [Authorize]
    public class ProductosController : Controller
    {

Nuevamente, si echamos un ojo al atributo, vemos que podemos personalizarlo indicando un conjunto de roles (Roles) o usuarios (Users) que tendrán permiso para ejecutar el controlador. El parámetro Order es común a todos los filtros, e indica el orden de ejecución de éstos en el caso de que exista más de uno para un mismo elemento.


Nuevamente, lanzamos la aplicación para comprobar si se ejecuta correctamente.


Como podemos ver, la aplicación nos redirecciona automáticamente a la página de login, ya que es obligatorio estar identificado en el sistema para poder hacer uso del controlador Productos.

Creando un filtro personalizado

Un filtro no es más que un objeto que tiene la posibilidad de ejecutar código durante cuatro posibles instantes del ciclo de vida de una petición:

  • Antes de ejecutar la acción
  • Después de ejecutar la acción
  • Antes de generar el resultado
  • Después de generar el resultado

Para crear nuestro filtro personalizado deberemos dirigirnos a la carpeta Filters de nuestra aplicación, hacer click derecho y seleccionar Add > Class…


A continuación asignaremos un nombre a la clase. Como podemos ver en la clase que ya existía en la carpeta Filters, su nomenclatura indica que debe finalizar con el sufijo Attribute para poder ser utilizado como tal en MVC. Por lo tanto, le asignamos un nombre que termine con ese sufijo, como por ejemplo Log4NetAttribute.


Una vez creada la clase, dado que se va a tratar de un ActionFilter, haremos que herede de la clase ActionFilterAttribute, añadiendo la cláusula using correspondiente (System.Web.Mvc). Hecho esto, añadiremos un atributo estático que se encargará de realizar las tareas de logging. Para ello haremos uso de log4net tal y como vimos en el artículo anterior.


    public class Log4NetAttribute : ActionFilterAttribute
    {
        private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    }

A continuación sobrecargaremos los métodos encargados de ejecutarse en los momentos que vimos un poco más arriba. Para aprovecharnos del Intellisense, escribiremos la palabra override y echaremos un ojo a los métodos que podemos sobrecargar.


Sobrecargaremos los cuatro métodos que vimos previamente y efectuaremos una escritura en el log indicando la etapa en la que nos encontramos, extrayendo de paso alguna información relevante del parámetro que recibe cada método. Este objeto transporta información diferente en cada uno de los métodos, pudiendo consultarla y modificarla (por ello se denominan “filtros”).


Codificaremos los métodos de forma sencilla, con algo similar a lo siguiente:


        // Método ejecutado justo antes de la ejecución de la acción
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            string tmpMsg = string.Empty;
            base.OnActionExecuting(filterContext);

            // Almacenamos el nombre del método
            log.Debug(System.Reflection.MethodBase.GetCurrentMethod().ToString());

            // Recorremos los parámetros de la acción y los mostramos
            IDictionary<string, object> actionParameters = filterContext.ActionParameters;
            foreach (string key in actionParameters.Keys)
                tmpMsg += "[" + key + ": " + actionParameters[key].ToString() + "] ";

            log.Debug("ActionParameters: " + tmpMsg);
            log.Debug(" --------------------------------------- ");
        }

        // Método ejecutado justo después de la ejecución de la acción
        public override void OnActionExecuted(ActionExecutedContext filterContext)
        {
            string tmpMsg = string.Empty;
            base.OnActionExecuted(filterContext);

            // Almacenamos el nombre del método
            log.Debug(System.Reflection.MethodBase.GetCurrentMethod().ToString());

            // Recogemos el resultado
            ActionResult result = filterContext.Result;
            log.Debug("ActionResult: " + result.ToString());

            // Comprobamos si se ha producido alguna excepción durante la ejecución.
            // En caso afirmativo, la almacenamos
            if (filterContext.Exception != null)
                log.Error("Error durante la ejecución de la acción", filterContext.Exception);

            log.Debug(" --------------------------------------- ");

        }

        // Método ejecutado justo antes de la ejecución del resultado
        public override void OnResultExecuting(ResultExecutingContext filterContext)
        {
            base.OnResultExecuting(filterContext);
            log.Debug(System.Reflection.MethodBase.GetCurrentMethod().ToString());

            // Recogemos el resultado
            ActionResult result = filterContext.Result;
            log.Debug("ActionResult: " + result.ToString());

            log.Debug(" --------------------------------------- ");
        }

        // Método ejecutado justo después de la ejecución del resultado
        public override void OnResultExecuted(ResultExecutedContext filterContext)
        {
            base.OnResultExecuted(filterContext);
            log.Debug(System.Reflection.MethodBase.GetCurrentMethod().ToString());

            // Recogemos el resultado
            ActionResult result = filterContext.Result;
            log.Debug("ActionResult: " + result.ToString());

            log.Debug(" --------------------------------------- ");
        }

A continuación, acudiremos a nuestra acción (por ejemplo, Buscar) y añadiremos el atributo, sin olvidarnos de agregar el espacio de nombres en el que definimos previamente la clase encargada de realizar el filtrado.

Si lanzamos la aplicación invocando la ruta /Buscar/Comics/Ironman, tras echar una ojeada al fichero log.txt situado en el directorio principal de la aplicación, veremos que contiene la siguiente información.


2013-11-14 22:06:52,112 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] - Void OnActionExecuting(System.Web.Mvc.ActionExecutingContext)

2013-11-14 22:06:52,139 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] - ActionParameters: [tipoProducto: Comics] [nombreProducto: Ironman]

2013-11-14 22:06:52,140 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] -  ---------------------------------------

2013-11-14 22:06:52,142 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] - Void OnActionExecuted(System.Web.Mvc.ActionExecutedContext)

2013-11-14 22:06:52,143 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] - ActionResult: System.Web.Mvc.ContentResult

2013-11-14 22:06:52,143 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] -  ---------------------------------------

2013-11-14 22:06:52,144 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] - Void OnResultExecuting(System.Web.Mvc.ResultExecutingContext)

2013-11-14 22:06:52,144 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] - ActionResult: System.Web.Mvc.ContentResult

2013-11-14 22:06:52,145 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] -  ---------------------------------------

2013-11-14 22:06:52,145 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] - Void OnResultExecuted(System.Web.Mvc.ResultExecutedContext)

2013-11-14 22:06:52,146 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] - ActionResult: System.Web.Mvc.ContentResult

2013-11-14 22:06:52,146 [6] DEBUG MvcApp.Filters.Log4NetAttribute [(null)] -  ---------------------------------------

Aplicando un filtro a toda la aplicación

Si en lugar de aplicar la operación de logging a una acción o controlador concreto quisiéramos que se aplicara a todas y cada una de las acciones de nuestra aplicación, tendremos que cambiar el lugar donde asignamos el filtro. Eliminaremos por lo tanto el atributo [Log4Net] de la acción actual y echamos un ojo al fichero que controla las acciones globales de la aplicación: global.asax.cs.


        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            AuthConfig.RegisterAuth();

            // Iniciamos log4net
            log4net.Config.XmlConfigurator.Configure();
        }

Observamos que una de las líneas de código es la encargada de registrar los filtros globales. Esto se realiza invocando el método FilterConfig.RegisterGlobalFilters. Si nos situamos sobre el método y pulsamos la tecla F12, nos llevará directamente al código de este método, que se encontrará en la carpeta App_Start/FilterConfig y tendrá el siguiente aspecto:


    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }
    }

Como podremos imaginar, para añadir nuestro filtro bastará con añadir una línea similar a la existente indicando el filtro que creamos previamente (sin olvidarnos de la cláusula using), dejando el código de la siguiente manera:


    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new Log4NetAttribute());
        }
    }

Con esta operación, cada vez que una acción sea ejecutada, nuestro filtro entrará en acción.

Gestión de errores

Si nos fijamos bien, ya existía un filtro global antes de nuestra llegada: HandleErrorAttribute. Este atributo se encarga, principalmente, de gestionar las excepciones no controladas que se producen en la aplicación, redireccionando al usuario a una página que indique, de la forma más elegantemente posible, que se ha producido un problema en la aplicación.

A estas alturas es probable que alguien piense “yo no conozco esa página, siempre que he sufrido la aparición de un error aparece la maldita página con información sobre la excepción, y de elegante no tiene nada”. Esto se debe a la configuración por defecto con la que ASP.NET inicia la aplicación inicialmente.

Por defecto, ASP.NET configura la aplicación para que este tipo de información sólo sea visible para aquellos usuarios conectados desde la misma máquina en la que se aloja la aplicación, estableciendo el valor de la propiedad customErrors a RemoteOnly. De modo que los usuarios reales no verán lo mismo que estamos viendo nosotros, sino que serán redirigidos a una página menos “violenta”.

Para cambiar esto, abriremos el fichero web.config y añadiremos la siguiente línea en el apartado <system.web>


    <customErrors mode="On" />

Esto provocará que nuestro error se muestre ahora de una forma más “presentable”.

Hasta aquí bien, pero… ¿dónde se encuentra esa página de error? ¿Puedo cambiarla o personalizarla? Por supuesto que sí. Por defecto, la página de error en Razor se encuentra en la ruta Views/Shared/Error.cshtml.

Su contenido, que por supuesto podemos modificar, es el siguiente:


    @model System.Web.Mvc.HandleErrorInfo

    @{
        ViewBag.Title = "Error";
    }

    <hgroup class="title">
        <h1 class="error">Error.</h1>
        <h2 class="error">An error occurred while processing your request.</h2>
    </hgroup>

Anuncios

2 comments

  1. Hola quisiera saber como configuro el cache para usar el atributo outputcache en los controladores especificamente en los actionresult, pero configurando que el cache se almacene en el explorador de cliente, ayuda por favor

    1. Hola, Joel. Para que la página permanezca cacheada en el navegador del cliente deberás utilizar el parámetro Location=OutputCacheLocation.Client, de la forma:

      [OutputCache(Duration=60, Location=OutputCacheLocation.Client)]
      public ActionResult MiActionResult()
      {
      / …

      Recuerda que si realizas esta operación, el caché se sobreescribirá si se refresca la página del navegador (por ejemplo, pulsando F5), haciendo que el servidor vuelva a enviar una nueva respuesta.

      Puedes echar un vistazo a los valores que puede aceptar el parámetro Location en la MSDN: http://msdn.microsoft.com/es-es/library/system.web.ui.outputcachelocation.aspx

      Un saludo.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s