Wcf, i Known Service Type e non solo :-)
probabilmente perchè “MyService” è il nostro strato di business.var p = new Person(); var svc = new MyService( new MyRepository() ); svc.Save( p );
Capita che quella struttura sia fatta in tal modo perchè ad esempio MyService ha della logica di contorno, come ad esempio la gestione degli errori che deve stare sempre “intorno” a MyRepository, si lo so è un esempio un po’ stupido ma sto cercando di tenere basso il profilo e inoltre un esempio migliore non mi viene :-)
Visto che la logica di contorno è comune potrebbe avere senso un refactoring del tipo:
Che cosa è cambiato? che abbiamo un engine, MyService, che ha la logica di contorno e delega all’esterno l’esecuzione della logica vera e propria.var p = new Person(); var svc = new MyService(); svc.Execute( p, ( entity, repository ) => { repository.Save( entity ); } );
Distributed, distributed, distributed…
Bello direte voi… ma che ci azzecca con il titolo del post? ok, complichiamoci un po’ la vita, altrimenti non c’è gusto ;-) diciamo che MyService deve essere un servizio Wcf… è ovvio che quella sintassi non potrà mai funzionare, ma il concetto non cambia di molto:
- abbiamo un endpoint che è un mero esecutore, o meglio un passacarte;
- abbiamo un set di messaggi (quello che nel nostro esempio di cui sopra sono delegati) che possono viaggiare avanti e indietro passando da e per il nostro passacarte;
- abbiamo degli esecutori che conoscono i messaggi e sanno cosa farci e soprattutto come;
Che in codice potrebbe essere espresso così:
A cui per chiarezza ho tolto gli attributi del service contract di Wcf, vi ricorda qualche cosa? :-).public interface IMessageBrokerService { ResponseMessage[] Dispatch( params TwoWayRequestMessage[] requests ); void Broadcast( params OneWayRequestMessage[] requests ); }
ndr: perchè degli array e non delle istanze singole? perchè il chiamante ha bisogno di poter inoltrare un set di richieste che devono essere eseguite in maniera atomica/seriale (transazionale diremmo noi), e non in parallelo, senza che la transazione sia aperta dal client e si trasformi quindi in una distribuited transaction che in questo scenario rischia di diventare il male assoluto.Il primo problema da affrontare è che naturalmente nel nostro mondo non viaggeranno istanze dirette di TwoWayRequestMessage o di ResponseMessage, che sono classi astratte, ma piuttosto classi concrete come SearchCustomerRequestMessage che trasporterà le informazioni per la ricerca e si aspetterà come risposta una CustomersSearchResultMessage; l’inghippo è che a Wcf questa cosa non piace per nulla e a runtime il DataContractSerializer va dal DataContractResolver e gli chiede di dargli qualcosa per serializzare quel robo (aka messaggio) che sta cercando di passare il boundary. Naturalmente per noi non ha nessun senso scrivere dei serializer ad hoc per ogni messaggio, o meglio non è detto che lo abbia se il messaggio è naturalmente serializzabile di suo.
ServiceKnownTypeAttribute
L’infrastruttura di Wcf ci viene in aiuto e ci permette di decorare i metodi di un servizio con uno o più attributi ServiceKnownType con cui specifichiamo quali sono i tipi noti che possono passare da li. A prima vista però potrebbe esserci un problema questi tipi devono essere noti a compile-time ma in un’applicazione a plugin non ha molto senso perchè con la nascita di un nuovo plugin che probabilmente si porta dietro i suoi messaggi il servizio andrabbe ricompilato… ServiceKnownType ha però un costruttore pensato proprio per questo scenario:
Stiamo dicendo a Wcf: quando hai bisogno di sapere i ServiceKnownType vai dalla nostra classe ServiceKnownTypesProvider e chiama il metodo GetServiceKnownTypes che deve essere definito così:[ServiceKnownType( "GetServiceKnownTypes", typeof( ServiceKnownTypesProvider ) )] ResponseMessage[] Dispatch( params TwoWayRequestMessage[] requests );
Wcf vi passa le informazioni relative a metodo del servizio che sta indagando e si aspetta in cambio una lista di tipi che lui dovrà considerare come “noti” per quello specifico metodo.static IEnumerable<Type> GetServiceKnownTypes( ICustomAttributeProvider provider )
{
...
}
E’ quindi possibile pensare di esternalizzare questa configurazione e iniettarla a runtime all’avvio del servizio? certo che si :-), ne parleremo quando parleremo di custom service hosting di un servizio Wcf.
L’importante è che la stessa cosa deve naturalmente essere fatta anche lato client altrimenti anche li il processo di serializzazione/deserializzazione fallisce.
Uff… perchè non c’è mai nulla di immediato :-)
Il concetto di ServiceKnownType funziona perfettamente anche lato client e quindi anche qui è possibilissimo esternalizzare la configurazione e iniettarla a runtime ma non è tutto ora quel che luccica… grrrr :-)
vediamo il nostro scenario: il client è un’applicazione Silverlight che usa Prism sfruttando la possibilità di caricare on-demand i moduli, siccome l’applicazione ha un nutrito, molto nutrito, set di plugin si è deciso di sfruttare i moduli di Prism per ospitare anche i plugin e realizzare un plugin manager che mascheri e renda trasparente il fatto che i plugin sono hostati un po’ ovunque e vengano scaricati on-demand anch’essi. Per la cronaca “it works like a charme” :-)
Ma c’è un problema, che ci ha fatto dannare non poco…
Wcf fa una cosa molto sensata che è cachare la Channel Factory…un passo indietro…
Come funziona il robo?
Alla prima richiesta di creazione di un proxy (aka il client del servizio) viene avviata la configurazione di tutto il mondo, il client si appoggia per la comunicazione ad un ClientChannel e per crearne uno si rivolge ad una ChannelFactory. La ChannelFactory viene creata la prima volta e cachata perchè finchè non cambia la configurazione non ha senso sprecare risorse inutilmente… ecco… da qualche parte nei meandri della ChannelFactory c’è il processo di lettura dei “Service Known Type” ergo? non è possibile aggiungere Service Known Type a runtime dopo che la Channel Factory è stata creata e cachata…ma questa cosa cozza, e pure di molto, con il fatto che il nostro client Silverlight scarica i plugin on-demand e questi ultimi hanno ovviamente bisogno di aggiungere Service Known Type a loro volta on-demand…
La soluzione, sporchissima ma perfettamente funzionante e con un impatto prestazionale assai trascurabile, in questo momento è quella di far credere a Wcf che la configurazione sia cambiata:
Si si… siamo un po’ buffaldini e pure un po’ sacripanti ;-), cambianodo infatti qualsiasi cosa dell’endpoint facciamo credere a Wcf che la configurazione sia cmabiata e che probabilmente ha senso ricostruire la ChannelFactory: tribute to Gioffy that turned on the light :-)protected void ResetConfiguration() { var tmp = this.Endpoint.Name; this.Endpoint.Name = "_" + System.Guid.NewGuid().ToString(); this.Endpoint.Name = tmp; }
Proxy perchè sei tu proxy… :-)
ma… probabilmente qualcuno si chiederà: ok, ma dove l’avete messo quel codice? nel proxy generato lato client, rispondiamo noi… e come fate visto che il proxy è generato? semplice, non è generato ma è compilato a monte.
Il tutto si rende necessario non tanto per poter gestire quello di cui sopra ma perchè abbiamo bisogno che svcutil.exe non metta il naso nei nostri messaggi generando i suoi dto, ergo c’è un assembly condiviso che contiene sia i tipi di base dei messaggi sia proxy “pregenerati” da noi.
Quindi?
adesso siamo in grado di fare questo, dal nostro simpatico client Silverlight:
.mvar msg = new LogonRequestMessage( username, password ); var messages = new TwoWayRequestMessage[] { msg };
var client = new AsyncWcfMessageBrokerServiceClient(); client.DispatchAsync( messages, callback => { LogonResponseMessage response; if( callback.TryGetResponse( out response ) ) { var isLogged = response.IsSignedOn; } } );
p.s. giusto per dare a cesare quel che è di cesare l’idea originale di sposare il concetto di broker con wcf è di Roberto Messora, anche se lui probabilmente non lo sa :-)