ASP.NET Core – IStartupFilter

Buenas! Vamos a explorar en este post la interfaz IStartupFilter, por lo general un desconocido de ASP.NET Core, pero bueno… que está por ahí y no está de más conocerlo un poco. ¡Vamos allá!

Middlewares

En este punto voy a asumir que todos conocemos el concepto de middleware de asp.net core, ¿no? Si alguien no lo conoce, es de lectura obligatoria este post del maestro Jose María Aguilar.

Bueno, pues eso… ahora ya sabemos que los middlewares en ASP.NET Core son los encargados de ir procesando las peticiones. La petición viaja por todos los middlewares y cada uno de ellos puede realizar distintas tareas (entre ellas modificar la petición o el contexto http) y luego o bien pasar la petición al siguiente middleware o bien cortocircuitar y devolver una respuesta… Respuesta que viaja otra vez por la cadena de middlewares (en orden inverso) donde de nuevo puede ser procesada por cada uno de ellos hasta ser enviada al navegador.

Veamos un ejemplo de middleware, que nos permite cortocircuitar cualquier petición y mandar un 500 o bien no hacer nada. Esto nos permitiría de forma fácil poner un servicio “en modo de fallos” para probar distintas casuísticas. En github está el código entero del middleware, por si queréis echarle un ojo. Hay tres ficheros. El primero es FailingMiddleware.cs que contiene el middleware en sí. Este middleware tiene una variable (_mustFail) y si dicha variable value true, el middleware cortocircuita todas las peticiones y envía un 500:

public async Task Invoke(HttpContext context)
{
    var path = context.Request.Path;
    if (path.Equals(_options.ConfigPath, StringComparison.OrdinalIgnoreCase))
    {
        await ProcessConfigRequest(context);
        return;
    }
    if (_mustFail)
    {
        context.Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError;
        context.Response.ContentType = "text/plain";
        await context.Response.WriteAsync("Failed due to FailingMiddleware enabled.");
    }
    else
    {
        await _next.Invoke(context);
    }
}

Se puede ver que si _mustFail vale true se envía un 500 y si no, entonces se pasa la request al siguiente middleware. Por supuesto se ofrece un endpoint (en _options.ConfigPath) que permite habilitar y deshabilitar este middleware.

El fichero FailingMiddlewareAppBuilderExtensions.cs contiene los métodos de extensión que nos permiten “enchufar” este middleware de forma fácil:

public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder)
{
    return UseFailingMiddleware(builder, null);
}
public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder, Action<FailingOptions> action)
{
    var options = new FailingOptions();
    action?.Invoke(options);
    builder.UseMiddleware<FailingMiddleware>(options);
    return builder;
}

El último fichero es FailingOptions.cs que contiene la clase FailingOptions que se usa para configurar el middleware  (a través del método de extensión) de una forma como la siguiente (y indicar el endpoint para configurar el middleware):

app.UseFailingMiddleware(options =>
{
    options.ConfigPath = "/FailingEndpoint";
});

Si enchufamos este middleware antes de cualquier otro (y lo habilitamos llamando a su endpoint) cualquier otra petición devolverá siempre un 500 (hasta que lo deshabilitemos otra vez).

IStartupFilter

Vale, veamos ahora que rol juega esta interfaz. Empezaremos por su declaración:

namespace Microsoft.AspNetCore.Hosting
{
    public interface IStartupFilter
    {
        Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
    }
}

Tiene un solo método que toma un Action<IApplicationBuilder> y devuelve un Action<IApplicationBuilder>. Vale, eso no nos dice mucho, pero la idea es la siguiente: un filtro de startup es un elemento que nos permite hacer “algo” (lo que queramos) con el IApplicationBuilder incluso antes que se ejecute el código de Startup.

Solemos pensar que el punto de entrada de una aplicación ASP.NET Core, es la clase Startup. Esa percepción viene seguramente de los tiempos de OWIN, donde eso era así (el código que llamaba a Startup formaba parte del framework y no lo teníamos en nuestro proyecto) pero en ASP.NET Core eso es falso. En efecto, las aplicaciones ASP.NET Core son, en el fondo, aplicaciones de consola y si buscas en tu proyecto encontrarás el fichero Program.cs:

public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>()
            .Build();

        host.Run();
    }
}

Este es el verdadero punto de entrada de una aplicación ASP.NET Core. ¿Ves el método “UseStartup”? Pues ese es el método que cede el control a la clase Startup que tenemos en nuestro proyecto. Observa que realmente, además del pipeline de middlewares que tenemos en Startup existe “otro pipeline” construído sobre WebHostBuilder. Este otro pipeline es el que establecemos usando IStartupFilter. La diferencia es que no es un pipeline de middlewares si no un pipeline de… configuraciones 🙂

Veamos un ejemplo de ello. Para ello vamos a crear un filtro de startup:

public class FailingStartupFilter : IStartupFilter
{
    private readonly string _path;
    public FailingStartupFilter(string path)
    {
        _path = path;
    }

    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return app =>
        {
            app.UseFailingMiddleware(options =>
            {
                options.ConfigPath = _path;
            });
            next(app);
        };
    }
}

Vale, podemos ver lo simple que es. Al final simplemente debemos colocar el código en el método Configure. La clave es entender que este método Configure hace lo mismo que el método Configure de la clase Startup. En este caso, lo que hacemos es agregar el FailingMiddleware que hemos visto antes, pero podríamos agregar varios. Este es uno de los usos habituales de los filtros de startup: agregar N middlewares que van todos ellos juntos, lo que puede ser útil si desarrollas librerías.

Ahora nos falta ver como lo invocamos. Pues es muy sencillo, para ello creamos un método de extensión sobre IWebHostBuilder:

public static class WebHostBuildertExtensions
{
    public static IWebHostBuilder UseFailing(this IWebHostBuilder builder, string path)
    {
        builder.ConfigureServices(services =>
        {
            // Registrar el propio filtro
            services.AddSingleton<IStartupFilter>(new FailingStartupFilter(path));
        });
        return builder;
    }

}

Básicamente registramos el filtro como singleton, con el tipo IStartupFilter (observa que aquí podrías registrar más elementos si quisieras). Y ahora por supuesto ya puedes añadir tu filtro en Program.cs:

public static void Main(string[] args)
{
    var host = new WebHostBuilder()
        .UseKestrel()
        .UseFailing("/failing")
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseIISIntegration()
        .UseStartup<Startup>()
        .UseApplicationInsights()
        .Build();

    host.Run();
}

Observa como lo hemos añadido después del “UseKestrel”. ¿Qué conseguimos con esto? Pues instalar el FailingMiddleware delante de cualquier otro middleware que el desarrollador ponga en su Startup. Algo que en algunos escenarios puede ser muy interesante.

Si te preguntas quien es el encargado de “procesar todos los IStartupFilter” que tenemos, pues… es el método Build(). Es este método quien recoje todos los IStartupFilter y va llamando a sus métodos Configure y configura de esa manera la aplicación.

¿Cuando usar IStartupFilter?

Pues por norma general diría que no veo muchos escenarios para hacerlo, a no ser que construyas una librería que requiera realizar ciertas tareas antes del Startup (o bien después). En aplicaciones normales no veo que sea necesario bajar a este nivel, cuando la configuración tradicional mediante Startup es más que suficiente.

Espero que te haya resultado interesante 😉

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *