Informazioni generali sull'architettura Windows Communication Foundation

Microsoft Corporation

Marzo 2006

Riassunto:L'articolo fornisce una panoramica generale dell'architettura Windows Communication Foundation (WCF) e dei suoi concetti di base. Mediante esempi di codice vengono illustrati contratti, endpoint e comportamenti di WCF (17 pagine stampate).

Sommario

Introduzione
Concetti di base di WCF
Esempi di codice
Riepilogo

Introduzione

Nel presente documento viene fornita una panoramica generale dell'architettura Windows Communication Foundation (WCF) allo scopo di illustrarne i concetti di base e le relative interconnessioni. Per chiarire ulteriormente i concetti esposti, vengono presentati alcuni esempi di codice, che non rappresentano però la parte sostanziale del documento.

Il documento è organizzato in due sezioni principali:

  • Concetti di base di WCF: prende in esame i principali concetti di WCF, i termini e i componenti dell'architettura.
  • Esempi di codice: contiene alcuni brevi esempi di codice intesi a illustrare, anche nella loro applicazione concreta, i concetti esposti nella sezione precedente.

Concetti di base di WCF

Un servizio WCF è un programma che espone un insieme di endpoint. Ogni endpoint è come un portale che consente di comunicare con il mondo.

Un client è un programma che scambia messaggi con uno o più endpoint. Un client può anche esporre un endpoint per ricevere messaggi da un servizio, in uno schema di scambio di messaggi bidirezionale.

Questi concetti di base saranno descritti più approfonditamente nelle prossime sezioni.

Endpoint

L'endpoint di un servizio è dotato di un indirizzo, un'associazione e un contratto.

L'indirizzo dell'endpoint è un indirizzo di rete che indica dove risiede l'endpoint. La classe EndpointAddress rappresenta l'indirizzo di un endpoint WCF.

L'associazione dell'endpoint indica come l'endpoint comunica con il mondo, ossia con quale protocollo di trasporto (ad esempio TCP, HTTP), codifica (ed esempio testo, binaria) e requisiti di protezione (ad esempio protezione dei messaggi SOAP, SSL). La classe Binding rappresenta un'associazione WCF.

Il contratto dell'endpoint indica cosa comunica l'endpoint ed è costituito essenzialmente da un insieme di messaggi organizzati in operazioni con MEP (Message Exchange Patterns) di base, ad esempio unidirezionale, bidirezionale e di tipo richiesta/risposta. La classe ContractDescription rappresenta un contratto WCF.

La classe ServiceEndpoint rappresenta un endpoint e presenta classi EndpointAddress, Binding e ContractDescription che corrispondono rispettivamente all'indirizzo, all'associazione e al contratto dell'endpoint (vedere la Figura 1).

Figura 1. Ogni endpoint del servizio contiene tre classi: EndpointAddress, Binding e Contract, rappresentate da ContractDescription.

EndpointAddress

EndpointAddress è sostanzialmente un URI, un'identità e un insieme di intestazioni opzionali, come illustrato nella Figura 2.

In genere l'identità di protezione di un endpoint è rappresentata dall'URI, ma in alcuni scenari avanzati essa può essere impostata in maniera esplicita indipendentemente dall'URI, utilizzando la proprietà dell'indirizzo Identity.

Le intestazioni opzionali consentono di fornire informazioni aggiuntive concernenti l'indirizzo, oltre all'URI dell'endpoint. Le intestazioni di indirizzo sono utili, ad esempio, per differenziare più endpoint che presentano lo stesso URI.

Figura 2. EndpointAddress contiene un URI e AddressProperties contiene un'identità e un insieme di AddressHeaders.

Associazioni

Un'associazione presenta un nome, uno spazio dei nomi e un insieme di elementi di associazione componibili (Figure 3). Il nome e lo spazio dei nomi identificano l'associazione in modo univoco nei metadati del servizio. Ciascun elemento di associazione descrive un aspetto relativo a come l'endpoint comunica con il mondo.

Figura 3. Classe Binding e relativi membri

Ad esempio, nella Figura 4 è illustrato un insieme di elementi di associazione che contiene tre elementi di associazione. Ciascuno di essi descrive parte del come, ossia delle modalità di comunicazione con l'endpoint. TcpTransportBindingElement indica che l'endpoint comunica con il mondo utilizzando TCP come protocollo di trasporto. ReliableSessionBindingElement indica che l'endpoint utilizza messaggistica affidabile per fornire garanzie sul recapito dei messaggi. SecurityBindingElement indica che l'endpoint utilizza protezione dei messaggi SOAP. In genere per ogni elemento di associazione sono presenti proprietà che descrivono ulteriormente le specificità delle modalità di comunicazione con l'endpoint. Ad esempio, ReliableSessionBindingElement presenta una proprietà Assurances che specifica le garanzie richieste per il recapito dei messaggi, quali: nessuna, almeno una volta, al massimo una volta o esattamente una volta.

Figura 4. Esempio di classe Binding con tre elementi di associazione

L'ordine e il tipo degli elementi di associazione nelle classi Binding sono significativi: l'insieme degli elementi di associazione viene utilizzato per creare uno stack di comunicazioni ordinato in base all'ordine degli elementi di associazione nel relativo insieme. L'ultimo elemento di associazione aggiunto all'insieme corrisponde al componente al livello più basso dello stack di comunicazioni, mentre il primo corrisponde all'elemento che si trova più in alto. I messaggi in arrivo fluiscono attraverso lo stack dal basso verso l'alto, i messaggi in uscita dall'alto verso il basso. Pertanto, l'ordine degli elementi di associazione presenti nell'insieme influisce direttamente sull'ordine in cui i componenti dello stack di comunicazioni elaborano i messaggi. WCF fornisce una serie di associazioni predefinite che è possibile utilizzare nella maggior parte degli scenari invece di definire associazioni personalizzate.

Contratti

Un contratto WCF è un insieme di operazioni che specifica cosa viene comunicato dall'endpoint al mondo esterno. Ogni operazione è un semplice scambio di messaggi, ad esempio unidirezionale o di tipo richiesta/risposta.

La classe ContractDescription consente di descrivere i contratti WCF e le relative operazioni. All'interno di ContractDescription, a ogni operazione Contract corrisponde una OperationDescription che ne descrive determinati aspetti, ad esempio se si tratta di un'operazione unidirezionale o di tipo richiesta/risposta. Inoltre, ciascuna OperationDescription descrive i messaggi da cui è composta l'operazione mediante un insieme di MessageDescriptions.

In genere la creazione di ContractDescription avviene a partire da un'interfaccia o classe che definisce il contratto utilizzando il modello di programmazione WCF. Questo tipo è annotato con ServiceContractAttribute e i suoi metodi, che corrispondono a operazioni di endpoint, sono annotati con OperationContractAttribute. È anche possibile creare una ContractDescription manualmente, senza partire da un tipo CLR annotato con attributi.

Un contratto bidirezionale definisce due insiemi logici di operazioni: un insieme esposto dal servizio per le chiamate del client e uno esposto dal client per le chiamate del servizio. Il modello di programmazione per la definizione di un contratto bidirezionale consiste nella suddivisione di ciascun insieme in un tipo separato (ogni tipo deve essere una classe o un'interfaccia) e nell'annotazione del contratto che rappresenta le operazioni del servizio con ServiceContractAttribute, facendo riferimento al contratto che definisce le operazioni del client (o di callback). In più, ContractDescription contiene un riferimento a ciascuno dei tipi, che vengono così raggruppati in un unico contratto bidirezionale.

Come le associazioni, i singoli contratti presentano un nome e uno spazio dei nomi che li identificano in modo univoco nei metadati del servizio.

Per ogni contratto è presente anche un insieme di ContractBehaviors, moduli che ne modificano o estendono il comportamento. I comportamenti vengono analizzati più approfonditamente nella prossima sezione.

Figura 5. La classe ContractDescription descrive un contratto WCF

Comportamenti

I comportamenti sono tipi che modificano o estendono le funzionalità del servizio o del client. Ad esempio, il comportamento dei metadati implementato da ServiceMetadataBehavior controlla se il servizio pubblica metadati. Analogamente, il comportamento della protezione controlla la rappresentazione e l'autorizzazione, mentre il comportamento delle transazioni controlla l'integrazione nelle transazioni e il loro completamento automatico.

I comportamenti intervengono anche nel processo di creazione del canale, che possono modificare sulla base di impostazioni specificate dall'utente e/o di altri aspetti del servizio o del canale.

Il comportamento di un servizio è un tipo che implementa IServiceBehavior e si applica ai servizi. Analogamente, il comportamento di un canale è un tipo che implementa IChannelBehavior e si applica ai canali client.

Descrizioni di servizi e canali

La classe ServiceDescription è una struttura in memoria che descrive un servizio WCF, inclusi gli endpoint esposti dal servizio, i comportamenti applicati a quest'ultimo e il tipo (una classe) che lo implementa (vedere la Figura 6). ServiceDescription consente di creare metadati, codice/configurazione e canali.

L'oggetto ServiceDescription può essere creato manualmente oppure a partire da un tipo annotato con determinati attributi WCF, che è lo scenario più comune. Il codice per questo tipo può essere scritto manualmente o generato da un documento WSDL mediante uno strumento WCF denominato svcutil.exe.

Sebbene sia possibile crearli e popolarli in modo esplicito, gli oggetti ServiceDescription vengono spesso creati dietro le quinte nell'ambito dell'esecuzione del servizio.

Figura 6. Modello di oggetti ServiceDescription

Analogamente, dal lato client ChannelDescription descrive un canale client WCF per uno specifico endpoint (Figura 7). Alla classe ChannelDescription sono associati un insieme di IchannelBehaviors, che sono comportamenti applicati al canale, e un ServiceEndpoint che descrive l'endpoint con cui comunicherà il canale.

Diversamente da ServiceDescription, ChannelDescription contiene un solo ServiceEndpoint che rappresenta l'endpoint di destinazione con cui comunicherà il canale.

Figura 7. Modello di oggetti ChannelDescription

Runtime WCF

Il runtime WCF è l'insieme di oggetti da cui dipendono l'invio e la ricezione dei messaggi. Rientrano nel runtime WCF, ad esempio, la formattazione dei messaggi, l'applicazione della protezione, la trasmissione e la ricezione dei messaggi con diversi protocolli di trasporto, così come l'invio dei messaggi ricevuti all'operazione appropriata. Nelle prossime sezioni verranno illustrati i principali concetti relativi al runtime WCF.

Messaggio

Il messaggio WCF è l'unità dello scambio di dati tra un client e un endpoint. Un messaggio è essenzialmente una rappresentazione in memoria dell'InfoSet di un messaggio SOAP. I messaggi non sono vincolati al testo XML e, a seconda del meccanismo di codifica in uso, possono essere serializzati utilizzando il formato binario WCF, testo XML o qualsiasi altro formato personalizzato.

Canali

I canali costituiscono l'astrazione fondamentale per l'invio e la ricezione di messaggi da e verso un endpoint. Parlando in generale, esistono due categorie di canali: i canali di trasporto, che gestiscono l'invio o la ricezione di flussi di ottetti non trasparenti mediante un protocollo di trasporto come TCP, UDP o MSMQ, e i canali di protocollo che, invece, implementano un protocollo basato su SOAP mediante l'elaborazione e talora la modifica dei messaggi. Ad esempio, il canale di protezione aggiunge ed elabora intestazioni di messaggi SOAP e può modificare il corpo del messaggio crittografandolo. I canali sono componibili, nel senso che un canale può essere sovrapposto a un altro canale, che a sua volta è sovrapposto a un terzo.

EndpointListener

EndpointListener è l'equivalente di runtime di ServiceEndpoint. EndpointAddress, Contract e Binding di ServiceEndpoint (che rappresentano il dove, il cosa e il come), corrispondono rispettivamente all'indirizzo in attesa, al filtro e invio dei messaggi e allo stack di canale di EndpointListener. EndpointListener contiene lo stack di canale responsabile dell'invio e della ricezione dei messaggi.

ServiceHost e ChannelFactory

In genere il runtime del servizio WCF viene creato dietro le quinte chiamando ServiceHost.Open. ServiceHost (Figura 6) determina la creazione di una ServiceDescription in base al tipo di servizio e inserisce nell'insieme ServiceEndpoint di ServiceDescription gli endpoint definiti nella configurazione, nel codice o in entrambi. Quindi ServiceHost utilizza ServiceDescription per creare lo stack di canale in forma di oggetto EndpointListener per ogni ServiceEndpoint in ServiceDescription.

Figura 8. Modello di oggetti ServiceHost

Analogamente, dal lato client il runtime viene creato mediante ChannelFactory, che è l'equivalente client di ServiceHost.

ChannelFactory determina la creazione di una ChannelDescription in base a un tipo di contratto, un'associazione e un EndpointAddress. Quindi utilizza ChannelDescription per creare lo stack di canale del client.

Diversamente dal runtime del servizio, il runtime client non contiene EndpointListeners perché la connessione al servizio è sempre avviata dal client e quindi non è necessario "restare in attesa" di eventuali connessioni in ingresso.

Esempi di codice

In questa sezione vengono forniti esempi di codice che illustrano le modalità di creazione di servizi e client. Gli esempi mostrano l'applicazione pratica dei concetti esposti finora e non hanno lo scopo di insegnare la programmazione WCF.

Definizione e implementazione di un contratto

Come si è già accennato, il modo più semplice per definire un contratto consiste nel creare un'interfaccia o una classe e annotarla con ServiceContractAttribute, facilitando al sistema la creazione di una ContractDescription su questa base.

Quando si utilizzano interfacce o classi per definire contratti, occorre annotare con OperationContractAttribute ogni metodo di classe o interfaccia membro del contratto. Ad esempio:

                  using System.ServiceModel;

                  //a WCF contract defined using an interface
                  [ServiceContract]
                  public interface IMath
                  {
                  [OperationContract]
                  int Add(int x, int y);
                  }
                

In questo caso l'implementazione del contratto comporta semplicemente la creazione di una classe che implementi IMath. Tale classe diviene la classe del servizio WCF. Ad esempio:

                  //the service class implements the interface
                  public class MathService : IMath
                  {
                  public int Add(int x, int y)
                  { return x + y; }
                  }
                
Definizione di endpoint e avvio del servizio

È possibile definire gli endpoint nel codice o nella configurazione. Nell'esempio che segue, il metodo DefineEndpointImperatively rappresenta il sistema più semplice per definire endpoint nel codice e avviare il servizio.

Il metodo DefineEndpointInConfig mostra l'endpoint equivalente definito nella configurazione (l'esempio di configurazione segue il codice fornito di seguito).

                  public class WCFServiceApp
                  {
                  public void DefineEndpointImperatively()
                  {
                  //create a service host for MathService
                  ServiceHost sh = new ServiceHost(typeof(MathService));

                  //use the AddEndpoint helper method to
                  //create the ServiceEndpoint and add it
                  //to the ServiceDescription
                  sh.AddServiceEndpoint(
                  typeof(IMath), //contract type
                  new WSHttpBinding(), //one of the built-in bindings
                  "http://localhost/MathService/Ep1"); //the endpoint's address

                  //create and open the service runtime
                  sh.Open();

                  }

                  public void DefineEndpointInConfig()
                  {
                  //create a service host for MathService
                  ServiceHost sh = new ServiceHost (typeof(MathService));

                  //create and open the service runtime
                  sh.Open();

                  }
                  }
                  <!-- configuration file used by above code -->
                  <configuration
                  xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
                  <system.serviceModel>
                  <services>
                  <!-- service element references the service type -->
                  <service type="MathService">
                  <!-- endpoint element defines the ABC's of the endpoint -->
                  <endpoint
                  address="http://localhost/MathService/Ep1"
                  binding="wsHttpBinding"
                  contract="IMath"/>
                  </service>
                  </services>
                  </system.serviceModel>
                  </configuration>
                
