Compartir vía


Controladores de mensajes HTTP en ASP.NET Web API

Un controlador de mensajes es una clase que recibe una solicitud HTTP y devuelve una respuesta HTTP. Los controladores de mensajes derivan de la clase abstracta HttpMessageHandler.

Normalmente, una serie de controladores de mensajes se encadenan. El primer controlador recibe una solicitud HTTP, realiza algún procesamiento y proporciona la solicitud al siguiente controlador. En algún momento, se crea la respuesta y se realiza una copia de seguridad de la cadena. Este patrón se denomina controlador de delegación.

Diagram of message handlers chained together, illustrating process to receive an H T T P request and return an H T T P response.

Controladores de mensajes del lado servidor

En el lado servidor, la canalización de API web usa algunos controladores de mensajes integrados:

  • HttpServer obtiene la solicitud del host.
  • HttpRoutingDispatcher envía la solicitud en función de la ruta.
  • HttpControllerDispatcher envía la solicitud a un controlador de API web.

Puede agregar controladores personalizados a la canalización. Los controladores de mensajes son adecuados para problemas transversales que funcionan en el nivel de mensajes HTTP (en lugar de las acciones del controlador). Por ejemplo, un controlador de mensajes podría:

  • Leer o modificar encabezados de solicitud.
  • Agregar un encabezado de respuesta a las respuestas.
  • Validar las solicitudes antes de llegar al controlador.

En este diagrama se muestran dos controladores personalizados insertados en la canalización:

Diagram of server-side message handlers, displaying two custom handlers inserted into the Web A P I pipeline.

Nota:

En el lado cliente, HttpClient también usa controladores de mensajes. Para obtener más información, consulte controladores de mensajes HttpClient.

Controladores de mensajes personalizados

Para escribir un controlador de mensajes personalizado, derive de System.Net.Http.DelegatingHandler e invalide el método SendAsync. Este método tiene la siguiente firma:

Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken);

El método toma un HttpRequestMessage como entrada y devuelve de forma asincrónica un HttpResponseMessage. Una implementación típica hace lo siguiente:

  1. Procesar el mensaje de solicitud.
  2. Llamar a base.SendAsync para enviar la solicitud al controlador interno.
  3. El controlador interno devuelve un mensaje de respuesta. (Este paso es asincrónico).
  4. Procesar la respuesta y devolverla al autor de la llamada.

Este es un ejemplo aleatorio:

public class MessageHandler1 : DelegatingHandler
{
    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Debug.WriteLine("Process request");
        // Call the inner handler.
        var response = await base.SendAsync(request, cancellationToken);
        Debug.WriteLine("Process response");
        return response;
    }
}

Nota:

La llamada a base.SendAsync es asincrónica. Si el controlador realiza algún trabajo después de esta llamada, use la palabra clave await, como se muestra.

Un controlador de delegación también puede omitir el controlador interno y crear directamente la respuesta:

public class MessageHandler2 : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Create the response.
        var response = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("Hello!")
        };

        // Note: TaskCompletionSource creates a task that does not contain a delegate.
        var tsc = new TaskCompletionSource<HttpResponseMessage>();
        tsc.SetResult(response);   // Also sets the task state to "RanToCompletion"
        return tsc.Task;
    }
}

Si un controlador de delegación crea la respuesta sin llamar a base.SendAsync, la solicitud omite el resto de la canalización. Esto puede ser útil para un controlador que valide la solicitud (creando una respuesta de error).

Diagram of custom message handlers, illustrating process to create the response without calling base dot Send Async.

Adición de un controlador a la canalización

Para agregar un controlador de mensajes en el lado servidor, agregue el controlador a la colección HttpConfiguration.MessageHandlers. Si usó la plantilla "Aplicación web ASP.NET MVC 4" para crear el proyecto, puede hacerlo dentro de la clase WebApiConfig:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MessageHandlers.Add(new MessageHandler1());
        config.MessageHandlers.Add(new MessageHandler2());

        // Other code not shown...
    }
}

Se llama a los controladores de mensajes en el mismo orden en que aparecen en la colección MessageHandlers. Dado que están anidados, el mensaje de respuesta viaja en la otra dirección. Es decir, el último controlador es el primero para obtener el mensaje de respuesta.

Observe que no es necesario establecer los controladores internos; el marco de API web conecta automáticamente los controladores de mensajes.

Si está autohospedado, cree una instancia de la clase HttpSelfHostConfiguration y agregue los controladores a la colección MessageHandlers.

var config = new HttpSelfHostConfiguration("http://localhost");
config.MessageHandlers.Add(new MessageHandler1());
config.MessageHandlers.Add(new MessageHandler2());

Ahora echemos un vistazo a algunos ejemplos de controladores de mensajes personalizados.

Ejemplo: X-HTTP-Method-Override

X-HTTP-Method-Override es un encabezado HTTP no estándar. Está diseñado para clientes que no pueden enviar determinados tipos de solicitud HTTP, como PUT o DELETE. En su lugar, el cliente envía una solicitud POST y establece el encabezado X-HTTP-Method-Override en el método deseado. Por ejemplo:

X-HTTP-Method-Override: PUT

Este es un controlador de mensajes que agrega compatibilidad con X-HTTP-Method-Override:

public class MethodOverrideHandler : DelegatingHandler      
{
    readonly string[] _methods = { "DELETE", "HEAD", "PUT" };
    const string _header = "X-HTTP-Method-Override";

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Check for HTTP POST with the X-HTTP-Method-Override header.
        if (request.Method == HttpMethod.Post && request.Headers.Contains(_header))
        {
            // Check if the header value is in our methods list.
            var method = request.Headers.GetValues(_header).FirstOrDefault();
            if (_methods.Contains(method, StringComparer.InvariantCultureIgnoreCase))
            {
                // Change the request method.
                request.Method = new HttpMethod(method);
            }
        }
        return base.SendAsync(request, cancellationToken);
    }
}

En el método SendAsync, el controlador comprueba si el mensaje de solicitud es una solicitud POST y si contiene el encabezado X-HTTP-Method-Override. Si es así, valida el valor de encabezado y, a continuación, modifica el método de solicitud. Por último, el controlador llama a base.SendAsync para pasar el mensaje al siguiente controlador.

Cuando la solicitud alcanza la clase HttpControllerDispatcher, HttpControllerDispatcher enrutará la solicitud en función del método de solicitud actualizado.

Ejemplo: adición de un encabezado de respuesta personalizado

Este es un controlador de mensajes que agrega un encabezado personalizado a cada mensaje de respuesta:

// .Net 4.5
public class CustomHeaderHandler : DelegatingHandler
{
    async protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
        response.Headers.Add("X-Custom-Header", "This is my custom header.");
        return response;
    }
}

En primer lugar, el controlador llama a base.SendAsync para pasar la solicitud al controlador de mensajes interno. El controlador interno devuelve un mensaje de respuesta, pero lo hace de forma asincrónica mediante un objeto Task<T>. El mensaje de respuesta no está disponible hasta que base.SendAsync se complete de forma asincrónica.

En este ejemplo se usa la palabra clave await para realizar el trabajo de forma asincrónica después de que se complete SendAsync. Si tiene como destino .NET Framework 4.0, use el método Task<T>.ContinueWith:

public class CustomHeaderHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith(
            (task) =>
            {
                HttpResponseMessage response = task.Result;
                response.Headers.Add("X-Custom-Header", "This is my custom header.");
                return response;
            }
        );
    }
}

Ejemplo: comprobación de una clave de API

Algunos servicios web requieren que los clientes incluyan una clave de API en su solicitud. En el siguiente ejemplo se muestra cómo un controlador de mensajes puede comprobar las solicitudes de una clave de API válida:

public class ApiKeyHandler : DelegatingHandler
{
    public string Key { get; set; }

    public ApiKeyHandler(string key)
    {
        this.Key = key;
    }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!ValidateKey(request))
        {
            var response = new HttpResponseMessage(HttpStatusCode.Forbidden);
            var tsc = new TaskCompletionSource<HttpResponseMessage>();
            tsc.SetResult(response);    
            return tsc.Task;
        }
        return base.SendAsync(request, cancellationToken);
    }

    private bool ValidateKey(HttpRequestMessage message)
    {
        var query = message.RequestUri.ParseQueryString();
        string key = query["key"];
        return (key == Key);
    }
}

Este controlador busca la clave de API en la cadena de consulta de URI. (En este ejemplo, se supone que la clave es una cadena estática. Una implementación real probablemente usaría una validación más compleja). Si la cadena de consulta contiene la clave, el controlador pasa la solicitud al controlador interno.

Si la solicitud no tiene una clave válida, el controlador crea un mensaje de respuesta con el estado 403: Prohibido. En este caso, el controlador no llama a base.SendAsync, por lo que el controlador interno nunca recibe la petición, ni tampoco el controlador. Por lo tanto, el controlador puede suponer que todas las solicitudes entrantes tienen una clave de API válida.

Nota:

Si la clave de API solo se aplica a determinadas acciones del controlador, considere la posibilidad de usar un filtro de acciones en lugar de un controlador de mensajes. Los filtros de acción se ejecutan después de realizar el enrutamiento de URI.

Controladores de mensajes por ruta

Los controladores de la colección HttpConfiguration.MessageHandlers se aplican globalmente.

Como alternativa, puede agregar un controlador de mensajes a una ruta específica al definir la ruta:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "Route1",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Routes.MapHttpRoute(
            name: "Route2",
            routeTemplate: "api2/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional },
            constraints: null,
            handler: new MessageHandler2()  // per-route message handler
        );

        config.MessageHandlers.Add(new MessageHandler1());  // global message handler
    }
}

En este ejemplo, si el URI de solicitud coincide con "Route2", la solicitud se envía a MessageHandler2. En el siguiente diagrama se muestra la canalización para estas dos rutas:

Diagram of per route message handlers pipeline, illustrating process to add a message handler to a specific route by defining the route.

Tenga en cuenta que MessageHandler2 reemplaza el HttpControllerDispatcher predeterminado. En este ejemplo, MessageHandler2 crea la respuesta y las solicitudes que coinciden con "Route2" nunca van a un controlador. Esto le permite reemplazar todo el mecanismo del controlador de API web por su propio punto de conexión personalizado.

Como alternativa, un controlador de mensajes por ruta puede delegar en HttpControllerDispatcher, que luego despacha a un controlador.

Diagram of per route message handlers pipeline, showing process to delegate to h t t p Controller Dispatcher, which then dispatches to a controller.

En el siguiente código se muestra cómo configurar esta ruta:

// List of delegating handlers.
DelegatingHandler[] handlers = new DelegatingHandler[] {
    new MessageHandler3()
};

// Create a message handler chain with an end-point.
var routeHandlers = HttpClientFactory.CreatePipeline(
    new HttpControllerDispatcher(config), handlers);

config.Routes.MapHttpRoute(
    name: "Route2",
    routeTemplate: "api2/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional },
    constraints: null,
    handler: routeHandlers
);