Condividi tramite


Gestori di messaggi HTTP nell'API Web di ASP.NET

Un gestore di messaggi è una classe che riceve una richiesta HTTP e restituisce una risposta HTTP. I gestori di messaggi derivano dalla classe abstract HttpMessageHandler .

In genere, una serie di gestori di messaggi viene concatenata. Il primo gestore riceve una richiesta HTTP, esegue un'elaborazione e assegna la richiesta al gestore successivo. A un certo punto, la risposta viene generata e risale la catena. Questo modello è denominato gestore di delega .

Diagramma dei gestori di messaggi concatenati, che illustra il processo per ricevere una richiesta H T T P e restituire una risposta H T T P.

gestori di messaggi Server-Side

Sul lato server, la pipeline dell'API Web usa alcuni gestori di messaggi predefiniti:

  • HttpServer ottiene la richiesta dall'host.
  • HttpRoutingDispatcher invia la richiesta in base alla route.
  • HttpControllerDispatcher invia la richiesta a un controller API Web.

È possibile aggiungere gestori personalizzati alla pipeline. I gestori di messaggi sono validi per problemi trasversali che operano a livello di messaggi HTTP (anziché azioni del controller). Ad esempio, un gestore di messaggi potrebbe:

  • Leggere o modificare le intestazioni della richiesta.
  • Aggiungere un'intestazione di risposta alle risposte.
  • Convalidare le richieste prima di raggiungere il controller.

Questo diagramma mostra due gestori personalizzati inseriti nella pipeline:

Diagramma dei gestori di messaggi lato server, che visualizza due gestori personalizzati inseriti nella pipeline Web A P I.

Annotazioni

Sul lato client HttpClient usa anche gestori di messaggi. Per altre informazioni, vedere Gestori di messaggi HttpClient.

Gestori di messaggi personalizzati

Per scrivere un gestore di messaggi personalizzato, derivare da System.Net.Http.DelegatingHandler ed eseguire l'override del metodo SendAsync . Questo metodo ha la firma seguente:

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

Il metodo accetta HttpRequestMessage come input e restituisce in modo asincrono un HttpResponseMessage. Un'implementazione tipica esegue le operazioni seguenti:

  1. Elaborare il messaggio di richiesta.
  2. Chiamare base.SendAsync per inviare la richiesta al gestore interno.
  3. Il gestore interno restituisce un messaggio di risposta. Questo passaggio è asincrono.
  4. Elaborare la risposta e restituirla al chiamante.

Ecco un esempio semplice:

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

Annotazioni

La chiamata a base.SendAsync è asincrona. Se il gestore esegue operazioni dopo questa chiamata, usare la parola chiave await , come illustrato.

Un gestore di delega può anche ignorare il gestore interno e creare direttamente la risposta:

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 un gestore di delega crea la risposta senza chiamare base.SendAsync, la richiesta ignora il resto della pipeline. Può essere utile per un gestore che convalida la richiesta (creando una risposta di errore).

Diagramma dei gestori di messaggi personalizzati, che illustra il processo per creare la risposta senza chiamare il punto di base Send Async.

Aggiunta di un gestore alla pipeline

Per aggiungere un gestore di messaggi sul lato server, aggiungere il gestore all'insieme HttpConfiguration.MessageHandlers . Se è stato usato il modello "applicazione Web ASP.NET MVC 4" per creare il progetto, è possibile eseguire questa operazione all'interno della 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...
    }
}

I gestori di messaggi vengono chiamati nello stesso ordine in cui vengono visualizzati nell'insieme MessageHandlers . Poiché sono annidati, il messaggio di risposta si sposta nell'altra direzione. Ovvero, l'ultimo gestore è il primo a ottenere il messaggio di risposta.

Si noti che non è necessario impostare i gestori interni; Il framework API Web connette automaticamente i gestori di messaggi.

Se si esegue l'hosting autonomo, creare un'istanza della classe HttpSelfHostConfiguration e aggiungere i gestori alla collezione MessageHandlers.

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

Verranno ora esaminati alcuni esempi di gestori di messaggi personalizzati.

Esempio: X-HTTP-Method-Override

X-HTTP-Method-Override è un'intestazione HTTP non standard. È progettato per i client che non possono inviare determinati tipi di richiesta HTTP, ad esempio PUT o DELETE. Il client invia invece una richiesta POST e imposta l'intestazione X-HTTP-Method-Override sul metodo desiderato. Per esempio:

X-HTTP-Method-Override: PUT

Ecco un gestore di messaggi che aggiunge il supporto per 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);
    }
}

Nel metodo SendAsync il gestore controlla se il messaggio di richiesta è una richiesta POST e se contiene l'intestazione X-HTTP-Method-Override. In tal caso, convalida il valore dell'intestazione e quindi modifica il metodo di richiesta. Infine, il gestore chiama base.SendAsync per passare il messaggio al gestore successivo.

Quando la richiesta raggiunge la classe HttpControllerDispatcher , HttpControllerDispatcher instrada la richiesta in base al metodo di richiesta aggiornato.

Esempio: Aggiunta di un'intestazione di risposta personalizzata

Ecco un gestore di messaggi che aggiunge un'intestazione personalizzata a ogni messaggio di risposta:

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

In primo luogo, il gestore chiama base.SendAsync per passare la richiesta al gestore del messaggio interno. Il gestore interno restituisce un messaggio di risposta, ma lo usa in modo asincrono usando un oggetto Task<T> . Il messaggio di risposta non è disponibile fino a quando base.SendAsync non completa il processo in modalità asincrona.

In questo esempio viene usata la parola chiave await per eseguire il lavoro in modo asincrono dopo SendAsync. Se si ha come destinazione .NET Framework 4.0, usare Task<T>.ContinueWith method.

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

Esempio: verifica della presenza di una chiave API

Alcuni servizi Web richiedono ai client di includere una chiave API nella richiesta. L'esempio seguente illustra come un gestore di messaggi può controllare le richieste di una chiave API valida:

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

Questo gestore cerca la chiave API nella stringa di query URI. Per questo esempio si presuppone che la chiave sia una stringa statica. Un'implementazione reale userebbe probabilmente una convalida più complessa. Se la stringa di query contiene la chiave, il gestore passa la richiesta al gestore interno.

Se la richiesta non dispone di una chiave valida, il gestore crea un messaggio di risposta con stato 403, Accesso negato. In questo caso, il gestore non chiama base.SendAsync, quindi il gestore interno non riceve mai la richiesta, né il controller. Pertanto, il controller può presupporre che tutte le richieste in ingresso abbiano una chiave API valida.

Annotazioni

Se la chiave API si applica solo a determinate azioni del controller, è consigliabile usare un filtro azione anziché un gestore di messaggi. I filtri di azione vengono eseguiti dopo l'esecuzione del routing URI.

gestori di messaggi Per-Route

I gestori nell'insieme HttpConfiguration.MessageHandlers si applicano a livello globale.

In alternativa, è possibile aggiungere un gestore di messaggi a una route specifica quando si definisce la route:

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

In questo esempio, se l'URI della richiesta corrisponde a "Route2", la richiesta viene inviata a MessageHandler2. Il diagramma seguente illustra la pipeline per queste due route:

Diagramma della pipeline dei gestori di messaggi di route, che illustra il processo per aggiungere un gestore di messaggi a una route specifica definendo la route.

Si noti che MessageHandler2 sostituisce il valore predefinito HttpControllerDispatcher. In questo esempio, MessageHandler2 crea la risposta e le richieste che corrispondono a "Route2" non passano mai a un controller. In questo modo è possibile sostituire l'intero meccanismo di controller API Web con un endpoint personalizzato.

In alternativa, un gestore di messaggi per route può delegare a HttpControllerDispatcher, che quindi invia a un controller.

Diagramma della pipeline dei gestori di messaggi per singola route, che mostra il processo per delegare al Dispatcher del Controller HTTP, che quindi inoltra a un controller.

Il codice seguente illustra come configurare questa route:

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