Gestione di una coda di mail in architettura SOA con WCF

Transcript

Gestione di una coda di mail in architettura SOA con WCF
F
OCUS
Gestione di una coda di mail
in architettura SOA con WCF
L’invio di email è una funzionalità molto diffusa nelle applicazioni moderne e la gestione
di più applicazioni che devono inviare email è una condizione presente in molte aziende.
di Omar Venturi
I
n questo articolo vengono analizzati alcuni scenari relativi ad applicazioni che inviano email
agli utenti e vengono descritti i passaggi principali per realizzare un servizio centralizzato con
gestione di una coda.
Vengono esaminate alcune delle caratteristiche dell’architettura SOA (Service Oriented Architecure) e
delle API di WCF (Windows Communication Foundation), evidenziando gli aspetti che semplificano lo
sviluppo e la gestione della comunicazione tra applicazioni e servizi.
L’articolo è di livello introduttivo e si rivolge a
chi, pur conoscendo la programmazione ad oggetti,
conosce poco il modello architetturale SOA e la
tecnologia WCF di Microsoft; l’obiettivo è di guidare
il lettore nella progettazione, nella realizzazione, ma
soprattutto nella comprensione di una soluzione
reale che utilizza questi strumenti.
Nella trattazione si vedrà il percorso ipotetico
affrontato da alcuni sviluppatori che, discutendo
Omar Venturi
[email protected]
Si occupa di progettazione e sviluppo di soluzioni E-Learning
e Cross Media Publishing con tecnologie Microsoft .NET.
Lavora come Project Manager per ONION S.p.A. – società
di servizi IT con sedi a Brescia, Milano e Torino. Laureato
presso l’Università degli studi di Milano in Tecnologie per
la società dell’informazione. È MPC (Microsoft Certified
Professional) dal 1999 e MCSD (Microsoft Certified Solution
Developer) dal 2001.
8
delle soluzioni adottate nelle loro applicazioni per
l’invio delle email, ne evidenziano le problematiche,
le analizzano, progettano una soluzione comune per
sostituire quelle esistenti ed infine la realizzano, soffermandosi sugli aspetti più importanti ed utilizzando
esempi del mondo reale.
L’invio di email è una funzionalità molto diffusa
nelle applicazioni moderne (si pensi ad esempio ad un
sito web che invia una mail agli utenti al termine del
processo di registrazione) e la gestione di più applicazioni che devono inviare email, spesso implementando soluzioni differenti, è una condizione presente
in molte aziende. In particolare, le caratteristiche
comuni di tali applicazioni sono:
• La mail rappresenta un punto all’interno di un
flusso di lavoro fondamentale per il completamento di un’attività complessa (registrazione
utenti, ordini, segnalazioni di eventi, etc);
• È indispensabile poter verificare se e quando la mail
è stata inviata;
• La presenza di soluzioni diverse per ciascuna applicazione può rappresentare un costo da ridurre.
Spesso le applicazioni si appoggiano a componenti
esterni per l’invio della mail, oppure sfruttano le
funzionalità più semplici messe a disposizione dai
framework applicativi. Inoltre, benché le code siano
uno strumento per migliorare l’affidabilità dell’invio
Computer Programming - n. 177 - Marzo 2008
SOA con WCF
della mail, non sempre le applicazioni le utilizzano;
talvolta perché si ritiene troppo onerosa l’integrazione, talvolta perché il componente o il framework
non le supporta.
Iniziamo quindi analizzando il caso della “Software
Meraviglioso S.p.A.”, software house specializzata
nella realizzazione di applicazioni web-oriented.
Software Meraviglioso è un’azienda organizzata in
molte aree, ognuna composta da più gruppi di lavoro.
Tutti i gruppi hanno sviluppato almeno una volta un
progetto per il quale è stato necessario inviare mail
agli utenti e ciascun team è soddisfatto della soluzione implementata.
Il problema
diverse.
Mentre i nostri tornano alle loro scrivanie, fermiamoci un momento a pensare alla situazione appena
descritta. Le casistiche analizzate non evidenziano
problematiche tecnologiche; è vero che qualche mail
talvolta non arriva a destinazione (Matthew e Luisa
hanno preferito non parlare di questo), ma tutto sommato questo non rappresenta un grosso problema per
le loro applicazioni.
Il problema è invece l’adozione di soluzioni differenti per coprire le stesse esigenze, con conseguente
aumento dei costi sia in fase di sviluppo che in quella
di manutenzione. Inoltre, ogni scenario presenta problematiche specifiche che proviamo a riassumere:
• l’utilizzo di componenti esterni è certamente una
buona soluzione poiché garantisce il riuso del
software. Tuttavia questa soluzione impone dei
vincoli sulla tecnologia utilizzata. Il progetto di
Claudio utilizza un componente COM, il progetto
di Luisa una classe Java ed il progetto di Matthew
una classe del framework .NET: il progetto ed i
suoi componenti devono utilizzare la stessa tecnologia;
• i componenti che non supportano le code scaricano
la responsabilità dell’invio della mail sull’applica-
Davanti alla macchinetta del caffè un giorno si sono
incontrati Claudio, Luisa e Matthew (appartenenti a
tre gruppi di lavoro differenti) e chiacchierando si
sono accorti che:
• Il progetto di Claudio utilizza un componente
COM chiamato SuperMail che supporta le code
ed ha un prezzo di 300$;
• Il progetto di Luisa, più recente, utilizza un componente open source chiamato YourPostman; è
disponibile la versione 0.95 per .NET
e la versione 0.98 per Java (quella utiFIGURA 1 - La soluzione è ospitata all'interno di due servizi Windows
lizzata da Luisa). La documentazione
è buona ed il componente è freeware.
Il supporto per le code sarà implementato con il prossimo rilascio;
• Il progetto di Matthew utilizza invece
il client SMTP presente nel namespace System.Net.Mail del framework
.NET. Questo componente non offre
supporto nativo per le code, ma il
gruppo di Matthew doveva realizzare
velocemente la funzionalità di invio
di password dimenticata (si erano
proprio dimenticati di implementarla!) ed hanno deciso che questa
era la soluzione migliore; avrebbero
pensato successivamente alla gestione
delle code.
Nessun problema funzionale quindi,
tutto il software gira che è una meraviglia, però tre gruppi diversi hanno
implementato tre soluzioni diverse per
lo stesso problema e, immaginano i
nostri amici, è probabile che anche gli
altri team abbiano adottato soluzioni
Computer Programming - n. 177 - Marzo 2008
9
F
OCUS
zione stessa. Cosa succede se il server di mail restituisce un errore durante l’invio?
• il codice infrastrutturale per l’utilizzo dei componenti non può essere riutilizzato: istanziare ed
utilizzare il componente COM richiede codice
differente rispetto a quello del componente .NET
o Java;
• implementare le funzionalità di invio di e-mail
per un progetto ha un costo che si ripete per ogni
progetto, con un valore diverso in funzione della
soluzione adottata.
“Un servizio! Ecco quello di cui abbiamo bisogno!”
esclama ad alta voce Luisa e l’indomani si confronta
con i suoi colleghi per valutare gli impatti che questa
soluzione comporta sulle applicazioni esistenti.
Luisa spiega che il loro problema è comune e facilmente risolvibile; la maggior parte delle applicazioni
presenti in azienda necessita di inviare una mail
e tutti implementano una soluzione diversa, con
diverse tecnologie.
“Non risparmieremmo tempo se in azienda fosse disponibile un sistema in grado di registrare le mail che le nostre
applicazioni devono inviare gestendo le code ed offrendo
anche meccanismi per il controllo degli errori, della pianificazione delle spedizioni e dei log? Le nostre applicazioni
vedrebbero l’invio della mail come un servizio esterno,
FIGURA 2 - Il diagramma delle classi della soluzione
senza preoccuparsi di quale componente è utilizzato o di
come è gestita la coda”.
“Certo, però non vedo vantaggi rispetto a quello che
ho implementato” – ribatte Claudio – “attualmente sto
utilizzando SuperMail e non mi preoccupo di come lui
gestisce le code. Semplicemente la mia applicazione crea
un’istanza dell’oggetto SuperMail.Message, gli assegna i
valori corretti e quindi chiama il metodo Send. Più facile
di così!”.
“Prova a pensare”, interviene Matthew “a cosa succederebbe se utilizzassi il tuo componente in altri progetti e
fra un po’ volessi passare alla nuova versione, magari per
.NET o Python. Dovresti sicuramente modificare le parti
di codice che utilizzano SuperMail, poiché i tuoi progetti
referenziano direttamente il componente. Se, come suggerisce Luisa, utilizzassimo un servizio esterno, le nostre
applicazioni non sarebbero più dipendenti dalla tecnologia
del componente per l’invio delle mail, che potrebbe quindi
essere aggiornato in maniera indipendente”.
“Beh, è un bel vantaggio, ma devo modificare tutto
quello che ho già scritto?” chiede Claudio.
“No”, risponde Luisa, “le applicazioni esistenti possono continuare con la soluzione attuale oppure passare
gradualmente alla nuova soluzione; dipende dal rapporto
tra costi e benefici. Questo è un altro dei vantaggi offerti
dall’architettura SOA: è possibile gestire gli aggiornamenti in base alle proprie esigenze”.
Quelli della Software Meraviglioso si
mettono quindi al lavoro ed iniziano a
progettare l’architettura della soluzione.
L’architettura ed il modello
I nostri bravi sviluppatori (che in
questo momento stanno ricoprendo il
ruolo di architetti) hanno pensato di
ospitare la soluzione all’interno di due
servizi Windows, come mostrato in
Figura 1:
• MailQueue_Service: è responsabile della scrittura e della lettura dei
messaggi dalla coda. Le applicazioni
che hanno bisogno di inviare e-mail
accedono a questo servizio utilizzando
due interfacce, delle quali parleremo
tra poco. Queste due interfacce sono
l’unico punto pubblico del servizio.
• MailQueue_Sender: è il servizio
responsabile di estrarre un messaggio
dalla coda e di inviarlo al mail server
configurato per quel messaggio (le mail
possono essere inviate utilizzando server
10
Computer Programming - n. 177 - Marzo 2008
SOA con WCF
differenti). Anche se questo servizio colloquia
direttamente con MailQueue_Service, è stato
disaccoppiato e caricato in un servizio diverso
e la comunicazione tra i due avviene attraverso
una terza interfaccia. In questo modo è possibile
sospendere il servizio che invia le mail (ad esempio
per manutenzione del server di mail), continuando
a registrare le mail nella coda.
più istanze delle altre classi: contiene un’istanza di
Message (il messaggio) che a sua volta può contenere
delle istanze di MessageSchedule (le regole di schedulazione). La busta può contenere inoltre degli allegati (MessageAttachmentBuffered) ed i parametri di
connessione al server (ApplicationConfiguration).
Infine, partendo dalla classe MessageStatus, la busta
può accedere all’elenco di eventi legati al messaggio,
relativamente all’invio (MessageAttempt) o alle
operazioni effettuate su di esso, come la creazione o
eventualmente la modifica (MessageHistory).
Introdotta l’architettura, vediamo ora il modello: le
classi utilizzate per modellare il dominio sono semplici
ed intuitive. Oltre alla classe principale Messaggio,
sono state definite altre classi che permettono alle
Analizziamo un po’ più in dettaglio le interfacce.
applicazioni di controllare il messaggio da inviare.
L’interfaccia IMailQueueWriter prevede solo il metodo
Innanzitutto ogni applicazione ha la possibilità
AddMessage(): questo è il metodo principale che
di definire quale server SMTP utilizzare per l’invio
permette di registrare un messaggio nella coda. La
dei messaggi (le applicazioni autorizzate all’invio
chiamata a questo metodo prevede due parametri:
dei messaggi sono registrate nel sistema), inoltre è
un’istanza della classe Message e la chiave dell’applipossibile specificare, per ogni messaggio, un insieme
cazione mittente (quest’ultima impedisce che applidi caratteristiche che influiscono direttamente sul
cazioni non registrate possano inviare messaggi).
posizionamento del messaggio nella coda.
Per scelta architetturale, si è deciso che per allegare
“Cosa? Le applicazioni possono controllare direttamente
documenti alla mail è necessario utilizzare un’inla posizione del messaggio nella coda?”
chiede Claudio sorpreso di questo strano
comportamento, “Non è esatto” risponde
FIGURA 3 - Schema del database utilizzato per memorizzare i messaggi
Matthew giustificando la scelta, “Le
applicazioni non possono definire la posizione del messaggio, ma per rendere più flessibile l’applicazione possono richiedere che il
messaggio sia inviato in uno specifico istante
temporale – usando la proprietà Planning
– oppure che venga inviato secondo una
schedulazione predefinita, usando la classe
MessageSchedule. Nel database i messaggi
sono sempre memorizzati in tabella secondo
l’ordine d’inserimento, ma da questa tabella
sono recuperati in ordine crescente rispetto
alla data di invio richiesta”.
“Comunque” continua Matthew “in
genere un’applicazione non richiede che
lo stesso messaggio sia inviato più volte.
Se i messaggi non contengono né una
pianificazione, né uno scheduling, la coda
si comporta come una classica FIFO: il
primo messaggio inserito è il primo inviato.
Abbiamo deciso di aggiungere queste
funzionalità per rendere più flessibile il
servizio”.
In Figura 2 è presentato il diagramma
delle classi della soluzione. La classe
MessageEnvelope (la busta) rappresenta
la radice del modello e contiene una o
Computer Programming - n. 177 - Marzo 2008
11
F
OCUS
terfaccia differente (IMailQueueAttachmentWriter).
Come si vedrà meglio tra poco, i metodi definiti in
queste interfacce possono essere attivati inviando dei
messaggi SOAP a determinate URL.
WCF controlla queste URL e può gestire indirizzi
e protocolli differenti per ciascuna di esse; in questo
modo si possono controllare separatamente le chiamate che inviano un semplice messaggio (poche
decine di KB) da quelle che trasferiscono megabyte.
È possibile ad esempio configurare il servizio affinché
la dimensione dei messaggi non superi i 100 KB
mentre quella degli allegati non superi i 20 MB.
La terza interfaccia, IMailQueueReader, è utilizzata
esclusivamente dal servizio MailQueue_Sender che
ad intervalli regolari interroga la coda chiamando il
metodo NextMessageToSend() il quale restituisce un
oggetto di tipo MessageEnvelope. Come descritto
poco sopra, questo oggetto contiene tutte le informazioni necessarie ad inviare la mail. Dopo aver
ottenuto la risposta dal server (positiva o negativa
che sia), MailQueue_Sender registra l’esito dell’invio utilizzando il metodo SetResponse(). Se l’esito è
negativo, il metodo NextMessageToSend() restituirà
nuovamente il messaggio per un nuovo tentativo; è
possibile controllare il numero di tentativi di invio
cui un messaggio può essere sottoposto tramite la
LISTATO 1 - Versione molto semplice della classe Message
public class Message
{
private int _id;
private int _attempts;
private string _subject;
private string _body;
private string[] _to;
private DateTime[] _planning;
public Message(
string Subject,
string Body,
string To,
string From,
DateTime DispatchDate)
{
_id = -1;
_attempts = int.MaxValue;
_subject = Subject;
_body = Body;
_to = new string[] {To};
_planning = new DateTime[]
{DispatchDate};
}
}
12
proprietà Attempts della classe Message.
La prima classe con WCF
Abbiamo definito l’architettura della soluzione,
abbiamo descritto le classi e sappiamo quali relazioni
esistono tra loro; possiamo iniziare a scrivere la classe
Message.
Il Listato 1 contiene una versione molto semplice
di questa classe dove, per brevità, sono stati omessi
alcuni campi. È stato inoltre definito un semplice
costruttore (attenzione a questo aspetto, lo approfondiremo nel paragrafo Messaggi ed oggetti) che
dovrebbe garantire la creazione di istanze coerenti
con la nostra logica.
Torniamo dai nostri amici ai quali si è aggiunto
Giorgio, un giovane sviluppatore che sta partecipando insieme a loro ad un brainstorming. Giorgio
sta cercando di capire quello che avviene dietro le
quinte quando un’applicazione si collega al servizio
e crea un’istanza della classe Message. Ha capito che
Message, definita in MailQueue_Service, deve in
qualche modo essere resa disponibile anche alla sua
applicazione affinchè questa possa crearne un’istanza.
Ascoltiamolo: “…in effetti non è affatto banale che la
mia applicazione possa istanziare una classe della quale
non possiede la definizione. La classe Message è definita
nel servizio, ma la mia applicazione deve comunque essere
in grado di crearla. Inoltre, una volta passata l’istanza al
servizio, la mia applicazione non deve più occuparsi di
come il messaggio venga inviato, giusto?”
“Esatto Giorgio”, risponde Matthew, “hai compreso
l’aspetto fondamentale di questa architettura. Per farti
un esempio, immagina di andare al ristorante ed ordinare
una pizza: non è sufficiente entrare, dobbiamo anche
essere in grado di ordinare quello che vogliamo secondo il
menu del ristorante. La pizza è il risultato che chiediamo
al ristorante (il servizio) e l’ordine è quello che dobbiamo
‘costruire e passare’ al cameriere. Dopo aver ‘costruito
l’ordine’, possiamo tranquillamente aspettare; a noi interessa mangiare, non come viene preparata la pizza”.
Poi continua “La soluzione che utilizza la tua applicazione per creare un’istanza di Message è analoga a quella
che utilizziamo noi per ordinare: leggiamo il menu. Non
dobbiamo andare dietro il banco e fare la pizza, e non
dobbiamo nemmeno assumere un pizzaiolo privato a casa
(cioè acquistare un componente tutto per noi), è sufficiente chiedere il servizio che vogliamo seguendo il menu
del ristorante”.
Fantastico! Adesso che abbiamo capito i vantaggi
Computer Programming - n. 177 - Marzo 2008
SOA con WCF
offerti dall’architettura orientata ai servizi ed i passi
fondamentali per scambiare dati con il servizio, siamo
pronti per guardare un po’ più da vicino gli aspetti
legati alla comunicazione.
Iniziamo con qualche domanda: come fa l’applicazione ad inviare i dati a MailQueue_Service?
MailQueue_Service gestisce direttamente la comunicazione con MailQueue_Sender e le applicazioni
oppure si appoggia ad altri componenti? Come è
possibile inviare un’istanza della classe Message, in
esecuzione su un server, ad un altro server sul quale è
in esecuzione MailQueue_Service?
Le risposte sono fornite da WCF, un insieme di
classi (un framework) che si occupa della comunicazione tra applicazioni. Utilizzando WCF, le nostre
applicazioni non devono occuparsi di aprire connessioni di rete, di gestire buffer di trasmissione, di serializzare gli oggetti o altri aspetti del genere, ma è WCF
che si occupa di tutto ciò.
WCF non è la prima né l’unica tecnologia per
la gestione semplificata della comunicazione tra le
applicazioni; i Web Service ad esempio erano la soluzione precedente prodotta da Microsoft. WCF si presenta come l’evoluzione dei Web Service con l’obiettivo di unificare, sotto un unico nome, le modalità
di comunicazione che utilizzano anche diversi protocolli come HTTP, TCP, MSMQ, NamePipe, etc.
Alla base di tutto ci sono i concetti di serializzazione,
SOAP e contratto: il primo è il processo con il quale
un oggetto viene salvato su un supporto oppure
inviato attraverso un canale di comunicazione (mettendo in sequenza i dati che rappresentano l’istanza),
LISTATO 2 - DataContract e ServiceContract
[DataContract]
public class Message
{
[DataMember(Name=”Id”)]
private int _id;
[DataMember(Name=”Attempt”)]
private int _attempts;
[DataMember(Name=”Subject”)]
private string _subject;
[DataMember(Name=”Body”)]
private string _body;
[DataMember(Name=”To”)]
private string[] _to;
[DataMember(Name=”Plannin”)]
private DateTime[] _planning;
/* Costruttore */
}
Computer Programming - n. 177 - Marzo 2008
mentre il secondo è uno standard per descrivere i
dati serializzati. Il contratto infine, utilizza i primi due
per astrarre e semplificare la descrizione dei servizi
offerti da un’applicazione, dei dati scambiati tra gli
attori coinvolti e di altro ancora. In WCF sono previste cinque tipologie di contratto che introduciamo
brevemente:
• Service contract: definisce le operazioni esposte
da un servizio, cioè il cosa viene offerto. IMailQueueWriter è definito ad esempio come service
contract (un service contract per il ristorante
potrebbe essere “si fanno pizze!”).
• Data contract: descrive gli oggetti utilizzati dal servizio e la loro struttura. Tutte le classi del nostro
modello ad oggetti che devono essere utilizzate
dalle applicazioni sono definite come Data Contract.
• Message contract: questo tipo di contratto è utilizzato per avere un controllo completo su come gli
oggetti viaggiano sulla rete, cioè su come vengono
serializzati e descritti con SOAP; in pratica permette di controllare il messaggio SOAP in tutti i
suoi aspetti. È un po’ come quando lasciate chiuso
il menu ed ordinate una pizza ingrediente per
ingrediente. Niente menu, gli ingredienti li scelgo
io!
• Fault contract: descrive i messaggi di errore e permette alle applicazioni di interpretarli correttamente.
• Callback contract: in genere la comunicazione parte
dall’applicazione richiedente (il consumer) ed
arriva al servizio. Questo contratto permette al
consumer di farsi chiamare dal servizio, stabilendo
una comunicazione bidirezionale ("Quando le pizze
sono pronte chiamami al 338.123456").
È importante chiarire un aspetto fondamentale
prima di proseguire: WCF è una tecnologia .NET
e richiede l’utilizzo di strumenti specifici per questo
framework. Tuttavia le applicazioni che utilizzano i
LISTATO 3 - Definizione di IMailQueueWriter
[ServiceContract]
public interfaceIMailQueueWriter
{
[OperationContract()]
int AddMessage(
Message Mail,
string ApplicationKey);
}
13
F
OCUS
servizi esposti tramite WCF possono essere realizzati
con qualunque tecnologia in grado di comunicare
con messaggi SOAP. Ecco perché, (riprendendo per
un istante l’inizio dell’articolo) mentre la scelta di un
componente è vincolata dalla tecnologia del progetto
(COM, Java, .NET, etc), l’utilizzo di un servizio è
svincolato dalla tecnologia dell’applicazione consumer.
Affrontiamo adesso un aspetto pratico, quale la
definizione dei contratti.
WCF fa ampio uso degli attributi .NET per questo
aspetto. Ogni tipologia di contratto prevede un
insieme definito di attributi con i quali decorare le
proprie classi. Il run-time di WCF legge questi attributi e crea automaticamente il codice necessario per
la pubblicazione del servizio. Vediamo come definire
un DataContract ed un ServiceContract (Listato 2).
LISTATO 4 - Configurazione del servizio per IMailQueueWriter
<services>
<service
behaviorConfiguration=”MQWB”
name=”MQService”>
<endpoint
address=”net.tcp://mypc:8000/mqW/”
binding=”netTcpBinding”
bindingConfiguration=”bndTCP”
contract=
“MailQueue.service.IMailQueueWriter”
/>
<service/>
<services/>
<behaviors>
<serviceBehaviors>
<behavior name=”MQWB”>
<serviceMetadata
httpGetEnabled=”True”
httpGetUrl=”http://mypc:8080/mqW”/>
</behavior>
<serviceBehaviors/>
<behaviors/>
<bindings>
<netTcpBinding>
<binding name=”bndTCP”
transferMode=”Buffered”
maxBufferSize=”2048”/>
</binding>
<netTcpBinding/>
<bindings/>
14
Il Data contract utilizza due attributi:
• DataContract: da utilizzare sulla classe (o sulla
struct)
• DataMember: da utilizzare sui campi o sulle proprietà
Si noti che i campi esposti con l’attributo DataMember sono definiti come privati: i modificatori
di visibilità non hanno effetto sul messaggio. Tutti i
campi (o le proprietà get/set) decorati con l’attributo
DataMember sono pubblicati con il proprio nome o
con quello definito dal parametro Name, indipendentemente dal fatto che siano dichiarati come privati.
Il Service contract utilizza invece gli attributi:
• ServiceContract: da utilizzare sulla classe o sull’interfaccia
• OperationContract: da utilizzare sui metodi
Si è già accennato al fatto che i consumer accedono ai servizi attraverso le interfacce. Queste sono
definite come Service Contract ed esposte pertanto
come servizi. IMailQueueWriter è definita ad esempio come mostrato nel Listato 3.
Naturalmente l’interfaccia è implementata da una
classe concreta (MailQueueWriter) responsabile di
LISTATO 5 - Attivazione del servizio
using System.ServiceModel;
using MailQueue.service;
public partial class MQHost:ServiceBase
{
private static ServiceHost _mqH = null;
protected override void
OnStart(string[] args)
{
_ mqH = new
ServiceHost(typeof(MailQueueWriter));
_ mqH.Open();
}
protected override void OnStop()
{
if (_mqH.State !=
CommunicationState.Closed)
_mqH.Close();
}
}
Computer Programming - n. 177 - Marzo 2008
SOA con WCF
memorizzare il messaggio nella coda. Per completezza
in Figura 3 è riportato lo schema del database utilizzato per memorizzare i messaggi; gli aspetti che si
riferiscono alla memorizzazione dei dati nelle tabelle
esulano dagli obiettivi di questo articolo. È sufficiente
in questo caso evidenziare l’esistenza di relazioni 11 tra le tabelle e le classi del modello (Message,
Application, Attachment, etc) per comprenderne le
logiche di memorizzazione principali. Si noti inoltre
che la tabella [MQ.MESSAGE.QUEUE] non ha una
classe relativa nel modello ad oggetti: questa tabella
contiene la coda ed è la tabella dalla quale estrarre un
messaggio ad ogni chiamata del metodo NextMessageToSend().
ABC: Address, Binding, Contract
Il nostro servizio è quasi pronto: abbiamo creato le
classi ed abbiamo definito i contratti; dobbiamo ora
preparare l’ambiente necessario a pubblicarlo.
Sappiamo che i nostri amici della “Software Meraviglioso” hanno deciso di esporre il servizio come un
Windows Service, ma non abbiamo detto ancora
niente sul protocollo utilizzato, su quale porta rimarrà
in ascolto e se, ad esempio, i messaggi necessitano di
criteri di protezione. Questi aspetti sono gestiti da
WCF e controllabili da un file di configurazione, il
che semplifica molto le attività di pubblicazione e
manutenzione del servizio.
L’infrastruttura di WCF che permette l’esecuzione
dei servizi è controllata da una tripletta di parametri
il cui acronimo è ABC (Address, Binding e Contract)
ed alla quale ci si riferisce con il nome "endpoint". Il
Contract lo abbiamo incontrato nel paragrafo precedente, vediamo ora cosa sono Address e Binding.
L’address è l’indirizzo (URL) al quale il servizio è
raggiungibile. Il formato dell’address è:
protocollo://server:porta/extension
Ad esempio http://localhost:9190/MailWriter è un
address valido per il nostro servizio, così come lo è
net.tcp://192.168.0.5:9191/MailRead.
Il binding è un elemento fondamentale di WCF
poiché oltre al protocollo di comunicazione controlla
un insieme di altre caratteristiche, quali ad esempio
la capacità di stabilire comunicazioni bidirezionali
(duplex) o a senso unico. Per un approfondimento di
questi aspetti si rimanda a [1].
Prima di procedere, torniamo per un istante
all’esempio della pizzeria ricordando che prima di
poter ordinare abbiamo chiesto il menu e lo abbiamo
Computer Programming - n. 177 - Marzo 2008
LISTATO 6 - Descrizione degli errori restituiti
[ServiceContract]
public interface IMailQueueWriter
{
[OperationContract()]
[FaultContract(typeof(MailException))]
[FaultContract(typeof(Exception))]
int AddMessage(
Message Mail,
string ApplicationKey);
}
letto. Anche i consumer della nostra applicazione
prima di poter comunicare con il servizio devono
richiedere il contract al servizio e leggerlo: in questo
caso il contract è scritto in WSDL, un linguaggio
derivato dall’XML per la descrizione delle interfacce dei servizi web. WCF, a differenza di quello
che accade con i Web Service, nasconde di default
il contratto; affinché questo possa essere letto dai
consumer è necessario impostare i valori appropriati
all’interno della sezione Behavior del file di configurazione. I consumer hanno bisogno del WSDL per
chiamare il servizio e lo possono leggere con una
chiamata HTTP-GET.
Vediamo come configurare il servizio per la classe
MailQueueWriter. Nel Listato 4 sono presenti tre
sezioni utilizzate per configurare gli elementi fondamentali del run-time di WCF:
• services: questa è la sezione principale utilizzata per
configurare l’endpoint del servizio. Questa sezione
utilizza valori definiti nelle altre due sezioni,
vedremo tra poco come sono costruiti i collegamenti tra le sezioni;
• behaviors: questa sezione è utilizzata in questo caso
per pubblicare il contratto WSDL. In particolare
il valore dell’attributo httpGetUrl rappresenta
l’indirizzo http dal quale il consumer può leggere il
WSDL;
• bindings: questa sezione controlla alcuni aspetti
di basso livello della comunicazione, come ad
esempio la dimensione del buffer utilizzato per la
comunicazione.
Si noti infine che tutti i parametri definiti nel
file di configurazione si possono impostare anche in
maniera programmatica, potendo in questo modo
realizzare delle combinazioni molto flessibili.
Il collegamento service-binding si ottiene confrontando il valore dell’attributo service/endpoint/
15
F
OCUS
@binding con il nodo figlio di bindings con lo stesso
valore.
Il collegamento service-behavior si ottiene invece
confrontando il valore dell’attributo service/@behaviorConfiguration con il valore dell’attributo
behavior/@name.
Il servizio configurato con questo file apre un socket
TCP all’indirizzo mypc:8000/mqW/ e pubblica il
proprio WSDL all’indirizzo http://mypc:8080/mqW.
Hosting del servizio
Si deve ora attivare il servizio ed attendere le
richieste inviate dai consumer. Per essere attivato,
un servizio deve essere ospitato all’interno di un processo controllato dal runtime di WCF (si pensi al servizio come ad una dll). Oltre ai servizi Windows che
utilizzeremo tra breve, un servizio può essere ospitato
ad esempio in una Console Application, in una Windows Forms Application, in IIS ed altri ancora (per
approfondimenti si veda [4]).
WCF fornisce la classe ServiceHost per la gestione
del servizio e dell’infrastruttura di comunicazione.
È sufficiente passare al costruttore di questa classe
il tipo del nostro servizio (in questo esempio MailQueueWriter) e WCF si fa carico di creare un’istanza
LISTATO 7 - Il polling di MailQueue_Sender
_smtp = new SmtpClient();
while (!_stop){ // polling
mailSnd =_client.NextMessageToSend();
while (mailSnd!= null){ // coda
try{
MailMessage mail = new MailMessage();
/* valorizza le proprietà della mail */
mail.Subject = mailSnd.Subject;
mail.Body = mailSnd.Body;
/* altre proprietà... */
smpt.Send(mail);
_client.SetResponse(
mailSnd.CurrentAttemptId,
string.Empty, true);
}catch (Exception ex) {
_client.SetResponse(
mailToSend.CurrentAttemptId,
ex.Message, false);}
mailSnd =_client.NextMessageToSend();
} // fine coda
Thread.Sleep(pollingSecond);
} // fine polling
16
e di passarle le richieste in accordo a quanto definito
nel file di configurazione.
Tutto quello che dobbiamo fare per attivare il servizio è presentato nel Listato 5 e riassunto di seguito:
1. Creare in Visual Studio 2005 un nuovo progetto di
tipo Windows Service;
2. Aggiungere un riferimento al componente .NET
System.ServiceModel;
3. Aggiungere un riferimento al progetto contenente
le classi che implementano i ServiceContract
(MailQueue.service);
4. Dichiarare una variabile di tipo ServiceHost;
5. Creare un’istanza di ServiceHost e ed aprire il servizio nel metodo OnStart() del Windows Service;
6. Chiudere il servizio nel metodo OnStop() del servizio.
Quando il servizio viene avviato, viene eseguito il
metodo OnStart nel quale viene creata un’istanza di
ServiceHost (gestita completamente da WCF) che
prepara l’infrastruttura di rete necessaria alla comunicazione con il servizio. La chiamata al metodo
Open() apre la comunicazione e mette il servizio in
attesa.
Messaggi ed oggetti
Affrontiamo ora il punto che abbiamo lasciato
aperto relativo al costruttore della classe Message.
Si affrontano qui alcuni argomenti di carattere generale che valgono anche al di fuori dell’applicazione
che stiamo realizzando. È importante comprenderli
poiché questi concetti hanno un impatto diretto sulle
classi e sull’architettura delle nostre applicazioni.
Scegliere di realizzare un’applicazione con architettura SOA offre certamente dei vantaggi, ma richiede
anche attenzione e consapevolezza poiché non è possibile applicare tutte le tecniche di programmazione
utilizzate ad esempio con un’architettura basata sui
componenti. Applicare un’architettura SOA senza
conoscerne le caratteristiche può portare a grossi
problemi strutturali o al fallimento del progetto. Per
ulteriori informazioni sull’architettura SOA si può
fare riferimento a [3].
Torniamo quindi al problema: l’obiettivo del
costruttore era di permettere al consumer di creare
un’istanza della classe Message coerente con la nostra
logica applicativa, garantendo quindi che tutte le
istanze di quella classe contenessero un valore per
i campi “corpo”, “mittente”, “oggetto” e “data di
spedizione”.
Riprendiamo per l’ultima volta l’esempio della
Computer Programming - n. 177 - Marzo 2008
SOA con WCF
pizzeria per capire il problema
LISTATO 8 - Come inserire un messaggio nella coda
del costruttore: sappiamo che
il menu descrive le pizze dispostatic void Main(string[] args)
nibili e gli ingredienti utilizzati
{
per ciascuna pizza, così come
int messageId;
il DataContract descrive le
mq.Message m = new mq.Message();
proprietà della classe. Menu e
m.From = “[email protected]”;
DataContract non descrivono
m.FromName = “Omar Venturi”;
il comportamento degli oggetti,
m.Html = false;
ma solo le loro caratteristiche.
m.Subject = “Test message #1”;
Ordinare una pizza “quattro
m.Body = “Hello World”;
formaggi senza gorgonzola”
m.To = new string[1];
significa, nell’ottica SOA, fare
m.To[0] = “[email protected]”;
una richiesta al pizzaiolo (il sermq.MailQueueServiceClient client = new mq.MailQueueServiceClient();
vizio) e non alla pizza (la classe)
che in questo caso è un oggetto
messageId = client.AddMessage(m, “DSKWSMPWPECFOC000334IKJ”);
“stupido”. Analogamente, per
}
verificare se una mail senza
destinatari è valida, è necessario
contattare il servizio, non la mail
al consumer che dovrà essere in grado di gestirlo.
stessa.
Come accennato in precedenza, il Fault Contract è
È chiaro quindi che lasciare il costruttore sulla
il tipo di contratto utilizzato per descrivere i messaggi
classe Message non ha senso in questo contesto
di errore che il servizio può restituire. I fault contract
perché Message non deve implementare alcuna
sono pubblicati attraverso il WSDL e di conseguenza
funzionalità; sarebbe come se la pizza che ordiniamo
i consumer li conoscono in anticipo e li possono
fosse in grado di rifiutarsi di cuocere se la combinagestire. Come mostrato nel Listato 6, FaultContract
zione di ingredienti richiesta non fosse valida!
è un attributo che va utilizzato insieme ad OperaQuesto è il punto: le classi che decoriamo con gli
tionContract ed aggiunge ai metodi una descrizione
attributi WCF non sono esposte come oggetti (cioè
di quali errori possono essere restituiti. Il parametro
come entità con delle caratteristiche e dei comportapassato è un DataContract (si veda il codice allegato
menti), ma come dei semplici messaggi. WCF non è
per maggiori dettagli) e pertanto il meccanismo di
una tecnologia che permette di serializzare oggetti e
inserimento nel WSDL è analogo per i DataContract
trasferirli attraverso la rete (questo è un compito che
e per i FaultContract, cambia solo la modalità di
può svolgere ad esempio una tecnologia come Remoattivazione.
ting del .NET Framework): WCF è una tecnologia
che si occupa di trasferire dati, non comportamenti.
Sollevare un’eccezione nel servizio è un’operazione
Quando un consumer legge il WSDL del servizio,
che richiede poche righe di codice:
non riesce a leggere il codice all’interno del costrut• creare un’istanza della classe MailException;
tore della classe (e nemmeno di un qualunque altro
• effettuare il throw di una nuova FaultException
metodo) e pertanto non può utilizzarli, poiché l’inalla quale si passa l’istanza del punto precedente.
frastruttura utilizzata non è semplicemente progettata per farlo. È necessario quindi implementare la
MailException me =
validazione dei dati nel servizio; nel nostro caso, la
MailException.Create(“Istanza non valida”);
validazione è realizzata all’interno del metodo Addthrow new
Message(), cioè del punto d’ingresso del servizio.
FaultException<MailException>(me);
Gestione degli errori
Se l’istanza del messaggio passata al metodo AddMessage() non è valorizzata correttamente, ad esempio non contiene alcun indirizzo di destinazione,
allora il servizio può restituire un messaggio d’errore
Computer Programming - n. 177 - Marzo 2008
Inviare un messaggio
A questo punto non ci resta che creare il servizio
MailQueue_Sender per l’invio dei messaggi ed un
semplice client per l’inserimento delle mail nella coda
17
F
OCUS
(nella realtà i client saranno più complessi, come ad
esempio applicazioni web).
La struttura di MailQueue_Sender è ancora un
progetto di tipo Windows Service. È in questo servizio che possiamo decidere la tecnologia da utilizzare
per inviare le mail: COM, Java, .NET o altro ancora.
Nel nostro caso utilizziamo il client SMTP di .NET.
Dopo aver avviato il servizio MailQueue_Service,
da Visual Studio 2005 è possibile aggiungere una
Service Reference inserendo l’url del WSDL dell’endopoint che espone l’interfaccia IMailQueueReader:
Visual Studio legge il contratto (ecco perché il servizio deve essere attivo) e crea automaticamente una
classe proxy che si interfaccia con il servizio. Questa
classe, implementando l’interfaccia IMailQueueReader, espone il metodo NextMessageToSend() che
estrae dalla coda il primo messaggio.
MailQueue_Sender effettua un polling sul servizio
MailQueue_Service con una frequenza configurabile: se non ci sono messaggi pronti per essere inviati
si mette in attesa, altrimenti invia uno ad uno i
messaggi utilizzando per ognuno il server SMTP associato al messaggio, infine contatta nuovamente MailQueue_Service per registrare l’esito dell’operazione,
utilizzando il metodo SetResponse, come mostrato
nel Listato 7.
Vediamo infine come inserire un messaggio nella
coda. Creiamo un progetto di tipo Console Application (ricordiamoci di registrare questa applicazione
nella tabella del DB delle applicazioni, assegnando
anche una chiave), aggiungiamo una Service Reference al contratto WSDL relativo all’interfaccia
IMailQueueWriter (se vogliamo allegare documenti
dobbiamo aggiungere una Service Reference anche
a IMailQueueAttachmentWriter), quindi istanziamo
un nuovo messaggio, valorizziamo le proprietà e passiamo l’istanza, insieme alla chiave dell’applicazione,
al metodo AddMessage, come mostrato nel codice
seguente e nel Listato 8.
static void Main(string[] args)
{
int messageId;
mq.Message m = new mq.Message();
m.From = “[email protected]”;
m.FromName = “Omar Venturi”;
m.Html = false;
m.Subject = “Test message #1”;
m.Body = “Hello World”;
m.To = new string[1];
m.To[0] = “[email protected]”;
mq.MailQueueServiceClient client =
new mq.MailQueueServiceClient();
messageId = client.AddMessage(
m, “DSKWSMPWPECFOC000334IKJ”);
}
Conclusioni
Abbiamo visto come l’architettura SOA definisca
linee guida che permettono di disaccoppiare i servizi
utilizzati dalle applicazioni consumer. Naturalmente
SOA non è la soluzione a tutti i problemi ed in
questo articolo abbiamo dato alcuni elementi per
capire quando può essere efficace questa scelta architetturale e quali problemi debbano essere affrontati
a seguito della sua adozione. Benché la tecnologia
WCF non sia stata creata esclusivamente per le
architetture SOA, ne è sicuramente un componente
molto importante che permette ai progettisti di concentrarsi su quelle che sono le problematiche di business dell’applicazione, demandando al framework le
problematiche di comunicazione.
CODICE ALLEGATO
ftp.infomedia.it
SOA-WCF
Sistema per la gestione di una coda centralizzata di email
completamente configurabile. Il progetto consiste di due
servizi Windows (uno per la ricezione dei messaggi ed uno
per l’invio) e di un database.
BIBLIOGRAFIA E RIFERIMENTI
[1] Juval Lowy – “Programming WCF Services”. O’Reilly, 2007
[2] Scott Klein – “Professional WCF Programming”, Wrox, 2007
[3] “Service Oriented Architecture (SOA) in the Real World”, http://msdn2.microsoft.com/en-us/architecture/
bb833022.aspx
[4] Chris Peiris, Dennis Mulder – “Hosting and Consuming WCF Services”, http://msdn2.microsoft.com/en-us/library/
bb332338.aspx
18
Computer Programming - n. 177 - Marzo 2008