Invio di messaggi a un endpoint

Nel codice riportato di seguito sono illustrati due modi per inviare un messaggio all'endpoint IMath. SendMessageToEndpoint nasconde la creazione del canale, che avviene dietro le quinte, mentre nell'esempio SendMessageToEndpointUsingChannel avviene in modo esplicito.

Nel primo esempio in SendMessageToEndpoint vengono utilizzati uno strumento denominato svcutil.exe e i metadati del servizio per generare un contratto (in questo caso IMath), una classe proxy (in questo caso MathProxy) che implementa il contratto e la configurazione associata (che qui non viene riportata). Di nuovo, il contratto definito da IMath specifica il cosa (ossia le operazioni che è possibile eseguire), mentre la configurazione generata contiene un'associazione (il come) e un indirizzo (il dove).

Per utilizzare questa classe proxy è sufficiente crearne un'istanza e chiamare il metodo Add. Dietro le quinte, la classe proxy creerà un canale e lo utilizzerà per comunicare con l'endpoint.

Il secondo esempio in SendMessageToEndpointsUsingChannel, in basso, illustra la comunicazione con un endpoint mediante l'utilizzo diretto di ChannelFactory. Nell'esempio viene creato un canale utilizzando direttamente ChannelFactory<IMath>.CreateChannel invece di una classe proxy e la configurazione. Inoltre, invece di utilizzare la configurazione per definire l'indirizzo e l'associazione dell'endpoint, il costruttore ChannelFactory<IMath> assume le due informazioni come parametri. La terza informazione necessaria per definire un endpoint, vale a dire il contratto, viene passata come tipo T.

                  using System.ServiceModel;
                  //this contract is generated by svcutil.exe
                  //from the service's metadata
                  public interface IMath
                  {
                  [OperationContract]
                  public int Add(int x, int y)
                  { return x + y; }
                  }


                  //this class is generated by svcutil.exe
                  //from the service's metadata
                  //generated config is not shown here
                  public class MathProxy : IMath
                  {
                  ...
                  }

                  public class WCFClientApp
                  {
                  public void SendMessageToEndpoint()
                  {
                  //this uses a proxy class that was
                  //created by svcutil.exe from the service's metadata
                  MathProxy proxy = new MathProxy();

                  int result = proxy.Add(35, 7);
                  }
                  public void SendMessageToEndpointUsingChannel()
                  {
                  //this uses ChannelFactory to create the channel
                  //you must specify the address, the binding and
                  //the contract type (IMath)
                  ChannelFactory<IMath> factory=new ChannelFactory<IMath>(
                  new WSHttpBinding(),
                  new EndpointAddress("http://localhost/MathService/Ep1"));
                  IMath channel=factory.CreateChannel();
                  int result=channel.Add(35,7);
                  factory.Close();

                  }
                  }
                
