Manipuladores de mensagens HTTP no ASP.NET Web API

por Mike Wasson

Um manipulador de mensagens é uma classe que recebe uma solicitação HTTP e retorna uma resposta http. Os manipuladores de mensagens derivam da classe abstrata HttpMessageHandler .

Normalmente, uma série de manipuladores de mensagens é encadeada. O primeiro manipulador recebe uma solicitação HTTP, executa algum processamento e fornece a solicitação para o próximo manipulador. Em algum momento, a resposta é criada e retorna o backup da cadeia. Esse padrão é chamado de manipulador de delegação .

Manipuladores de mensagens do lado do servidor

No lado do servidor, o pipeline da API Web usa alguns manipuladores de mensagens internos:

  • HttpServer Obtém a solicitação do host.
  • HttpRoutingDispatcher despacha a solicitação com base na rota.
  • HttpControllerDispatcher envia a solicitação para um controlador de API da Web.

Você pode adicionar manipuladores personalizados ao pipeline. Os manipuladores de mensagens são bons para preocupações abrangentes que operam no nível de mensagens HTTP (em vez de ações do controlador). Por exemplo, um manipulador de mensagens pode:

  • Ler ou modificar cabeçalhos de solicitação.
  • Adicione um cabeçalho de resposta às respostas.
  • Valide as solicitações antes que elas atinjam o controlador.

Este diagrama mostra dois manipuladores personalizados inseridos no pipeline:

Note

No lado do cliente, o HttpClient também usa manipuladores de mensagens. Para obter mais informações, consulte manipuladores de mensagens HttpClient.

Manipuladores de mensagens personalizadas

Para gravar um manipulador de mensagens personalizado, derive de System .net. http. DelegatingHandler e substitua o método SendAsync . Esse método tem a seguinte assinatura:

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

O método usa um HttpRequestMessage como entrada e retorna de forma assíncrona um HttpResponseMessage. Uma implementação típica faz o seguinte:

  1. Processar a mensagem de solicitação.
  2. Chame base.SendAsync para enviar a solicitação para o manipulador interno.
  3. O manipulador interno retorna uma mensagem de resposta. (Essa etapa é assíncrona.)
  4. Processar a resposta e retorná-la ao chamador.

Aqui está um exemplo trivial:

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;
    }
}

Note

A chamada a base.SendAsync é assíncrona. Se o manipulador funcionar após essa chamada, use a palavra-chave Await , conforme mostrado.

Um manipulador de delegação também pode ignorar o manipulador interno e criar a resposta diretamente:

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;
    }
}

Se um manipulador de delegação criar a resposta sem chamar base.SendAsync, a solicitação ignorará o restante do pipeline. Isso pode ser útil para um manipulador que valida a solicitação (criando uma resposta de erro).

Adicionando um manipulador ao pipeline

Para adicionar um manipulador de mensagens no lado do servidor, adicione o manipulador à coleção HttpConfiguration. MessageHandlers . Se você usou o modelo "aplicativo Web ASP.NET MVC 4" para criar o projeto, poderá fazer isso dentro da classe 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...
    }
}

Os manipuladores de mensagens são chamados na mesma ordem em que aparecem na coleção MessageHandlers . Como eles são aninhados, a mensagem de resposta viaja na outra direção. Ou seja, o último manipulador é o primeiro a obter a mensagem de resposta.

Observe que você não precisa definir os manipuladores internos; a estrutura da API Web conecta automaticamente os manipuladores de mensagens.

Se você estiver hospedando internamente, crie uma instância da classe HttpSelfHostConfiguration e adicione os manipuladores à coleção MessageHandlers .

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

Agora, vejamos alguns exemplos de manipuladores de mensagens personalizadas.

Exemplo: X-HTTP-Method-override

X-HTTP-Method-override é um cabeçalho HTTP não padrão. Ele foi projetado para clientes que não podem enviar determinados tipos de solicitação HTTP, como PUT ou DELETE. Em vez disso, o cliente envia uma solicitação POST e define o cabeçalho X-HTTP-Method-override para o método desejado. Por exemplo:

X-HTTP-Method-Override: PUT

Aqui está um manipulador de mensagens que adiciona suporte para 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);
    }
}

No método SendAsync , o manipulador verifica se a mensagem de solicitação é uma solicitação post e se contém o cabeçalho X-http-Method-override. Nesse caso, ele valida o valor do cabeçalho e, em seguida, modifica o método de solicitação. Por fim, o manipulador chama base.SendAsync para passar a mensagem para o próximo manipulador.

Quando a solicitação atingir a classe HttpControllerDispatcher , HttpControllerDispatcher roteará a solicitação com base no método de solicitação atualizado.

Exemplo: adicionando um cabeçalho de resposta personalizado

Aqui está um manipulador de mensagens que adiciona um cabeçalho personalizado a cada mensagem de resposta:

// .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;
    }
}

Primeiro, o manipulador chama base.SendAsync para passar a solicitação para o manipulador de mensagens internas. O manipulador interno retorna uma mensagem de resposta, mas faz isso de forma assíncrona usando um objeto Task<t> . A mensagem de resposta não estará disponível até que base.SendAsync seja concluída de forma assíncrona.

Este exemplo usa a palavra-chave Await para executar o trabalho de forma assíncrona após a conclusão da SendAsync. Se você estiver direcionando .NET Framework 4,0, use a tarefa<t> . Método 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;
            }
        );
    }
}

Exemplo: verificando uma chave de API

Alguns serviços Web exigem que os clientes incluam uma chave de API em sua solicitação. O exemplo a seguir mostra como um manipulador de mensagens pode verificar solicitações para uma chave 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);
    }
}

Esse manipulador procura a chave de API na cadeia de caracteres de consulta do URI. (Para este exemplo, presumimos que a chave é uma cadeia de caracteres estática. Uma implementação real provavelmente usaria uma validação mais complexa.) Se a cadeia de caracteres de consulta contiver a chave, o manipulador passará a solicitação para o manipulador interno.

Se a solicitação não tiver uma chave válida, o manipulador criará uma mensagem de resposta com o status 403, proibido. Nesse caso, o manipulador não chama base.SendAsync, portanto, o manipulador interno nunca recebe a solicitação, nem o controlador. Portanto, o controlador pode assumir que todas as solicitações de entrada têm uma chave de API válida.

Note

Se a chave de API se aplicar somente a determinadas ações do controlador, considere usar um filtro de ação em vez de um manipulador de mensagens. Filtros de ação são executados após o roteamento do URI ser executado.

Manipuladores de mensagens por rota

Os manipuladores na coleção HttpConfiguration. MessageHandlers se aplicam globalmente.

Como alternativa, você pode adicionar um manipulador de mensagens a uma rota específica ao definir a rota:

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
    }
}

Neste exemplo, se o URI de solicitação corresponder a "Route2", a solicitação será expedida para MessageHandler2. O diagrama a seguir mostra o pipeline para essas duas rotas:

Observe que MessageHandler2 substitui o HttpControllerDispatcherpadrão. Neste exemplo, MessageHandler2 cria a resposta e as solicitações que correspondem a "Route2" nunca vão para um controlador. Isso permite que você substitua todo o mecanismo do controlador da API Web pelo seu próprio ponto de extremidade personalizado.

Como alternativa, um manipulador de mensagens por rota pode delegar para HttpControllerDispatcher, que, em seguida, é expedido para um controlador.

O código a seguir mostra como configurar essa rota:

// 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
);