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!
martes, 16 de diciembre de 2014
ASP.NET MVC 6Como ya hemos visto por aquí en alguna ocasión, hace tiempo que MVC soporta controladores asíncronos, permitiendo la implementación de acciones muy eficientes desde el punto de vista de la utilización de los recursos del servidor.

Sin embargo, en cuanto pretendíamos llevar a los action filters la misma filosofía nos encontrábamos con que la infraestructura no estaba preparada, es decir, no teníamos una forma clara de introducir llamadas asíncronas en el cuerpo de los filtros. Como consecuencia, todas las tareas que se realizaban en su interior eran puramente síncronas y dejaban bloqueados los hilos destinados a procesar peticiones mientras se completaban las operaciones, lo cual es especialmente un despilfarro cuando se trata de tareas de entrada/salida.

Afortunadamente esto parece que va a terminar con ASP.NET Core MVC, que soportará ya esta solicitada característica. Pero ojo, que Core MVC está aún en desarrollo, por lo que todo lo que cuento a continuación puede cambiar.

Pero empecemos desde el principio, viendo qué ha cambiado por abajo para que sea posible crear filtros con código asíncrono en su interior. Quizás sea una lectura un poco densa, pero creo que es interesante para comprender cómo funcionan las cosas por dentro.

Abriendo el capó

Hasta MVC 5.x, era posible crear filtros personalizados tomando como base varias clases abstractas e interfaces, ofreciendo cada una de ellas un conjunto de métodos a implementar o sobrescribir para introducir nuestro código personalizado.

Por ejemplo, la implementación de la clase abstracta ActionFilterAttribute, una base usada con mucha frecuencia para crear filtros personalizados, era de la siguiente forma:
public abstract class ActionFilterAttribute : 
        FilterAttribute, IActionFilter, IResultFilter
{
    public virtual void OnActionExecuting(ActionExecutingContext filterContext) { }
    public virtual void OnActionExecuted(ActionExecutedContext filterContext)   { }
    public virtual void OnResultExecuting(ResultExecutingContext filterContext) { }
    public virtual void OnResultExecuted(ResultExecutedContext filterContext)   { }
}
Como se puede observar, no hay rastro de las “marcas” que indican la presencia de asincronía, como el uso de las clases Task o Task<T>, o de las palabras clave async y await. Los métodos OnXXX() presentes en la signatura eran invocados de forma síncrona antes y después de la ejecución de las acciones y del  resultado (objetos ActionResult) retornado por éstas. Dado que estos métodos son virtuales, la creación de filtros personalizados consistía normalmente en heredar de ActionFilterAttribute y sobreescribir uno o varios de ellos.

Pues fijaos ahora en la implementación de esta misma clase en Core MVC:
public abstract class ActionFilterAttribute: Attribute, IOrderedFilter,
             IActionFilter, IAsyncActionFilter, 
             IResultFilter, IAsyncResultFilter 
{
    public int Order { get; set; }

    public virtual void OnActionExecuting(ActionExecutingContext context) { }
    public virtual void OnActionExecuted(ActionExecutedContext context)   { }

    public virtual async Task OnActionExecutionAsync(
              ActionExecutingContext context, ActionExecutionDelegate next)
    {
        OnActionExecuting(context);
        if (context.Result == null)
        {
            OnActionExecuted(await next());
        }
    }

    public virtual void OnResultExecuting(ResultExecutingContext context) { }
    public virtual void OnResultExecuted(ResultExecutedContext context)   { }

    public virtual async Task OnResultExecutionAsync(
              ResultExecutingContext context, ResultExecutionDelegate next)
    {
        OnResultExecuting(context);
        if (!context.Cancel)
        {
            OnResultExecuted(await next());
        }
    }
}
¡Esto ya es otra cosa! Siguen existiendo los mismos métodos síncronos OnXXX() que teníamos en las versiones anteriores, pero ahora se han implementado un par de métodos nuevos, OnActionExecutionAsync() y OnResultExecutionAsync(), ambos definidos en sus respectivos interfaces IAsyncActionFilter y IAsyncResultFilter, que brindan la posibilidad de introducir código asíncrono de forma sencilla.

