Spécification du transfert de données dans des contrats de service
Windows Communication Foundation (WCF) peut être considéré comme une infrastructure de messagerie. Les opérations de service peuvent recevoir des messages, les traiter et leur envoyer des messages. Les messages sont décrits à l'aide de contrats d'opérations. Par exemple, considérons le contrat suivant.
[ServiceContract]
public interface IAirfareQuoteService
{
[OperationContract]
float GetAirfare(string fromCity, string toCity);
}
<ServiceContract()>
Public Interface IAirfareQuoteService
<OperationContract()>
Function GetAirfare(fromCity As String, toCity As String) As Double
End Interface
Ici, l'opération GetAirfare
accepte un message avec des informations à propos de fromCity
et toCity
, puis retourne un message qui contient un nombre.
Cette rubrique explique les différentes manières par lesquelles un contrat d'opération peut décrire des messages.
Description de messages à l'aide de paramètres
La méthode la plus simple pour décrire un message consiste à utiliser une liste de paramètres et la valeur de retour. Dans l'exemple précédent, les paramètres de chaîne fromCity
et toCity
ont été utilisés pour décrire le message de demande et la valeur de retour float a été utilisée pour décrire le message de réponse. Si la valeur de retour seule n'est pas suffisante pour décrire un message de réponse, les paramètres de sortie peuvent être utilisés. Par exemple, l'opération suivante a fromCity
et toCity
dans son message de demande, et un nombre et une monnaie dans son message de réponse :
[OperationContract]
float GetAirfare(string fromCity, string toCity, out string currency);
<OperationContract()>
Function GetAirfare(fromCity As String, toCity As String) As Double
En outre, vous pouvez utiliser des paramètres de référence pour intégrer un paramètre à la fois au message de demande et au message de réponse. Les paramètres doivent être de types pouvant être sérialisés (convertis en XML). Par défaut, WCF utilise un composant appelé classe DataContractSerializer pour exécuter cette conversion. La plupart des types primitifs (tels que int
, string
, float
et DateTime
) sont pris en charge. Les types définis par l'utilisateur doivent normalement avoir un contrat de données. Pour plus d’informations, consultez Utilisation de contrat de données.
public interface IAirfareQuoteService
{
[OperationContract]
float GetAirfare(Itinerary itinerary, DateTime date);
[DataContract]
public class Itinerary
{
[DataMember]
public string fromCity;
[DataMember]
public string toCity;
}
}
Public Interface IAirfareQuoteService
<OperationContract()>
GetAirfare(itinerary as Itinerary, date as DateTime) as Double
<DataContract()>
Class Itinerary
<DataMember()>
Public fromCity As String
<DataMember()>
Public toCity As String
End Class
End Interface
Parfois, DataContractSerializer
ne convient pas pour sérialiser vos types. WCF prend en charge un autre moteur de sérialisation, XmlSerializer, que vous pouvez également utiliser pour sérialiser des paramètres. Le XmlSerializer vous permet de davantage contrôler le XML résultant à l'aide d'attributs tels que XmlAttributeAttribute
. Pour utiliser le XmlSerializer pour une opération particulière ou pour le service entier, appliquez l'attribut XmlSerializerFormatAttribute à une opération ou à un service. Par exemple :
[ServiceContract]
public interface IAirfareQuoteService
{
[OperationContract]
[XmlSerializerFormat]
float GetAirfare(Itinerary itinerary, DateTime date);
}
public class Itinerary
{
public string fromCity;
public string toCity;
[XmlAttribute]
public bool isFirstClass;
}
<ServiceContract()>
Public Interface IAirfareQuoteService
<OperationContract()>
<XmlSerializerFormat>
GetAirfare(itinerary as Itinerary, date as DateTime) as Double
End Interface
Class Itinerary
Public fromCity As String
Public toCity As String
<XmlSerializerFormat()>
Public isFirstClass As Boolean
End Class
Pour plus d’informations consultez Utilisation de la classe XmlSerializer. Souvenez-vous que le basculement manuel vers le XmlSerializer, comme illustré ici, n'est pas recommandé, à moins que vous n'ayez des raisons spécifiques telles que celles détaillées dans cette rubrique.
Pour isoler des noms de paramètres .NET des noms de contrats, vous pouvez utiliser l'attribut MessageParameterAttribute et utiliser la propriété Name
pour définir le nom de contrat. Par exemple, le contrat d'opération suivant est équivalent au premier exemple dans cette rubrique.
[OperationContract]
public float GetAirfare(
[MessageParameter(Name="fromCity")] string originCity,
[MessageParameter(Name="toCity")] string destinationCity);
<OperationContract()>
Function GetAirfare(<MessageParameter(Name := "fromCity")> fromCity As String, <MessageParameter(Name := "toCity")> toCity As String) As Double
Description de messages vides
Un message de demande vide peut être décrit en n'ayant ni entrée ni paramètre de référence. Par exemple, en C# :
[OperationContract]
public int GetCurrentTemperature();
Par exemple, en Visual Basic :
<OperationContract()>
Function GetCurrentTemperature() as Integer
Un message de réponse vide peut être décrit en ayant un type de retour void
et aucune sortie ni paramètre de référence. Par exemple, en :
[OperationContract]
public void SetTemperature(int temperature);
<OperationContract()>
Sub SetTemperature(temperature As Integer)
Ceci est différent d'une opération unidirectionnelle telle que :
[OperationContract(IsOneWay=true)]
public void SetLightbulbStatus(bool isOn);
<OperationContract(IsOneWay:=True)>
Sub SetLightbulbStatus(isOne As Boolean)
L'opération SetTemperatureStatus
retourne un message vide. Elle peut retourner une erreur à la place, en cas de problème au niveau du traitement du message d'entrée. L'opération SetLightbulbStatus
ne retourne rien. Il n'existe aucun moyen de communiquer une condition d'erreur à partir de cette opération.
Description de messages à l'aide de contrats de message
Vous souhaiterez peut-être utiliser un type unique pour représenter le message entier. Bien qu'il soit possible d'utiliser un contrat de données dans ce but, la méthode recommandée consiste à utiliser un contrat de message ; cela évite des niveaux d'enveloppement inutiles dans le XML résultant. En outre, les contrats de message vous permettent de mieux contrôler les messages résultants. Par exemple, vous pouvez décider des informations qui doivent être dans le corps du message et de celles qui doivent être dans les en-têtes de message. L'exemple suivant illustre l'utilisation des contrats de message.
[ServiceContract]
public interface IAirfareQuoteService
{
[OperationContract]
GetAirfareResponse GetAirfare(GetAirfareRequest request);
}
[MessageContract]
public class GetAirfareRequest
{
[MessageHeader] public DateTime date;
[MessageBodyMember] public Itinerary itinerary;
}
[MessageContract]
public class GetAirfareResponse
{
[MessageBodyMember] public float airfare;
[MessageBodyMember] public string currency;
}
[DataContract]
public class Itinerary
{
[DataMember] public string fromCity;
[DataMember] public string toCity;
}
<ServiceContract()>
Public Interface IAirfareQuoteService
<OperationContract()>
Function GetAirfare(request As GetAirfareRequest) As GetAirfareResponse
End Interface
<MessageContract()>
Public Class GetAirfareRequest
<MessageHeader()>
Public Property date as DateTime
<MessageBodyMember()>
Public Property itinerary As Itinerary
End Class
<MessageContract()>
Public Class GetAirfareResponse
<MessageBodyMember()>
Public Property airfare As Double
<MessageBodyMember()> Public Property currency As String
End Class
<DataContract()>
Public Class Itinerary
<DataMember()> Public Property fromCity As String
<DataMember()> Public Property toCity As String
End Class
Pour plus d’informations, consultez Utilisation des contrats de messages.
Dans l'exemple précédent, la classe DataContractSerializer est encore utilisée par défaut. La classe XmlSerializer peut également être utilisée avec des contrats de message. Pour cela, appliquez l'attribut XmlSerializerFormatAttribute à l'opération ou au contrat et utilisez des types compatibles avec la classe XmlSerializer dans les en-têtes de message et les membres de corps.
Description de messages à l'aide de flux
Une autre méthode pour décrire des messages dans des opérations consiste à utiliser la classe Stream ou une de ses classes dérivées dans un contrat d'opération ou en tant que membre de corps de contrat de message (il doit s'agir du seul membre dans ce cas). Pour les messages entrants, le type doit être Stream
; vous ne pouvez pas utiliser de classes dérivées.
Au lieu d’appeler le sérialiseur, WCF récupère les données d’un flux et les met directement dans un message sortant, ou récupère les données d’un message entrant et les met directement dans un flux. L'exemple suivant illustre l'utilisation des flux.
[OperationContract]
public Stream DownloadFile(string fileName);
<OperationContract()>
Function DownloadFile(fileName As String) As String
Vous ne pouvez pas combiner des données Stream
et des données de non-flux dans un même corps de message. Utilisez un contrat de message pour mettre les données supplémentaires dans les en-têtes de message. L'exemple suivant illustre l'utilisation incorrecte de flux lors de la définition du contrat d'opération.
//Incorrect:
// [OperationContract]
// public void UploadFile (string fileName, Stream fileData);
'Incorrect:
'<OperationContract()>
Public Sub UploadFile(fileName As String, fileData As StreamingContext)
L'exemple suivant illustre l'utilisation correcte de flux lors de la définition d'un contrat d'opération.
[OperationContract]
public void UploadFile (UploadFileMessage message);
//code omitted
[MessageContract]
public class UploadFileMessage
{
[MessageHeader] public string fileName;
[MessageBodyMember] public Stream fileData;
}
<OperationContract()>
Public Sub UploadFile(fileName As String, fileData As StreamingContext)
'Code Omitted
<MessageContract()>
Public Class UploadFileMessage
<MessageHeader()>
Public Property fileName As String
<MessageBodyMember()>
Public Property fileData As Stream
End Class
Pour plus d’informations, consultez Données volumineuses et diffusion en continu.
Utilisation de la classe Message
Pour avoir un contrôle complet par programmation des messages envoyés ou reçus, vous pouvez utiliser la classe Message directement, comme illustré dans l'exemple de code suivant.
[OperationContract]
public void LogMessage(Message m);
<OperationContract()>
Sub LogMessage(m As Message)
Il s’agit d’un scénario avancé, décrit en détail dans Utilisation de la classe de message.
Description des messages d'erreur
En plus des messages décrits par la valeur de retour et les paramètres de sortie ou de référence, toute opération qui n'est pas unidirectionnelle peut retourner au moins deux messages possibles : son message de réponse normal et un message d'erreur. Considérez le contrat d'opération suivant.
[OperationContract]
float GetAirfare(string fromCity, string toCity, DateTime date);
<OperationContract()>
Function GetAirfare(fromCity As String, toCity As String, date as DateTime)
Cette opération peut retourner un message normal qui contient un nombre float
ou un message d'erreur qui contient un code d'erreur et une description. Vous pouvez accomplir cela en levant une FaultException dans votre implémentation de service.
Vous pouvez spécifier des messages d'erreur possibles supplémentaires en utilisant l'attribut FaultContractAttribute. Les erreurs supplémentaires doivent être sérialisables à l'aide du DataContractSerializer, comme illustré dans l'exemple de code suivant.
[OperationContract]
[FaultContract(typeof(ItineraryNotAvailableFault))]
float GetAirfare(string fromCity, string toCity, DateTime date);
//code omitted
[DataContract]
public class ItineraryNotAvailableFault
{
[DataMember]
public bool IsAlternativeDateAvailable;
[DataMember]
public DateTime alternativeSuggestedDate;
}
<OperationContract()>
<FaultContract(GetType(ItineraryNotAvailableFault))>
Function GetAirfare(fromCity As String, toCity As String, date as DateTime) As Double
'Code Omitted
<DataContract()>
Public Class
<DataMember()>
Public Property IsAlternativeDateAvailable As Boolean
<DataMember()>
Public Property alternativeSuggestedDate As DateTime
End Class
Ces erreurs supplémentaires peuvent être générées en levant une FaultException<TDetail> du type de contrat de données approprié. Pour plus d’informations, consultez Gestion des exceptions et des erreurs.
Vous ne pouvez pas utiliser la classe XmlSerializer pour décrire des erreurs. Le XmlSerializerFormatAttribute n'a aucun effet sur les contrats d'erreur.
Utilisation de types dérivés
Vous pouvez utiliser un type de base dans une opération ou un contrat de message, puis utiliser un type dérivé lors de l'appel réel à l'opération. Dans ce cas, vous devez utiliser l'attribut ServiceKnownTypeAttribute ou un autre mécanisme pour autoriser l'utilisation de types dérivés. Considérez l'opération suivante.
[OperationContract]
public bool IsLibraryItemAvailable(LibraryItem item);
<OperationContract()>
Function IsLibraryItemAvailable(item As LibraryItem) As Boolean
Supposez que deux types, Book
et Magazine
, dérivent de LibraryItem
. Pour utiliser ces types dans l'opération IsLibraryItemAvailable
, vous pouvez modifier l'opération comme suit :
[OperationContract]
[ServiceKnownType(typeof(Book))]
[ServiceKnownType(typeof(Magazine))]
public bool IsLibraryItemAvailable(LibraryItem item);
Vous pouvez également utiliser l'attribut KnownTypeAttribute lorsque le DataContractSerializer par défaut est en cours d'utilisation, comme illustré dans l'exemple de code suivant.
[OperationContract]
public bool IsLibraryItemAvailable(LibraryItem item);
// code omitted
[DataContract]
[KnownType(typeof(Book))]
[KnownType(typeof(Magazine))]
public class LibraryItem
{
//code omitted
}
<OperationContract()>
Function IsLibraryItemAvailable(item As LibraryItem) As Boolean
'Code Omitted
<DataContract()>
<KnownType(GetType(Book))>
<KnownType(GetType(Magazine))>
Public Class LibraryItem
'Code Omitted
End Class
Vous pouvez utiliser l'attribut XmlIncludeAttribute lors de l'utilisation du XmlSerializer.
Vous pouvez appliquer l'attribut ServiceKnownTypeAttribute à une opération ou au service entier. Il accepte un type ou le nom de la méthode à appeler pour obtenir une liste des types connus, tout comme l'attribut KnownTypeAttribute. Pour plus d’informations, consultez Types connus de contrats de données.
Spécification de l'utilisation et du style
Lors de la description de services à l'aide du langage WSDL (Web Services Description Language), les deux styles couramment utilisés sont Document et appel de procédure distante (RPC). Dans le style Document, le corps du message entier est décrit à l'aide du schéma et le WSDL décrit les différentes parties du corps de message en faisant référence à des éléments dans ce schéma. Dans le style RPC, le WSDL fait référence à un type de schéma pour chaque partie de message plutôt qu'à un élément. Dans certains cas, vous devez sélectionner manuellement l'un de ces styles. Pour cela, vous pouvez appliquer l'attribut DataContractFormatAttribute et définir la propriété Style
(lorsque le DataContractSerializer est en cours d'utilisation), ou définir Style
sur l'attribut XmlSerializerFormatAttribute (lors de l'utilisation du XmlSerializer).
En outre, XmlSerializer prend en charge deux formes de XML sérialisé : Literal
et Encoded
. Literal
est la forme la plus couramment acceptée et la seule prise en charge par le DataContractSerializer. Encoded
est une forme héritée décrite à la section 5 de la spécification SOAP qui n'est pas recommandée pour les nouveaux services. Pour basculer en mode Encoded
, affectez à la propriété Use
sur l'attribut XmlSerializerFormatAttribute la valeur Encoded
.
Dans la plupart des cas, vous ne devez pas modifier les paramètres par défaut des propriétés Style
et Use
.
Contrôle du processus de sérialisation
Vous pouvez effectuer plusieurs actions pour personnaliser la manière dont les données sont sérialisées.
Modification des paramètres de sérialisation du serveur
Lorsque le DataContractSerializer par défaut est en cours d'utilisation, vous pouvez contrôler certains aspects du processus de sérialisation sur le service en appliquant l'attribut ServiceBehaviorAttribute au service. Spécifiquement, vous pouvez utiliser la propriété MaxItemsInObjectGraph
pour définir le quota qui limite le nombre maximal d'objets que le DataContractSerializer désérialise. Vous pouvez utiliser la propriété IgnoreExtensionDataObject
pour désactiver la fonctionnalité de suivi des versions aller-retour. Pour plus d’informations sur les quotas, consultez Considérations sur la sécurité des données. Pour plus d’informations sur l’aller-retour, consultez Contrats de données compatibles avec des versions ultérieures.
[ServiceBehavior(MaxItemsInObjectGraph=100000)]
public class MyDataService:IDataService
{
public DataPoint[] GetData()
{
// Implementation omitted
}
}
<ServiceBehavior(MaxItemsInObjectGraph:=100000)>
Public Class MyDataService Implements IDataService
Function GetData() As DataPoint()
‘ Implementation omitted
End Function
End Interface
Comportements de sérialisation
Deux comportements sont disponibles dans WCF, le DataContractSerializerOperationBehavior et le XmlSerializerOperationBehavior qui sont connectés automatiquement en fonction du sérialiseur utilisé pour une opération particulière. Ces comportements étant appliqués automatiquement, il n'est normalement pas nécessaire de s'en soucier.
Toutefois, le DataContractSerializerOperationBehavior
a les propriétés MaxItemsInObjectGraph
, IgnoreExtensionDataObject
et DataContractSurrogate
que vous pouvez utiliser pour personnaliser le processus de sérialisation. Les deux premières propriétés ont la même signification que dans la section précédente. Vous pouvez utiliser la propriété DataContractSurrogate
pour activer des substituts de contrat de données, qui sont un mécanisme puissant pour personnaliser et étendre le processus de sérialisation. Pour plus d’informations, consultez Substituts de contrats de données.
Vous pouvez utiliser le DataContractSerializerOperationBehavior
pour personnaliser à la fois la sérialisation de client et de serveur. L'exemple suivant indique comment augmenter le quota MaxItemsInObjectGraph
sur le client.
ChannelFactory<IDataService> factory = new ChannelFactory<IDataService>(binding, address);
foreach (OperationDescription op in factory.Endpoint.Contract.Operations)
{
DataContractSerializerOperationBehavior dataContractBehavior =
op.Behaviors.Find<DataContractSerializerOperationBehavior>()
as DataContractSerializerOperationBehavior;
if (dataContractBehavior != null)
{
dataContractBehavior.MaxItemsInObjectGraph = 100000;
}
}
IDataService client = factory.CreateChannel();
Dim factory As ChannelFactory(Of IDataService) = New ChannelFactory(Of IDataService)(binding, address)
For Each op As OperationDescription In factory.Endpoint.Contract.Operations
Dim dataContractBehavior As DataContractSerializerOperationBehavior = op.Behaviors.Find(Of DataContractSerializerOperationBehavior)()
If dataContractBehavior IsNot Nothing Then
dataContractBehavior.MaxItemsInObjectGraph = 100000
End If
Next
Dim client As IDataService = factory.CreateChannel
Voici le code équivalent sur le service, dans le cas d’un auto-hébergement :
ServiceHost serviceHost = new ServiceHost(typeof(IDataService))
foreach (ServiceEndpoint ep in serviceHost.Description.Endpoints)
{
foreach (OperationDescription op in ep.Contract.Operations)
{
DataContractSerializerOperationBehavior dataContractBehavior =
op.Behaviors.Find<DataContractSerializerOperationBehavior>()
as DataContractSerializerOperationBehavior;
if (dataContractBehavior != null)
{
dataContractBehavior.MaxItemsInObjectGraph = 100000;
}
}
}
serviceHost.Open();
Dim serviceHost As ServiceHost = New ServiceHost(GetType(IDataService))
For Each ep As ServiceEndpoint In serviceHost.Description.Endpoints
For Each op As OperationDescription In ep.Contract.Operations
Dim dataContractBehavior As DataContractSerializerOperationBehavior = op.Behaviors.Find(Of DataContractSerializerOperationBehavior)()
If dataContractBehavior IsNot Nothing Then
dataContractBehavior.MaxItemsInObjectGraph = 100000
End If
Next
Next
serviceHost.Open()
Dans le cas d'un hébergement sur le Web, vous devez créer une classe dérivée ServiceHost
et utiliser une fabrication hôte de service pour le brancher.
Contrôle des paramètres de sérialisation dans la configuration
Le MaxItemsInObjectGraph
et le IgnoreExtensionDataObject
peuvent être contrôlés par le biais de la configuration en utilisant le comportement de service ou le point de terminaison dataContractSerializer
, comme illustré dans l'exemple suivant.
<configuration>
<system.serviceModel>
<behaviors>
<endpointBehaviors>
<behavior name="LargeQuotaBehavior">
<dataContractSerializer
maxItemsInObjectGraph="100000" />
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint address="http://example.com/myservice"
behaviorConfiguration="LargeQuotaBehavior"
binding="basicHttpBinding" bindingConfiguration=""
contract="IDataService"
name="" />
</client>
</system.serviceModel>
</configuration>
Sérialisation de type partagé, conservation de graphique d'objet et sérialiseurs personnalisés
Le DataContractSerializer sérialise à l'aide des noms de contrats de données, et non des noms de types .NET. Ceci est cohérent avec les doctrines d'architecture orientée services et procure un niveau élevé de flexibilité : les types .NET peuvent changer sans affecter le contrat de câble. Dans de rares cas, vous souhaiterez peut-être sérialiser des noms de types .NET réels, introduisant ainsi un couplage étroit entre le client et le serveur, semblable à la technologie de .NET Framework Remoting. Cette pratique est déconseillée, sauf dans les cas rares qui se produisent habituellement lors de la migration vers WCF à partir de .NET Framework Remoting. Dans ce cas, vous devez utiliser la classe NetDataContractSerializer au lieu de la classe DataContractSerializer.
Le DataContractSerializer sérialise normalement les graphiques d'objets en tant qu'arborescences d'objets. Autrement dit, s'il est fait référence plusieurs fois au même objet, il est sérialisé plusieurs fois. Par exemple, considérez une instance PurchaseOrder
qui a deux champs de type Adresse nommés billTo
et shipTo
. Si les deux champs ont pour valeur la même instance Adresse, il existe deux instances Adresse identiques après la sérialisation et la désérialisation. Cela est dû au fait qu'il n'existe aucune méthode interopérable standard pour représenter des graphiques d'objets en XML (hormis la norme encodée SOAP héritée disponible sur le XmlSerializer, comme décrit dans la section précédente sur Style
et Use
). La sérialisation de graphiques d’objets en tant qu’arborescences a certains inconvénients ; par exemple, les graphiques avec des références circulaires ne peuvent pas être sérialisés. Parfois, il est nécessaire de basculer vers la sérialisation de graphiques d'objets vraie, bien que cela ne soit pas interopérable. Vous devez pour cela utiliser le DataContractSerializer construit avec le paramètre preserveObjectReferences
défini à true
.
Parfois, les sérialiseurs intégrés ne sont pas suffisants pour votre scénario. Dans la plupart des cas, vous pouvez encore utiliser l'abstraction XmlObjectSerializer à partir de laquelle le DataContractSerializer et le NetDataContractSerializer dérivent.
Les trois cas précédents (conservation de type .NET, conservation des graphiques d'objets et sérialisation XmlObjectSerializer
complètement personnalisée) requièrent tous le branchement d'un sérialiseur personnalisé. Pour ce faire, procédez comme suit :
Écrivez votre propre comportement dérivant du DataContractSerializerOperationBehavior.
Substituez les deux méthodes
CreateSerializer
pour retourner votre propre sérialiseur (le NetDataContractSerializer, le DataContractSerializer avecpreserveObjectReferences
défini àtrue
, ou votre propre XmlObjectSerializer personnalisé).Avant d'ouvrir l'hôte de service ou de créer un canal client, supprimez le comportement DataContractSerializerOperationBehavior existant et branchez la classe dérivée personnalisée que vous avez créée aux étapes précédentes.
Pour plus d’informations sur les concepts de sérialisation avancés, consultez Sérialisation et désérialisation.