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 .

Diagramme de gestionnaires de messages chaînés, illustrant le processus de réception d’une requête H T T P et de retour d’une réponse H TT P.

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 :

Diagramme des gestionnaires de messages côté serveur, affichant deux gestionnaires personnalisés insérés dans le pipeline Web A P I.

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 :

  1. Traitez le message de demande.
  2. Appelez base.SendAsync pour envoyer la demande au gestionnaire interne.
  3. Le gestionnaire interne retourne un message de réponse. (Cette étape est asynchrone.)
  4. 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).

Diagramme de gestionnaires de messages personnalisés, illustrant le processus de création de la réponse sans appeler point de base Send Async.

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.SendAsyncpas , 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 :

Diagramme du pipeline des gestionnaires de messages par route, illustrant le processus d’ajout d’un gestionnaire de messages à un itinéraire spécifique en définissant l’itinéraire.

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.

Diagramme du pipeline des gestionnaires de messages par itinéraire, montrant le processus à déléguer à h t t p Controller Dispatcher, 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
);