Acerquémonos ahora a la implementación de estos métodos. Son muy similares, por lo que nos centraremos en un principio en OnActionExecutionAsync():
public virtual async Task OnActionExecutionAsync(
           ActionExecutingContext context, ActionExecutionDelegate next)
{
    OnActionExecuting(context);
    if (context.Result == null)
    {
        OnActionExecuted(await next());
    }
}
Como podemos observar, en su interior se encuentra en primer lugar una llamada síncrona a OnActionExecuting(). Esto lo pone sencillo para los escenarios en los que no sea necesario asincronía, manteniendo retrocompatibilidad con filtros escritos para versiones anteriores del framework.

Sólo si no se ha cortocircuitado la ejecución de la acción estableciendo un resultado “precocinado” desde el propio filtro, es cuando se ejecuta la acción realmente mediante la invocación al delegado next(), y su retorno será enviado como parámetro al método síncrono OnActionExecuted().

Exactamente lo mismo ocurre con OnResultExecutionAsync(); se encargará de invocar a los tradicionales métodos síncronos OnResultExecuting() y OnResultExecuted() antes y después de la ejecución del resultado, proporcionando una interfaz síncrona para aquellos escenarios donde no sea necesario la asincronía.

Pero en cualquier caso, lo interesante de esto es que tenemos un punto asíncrono de entrada a nuestros filtros que, como ya os habréis dado cuenta, son métodos virtuales, es decir, fácilmente reemplazables por código propio en nuestros filtros personalizados.

Creando filtros asíncronos personalizados

Si habéis llegado hasta este punto, probablemente la creación de un filtro asíncrono os resultará totalmente trivial. Basta con heredar de ActionFilterAttribute y sobrescribir OnActionExecutionAsync() u OnResultExecutionAsync() dependiendo de si queremos tomar el control durante la ejecución de la acción o del resultado, respectivamente.

El siguiente ejemplo muestra un sencillo profiler implementado en forma de filtro que envía de forma asíncrona los tiempos de ejecución de la acción y del resultado a un logger que podría estar almacenando esta información en disco o en una base de datos:
public class ProfilerAttribute : ActionFilterAttribute
{
    private ILogger _logger = new Logger();
    public override async Task OnActionExecutionAsync(
               ActionExecutingContext context, ActionExecutionDelegate next)
    {
        var stopWatch = Stopwatch.StartNew();
        await base.OnActionExecutionAsync(context, next);
        stopWatch.Stop();

        var message = FormatProfilerInfo("Action", context, stopWatch);
        await _logger.Log(message);
    }

    public override async Task OnResultExecutionAsync(
               ResultExecutingContext context, ResultExecutionDelegate next)
    {
        var stopWatch = Stopwatch.StartNew();
        await base.OnResultExecutionAsync(context, next);
        stopWatch.Stop();

        var message = FormatProfilerInfo("Result", context, stopWatch);
        await _logger.Log(message);
    }

    private string FormatProfilerInfo(
               string what, FilterContext context, Stopwatch stopWatch)
    {
        return string.Format("{0}.{1} >> {2} execution: {3}ms",
            context.RouteData.Values["Controller"],
            context.RouteData.Values["Action"],
            what,
            stopWatch.ElapsedMilliseconds);
    }
}
Por cierto, seguro que más de uno ha pensado que el filtro anterior usa una forma algo chunga para obtener la referencia a la clase Logger. Pues sí, pero está hecho aposta ;) La forma en que se consigue la inyección de dependencias en filtros de ASP.NET Core MVC es muy interesante, pero para no desviarnos mucho lo dejaré para un post futuro :)

Publicado en Variable not found.

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