Definizione di un comportamento personalizzato

La definizione di un comportamento richiede l'implementazione di IServiceBehavior (o, dal lato client, di IChannelBehavior). Il codice riportato di seguito illustra un esempio di comportamento che implementa IServiceBehavior. In IServiceBehavior.ApplyBehavior, viene esaminata la ServiceDescription e vengono trascritti l'indirizzo, l'associazione e il contratto di ogni ServiceEndpoint, nonché il nome di ciascun comportamento nella ServiceDescription.

Questo specifico comportamento costituisce anche un attributo (eredita da System.Attribute), rendendo possibile l'applicazione in modo dichiarativo, come verrà illustrato più avanti. Tuttavia, i comportamenti non devono necessariamente essere attributi.

                  [AttributeUsageAttribute(
                  AttributeTargets.Class,
                  AllowMultiple=false,
                  Inherited=false)]
                  public class InspectorBehavior : System.Attribute,
                  System.ServiceModel.IServiceBehavior
                  {
                  public void ApplyBehavior(
                  ServiceDescription description,
                  Collection<DispatchBehavior> behaviors)
                  {
                  Console.WriteLine("-------- Endpoints ---------");
                  foreach (ServiceEndpoint endpoint in description.Endpoints)
                  {
                  Console.WriteLine("--> Endpoint");
                  Console.WriteLine("Endpoint Address: {0}",
                  endpoint.Address);
                  Console.WriteLine("Endpoint Binding: {0}",
                  endpoint.Binding.GetType().Name);
                  Console.WriteLine("Endpoint Contract: {0}",
                  endpoint.Contract.ContractType.Name);
                  Console.WriteLine();
                  }
                  Console.WriteLine("-------- Service Behaviors --------");
                  foreach (IServiceBehavior behavior in description.Behaviors)
                  {
                  Console.WriteLine("--> Behavior");
                  Console.WriteLine("Behavior: {0}", behavior.GetType().Name);
                  Console.WriteLine();
                  }
                  }
                  }
                
