Gestionnaires de messages HTTP dans API Web ASP.NET
Un gestionnaire de messages est une classe qui reçoit une requête HTTP et retourne une réponse HTTP. Les gestionnaires de messages dérivent de la classe HttpMessageHandler abstraite.
En règle générale, une série de gestionnaires de messages sont chaînés. Le premier gestionnaire reçoit une requête HTTP, effectue un traitement et transmet la demande au gestionnaire suivant. À un moment donné, la réponse est créée et remonte la chaîne. Ce modèle est appelé gestionnaire de délégation .
gestionnaires de messages Server-Side
Côté serveur, le pipeline d’API web utilise certains gestionnaires de messages intégrés :
- HttpServer obtient la requête de l’hôte.
- HttpRoutingDispatcher répartit la requête en fonction de l’itinéraire.
- HttpControllerDispatcher envoie la demande à un contrôleur d’API web.
Vous pouvez ajouter des gestionnaires personnalisés au pipeline. Les gestionnaires de messages sont adaptés aux problèmes transversaux qui fonctionnent au niveau des messages HTTP (plutôt que des actions du contrôleur). Par exemple, un gestionnaire de messages peut :
- Lire ou modifier des en-têtes de requête.
- Ajoutez un en-tête de réponse aux réponses.
- Validez les demandes avant qu’elles n’atteignent le contrôleur.
Ce diagramme montre deux gestionnaires personnalisés insérés dans le pipeline :
Notes
Côté client, HttpClient utilise également des gestionnaires de messages. Pour plus d’informations, consultez Gestionnaires de messages HttpClient.
Gestionnaires de messages personnalisés
Pour écrire un gestionnaire de messages personnalisé, dérivez de System.Net.Http.DelegatingHandler et remplacez la méthode SendAsync . Cette méthode a la signature suivante :
Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken);
La méthode prend un HttpRequestMessage en tant qu’entrée et retourne de manière asynchrone un HttpResponseMessage. Une implémentation classique effectue les opérations suivantes :
- Traitez le message de demande.
- Appelez
base.SendAsync
pour envoyer la demande au gestionnaire interne. - Le gestionnaire interne retourne un message de réponse. (Cette étape est asynchrone.)
- Traitez la réponse et retournez-la à l’appelant.
Voici un exemple 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;
}
}
Notes
L’appel à base.SendAsync
est asynchrone. Si le gestionnaire effectue un travail après cet appel, utilisez le mot clé await, comme indiqué.
Un gestionnaire de délégation peut également ignorer le gestionnaire interne et créer directement la réponse :
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 gestionnaire de délégation crée la réponse sans appeler base.SendAsync
, la requête ignore le reste du pipeline. Cela peut être utile pour un gestionnaire qui valide la demande (création d’une réponse d’erreur).
Ajout d’un gestionnaire au pipeline
Pour ajouter un gestionnaire de messages côté serveur, ajoutez le gestionnaire à la collection HttpConfiguration.MessageHandlers . Si vous avez utilisé le modèle « ASP.NET application web MVC 4 » pour créer le projet, vous pouvez le faire à l’intérieur de la 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...
}
}
Les gestionnaires de messages sont appelés dans le même ordre qu’ils apparaissent dans la collection MessageHandlers . Étant donné qu’ils sont imbriqués, le message de réponse se déplace dans l’autre sens. Autrement dit, le dernier gestionnaire est le premier à obtenir le message de réponse.
Notez que vous n’avez pas besoin de définir les gestionnaires internes ; l’infrastructure d’API web connecte automatiquement les gestionnaires de messages.
Si vous êtes auto-hébergé, créez un instance de la classe HttpSelfHostConfiguration et ajoutez les gestionnaires à la collection MessageHandlers.
var config = new HttpSelfHostConfiguration("http://localhost");
config.MessageHandlers.Add(new MessageHandler1());
config.MessageHandlers.Add(new MessageHandler2());
Examinons maintenant quelques exemples de gestionnaires de messages personnalisés.
Exemple : X-HTTP-Method-Override
X-HTTP-Method-Override est un en-tête HTTP non standard. Il est conçu pour les clients qui ne peuvent pas envoyer certains types de requêtes HTTP, tels que PUT ou DELETE. Au lieu de cela, le client envoie une requête POST et définit l’en-tête X-HTTP-Method-Override sur la méthode souhaitée. Par exemple :
X-HTTP-Method-Override: PUT
Voici un gestionnaire de messages qui ajoute la prise en charge de 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);
}
}
Dans la méthode SendAsync , le gestionnaire vérifie si le message de demande est une requête POST et s’il contient l’en-tête X-HTTP-Method-Override. Si c’est le cas, il valide la valeur d’en-tête, puis modifie la méthode de requête. Enfin, le gestionnaire appelle base.SendAsync
pour passer le message au gestionnaire suivant.
Lorsque la requête atteint la classe HttpControllerDispatcher , HttpControllerDispatcher route la requête en fonction de la méthode de requête mise à jour.
Exemple : ajout d’un en-tête de réponse personnalisé
Voici un gestionnaire de messages qui ajoute un en-tête personnalisé à chaque message de réponse :
// .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;
}
}
Tout d’abord, le gestionnaire appelle base.SendAsync
pour passer la demande au gestionnaire de messages interne. Le gestionnaire interne retourne un message de réponse, mais il le fait de manière asynchrone à l’aide d’un objet Task<T> . Le message de réponse n’est pas disponible tant qu’il n’est base.SendAsync
pas terminé de manière asynchrone.
Cet exemple utilise le mot clé await pour effectuer le travail de manière asynchrone une fois SendAsync
l’opération terminée. Si vous ciblez .NET Framework 4.0, utilisez la tâche<T>. Méthode 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;
}
);
}
}
Exemple : Recherche d’une clé API
Certains services web exigent que les clients incluent une clé API dans leur demande. L’exemple suivant montre comment un gestionnaire de messages peut case activée demandes pour une clé API valide :
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);
}
}
Ce gestionnaire recherche la clé API dans la chaîne de requête URI. (Pour cet exemple, nous partons du principe que la clé est une chaîne statique. Une implémentation réelle utiliserait probablement une validation plus complexe.) Si la chaîne de requête contient la clé, le gestionnaire transmet la demande au gestionnaire interne.
Si la requête n’a pas de clé valide, le gestionnaire crée un message de réponse avec status 403, Interdit. Dans ce cas, le gestionnaire n’appelle base.SendAsync
pas , de sorte que le gestionnaire interne ne reçoit jamais la requête, pas plus que le contrôleur. Par conséquent, le contrôleur peut supposer que toutes les requêtes entrantes ont une clé API valide.
Notes
Si la clé API s’applique uniquement à certaines actions du contrôleur, envisagez d’utiliser un filtre d’action au lieu d’un gestionnaire de messages. Les filtres d’action s’exécutent après l’exécution du routage d’URI.
gestionnaires de messages Per-Route
Les gestionnaires de la collection HttpConfiguration.MessageHandlers s’appliquent globalement.
Vous pouvez également ajouter un gestionnaire de messages à un itinéraire spécifique lorsque vous définissez l’itinéraire :
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
}
}
Dans cet exemple, si l’URI de requête correspond à « Route2 », la requête est envoyée à MessageHandler2
. Le diagramme suivant montre le pipeline pour ces deux itinéraires :
Notez que MessageHandler2
remplace le HttpControllerDispatcher par défaut. Dans cet exemple, MessageHandler2
crée la réponse et les demandes qui correspondent à « Route2 » ne sont jamais envoyées à un contrôleur. Cela vous permet de remplacer l’ensemble du mécanisme du contrôleur d’API web par votre propre point de terminaison personnalisé.
Un gestionnaire de messages par route peut également déléguer à HttpControllerDispatcher, qui est ensuite distribué à un contrôleur.
Le code suivant montre comment configurer cet itinéraire :
// 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
);