L’altro giorno mi è capitato di avere una discussione su un problema di interazione fra sistemi. Immaginiamo uno scenario in cui un servizio A abbia bisogno delle informazioni esposte da un servizio esterno B che richiede un meccanismo di autenticazione ad hoc basato su un token generato da un terzo Web service. Il servizio B che detiene le informazioni, tra l’altro, può essere utilizzato da altri N sistemi, ciascuno dei quali dovrebbe implementare la stessa logica di autenticazione prima di effettuare la chiamata. Come fare per evitarlo?
La prima ipotesi è stata quella di un servizio wrapper che centralizzasse l’accesso ai dati, offrendo una nuova e più semplice API agli altri servizi. Questa soluzione però ha il problema di dover disegnare una API piuttosto complessa per via della tipologia dei dati da esporre. Inoltre, ogni modifica al servizio originale avrebbe comportato modifiche al servizio wrapper.
All’improvviso mi ha folgorato il ricordo di un esempio di service router che avevo visto quando WCF si chiamava ancora “Indigo”. Si trattava di un servizio che faceva da router trasparente della chiamata disinteressandosi totalmente dei dati di passaggio. Il servizio funzionava instradando la richiesta verso un endpoint X che in qualche modo era in grado di ricavare a runtime. Bene, lo scopo di questo articolo è mettere su un router ad hoc, sporcandosi un po’ le mani con l’object model di WCF. (Ma neanche tanto poi; basti pensare che per fare una cosa simile in ASMX bisognerebbe martellare un po’ più forte e il martello è più complicato da usare. :-)
Definiamo quindi la ricetta per il nostro piccolo esperimento di routing.
Supponiamo di avere un Web service di un qualche sistema legacy capace di scambiare dati solo in Basic Interoperability. Per dirla in soldoni, il servizio può essere chiamato solo usando basicHttpBinding come trasporto della chiamata. Supponiamo inoltre di volere dare ai fruitori di questo servizio anche la possibilità di utilizzare un binding più ricco come il wsHttpBinding. Detto fatto: mettiamo fra i client e il servizio target il nostro router che accetta entrambe le modalità di comunicazione preoccupandosi di correggere il tiro nel caso in cui i binding di andata e di ritorno non coincidano.

Realizziamo quindi il nostro router “discreto” che non mette il naso nel messaggio in arrivo, il servizio avrà un’interfaccia siffatta:
[ServiceContract]
public interface IServiceRouter
{
[OperationContract(Action="*",ReplyAction="*")]
System.ServiceModel.Channels.Message MessageRoute(
System.ServiceModel.Channels.Message value);
}
Notiamo le proprietà Action e ReplyAction sull’attributo OperationContract: a fronte di una qualunque operazione richiesta via SOAP, il metodo intercetterà la chiamata e restituirà la risposta.
Altra cosa fondamentale del metodo MessageRoute sono i parametri in ingresso e in uscita: la classe Message è la classe generica del messaggio. Diciamo che si tratta di una specie di classe Object, ma a livello di messaggio. In sintesi il servizio è un buco nero che accetta qualsiasi cosa in ingresso e restituisce qualsiasi cosa in uscita; per esso la struttura del messaggio è del tutto indifferente. Vediamo adesso qualche dettaglio della sua implementazione:
public class Router : IServiceRouter
{
public Message MessageRoute(Message value)
{
// recupera dal file di configurazione il nome dell’endpoint da utilizzare
string endpointName = ConfigurationManager.AppSettings["addresToRouteBasic"];
MessageVersion clientMsgVersion = value.Version;
var ch = new ChannelFactory(endpointName);
IServiceRouter router = ch.CreateChannel();
//Trasforma il messaggio per renderlo compatibile con il target service
Message toSend = TransformInputMessage(value);
//Invia il messaggio al target service
Message toReply = router.MessageRoute(toSend);
((IChannel)router).Close();
ch.Close();
//trasforma il messaggio in uscita per renderlo comprensibile al client
return TransformOutputMessage( toReply,clientMsgVersion) ;
}
private Message TransformInputMessage(Message value)
{
Message toSend;
if (value.Version.Envelope == System.ServiceModel.EnvelopeVersion.Soap11)
{
toSend = value;
}
else
{
//Trasforma il messaggio da inviare al servizion target in modo compatibile
// con SOAP 1.1 (basic httpbinding)
toSend = Message.CreateMessage(MessageVersion.Soap11,
value.Headers.Action, value.GetReaderAtBodyContents());
}
return toSend;
}
private Message TransformOutputMessage(Message value, MessageVersion version)
{
Message toSend;
if (value.Version == version)
{
toSend = value;
}
else
{
// trasforma il messaggio da restituire in una versione
// compatibile con il chiamante originale
toSend = Message.CreateMessage(version, value.Headers.Action,
value.GetReaderAtBodyContents());
}
return toSend;
}
}
Nell’implementazione del metodo MessageRoute bisogna osservare 3 cose fondamentali. La prima che salta all’occhio è: come fa il channel factory a creare il canale verso il servizio target? Il collegamento in realtà non è nel codice ma nel file di configurazione dell’host del servizio:
<configuration>
<appSettings>
<add key="addressToRouteBasic" value="BasicHttpBinding_IService1"/>
</appSettings>
:
<system.serviceModel>
:
<client>
<endpoint address="http://localhost:1080/Service1.svc"
binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService1"
contract="WcfServiceRouter.IServiceRouter" name="BasicHttpBinding_IService1" /> </client>
:
</system.serviceModel>
La chiave addressToRouteBasic indica il nome dell’endpoint da utilizzare per instradare la chiamata. Nella sezione l’attributo address specifica l’URI del servizio target e il contratto generico IServiceRouter che gli premette di avere le informazioni necessarie per gestire la comunicazione senza bisogno di conoscere il WSDL del servizio target o dei servizi target se gli endpoint fossero più di uno.
I due metodi di trasformazione verificano la versione del messaggio. Nel nostro esempio il messaggio potrebbe già arrivare in versione soap 1.1 dal chiamante. Se necessario, i metodi creano una nuova versione di messaggio con le specifiche che il target service si aspetta (TrasformInputMessage) e dopo la risposta del target service lo ritrasformano nella versione compatibile con le specifiche del binding del chiamante originale.
Il target service è del tutto ignaro dell’esistenza del router e non ha bisogno di accorgimenti particolari. Nè è necessario conoscere la struttura della sua interfaccia o dei suoi contratti. Come abbiamo visto il router ha solo bisogno delle informazioni di binding per instradare la chiamata.
Diamo un’occhiata al client. Possiamo generare la classe proxy tramite Visual Studio e usare il seguente codice:
var client = new ServiceReference1.Service1Client("wsbinding"); ServiceReference1.CompositeType t = new ServiceReference1.CompositeType();
t = client.GetDataUsingDataContract(t);
client.Close();
L’unico accorgimento sarà quello di modificare il binding. Nel nostro caso aggiungiamo la configurazione wsbinding all’ endpoint generato automaticamente per ServiceReference1.IService1. Inoltre, l’attributo address dell’endpoint punterà al router invece che al target.
<client>
<endpoint address="http://localhost:1041/Router.svc" binding="basicHttpBinding"
bindingConfiguration="BasicHttpBinding_IService1"
contract="ServiceReference1.IService1"
name="BasicHttpBinding" />
<endpoint address="http://localhost:1041/Router.svc/wsbinding"
behaviorConfiguration="" binding="ws2007HttpBinding"
bindingConfiguration="WSBinding"
contract="ServiceReference1.IService1" name="wsbinding" />
</client>
In questo modo il client è convinto di parlare con il servizio target (service1) mentre in realtà sta dialogando solo ed esclusivamente con servizio router
Un estratto di ciò che passa sul canale è visibile nel file allegato. Notate come la versione della busta soap è http://www.w3.org/2003/05/soap-envelope (soap 1.2) fra servizio client e router, mentre fra router e servizio target è http://schemas.xmlsoap.org/soap/envelope/ (soap 1.1).
Note finali
Questo esempio giocattolo di cui troverete il codice in allegato, non utilizza impostazioni di sicurezza a livello di messaggio fra client e router. La ragione è che bisogna fare delle considerazione aggiuntive in base al tipo di sicurezza che si desidera implementare e non basta più girare il messaggio cosi com’è ad ogni chiamata. Ma questa è un’altra storia.


Commenti