Applicazione di un comportamento personalizzato

Tutti i comportamenti possono essere applicati in modo imperativo, aggiungendone un'istanza alla ServiceDescription (o, dal lato client, alla ChannelDescription). Ad esempio, per applicare InspectorBehavior in modo imperativo si dovrebbe scrivere:

                  ServiceHost sh = new ServiceHost(typeof(MathService));
                  sh.AddServiceEndpoint(
                  typeof(IMath),
                  new WSHttpBinding(),
                  "http://localhost/MathService/Ep1");
                  //Add the behavior imperatively InspectorBehavior behavior
                   = new InspectorBehavior(); sh.Description.Behaviors.Add(behavior);
                  sh.Open();
                

In più, i comportamenti che ereditano da System.Attribute possono essere applicati al servizio in modo dichiarativo. Poiché, ad esempio, InspectorBehavior eredita da System.Attribute, è possibile applicarlo in modo dichiarativo, come segue:

                  [InspectorBehavior]
                  public class MathService : IMath
                  {
                  public int Add(int x, int y)
                  { return x + y; }
                  }
                

Riepilogo

I servizi WCF espongono un insieme di Endpoint, ciascuno dei quali rappresenta un portale per comunicare con il mondo. Ogni endpoint presenta un indirizzo, un'associazione e un contratto. L'indirizzo indica dove risiede l'endpoint, l'associazione indica come l'endpoint comunica e il contratto indica cosa l'endpoint comunica.

Nel servizio, una ServiceDescription contiene l'insieme di ServiceEndpoints, ciascuno dei quali descrive un endpoint esposto dal servizio. Da questa descrizione, ServiceHost crea un runtime che contiene un EndpointListener per ogni ServiceEndpoint della ServiceDescription. L'indirizzo, l'associazione e il contratto (che rappresentano il dove, il cosa e il come) corrispondono rispettivamente all'indirizzo in attesa, al filtro e invio dei messaggi e allo stack di canale di EndpointListener.

Analogamente, nel client una ChannelDescription contiene l'unico ServiceEndpoint con cui il client comunica. Da questa ChannelDescription, ChannelFactory crea lo stack di canale in grado di comunicare con l'endpoint del servizio.

Mostra: