Specifica del trasferimento di dati nei contratti di servizio
La piattaforma Windows Communication Foundation (WCF) può essere considerata un'infrastruttura di messaggistica. Le operazioni di servizio possono ricevere, elaborare e inviare messaggi. I messaggi vengono descritti tramite contratti di operazione. Si consideri ad esempio il contratto seguente:
[ServiceContract]
public interface IAirfareQuoteService
{
[OperationContract]
float GetAirfare(string fromCity, string toCity);
}
In questo contratto, l'operazione GetAirfare
accetta un messaggio contenente le informazioni fromCity
e toCity
e quindi restituisce un messaggio che contiene un numero.
In questo argomento vengono mostrati i vari modi in cui un contratto di operazione può descrivere i messaggi.
Descrizione dei messaggi tramite parametri
Il modo più semplice per descrivere un messaggio è utilizzare un elenco di parametri e il valore restituito. Nell'esempio precedente, i parametri stringa fromCity
e toCity
vengono utilizzati per descrivere il messaggio di richiesta, mentre il valore restituito float viene utilizzato per descrivere il messaggio di risposta. Se il valore restituito non è sufficiente a descrivere un messaggio di risposta, è possibile utilizzare i parametri out. Ad esempio, l'operazione seguente contiene i parametri fromCity
e toCity
nel messaggio di richiesta e una coppia numero/valuta nel messaggio di risposta:
[OperationContract]
float GetAirfare(string fromCity, string toCity, out string currency);
È inoltre possibile utilizzare i parametri referenziati di modo che un parametro appartenga sia al messaggio di richiesta sia al messaggio di risposta. Il tipo dei parametri deve essere serializzabile, ovvero convertibile in XML. Per impostazione predefinita, WCF utilizza il componente DataContractSerializer per eseguire questa conversione. Sono supportati i tipi più primitivi (quali int, string, float e DateTime) . I tipi definiti dall'utente in genere devono presentare un contratto di dati. Per ulteriori informazioni, vedere Utilizzo di contratti dati.
public interface IAirfareQuoteService
{
[OperationContract]
float GetAirfare(Itinerary itinerary, DateTime date);
}
[DataContract]
public class Itinerary
{
[DataMember]
public string fromCity;
[DataMember]
public string toCity;
}
Talvolta il componente DataContractSerializer non è adatto a serializzare i tipi definiti dall'utente. WCF consente di serializzare i parametri mediante un motore di serializzazione alternativo, ovvero XmlSerializer. Gli attributi del motore XmlSerializer, ad esempio XmlAttributeAttribute, consentono di migliorare il controllo sul codice XML risultante. Se si desidera attivare il motore XmlSerializer per una determinata operazione o per l'intero servizio, applicare l'attributo XmlSerializerFormatAttribute a un'operazione o al servizio. Ad esempio:
[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;
}
Per ulteriori informazioni, vedere Utilizzo della classe XmlSerializer. Si tenga presente che è consigliabile evitare l'attivazione manuale del motore XmlSerializer appena illustrata, salvo nei casi specifici in cui questa attivazione sia effettivamente necessaria, come descritto in questo argomento.
Per isolare i nomi di parametro .NET dai nomi di contratto è possibile utilizzare l'attributo MessageParameterAttribute. Per impostare i nomi di contratto è possibile utilizzare la proprietà Name. Ad esempio, il contratto dell'operazione seguente è equivalente al primo esempio di questo argomento.
[OperationContract]
public float GetAirfare(
[MessageParameter(Name="fromCity")] string originCity,
[MessageParameter(Name="toCity")] string destinationCity);
Descrizione dei messaggi vuoti
Per descrivere un messaggio di richiesta vuoto è possibile non specificare alcun parametro referenziato o di input. Ad esempio:
[OperationContract]
public int GetCurrentTemperature();
Per descrivere un messaggio di risposta vuoto è possibile utilizzare un tipo restituito void e non specificare alcun parametro referenziato o di output. Ad esempio:
[OperationContract]
public void SetTemperature(int temperature);
Si noti che questa operazione è diversa da un'operazione unidirezionale:
[OperationContract(IsOneWay=true)]
public void SetLightbulbStatus(bool isOn);
L'operazione SetTemperatureStatus
restituisce un messaggio vuoto e, se si verifica un problema durante l'elaborazione del messaggio di input, può restituire un errore. L'operazione SetLightbulbStatus
non restituisce invece alcun valore e non è in grado di segnalare il verificarsi di una condizione di errore.
Descrizione dei messaggi tramite i contratti di messaggio
In alcuni casi conviene utilizzare un unico tipo per rappresentare un intero messaggio. Benché a tale scopo sia possibile utilizzare un contratto di dati, è comunque consigliabile utilizzare un contratto di messaggio. Questo approccio consente infatti di evitare livelli di wrapping superflui nel codice XML risultante. Inoltre, i contratti di messaggio consentono di migliorare il controllo sui messaggi risultanti. È ad esempio possibile definire in modo specifico i tipi di informazione contenuti nel corpo e nelle intestazioni del messaggio. Nell'esempio seguente viene illustrato l'utilizzo dei contratti di messaggio.
[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;
}
Per ulteriori informazioni, vedere Utilizzo dei contratti di messaggio.
Nell'esempio precedente la classe DataContractSerializer viene utilizzata per impostazione predefinita. Nei contratti di messaggio è inoltre possibile utilizzare la classe XmlSerializer. A tale scopo, applicare l'attributo XmlSerializerFormatAttribute all'operazione oppure al contratto e quindi utilizzare tipi compatibili con la classe XmlSerializer nei membri delle intestazioni e del corpo del messaggio.
Descrizione dei messaggi tramite flussi
Un altro modo per descrivere i messaggi nelle operazioni consiste nell'utilizzare la classe Stream o una delle relative classi derivate in un contratto di operazione oppure come membro del corpo del contratto di messaggio. In quest'ultimo caso è necessario che questo membro sia l'unico. Per i messaggi in ingresso, il tipo deve essere Stream. Non è consentito l'utilizzo di classi derivate.
Anziché richiamare il serializzatore, WCF recupera i dati da un flusso e quindi li inserisce direttamente in un messaggio in uscita, oppure recupera i dati da un messaggio in ingresso e li inserisce direttamente in un flusso. Nell'esempio seguente viene illustrato l'utilizzo dei flussi.
[OperationContract]
public Stream DownloadFile(string fileName);
Non è consentito combinare dati Stream e dati non di flusso in un unico corpo di messaggio. Per aggiungere altri dati nelle intestazioni di messaggio è necessario utilizzare un contratto di messaggio. Nell'esempio seguente viene mostrato un utilizzo non corretto dei flussi quando si definisce un contratto di operazione.
//Incorrect:
// [OperationContract]
// public void UploadFile (string fileName, Stream fileData);
Nell'esempio seguente viene mostrato un utilizzo corretto dei flussi quando si definisce un contratto di operazione.
[OperationContract]
public void UploadFile (UploadFileMessage message);
//code omitted
[MessageContract]
public class UploadFileMessage
{
[MessageHeader] public string fileName;
[MessageBodyMember] public Stream fileData;
}
Per ulteriori informazioni, vedere Dati di grandi dimensioni e flussi.
Utilizzo della classe Message
Per controllare in modo completo a livello di codice lo scambio dei messaggi è possibile utilizzare direttamente la classe Message, come mostrato nell'esempio di codice seguente.
[OperationContract]
public void LogMessage(Message m);
Si tratta di uno scenario avanzato. Per informazioni dettagliate, vedere Utilizzo della classe Message.
Descrizione dei messaggi di errore
Oltre ai messaggi descritti in base al valore restituito e ai parametri referenziati o di output, qualsiasi operazione non unidirezionale può restituire almeno due messaggi: il messaggio di risposta normale e un messaggio di errore. Si consideri il contratto di operazione seguente.
[OperationContract]
float GetAirfare(string fromCity, string toCity, DateTime date);
Questa operazione può restituire un messaggio normale contenente un numero float o un messaggio di errore contenente un codice di errore e una descrizione. A tale scopo è possibile generare un'eccezione FaultException nell'implementazione del servizio.
Per specificare eventuali messaggi di errore aggiuntivi è possibile utilizzare l'attributo FaultContractAttribute. Gli errori aggiuntivi devono essere serializzabili tramite il componente DataContractSerializer, come mostrato nell'esempio di codice seguente.
[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;
}
Questi errori aggiuntivi possono essere generati mediante un'eccezione FaultException del tipo di contratto di dati appropriato. Per ulteriori informazioni, vedere Gestione di eccezioni ed errori.
Non è consentito utilizzare la classe XmlSerializer per descrivere errori. L'attributo XmlSerializerFormatAttribute non ha alcun effetto sui messaggi di errore di un contratto.
Utilizzo di tipi derivati
In alcuni casi conviene utilizzare un tipo di base in un'operazione o in un contratto di messaggio e quindi utilizzare un tipo derivato per richiamare effettivamente l'operazione. In questi casi è necessario utilizzare l'attributo ServiceKnownTypeAttribute oppure un meccanismo alternativo per consentire l'utilizzo di tipi derivati. Si consideri l'operazione seguente.
[OperationContract]
public bool IsLibraryItemAvailable(LibraryItem item);
Si supponga che due tipi, Book
e Magazine
, derivino dal tipo LibraryItem
. Per utilizzare questi tipi nell'operazione IsLibraryItemAvailable
è possibile modificare l'operazione come segue:
[OperationContract]
[ServiceKnownType(typeof(Book))]
[ServiceKnownType(typeof(Magazine))]
public bool IsLibraryItemAvailable(LibraryItem item);
In alternativa, quando si utilizza il componente DataContractSerializer predefinito, è possibile utilizzare l'attributo KnownTypeAttribute, come mostrato nell'esempio di codice seguente.
[OperationContract]
public bool IsLibraryItemAvailable(LibraryItem item);
// code omitted
[DataContract]
[KnownType(typeof(Book))]
[KnownType(typeof(Magazine))]
public class LibraryItem
{
//code omitted
}
Quando invece si utilizza il motore XmlSerializer è possibile utilizzare l'attributo XmlIncludeAttribute.
L'attributo ServiceKnownTypeAttribute può essere applicato a un'operazione specifica o all'intero servizio. Analogamente all'attributo KnownTypeAttribute, tale attributo accetta un tipo o il nome del metodo da chiamare per ottenere un elenco di tipi conosciuti. Per ulteriori informazioni, vedere Tipi conosciuti di contratto dati.
Specifica delle proprietà Use e Style
I due stili più comunemente utilizzati per descrivere i servizi tramite Web Services Description Language (WSDL) sono Document e Remote Procedure Call (RPC). Nello stile Document, l'intero corpo del messaggio viene descritto utilizzando un unico schema e WSDL descrive le varie parti del corpo del messaggio facendo riferimento agli elementi di tale schema. Nello stile RPC, invece, WSDL descrive le varie parti del corpo del messaggio facendo riferimento a vari tipi di schema. In alcuni casi occorre selezionare manualmente uno di questi stili. A tale scopo è possibile applicare l'attributo DataContractFormatAttribute e impostare la proprietà Style (quando si utilizza il componente DataContractSerializer) oppure impostare la proprietà Style dell'attributo XmlSerializerFormatAttribute (quando si utilizza il motore XmlSerializer).
Inoltre, il motore XmlSerializer supporta due formati di XML serializzato: Literal e Encoded. Literal è il formato in genere più accettato ed è l'unico a essere supportato dal componente DataContractSerializer. Encoded è un formato legacy descritto nella sezione 5 della specifica SOAP ed è consigliabile evitarne l'utilizzo nei servizi più recenti. Per passare alla modalità Encoded, impostare la proprietà Use dell'attributo XmlSerializerFormatAttribute su Encoded.
Nella maggior parte dei casi è consigliabile evitare di modificare le impostazioni predefinite delle proprietà Style e Use.
Controllo del processo di serializzazione
Sono disponibili diversi modi per personalizzare il processo di serializzazione dei dati.
Modifica delle impostazioni di serializzazione del server
Quando si utilizza il componente DataContractSerializer predefinito è possibile controllare alcuni aspetti del processo di serializzazione del servizio. A tale scopo è possibile applicare l'attributo ServiceBehaviorAttribute al servizio. In particolare, è possibile utilizzare la proprietà MaxItemsInObjectGraph per impostare la quota che limita il numero massimo di oggetti deserializzati dal componente DataContractSerializer. È possibile utilizzare la proprietà IgnoreExtensionDataObject per disattivare la funzionalità di controllo delle versioni delle sequenze di andata e ritorno. Per ulteriori informazioni su quote, vedere Considerazioni sulla protezione per i dati. Per ulteriori informazioni su controllo delle versioni delle sequenze di andata e ritorno, vedere Contratti dati compatibili con versioni successive.
[ServiceContract]
[ServiceBehavior(MaxItemsInObjectGraph=100000)]
public interface IDataService
{
[OperationContract] DataPoint[] GetData();
}
Comportamenti di serializzazione
In WCF sono disponibili due comportamenti: DataContractSerializerOperationBehavior e XmlSerializerOperationBehavior. Tali comportamenti vengono attivati automaticamente a seconda del serializzatore utilizzato per l'operazione in esecuzione. Poiché questi comportamenti vengono attivati automaticamente, in genere non è necessario gestirli.
Tuttavia, le proprietà MaxItemsInObjectGraph, IgnoreExtensionDataObject e DataContractSurrogate del comportamento DataContractSerializerOperationBehavior possono essere utilizzate per personalizzare il processo di serializzazione. Come indicato nella sezione precedente, le prime due proprietà presentano lo stesso significato. La proprietà DataContractSurrogate può essere utilizzata per abilitare i surrogati di contratti di dati, ovvero un meccanismo avanzato di personalizzazione ed estensione del processo di serializzazione. Per ulteriori informazioni, vedere Surrogati del contratto dati.
La proprietà DataContractSerializerOperationBehavior consente di personalizzare la serializzazione sia del client sia del server. Nell'esempio seguente viene illustrato come aumentare la quota MaxItemsInObjectGraph del 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();
Di seguito è riportato il codice equivalente del servizio, nel caso di server indipendente.
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();
Nel caso di servizio ospitato su Web è invece necessario creare una nuova classe derivata ServiceHost e utilizzare una factory di host del servizio per attivarla.
Controllo delle impostazioni di serializzazione in configurazione
Le proprietà MaxItemsInObjectGraph e IgnoreExtensionDataObject possono essere controllate in configurazione tramite il comportamento di endpoint o di servizio del componente dataContractSerializer, come mostrato nell'esempio seguente.
<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>
Serializzazione con condivisione di tipi, conservazione degli oggetti grafici e serializzatori personalizzati
La serializzazione eseguita dal componente DataContractSerializer si basa su nomi di contratto di dati e non su nomi di tipo .NET. Ciò è in linea con i concetti di base dell'architettura orientata ai servizi e offre inoltre un elevato livello di flessibilità, in quanto i tipi .NET possono cambiare senza influire sul contratto di transito. In casi rari può risultare utile serializzare i nomi di tipo .NET effettivi. Questo approccio, analogamente alla tecnologia .NET Framework Remoting, comporta l'introduzione di un accoppiamento stretto fra client e server. È consigliabile utilizzare questa pratica solo in determinati casi rari che in genere si presentano quando si esegue la migrazione da .NET Framework Remoting a WCF. In questo caso è necessario utilizzare la classe NetDataContractSerializer anziché la classe DataContractSerializer.
In genere il componente DataContractSerializer serializza gli oggetti grafici come oggetti struttura. Ovvero, se esistono più riferimenti a uno stesso oggetto, quest'ultimo viene serializzato più volte. Si consideri ad esempio il caso di un'istanza della classe PurchaseOrder contenente i campi billTo e shipTo di tipo Address. Se entrambi i campi vengono impostati sulla stessa istanza di Address, dopo la serializzazione e la deserializzazione esistono due istanze identiche di Address. Ciò è dovuto al fatto che non esiste alcuna modalità standard interoperativa per rappresentare gli oggetti grafici in XML, salvo nel caso dello standard legacy con codifica SOAP disponibile nel motore XmlSerializer, come descritto nella sezione precedente relativa alle proprietà Style e Use. La serializzazione degli oggetti grafici come strutture comporta alcuni svantaggi. Ad esempio, risulta impossibile serializzare i grafici aventi riferimenti circolari. Talvolta occorre attivare la serializzazione degli oggetti grafici, anche se non è interoperativa. A tale scopo è possibile utilizzare il componente DataContractSerializer costruito con il parametro preserveObjectReferences impostato su true.
Talvolta i serializzatori incorporati risultano insufficienti per lo scenario da gestire. Nella maggior parte dei casi è comunque possibile utilizzare l'astrazione XmlObjectSerializer dalla quale derivano sia il serializzatore DataContractSerializer sia il serializzatore NetDataContractSerializer.
Per tutti e tre i casi precedenti (conservazione dei tipi .NET, conservazione degli oggetti grafici e serializzazione completamente personalizzata basata su XmlObjectSerializer) è necessario attivare un serializzatore personalizzato. A tale scopo, attenersi alla procedura seguente:
Scrivere il comportamento personalizzato derivante dal comportamento DataContractSerializerOperationBehavior.
Eseguire l'override dei due metodi CreateSerializer in modo che restituiscano il serializzatore personalizzato, sia esso NetDataContractSerializer, DataContractSerializer con il parametro preserveObjectReferences impostato su true oppure il serializzatore personalizzato XmlObjectSerializer.
Prima di aprire l'host del servizio o creare un canale client, rimuovere il comportamento DataContractSerializerOperationBehavior esistente e attivare la classe derivata personalizzata creata nei passaggi precedenti.
Per ulteriori informazioni su concetti avanzati di serializzazione, vedere Serializzazione e deserializzazione.
Vedere anche
Attività
Procedura: attivare il flusso
Procedura: creare un contratto dati di base per una classe o una struttura