Gestori di messaggi HTTP in API Web 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 concatenato. Il primo gestore riceve una richiesta HTTP, esegue un'elaborazione e assegna la richiesta al gestore successivo. A un certo punto, la risposta viene creata e torna indietro nella catena. Questo modello è denominato gestore di delega .
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 adatti per problemi trasversali che operano a livello di messaggi HTTP (anziché azioni del controller). Ad esempio, un gestore di messaggi potrebbe:
- Consente di 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:
Nota
Sul lato client, HttpClient usa anche gestori di messaggi. Per altre informazioni, vedere Gestori 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 oggetto HttpResponseMessage. Un'implementazione tipica esegue le operazioni seguenti:
- Elaborare il messaggio di richiesta.
- Chiamare
base.SendAsync
per inviare la richiesta al gestore interno. - Il gestore interno restituisce un messaggio di risposta. Questo passaggio è asincrono.
- 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;
}
}
Nota
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. Ciò può essere utile per un gestore che convalida la richiesta (creazione di una risposta di errore).
Aggiunta di un gestore alla pipeline
Per aggiungere un gestore messaggi sul lato server, aggiungere il gestore all'insieme HttpConfiguration.MessageHandlers . Se per creare il progetto è stato usato il modello "applicazione Web ASP.NET MVC 4", è 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'self-hosting, creare un'istanza della classe HttpSelfHostConfiguration e aggiungere i gestori all'insieme 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. Al contrario, il client invia una richiesta POST e imposta l'intestazione X-HTTP-Method-Override sul metodo desiderato. Ad 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;
}
}
Innanzitutto, il gestore chiama base.SendAsync
per passare la richiesta al gestore messaggi interno. Il gestore interno restituisce un messaggio di risposta, ma lo fa in modo asincrono usando un oggetto Task<T> . Il messaggio di risposta non è disponibile fino al base.SendAsync
completamento asincrono.
In questo esempio viene utilizzata la parola chiave await per eseguire il lavoro in modo asincrono dopo SendAsync
il completamento. Se si ha come destinazione .NET Framework 4.0, usare l'attività<T>. Metodo 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;
}
);
}
}
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. In questo esempio si presuppone che la chiave sia una stringa statica. Un'implementazione reale probabilmente userebbe una convalida più complessa. Se la stringa di query contiene la chiave, il gestore passa la richiesta al gestore interno.
Se la richiesta non ha 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.
Nota
Se la chiave API si applica solo a determinate azioni del controller, è consigliabile usare un filtro azione anziché un gestore di messaggi. I filtri azione vengono eseguiti dopo l'esecuzione del routing degli URI.
gestori di messaggi Per-Route
I gestori nella raccolta 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:
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.
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
);