Di solito l'uso di WCF lo associamo ad applicazioni di una certa dimensione che dialogare tra tier differenti. Tutto bene ma c'è un problema che WCF non risolve rispetto a tecnologie più vecchiotte come COM e Remoting: la trasparenza.
In COM e Remoting posso istanziare un oggetto in-process usandolo come se fosse stata fatta la new, quindi senza perdite di performance, oppure istanziarlo in un altro processo o su un altro pc. Dal punto di vista dell'utilizzatore, performance a parte, non c'è differenza cioè il suo uso è traparente.
WCF per definizione non può realizzare la stessa cosa, la trasparenza è negata da uno dei 4 tenets di SOA, evidenziati da Don Box in uno dei primi articoli su WCF. Mi riferisco in particolare al tenet "Boundaries are explicit" che sottolinea come l'uso di una tecnologia SOA-oriented implichi che quando un client decide di accedere ad un servizio, prende atto del fatto che attraversare i confini (boundaries) avrà un costo di cui dovrà tenere presente. Questa è probabilmente il primo fattore di cui tenere conto quando si decide se vale la pena creare un servizio per una certa funzionalità.
In sostanza quello dei confini è il fattore che deve farci pensare bene prima di spostare una funzionalità da un oggetto ad un servizio.
Questo non toglie che ci siano delle funzionalità che noi useremo sempre da remoto ma che qualche volta vogliamo usare 'in locale' senza l'uso del servizio stesso. Nel mio caso specifico ho un servizio che si occupa di eseguire binding e stampa di un complesso sistema di etichette. Il servizio si occupa di cercare la stampante più vicina all'operatore sulla base del formato dell'etichetta, di eseguire la sostituzione dei valori (binding) ed infine di eseguire il taglio del rotolo solo a fine job di stampa.
Tutto bene fino a che vogliamo utilizzare anche le stampanti locali senza installare il servizio localmente.
Ahi, la trasparenza è persa e WCF non ci aiuta. Come fare? Aggiungere la reference alla dll che esegue la stampa sarebbe un errore perché non replicheremmo esattamente ciò che succede via WCF. La soluzione ovviamente sta nel titolo del post ed è di creare un servizio WCF locale, in-process. Questo implica aggiungere la reference alla dll del servizio (si, proprio quella che verrebbe messa in deploy in IIS) visto che la nostra UI in questo caso farebbe da host al posto di IIS.
In questo specifico caso è tedioso e inutile usare WCF tramite configurazione visto che client e server sono in-process.
Ecco quindi alcune piccole classi helper (ridotte all'osso) che consentono di eseguire il lavoro sporco
Questo è il semplice contratto e implementazione del servizio, giusto per fare un test.
[ServiceContract]
public interface IMyService
{
[OperationContract]
string Test(bool b);
}
public class MyService : IMyService
{
public string Test(bool b)
{
return b.ToString();
}
}
Poi passiamo ad una classe helper che gestisce il server in process:
public class WCFServerSide : IDisposable
{
private ServiceHost _host;
private Type _contract;
private Type _implementation;
public WCFServerSide(Type contract, Type implementation)
{
_contract = contract;
_implementation = implementation;
}
public void Listen(Uri baseAddress, Uri address)
{
if (_host != null)
return;
try
{
_host = new ServiceHost(_implementation, baseAddress);
var binding = WCFBindingHelper.GetBinding(address.Scheme);
_host.AddServiceEndpoint(_contract, binding, address);
_host.Open();
System.Diagnostics.Debug.WriteLine("host is listening ...");
}
catch (Exception err)
{
System.Diagnostics.Trace.WriteLine(err.ToString());
}
}
public void Close()
{
try
{
if (_host == null || _host.State != CommunicationState.Opened)
return;
_host.Close();
}
catch (Exception) { }
finally
{
_host = null;
}
}
public void Dispose()
{
Close();
}
}
Poi ancora un helper che gestisce il client in-process:
public class WCFClientSide<T> where T : class
{
private EndpointAddress _address;
public Uri Address
{
get { return _address.Uri; }
set { _address = new EndpointAddress(value); }
}
public WCFClientSide()
{
}
public string Invoke(Action<T> action)
{
if (_address == null)
return "Address is empty";
try
{
T svc = default(T);
var binding = WCFBindingHelper.GetBinding(_address.Uri.Scheme);
using (ChannelFactory<T> factory = new ChannelFactory<T>(binding, _address))
{
svc = factory.CreateChannel();
action(svc);
}
}
catch (Exception e)
{
Trace.WriteLine(e);
return e.Message;
}
return null;
}
}
Il wrapper lato client è molto comodo perché usa una lambda per eseguire le chiamate al server, come vedremo nell'esempio più sotto.
Entrambe le classi helper usano un altro helper, creato per gestire il binding. Qui l'helper crea binding molto semplice, più che sufficienti per la maggior parte dei casi quando si lavora in-process.
public static class WCFBindingHelper
{
public static Binding GetBindingByAddress(string address)
{
return GetBindingByAddress(new Uri(address));
}
public static Binding GetBindingByAddress(Uri uri)
{
return GetBinding(uri.Scheme);
}
public static Binding GetBinding(string scheme)
{
switch (scheme)
{
case "net.pipe":
return GetNamedPipesBinding();
case "http":
return GetHttpBinding();
case "net.tcp":
return GetTcpBinding();
default:
throw new NotSupportedException();
}
}
private static Binding GetNamedPipesBinding()
{
NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
return binding;
}
private static Binding GetHttpBinding()
{
BasicHttpBinding binding = new BasicHttpBinding(BasicHttpSecurityMode.None);
return binding;
}
private static Binding GetTcpBinding()
{
NetTcpBinding binding = new NetTcpBinding(SecurityMode.None);
return binding;
}
}
Questo helper crea automaticamente i binding sulla base del protocollo presente nell'address, e gestisce:
- named pipes che è il protocollo più performante e più indicato quando client e server stanno sullo stesso PC
- net tcp che funziona solo WCF su WCF
- http (basic) che è il modo più semplice per gestire un binding http
Fatto questo, da una applicazione basta lanciare il server…
private WCFServerSide _server;
private void StartServer()
{
_server = new WCFServerSide(typeof(IMyService), typeof(MyService));
_server.Listen(new Uri("http://localhost:8000/raf"), new Uri("http://localhost:8000/raf"));
}
… e poi chiamarlo con il client
var client = new WCFClientSide<IMyService>();
client.Address = new Uri("http://localhost:8000/raf");
client.Invoke(c => Console.WriteLine(c.Test(true)));
Infine attenzione ai processi che fanno uso di SynchronizationContext come winform. Il server deve essere lanciato prima dell'avvio della message pump:
private static WCFServerSide _server;
[STAThread]
static void Main()
{
using (_server = new WCFServerSide(typeof(IMyService), typeof(MyService)))
{
_server.Listen( new Uri("http://localhost:8000/raf"), new Uri("http://localhost:8000/raf"));
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new DesignerForm());
}
}
Happy WCF coding!