Saltar al contenido

Tech Blog | Pasando a .NET Core: HttpModules a Middleware

Muy a menudo, necesitamos inyectar un comportamiento personalizado en la tubería de procesamiento de solicitudes. Hay varias maneras de hacer esto en un proyecto de .NET Framework, dependiendo de en qué parte de la tubería debe tener lugar su comportamiento, y cómo ese comportamiento afecta al resto de la tubería. Una de las maneras más versátiles de inyectar el comportamiento fue construyendo un HttpModule personalizado. Aunque son potentes, los HttpModules son difíciles de probar, y no se integran bien con el resto del código de su proyecto.

Con .NET Core, Microsoft introdujo una nueva forma de construir un pipeline de comportamiento-middleware.Middleware resuelve muchos de los desafíos con HttpModules, y hace que construir un pipeline de solicitudes personalizadas sea fácil.Convertir nuestro comportamiento personalizado en middleware fue bastante fácil, pero había algunas sorpresas esperándonos. En este post, convertiremos un HttpModule que hace el registro de solicitudes personalizadas a middleware personalizado, y discutiremos los beneficios y potenciales trampas que esta nueva y poderosa herramienta trae.

Tech Blog | Pasando a .NET Core: HttpModules a Middleware
Tech Blog | Pasando a .NET Core: HttpModules a Middleware

Nuestro punto de partida

En primer lugar, empecemos por echar un vistazo a esta muestra de HttpModule. Este módulo escucha todas las solicitudes y respuestas, y registra la IP de llamada, la ruta de solicitud, el código de estado de la respuesta y la longitud de la respuesta en bytes.

publicclassWebRequestLoggerModule:IHttpModule{privatereadonlyILoglogger=LogManager.GetLogger("WebRequest");publicvoidInit(HttpApplicationcontext){contexto. BeginRequest+=AddContentLengthFilter;context.EndRequest+=LogResponse;}privadovoidAddContentLengthFilter(objectsender,EventArgse){varapplication=(HttpApplication)sender;application.Response. Filter=newContentLengthCountingStream(application.Response.Filter);}privado EvitarLogResponse(objectender,EventArgseventArgs){varapplication=(HttpApplication)sender;varrequest=application.Request;varresponse=application.Response;varresponseStream=response.FilterasContentLengthCountingStream;logger.Info($"Calling IP: {request.UserHostAddress} Camino: {request.Url.PathAndQuery} Código de estado: {Respuesta. Código de estado} Longitud: { respuestaStream.Length }");}publicvoidDispose(){}}

En este código se ha omitido el ContentLengthCountingStream. Resulta que la única manera de obtener la longitud de una respuesta (tanto en WebAPI como en .NET Core) es anular el Stream y contar el número de bytes que se escriben en el búfer. También merece la pena señalar que UserHostAddress probablemente no será la dirección IP de su usuario real, especialmente si está ejecutando un proxy inverso delante de Kestrel. Este es un código de muestra de Internet, ¡por favor no lo utilice en producción!

Hay un par de deficiencias obvias a tener en cuenta cuando miramos la forma en que los módulos Http interactúan con el conducto de solicitudes. La primera y más evidente es la falta de una mecanografía decente. BeginRequest y EndRequest son ambos manejadores de eventos, lo que significa que obtenemos la terrible firma de (objeto, EventArgs). Ese pequeño regalo de C# 1. Además del uso del manejador de eventos, tanto BeginRequest como EndRequest son eventos, lo que significa que no tenemos ninguna manera de definir el orden en que se manejan los eventos.

Fundamentos del middleware

El middleware es el bloque básico de la tubería de solicitud del núcleo de .NET. En su forma más básica, el middleware es sólo una serie de delegados anidados que terminan llamando a su controlador. Un beneficio obvio que esto proporciona sobre el viejo estilo de HttpModule de la tubería de extensión es un grado mucho mayor de control. En lugar de sólo conectar los manejadores de eventos, puede controlar el orden exacto en que el middleware se ejecuta, y puede fácilmente cortocircuitar la ejecución basada en el estado de la solicitud. Hay algunas limitaciones a este enfoque, que discutiremos más adelante, pero en general he estado muy contento con el poder y el control que ofrece el middleware.

El primer ejemplo de middleware mostrado en la documentación de Microsoft (que por lo demás es muy bueno) se ve así:

publicclassStartup{publicvoidConfigure(IApplicationBuilderapp){app.Use(async(context,next)={/// Haga un trabajo que no escriba en el Response.awaitnext. Invoke();// Haga el registro u otro trabajo que no escriba en el Response.});app.Run(asynccontext==;{awaitcontext.Response.WriteAsync("Hola del 2º delegado");};}}

El lambda definido en app.Use es el middleware más simple posible, porque en realidad no hace nada .context es un HttpContext, y contiene información sobre la solicitud y la respuesta, no muy diferente de la HttpApplication en nuestro HttpModule.next contiene un delegado que o bien apunta al siguiente middleware (si existe), o bien al controlador.

Lo que no te dicen (pero deberían) es que aunque es posible escribir un middleware como este, es una idea terrible .Hay tres problemas principales con el uso de lambdas en línea para escribir tu middleware. En primer lugar, esto hace que el middleware sea básicamente imposible de probar, ya que la única manera de ejecutarlo es ejecutar realmente una solicitud a través de todo el pipeline.En segundo lugar, añade lógica a su inicio, que ya es demasiado largo y complicado, por lo que añadir más configuración de la que estrictamente necesita es una mala idea.Por último, impide que se aproveche su contenedor de IO para inyectar dependencias.

Convirtiendo nuestro HttpModule a Middleware

Afortunadamente, hay una forma diferente de construir un middleware que resuelve todos estos problemas! Usémoslo para convertir nuestro HttpModule en un middleware que realice la misma función. Primero, haremos una clase de middleware:

clase públicaWebLoggingMiddleware{privatereadonlyRequestDelegatenext;privatereadonlyILoglog;publicWebLoggingMiddleware(RequestDelegatenext,ILoglog){this.next=next;this. log=log;}publicasyncTaskInvokeAsync(HttpContextcontext){varwrappedContentStream=newContentLengthCountingStream(context.Response.Body);context.Response.Body=wrappedContentStream;awaitnext(context);log.Info($"Calling IP: {context.Connection.RemoteIpAddress} Ruta: {contexto.Solicitud.Ruta} Código de estado: {contexto.Respuesta.Código.de.Estado} Largo: { wrappedContentStream.Length }");}}

Primero, notarán que estamos inyectando tanto un RequestDelegate como un ILog en el constructor. Esto nos da la capacidad de probar nuestro middleware de forma aislada, usando dobles de prueba para simular el comportamiento de nuestras dependencias. Es importante señalar, sin embargo, que el middleware se construye una vez durante el inicio de la aplicación, por lo que no se puede poner ninguna dependencia que esté vinculada a la petición en el constructor.Afortunadamente, el método InvokeAsync también se invoca utilizando el contenedor IoC, por lo que si tienes alguna dependencia de alcance, puedes añadirla como parámetro a la firma del método y las cosas saldrán bien.

A continuación, mirando a InvokeAsync, notarás que cambiamos la respuesta antes de llamando a await next(context);.Si quisiéramos añadir cabeceras personalizadas a la respuesta, este es un gran lugar para hacerlo. Sin embargo, necesitas hacer esos cambios antes de llamar a next(context), ya que para el momento en que esa llamada devuelve la petición ya ha sido serializada, por lo que la respuesta es de sólo lectura en ese punto.

Por último, una feliz consecuencia de tener el control total de la tubería de solicitud. Todavía tenemos que envolver el cuerpo con un ContentLengthCountingStream (la aplicación es la misma que con el .NET framework) para obtener la longitud de la respuesta en el .NET core.Por otra parte, porque lo añadimos a la respuesta en el mismo método en el que leemos el valor de la misma, podemos evitar el moldeado del cuerpo de la respuesta como un ContentLengthCountingStream.Aunque hay que reconocer que no es la consecuencia más importante de tener el control total, sigue siendo un beneficio agradable.

Ahora que hemos construido y probado nuestro middleware, es hora de configurarlo. Añadiendo una aplicación UseMiddleware() a nuestro método de configuración en el inicio, podemos añadir el middleware tecleado a nuestro pipeline.

Una de las cosas que realmente aprecio del uso del middleware es que el orden de las operaciones del middleware es explícito y fácil de configurar. Cada llamada a app.UseMiddleware se ejecuta en orden, por lo que si alguno de tus middleware necesita hacer un cortocircuito en el pipeline de ejecución, es fácil entender lo que se salta. Como estamos haciendo el registro, y queremos asegurarnos de que incluso las solicitudes que generan excepciones se registran, queremos asegurarnos de que WebLoggingMiddleware es el primer middleware que añadimos a nuestra aplicación. Además del registro, probablemente querrá que el manejo de excepciones sea su primer middleware, porque quiere asegurarse de que cualquier excepción lanzada por su middleware se maneje correctamente.

Middleware: La mejor manera de construir tuberías de solicitud

El middleware puede ser mi cambio favorito al pasar a .NET Core.De repente, la parte más esotérica e incuestionable de la construcción de tuberías de solicitud en ASP.NET se transforma en un código simple y ordinario. Es especialmente liberador poder tomar código que estaba tan fuertemente atado al framework, y dejar que se mantenga por sus propios méritos. Me recuerda la transición de los ORMs altamente invasivos y de las clases base para datos a las micro-ORMs y POCOs (Plain Old Class Objects).en este caso, al menos, el futuro es grande !

Esta es la tercera parte de una serie en curso sobre nuestra transición al .NET Core. Para más información, ver parte 1 y parte 2

Categorías: technicalTags: c-sharp, dotnet-core