Inversion of Control con WCF e Unity
Nei giorni scorsi mi è capitato spesse volte di parlare con amici e colleghi di Inversion of Control (IoC) e WCF. Perciò mi sono deciso a pubblicare un breve post per spiegare i passaggi necessari per ottenere l’utilizzo di IoC con un servizio WCF. Nel caso specifico ho scelto Unity come framework per l’IoC.
Sono innanzitutto necessari 3 componenti:
- InstanceProvider: è il componente che si incarica di creare l’istanza del servizio. In definitiva è quello che userà direttamente Unity;
- ServiceBehavior: ci consente di creare l’istanza dell’InstanceProvider e di “agganciarla” al servizio corrente;
- ServiceHost: è il gestore del nostro servizio. Il ServiceHost ha il compito di configurare il servizio per l’utilizzo e quindi si occuperà di applicare il ServiceBehavior creato;
Avremo quindi rispettivamente: UnityInstanceProvider, UnityServiceBehavior e UnityServiceHost.
UnityInstanceProvider
Il primo passo è, come detto, quello di creare l’InstanceProvider. Il codice è in realtà molto semplice, almeno in questa prima fase:
1: internal class UnityInstanceProvider : IInstanceProvider
2: {
3: private readonly IUnityContainer container;
4: private readonly Type contractType;
5:
6: public UnityInstanceProvider(IUnityContainer container, Type contractType)
7: {
8: this.container = container;
9: this.contractType = contractType;
10: }
11:
12: public object GetInstance(InstanceContext instanceContext)
13: {
14: return GetInstance(instanceContext, null);
15: }
16:
17: public object GetInstance(InstanceContext instanceContext, Message message)
18: {
19: return container.Resolve(contractType);
20: }
21:
22: public void ReleaseInstance(InstanceContext instanceContext, object instance)
23: {
24: container.Teardown(instance);
25: }
26: }
Il costruttore accetta in input l’istanza del container ed il tipo di contratto dello specifico endpoint. Entrambi verranno poi utilizzati dal GetInstance per risolvere l’istanza sulla base del contesto. Il metodo può chiaramente essere migliorato.
UnityServiceBehavior
Il ServiceBehavior, come detto, si occupa di creare l’InstanceProvider per gli specifici endpoint del servizio:
1: public class UnityServiceBehavior : IServiceBehavior
2: {
3: private readonly IUnityContainer container;
4:
5: public UnityServiceBehavior(IUnityContainer container)
6: {
7: this.container = container;
8: }
9:
10: public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
11: {
12: }
13:
14: public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
15: {
16: }
17:
18: public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
19: {
20: foreach (ChannelDispatcher channelDispatcher in serviceHostBase.ChannelDispatchers)
21: {
22: foreach (EndpointDispatcher endpointDispatcher in channelDispatcher.Endpoints)
23: {
24: if (endpointDispatcher.ContractName != "IMetadataExchange")
25: {
26: string contractName = endpointDispatcher.ContractName;
27: ServiceEndpoint serviceEndpoint = serviceDescription.Endpoints.FirstOrDefault(e => e.Contract.Name == contractName);
28: endpointDispatcher.DispatchRuntime.InstanceProvider = new UnityInstanceProvider(this.container, serviceEndpoint.Contract.ContractType);
29: }
30: }
31: }
32: }
33: }
Il costruttore riceve dal ServiceHost l’istanza del container che viene poi assegnata al DispatchRuntime dell’EndpointDispatcher, selezionando anche il tipo di contratto che deve essere utilizzato per la risoluzione dell’istanza del servizio.
UnityServiceHost
Il passo finale è la creazione del ServiceHost, il componente che ci consente di configurare il servizio:
1: public class UnityServiceHost : ServiceHost
2: {
3: private IUnityContainer unityContainer;
4:
5: public UnityServiceHost(IUnityContainer unityContainer, Type serviceType) : base(serviceType)
6: {
7: this.unityContainer = unityContainer;
8: }
9:
10: protected override void OnOpening()
11: {
12: base.OnOpening();
13:
14: if (this.Description.Behaviors.Find<UnityServiceBehavior>() == null)
15: {
16: this.Description.Behaviors.Add(new UnityServiceBehavior(this.unityContainer));
17: }
18: }
19: }
Anche qui l’implementazione è davvero molto semplice. Il ServiceHost riceve nel costruttore l’istanza del container e contemporaneamente il tipo che identifica il servizio. Nell’override del metodo OnOpening, eseguito prima dell’apertura dei channels, il servicehost si occupa di verificare l’esistenza dell’UnityServiceBehavior e, se non trovato, lo istanzia passando a sua volta l’istanza del container.
Utilizzo in una applicazione console self-hosted
Ora siamo pronti per provare. Questa è la configurazione di esempio che utilizzeremo per il nostro servizio:
1: <?xml version="1.0" encoding="utf-8" ?>
2: <configuration>
3: <configSections>
4: <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration" />
5: </configSections>
6:
7: <unity>
8:
9: <containers>
10:
11: <container>
12: <types>
13:
14: <type type="DotNetSide.WCF.IoC.Console.InnerBookService, DotNetSide.WCF.IoC.Console"
15: name="inner"/>
16:
17: <type type="DotNetSide.WCF.IoC.Console.IBookService, DotNetSide.WCF.IoC.Console"
18: mapTo="DotNetSide.WCF.IoC.Console.BookService, DotNetSide.WCF.IoC.Console">
19: <typeConfig>
20: <constructor>
21: <param name="innerService" parameterType="DotNetSide.WCF.IoC.Console.InnerBookService, DotNetSide.WCF.IoC.Console">
22: <dependency name="inner" />
23: </param>
24: </constructor>
25: </typeConfig>
26: </type>
27:
28: </types>
29: </container>
30:
31: </containers>
32:
33: </unity>
34:
35: </configuration>
Molto semplicemente deve essere creata un’istanza dell’InnerBookService e passata come parametro nel costruttore del nostro servizio BookService, implementazione del nostro contratto IBookService:
1: [ServiceContract]
2: internal interface IBookService
3: {
4: [OperationContract]
5: Book ReadBook(string bookId);
6: }
7:
8: class BookService : IBookService
9: {
10: private readonly InnerBookService innerService;
11:
12: public BookService(InnerBookService innerService)
13: {
14: this.innerService = innerService;
15: }
16:
17: public Book ReadBook(string bookId)
18: {
19: return this.innerService.ReadBook(bookId);
20: }
21: }
22:
23: class InnerBookService
24: {
25: public Book ReadBook(string bookId)
26: {
27: return new Book()
28: {
29: Title = "WCF",
30: Description = "A book on Windows Communication Foundation"
31: };
32: }
33: }
Costruiamo quindi l’istanza del ServiceHost passando il container UnityContainer:
1: var unityContainer = new UnityContainer();
2: var configurationSection = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
3: configurationSection.Containers.Default.Configure(unityContainer);
4:
5: Uri serviceAddress = new Uri("http://localhost:9001/BookService");
6: Uri mexAddress = new Uri("http://localhost:9001/BookService/mex");
7: using (UnityServiceHost host = new UnityServiceHost(unityContainer, unityContainer.Resolve<IBookService>().GetType()))
8: {
9: host.Description.Behaviors.Add(new ServiceMetadataBehavior());
10: host.AddServiceEndpoint(typeof(IBookService), new BasicHttpBinding(), serviceAddress);
11: host.AddServiceEndpoint(typeof(IMetadataExchange), MetadataExchangeBindings.CreateMexHttpBinding(), mexAddress);
12:
13: host.Open();
14:
15: System.Console.Read();
16:
17: host.Close();
18: }
Abbiamo anche aggiunto un endpoint di tipo WS-Metadata Exchange, per la creazione del quale abbiamo anche evitato di utilizzare l’IoC nel UnityServiceBehavior, così possiamo anche creare il nostro client e testare il tutto.
Davvero molto semplice, non credete ?? 
Nei prossimi post aggiungerò qualche tassello per rendere più completo il discorso, nel frattempo aspetto i vostri feedback.
bye