Supponiamo di avere un servizio WCF, molto semplice in questo esempio, la cui interfaccia è la seguente:
[ServiceContract]
public interface ICalculation
{
[OperationContract]
int Calculate(int a, int b);
}
C’è un unico metodo che prende due interi e ne restituisce un terzo. Supponiamo adesso di scrivere il servizio WCF vero e proprio, cablando la logica del metodo Calculate per farlo funzionare come somma. Quindi:
public class Calculation : ICalculation
{
public int Calculate(int a, int b)
{
return a + b;
}
}
Primo refactoring. Spostiamo la logica della somma in una classe dedicata, che magari a sua volta implementa un’altra interfaccia che chiameremo IOperation.
public class Calculation : ICalculation
{
public int Calculate(int a, int b)
{
IOperation operation = new SumOperation();
var result = operation.Calculate(a, b);
return result;
}
}
Quello che accade è che ogni volta che viene invocato il metodo Calculate sul servizio WCF, viene creata una nuova istanza di una ipotetica classe SumOperation, che implementa IOperation, che effettua il calcolo ed il tutto torna al client. L’interfaccia IOperation è identica alla ICalculation, in questo caso, ma potrebbe avere una definizione diversa.
La cosa bella di questo approccio è che a questo punto posso creare classi come MultiplyOperation, SubtractOperation e così via. Basta cambiare il tipo istanziato nel servizio WCF, mentre tutto il resto rimane identico.
Passiamo al secondo refactoring.
public class Calculation : ICalculation
{
IOperation calculationEngine;
public Calculation()
{
this.calculationEngine = new MultiplyOperation();
}
public int Calculate(int a, int b)
{
var result = this.calculationEngine.Calculate(a, b);
return result;
}
}
Evitiamo di istanziare la classe ad ogni chiamata di Calculate. Definisco un field di tipo IOperation, la cui istanza concreta viene creata ed assegnata nel costruttore. Nell’esempio qui sopra creo un’istanza di MultiplyOperation, giusto per far capire che volendo posso switchare da un comportamento all’altro senza troppi problemi.
Quarto refactoring. Usiamo un motore di Dependency Injection, il cui scopo – detto in breve – è rimuovere tutte le new inserite nel codice (LoL).
Quello che mi piacerebbe ottenere è un codice come questo:
public class Calculation : ICalculation
{
IOperation operationEngine;
public Calculation(IOperation operation)
{
this.operationEngine = operation;
}
public int Calculate(int a, int b)
{
var result = this.operationEngine.Calculate(a, b);
return result;
}
}
In questo caso il costruttore stesso del servizio non è più parameter-less, ma prende in input un’istanza di un oggetto IOperation. Qualcuno deve fornire al servizio WCF questa istanza, e questo qualcuno è proprio un motore di Dependency Injection, nel nostro caso Castle Windsor.
La cosa non è così immediata, tra l’altro, perchè se provate adesso a mandare in esecuzione il servizio WCF qui sopra, sia attraverso il tool WcfTestClient.exe, oppure se tentate di raggiungere l’url del file .svc via browser, giustamente vi viene restituito un errore, perchè manca un costruttore parameter-less e quant’altro.
Quindi, ecco la soluzione.
- Con NuGet installate i pacchetti Castle Windsor e Castle Windsor WCF Integration Facility
- Al progetto aggiungete il file Global.asax
Modificate il codice della classe Global come segue:
public class Global : HttpApplication, IContainerAccessor
{
public IWindsorContainer Container { get; private set; }
protected void Application_Start(object sender, EventArgs e)
{
ServiceMetadataBehavior metadata = new ServiceMetadataBehavior();
metadata.HttpGetEnabled = true;
var Container = new WindsorContainer(new XmlInterpreter())
.AddFacility<WcfFacility>()
.Register(Component.For<ICalculation>()
.ImplementedBy<Calculation>());
}
}
La classe Global implementa l’interfaccia IContainerAccessor (di Castle Windsor), che richiede l’implementazione di una proprietà IWindsorContainer, che difatti abbiamo aggiunto. Nel codice di Application_Start viene istanziato il container di Windsor, e viene registrato il tipo ICalculation (l’interfaccia del servizio WCF), ed il tipo concreto Calculation. Il fatto di aver specificato XmlInterpreter nel costruttore del container ha un significato molto semplice: le registrazione dei tipi viene fatta leggendo il file web.config, che dobbiamo ancora gestire. Queste righe di codice vengono eseguite una sola volta, ovvero quando il servizio WCF parte.
Il web.config ha la seguente struttura:
<?xml version="1.0"?>
<configuration>
<configSections>
<section name="castle"
type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
</configSections>
<castle>
<components>
<component id="calc" service="CalculationEngine.IOperation, CalculationEngine"
type="CalculationEngine.SumOperation, CalculationEngine" />
</components>
</castle>
<appSettings>
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
</appSettings>
<system.web>
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5"/>
</system.web>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<protocolMapping>
<add binding="basicHttpsBinding" scheme="https" />
</protocolMapping>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
<directoryBrowse enabled="true"/>
</system.webServer>
</configuration>
Nulla di complicato. Abbiamo definito una sezione dedicata a Castle Windsor, e poi abbiamo registrato i tipi che vogliamo che vengano gestiti da questo motore di DI. Nel nostro caso il tipo è uno solo, quello a cui fa riferimento l’interfaccia IOperation. Lo riporto qui sotto per chiarezza:
<castle>
<components>
<component id="calc"
service="CalculationEngine.IOperation, CalculationEngine"
type="CalculationEngine.SumOperation, CalculationEngine" />
</components>
</castle>
L’attributo service indica il nome completo dell’interfaccia. L’attributo type indica il tipo concreto che implementa l’interfaccia. Da questo momento in poi Castle Windsor sa risolvere questa dipendenza: ogni volta che nel codice il costruttore di una classe richiederà un’istanza di IOperation, verrà restituita la classe SumOperation. Chiaramente, possiamo modificare questo setting e specificare qualsiasi altra classe che implementi quell’interfaccia, senza ricompilare il nostro codice.
L’ultima cosa da fare è editare il file .svc, impostandolo come segue:
<%@ ServiceHost Language="C#" Debug="true" Service="CalculationEngine.Calculation"
Factory="Castle.Facilities.WcfIntegration.DefaultServiceHostFactory, Castle.Facilities.WcfIntegration"
%>
Qui è importante il parametro Factory, dove abbiamo specificato la factory che Castle Windsor utilizza per i servizi WCF. Basta, abbiamo finito.
Proviamo ad eseguire?
Se adesso proviamo ad eseguire il nostro progetto, vedrete che tutto funziona bene. Cosa intendo? Intendo che parte il browser, potete navigare verso il file Calculation.svc, e quindi il servizio WCF è pronto all’uso. Questo significa che per esempio possiamo utilizzare il tool WcfTestClient passandogli l’url del servizio. Quello che accade è che Castle Windsor:
- Legge il file web.config e registra i tipi nel suo container, così è pronto per risolvere le dipendenze
- Sa che deve istanziare il servizio WCF: quando tenta di farlo, trova un costruttore che richiede un’istanza di qualcosa che implementi IOperation. Sa risolvere questa dipendenza, per cui passa al costruttore un’istanza di ciò che è specificato nel web.config (nel nostro caso SumOperation)
Fine. Il WCF si mette in attesa di qualcuno che invochi il metodo Calculate. Quando qualcuno lo fa, viene effettuato il calcolo. Ovviamente, è sufficiente modificare il web.config, specificando un altra classe IOperation, per cambiare il comportamento della nostra classe senza ricompilare nulla.
Magie della Dependency Injection!!
Qualsiasi commento e/o correzioni è sempre ben accetta.
Download della solution con Visual Studio 2012.