REST and POX
Cet exemple montre comment utiliser le transport HTTP dans indigo1 afin d'envoyer et de recevoir des messages POX (Plain Old XML), c'est-à-dire des messages qui se composent uniquement de charges utiles XML sans aucune enveloppe SOAP englobante. Les messages POX peuvent être envoyés et reçus par de nombreux types de clients, y compris les navigateurs Web qui n'ont pas de prise en charge native pour les protocoles SOAP. POX est un choix approprié pour les services qui échangent des données sur HTTP et n'ont aucune spécification pour utiliser les fonctionnalités de protocole avancées de SOAP et WS-* comme les transports non HTTP, les modèles d'échange de messages autre que demande/réponse, ainsi que les transactions, la fiabilité et la sécurité basées sur les messages.
Remarque : |
---|
La prise en charge des services de style REST et POX est désormais directement assurée par le .NET Framework version 3.5. Pour plus d'informations, consultez la rubrique Web Programming Model dans la documentation relative au .NET Framework 3.5. |
Implémentation d'un service POX
Le service dans cet exemple implémente une base de données client élémentaire. Du point de vue du contrat, il expose une opération appelée ProcessMessage
qui prend Message
comme entrée et retourne Message
.
[ServiceContract]
public interface IUniversalContract
{
[OperationContract(Action = "*", ReplyAction = "*")]
Message ProcessMessage(Message input);
}
Toutefois, ce contrat n'est pas très fonctionnel. Nous souhaitons implémenter une convention d'adressage de base permettant d'accéder au contenu de cette collection de manière à pouvoir utiliser HTP :
- La collection se trouve à l'adresse https://localhost:8100/customers. Les demandes HTTP GET envoyées à cet URI retournent le contenu de la collection sous forme de liste d'URI qui pointent vers des entrées individuelles.
- Chaque entrée de la collection possède un URI unique formé en ajoutant l'ID de client à l'URI de collection. Par exemple, https://localhost:8100/customers/1 identifie le client avec ID 1.
- Grâce à un URI d'entrée, nous pouvons récupérer une représentation XML du client en émettant une demande HTTP GET à l'URI d'entrée.
- Nous pouvons modifier une entrée en utilisant PUT pour appliquer une nouvelle représentation à un URI d'entrée existant.
- L'ajout d'une entrée s'effectue en envoyant le contenu de la nouvelle entrée à l'URI de collection à l'aide de HTTP POST. L'URI de la nouvelle entrée est retourné par l'en-tête HTTP Location dans la réponse du serveur.
- Nous pouvons supprimer une entrée en envoyant une demande DELETE à l'URI de l'entrée.
Ce style architectural est appelé REST (Representational State Transfer) et constitue l'une des méthodes de conception des applications qui communiquent à l'aide des messages HTTP et POX.
Pour ce faire, nous devons d'abord créer un service qui implémente le contrat à exposer.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
AddressFilterMode = AddressFilterMode.Prefix)]
class CustomerService : IUniversalContract
{
Dictionary<string, Customer> customerList;
public Message ProcessMessage(Message request) { ... }
}
La variable locale customerList
stocke le contenu de la base de données. Nous souhaitons nous assurer que son contenu est conservé d'une demande à l'autre, et nous utilisons donc l'attribut ServiceBehavior pour spécifier InstanceContextMode.Single, qui indique à WCF d'utiliser la même instance de service physique d'une demande à l'autre. Nous définissons également AddressFilterMode.Prefix, qui prend en charge la structure d'adressage hiérarchique que nous souhaitons que notre service implémente. AddressFilterMode.Prefix oblige notre service à écouter sur tous les URI qui commencent par son adresse de point de terminaison, et non pas uniquement sur ceux qui correspondent exactement à l'adresse.
Le service est configuré comme suit.
<system.serviceModel>
<bindings>
<customBinding>
<binding name="poxBinding">
<textMessageEncoding messageVersion="None" />
<httpTransport />
</binding>
</customBinding>
</bindings>
<services>
<service name="Microsoft.ServiceModel.Samples.CustomerService">
<host>
<baseAddresses>
<add baseAddress="https://localhost:8100/" />
</baseAddresses>
</host>
<endpoint address="customers"
binding="customBinding"
bindingConfiguration="poxBinding"
contract="Microsoft.ServiceModel.Samples.IUniversalContract" />
</service>
</services>
</system.serviceModel>
Cette configuration installe un service unique (à l'adresse https://localhost:8100) avec un point de terminaison unique (à l'adresse https://localhost:8100/customers). Ce point de terminaison communique à l'aide d'une liaison personnalisée qui possède le transport HTTP et le codeur de texte. Ce codeur est configuré pour utiliser MessageVersion.None, qui permet au codeur d'accepter des messages qui n'ont pas d'enveloppe SOAP en lecture et l'oblige à supprimer l'enveloppe SOAP lorsqu'il écrit des messages de réponse. À des fins de simplicité et de clarté, cet exemple présente la communication sur un transport non sécurisé. Si la sécurité est requise, les applications POX doivent utiliser une liaison qui incorpore la sécurité de transport HTTP (HTTPS).
Implémentation de ProcessMessage()
Nous souhaitons que l'implémentation de ProcessMessage()
prenne des mesures différentes selon la méthode HTTP présente dans la requête entrante. Pour ce faire, nous devons accéder à des informations de protocole HTTP qui ne sont pas directement exposées sur Message. Toutefois, nous pouvons accéder à la méthode HTTP (et aux autres éléments de protocole utiles, tels que la collection Headers de la demande) via la classe HttpRequestMessageProperty.
public Message ProcessMessage(Message request)
{
Message response = null;
//The HTTP Method (for example, GET) from the incoming HTTP request
//can be found on the HttpRequestMessageProperty. The MessageProperty
//is added by the HTTP Transport when the message is received.
HttpRequestMessageProperty requestProperties =
(HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
…
}
Une fois que nous disposons de HttpRequestMessageProperty, nous pouvons l'utiliser pour distribuer aux différentes méthodes d'implémentation internes.
if (String.Equals("GET", requestProperties.Method,
StringComparison.OrdinalIgnoreCase))
{
response = GetCustomer(request);
}
else if (String.Equals("PUT", requestProperties.Method,
StringComparison.OrdinalIgnoreCase))
{
response = UpdateCustomer(request);
}
else if (String.Equals("POST", requestProperties.Method,
StringComparison.OrdinalIgnoreCase))
{
response = AddCustomer(request);
}
else if (String.Equals("DELETE", requestProperties.Method,
StringComparison.OrdinalIgnoreCase))
{
response = DeleteCustomer(request);
}
Même si GET et POST sont les méthodes HTTP les plus courantes (PUT et DELETE l'étant dans une moindre mesure), la spécification HTTP définit plusieurs autres verbes tels que HEAD et OPTIONS que nous ne prévoyons pas de prendre en charge dans notre exemple de service. Bien heureusement, la spécification HTTP définit également un code d'état (405 Méthode non autorisée) à cette fin spécifique. Par conséquent, nous disposons de la logique suivante dans le ProcessMessage
de notre service.
else
{
//This service does not implement handlers for other HTTP verbs (such as HEAD), so we
//construct a response message and use the HttpResponseMessageProperty to
//set the HTTP status code to 405 (Method Not Allowed) which indicates the client
//used an HTTP verb not supported by the server.
response = Message.CreateMessage(MessageVersion.None, String.Empty, String.Empty);
HttpResponseMessageProperty responseProperty = new HttpResponseMessageProperty();
responseProperty.StatusCode = HttpStatusCode.MethodNotAllowed;
response.Properties.Add( HttpResponseMessageProperty.Name, responseProperty );
}
HttpResponseMessageProperty est très similaire à HttpRequestMessageProperty excepté qu'il comporte des propriétés spécifiques à la réponse telles que le code d'état et la description d'état. Les messages ne disposant pas de cette propriété par défaut lors de leur création, celle-ci doit donc être ajoutée explicitement à la collection Properties avant de retourner le message de réponse.
Les autres méthodes d'implémentation de service (GetCustomer, AddCustomer, etc.) utilisent les propriétés de message HttpRequest/HttpResponse de la même manière et sont simples.
Implémentation d'un client POX
L'architecture REST étant utilisée dans notre exemple de service, le client utilisé est donc un client HTTP de base. Il est implémenté en haut de la pile de transport HTTP WCF à des fins d'illustration, mais peut l'être avec HttpWebRequest (si WCF n'était pas disponible sur le client, par exemple) ou même avec la prise en charge XmlHttpRequest offerte par la plupart des navigateurs Web actuels.
L'envoi de demandes HTTP brutes à l'aide de WCF implique la création de messages, la définition des valeurs appropriées sur HttpRequestMessageProperty, et le remplissage éventuel du corps d'entité avec des données à envoyer au serveur. Pour simplifier ce processus, nous pouvons écrire une classe de client HTTP de base.
public class HttpClient : ClientBase<IRequestChannel>
{
public HttpClient( Uri baseUri, bool keepAliveEnabled ) :
this( baseUri.ToString(), keepAliveEnabled )
{
}
public HttpClient( string baseUri, bool keepAliveEnabled ) :
base( HttpClient.CreatePoxBinding( keepAliveEnabled ),
new EndpointAddress( baseUri ) )
{
}
//Other members elided for clarity
}
À l'instar des classes de client générées à partir des métadonnées WSDL par ServiceModel Metadata Utility Tool (Svcutil.exe), notre classe HttpClient
hérite de ClientBase<TChannel>. Même si notre client est implémenté dans le cadre d'un contrat très général (IRequestChannel), ClientBase`1<TChannel> fournit toujours un grand nombre de fonctionnalités utiles. Par exemple, il crée automatiquement un ChannelFactory<IRequestChannel> et gère automatiquement sa durée de vie.
À l'instar de la liaison utilisée sur le service, notre client HTTP communique à l'aide d'une liaison personnalisée.
private static Binding CreatePoxBinding( bool keepAliveEnabled )
{
TextMessageEncodingBindingElement encoder =
new TextMessageEncodingBindingElement( MessageVersion.None, Encoding.UTF8 );
HttpTransportBindingElement transport = new HttpTransportBindingElement();
transport.ManualAddressing = true;
transport.KeepAliveEnabled = keepAliveEnabled;
return new CustomBinding( new BindingElement[] { encoder, transport } );
}
L'ajout majeur à la liaison de client est l'étape supplémentaire d'affectation de true à ManualAddressing sur HttpTransportBindingElement. Généralement, lorsque ManualAddressing a la valeur false, les messages envoyés via le transport sont adressés à l'URI fourni lors de la création du transport ChannelFactory. L'adressage manuel permet aux messages individuels envoyés via le transport de disposer d'URI différents d'une demande à l'autre tant que ces URI ont le même préfixe que l'URI de ChannelFactory. Les URI auxquels nous souhaitons envoyer des messages au niveau de la couche d'application sont sélectionnés, l'affectation de true à ManualAddressing s'avérant ainsi une option appropriée pour l'utilisation prévue.
La méthode la plus importante sur HttpClient est Request, qui envoie un message à un URI spécifique et retourne la réponse du serveur. Pour les méthodes HTTP telles que GET et DELETE qui ne permettent pas au message HTTP de contenir des données, nous définissons une surcharge de Request() qui affecte true à SuppressEntityBody.
public Message Request( Uri requestUri, string httpMethod )
{
Message request = Message.CreateMessage( MessageVersion.None, String.Empty );
request.Headers.To = requestUri;
HttpRequestMessageProperty property = new HttpRequestMessageProperty();
property.Method = httpMethod;
property.SuppressEntityBody = true;
request.Properties.Add( HttpRequestMessageProperty.Name, property );
return this.Channel.Request( request );
}
Pour prendre en charge des verbes tels que POST et PUT qui permettent au corps de la demande de contenir des données, nous définissons également une surcharge de Request() qui prend un objet représentant le corps d'entité.
public Message Request( Uri requestUri, string httpMethod, object entityBody )
{
Message request = Message.CreateMessage( MessageVersion.None, String.Empty, entityBody );
request.Headers.To = requestUri;
HttpRequestMessageProperty property = new HttpRequestMessageProperty();
property.Method = httpMethod;
request.Properties.Add( HttpRequestMessageProperty.Name, property );
return this.Channel.Request( request );
}
L'objet entityBody
étant directement passé à Message.CreateMessage()
, le comportement WCF par défaut consiste à convertir cette instance d'objet en XML à l'aide de DataContractSerializer. Si nous souhaitions l'autre comportement, nous pourrions encapsuler cet objet dans une implémentation de BodyWriter avant de le passer à Request().
Pour terminer l'implémentation de HttpClient
, quelques méthodes utilitaires sont ajoutées et permettent de créer un modèle de programmation autour des verbes à prendre en charge.
public Message Get( Uri requestUri )
{
return Request( requestUri, "GET" );
}
public Message Post( Uri requestUri, object body )
{
return Request( requestUri, "POST", body );
}
public Message Put( Uri requestUri, object body )
{
return Request( requestUri, "PUT", body );
}
public Message Delete( Uri requestUri )
{
return Request( requestUri, "DELETE" );
}
Maintenant que nous disposons de cette classe d'assistance HTTP de base, nous pouvons l'utiliser pour effectuer des demandes HTTP au service en écrivant du code similaire au suivant.
HttpClient client = new HttpClient( collectionUri );
//Get Customer 1 by doing an HTTP GET to the customer's URI
requestUri = new Uri( collectionUri, "1" );
Message response = client.Get( requestUri );
string statusCode = client.GetStatusCode( response );
string statusDescription = client.GetStatusDescription( response );
Customer aCustomer = response.GetBody<Customer>();
Exécution de l'exemple
Pour exécuter l'exemple, démarrez d'abord le projet Server. Ce projet démarre un service auto-hébergé dans une application console. Le serveur signale l'état des demandes qu'il reçoit dans la fenêtre de console.
Une fois que le projet de service s'exécute et attend les messages, vous pouvez démarrer le projet client. Le client émet une série de demandes HTTP au serveur pour montrer l'utilisation des messages HTTP et POX afin de modifier l'état sur le serveur. Plus précisément, le client effectue les actions suivantes :
- Il récupère Customer 1 et affiche les données.
- Il change le nom de Customer 1 de « Bob » en « Robert ».
- Il récupère de nouveau Customer 1 afin d'afficher que l'état du serveur a été modifié.
- Il crée deux nouveaux clients prénommés Alice et Charlie.
- Il supprime Customer 1 du serveur.
- Il récupère de nouveau Customer 1 à partir du serveur et obtient la réponse "Point de terminaison introuvable".
- Il obtient le contenu actuel de la collection, qui est une liste de liens.
- Il récupère chaque élément de la collection en émettant une demande GET sur chaque lien de celle-ci.
La sortie de chaque demande et réponse s'affiche dans la fenêtre de console du client.
Pour configurer, générer et exécuter l'exemple
Assurez-vous d'avoir effectué la procédure indiquée dans la section Procédure d'installation unique pour les exemples Windows Communication Foundation.
Pour générer l'édition C# ou Visual Basic .NET de la solution, suivez les instructions indiquées dans Génération des exemples Windows Communication Foundation.
Pour exécuter l'exemple dans une configuration à un seul ou multi-ordinateurs, suivez les instructions indiquées dans Exécution des exemples Windows Communication Foundation.
Voir aussi
Autres ressources
How To: Create a Basic Self-Hosted Service
How To: Create a Basic IIS-Hosted Service
Send comments about this topic to Microsoft.
© 2007 Microsoft Corporation. All rights reserved.