Dependency Injection und WCF Service Interface

1. April 2015

In diesem Beitrag geht es um das Dependency Injection Pattern und die Fallstricke bei der Verwendung eines WCF Services und dessen Interface als abhängige Komponente. Die Nutzung eines WCF Services in einem Programm, nachdem man sich einen Proxy erzeugt hat, folgt in der Regel dem Schema a) Proxy-Klasse erzeugen, b) Service-Methode aufrufen und c) dann den Channel wieder sauber aufräumen.

   1: var client = new SampleWcfServiceReference.SampleWcfServiceClient("tcpEndpoint");

   2: var message = "DirectServiceCall";

   3: try

   4: {

   5:   var result = client.Method(message);

   6:   client.Close();

   7: }

   8: catch (Exception ex)

   9: {

  10:   client.Abort();

  11: }

Zu beachten ist hier, dass die Methoden Close und Abort durch die Proxy-Klasse, die durch svcutil erzeugt wird, implementiert werden, genauer durch die Basis-Klasse ClientBase<T>. Und im Fehlerfall sollte die Methode Abort aufgerufen werden.

Wird nun in einem Projekt auch das DI-Pattern genutzt und dafür das Castle Windsor Framework verwendet, dann ist im Sinne von Abhängigkeiten der WCF Service eine zu injizierende Abhängigkeit für eine Businesskomponente:

   1: public class LocalBusinessService : ILocalBusinessService

   2: {

   3:   ISampleWcfService _service;

   4:  

   5:   public LocalBusinessService(ISampleWcfService service)

   6:   {

   7:     _service = service;

   8:   }

   9:  

  10:   public void Process(string message)

  11:   {

  12:

  13:     var result = _service.Method(message);

  14:

  15:   }

  16: }

Das Bootstrapping und die Verwendung sehen wie folgt aus:

   1: var container = new Castle.Windsor.WindsorContainer();

   2: container.Register(Component.For<ISampleWcfService>()

   3:   .ImplementedBy<SampleWcfServiceClient>()

   4:   .DependsOn(Dependency.OnValue("endpointConfigurationName", "tcpEndpoint")));

   5: container.Register(Component.For<ILocalBusinessService>()

   6:   .ImplementedBy<LocalBusinessService>());

   7: var localService = container.Resolve<ILocalBusinessService>(); 

   8: try

   9: {

  10:   localService.Process("CastleWindsorCall");

  11: }

  12: catch (Exception ex)

  13: {

  14:   Console.WriteLine(ex.Message);

  15: }

  16: container.Dispose();

Problem an dieser Stelle ist, dass das Interface nicht die Close und Abort Methoden anbietet und der Lifecycle des Proxy vom DI Container gemanaged wird und somit das Aufräumen des Proxy von diesem vorgenommen wird.

Die Basisklasse BaseClient<T> implementiert auch das IDisposable Interface, welches vom Castle Windsor Container genutzt wird, wenn die Komponente aufgeräumt wird, es wird also ClientBase<T>.Dispose aufgerufen. Innerhalb von Dispose wird die Close Methode aufgerufen. Ist jedoch der CommunicationState Faulted, so wirft die ClientBase<T>.Dispose Methode bzw. die CommunicationObject.Close eine CommunicationObjectFaultedException mit der Nachricht "The communication object, System.ServiceModel.Channels.ServiceChannel, cannot be used for communication because it is in the Faulted state". Um genau zu sein, tritt dieses Szenario ein, wenn die Kommunikation mit dem WCF Service über net.tcp läuft und die Exception in der Methode keine im Contract definierte FaultException ist. Letztendlich ist dies allerdings sehr unschön, da diese Exception an einer Stelle auftritt, die nicht mehr kontrolliert werden kann. Welche Möglichkeiten gibt es nun, dieses Problem einzufangen?

1. Ansatz: Castle Windsor Möglichkeiten

Für den Abbau von Objekten bietet Castle Windsor bei der Registrierung die Hinterlegung einer Funktion über die OnDestroy Methode an. Allerdings heißt es hier auch im Abschnitt zu IDisposable

„Note that if your class implements IDisposable, then Dispose will automatically be called on the object before your custom destroy function is invoked.”

Damit scheidet dieser Ansatz aus.

2. Ansatz: Wrapping des WebServices-Proxies

Es wird nicht die generierte Service-Proxy als Implementierung des WCF-Interfaces genutzt, sondern eine Klasse, die das WCF-Interface, das IDisposible Interface implementiert und intern die generierte Service-Proxy Klasse nutzt. Die WCF-Interface Methoden werden 1:1 an den Proxy weitergeleitet und im Dispose-Fall wird in Abhängigkeit des CommunicationState Abort oder Close am Proxy aufgerufen.

   1: public class SampleWcfServiceWrapper : ISampleWcfService, IDisposable

   2: {

   3:   readonly SampleWcfServiceClient _client;

   4:  

   5:   public SampleWcfServiceWrapper()

   6:   {

   7:     _client = new SampleWcfServiceClient("tcpEndpoint");

   8:   }

   9:  

  10:   ~SampleWcfServiceWrapper()

  11:   {

  12:     Dispose(false);

  13:   }

  14:  

  15:   public string Method (string message)

  16:   {

  17:     return _client.Method(message);

  18:   }

  19:  

  20:   public void Dispose()

  21:   {

  22:     Dispose(true);

  23:     GC.SuppressFinalize(this);

  24:   }

  25:  

  26:   private void Dispose(bool isDisposing)

  27:   {

  28:     if (isDisposing)

  29:     {

  30:       if (_client.State == System.ServiceModel.CommunicationState.Faulted)

  31:         _client.Abort();

  32:       else

  33:         _client.Close();

  34:     }

  35:   }

  36: }

Das Bootstrapping sieht dann wie folgt aus:

   1: var container = new Castle.Windsor.WindsorContainer();

   2: container.Register(Component.For<ISampleWcfService>()

   3:   .ImplementedBy<SampleWcfServiceWrapper>());

   4: container.Register(Component.For<ILocalBusinessService>()

   5:   .ImplementedBy<LocalBusinessService>());

   6:  

   7: var localService = container.Resolve<ILocalBusinessService>();

   8: try

   9: {

  10:   localService.Process("CastleWindsorCallWithWrapper");

  11: }

  12: catch (FaultException<SampleWcfServiceReference.FaultInformation> ex)

  13: {

  14:   Console.WriteLine(ex.Detail.Message);

  15: }

  16: catch (Exception ex)

  17: {

  18:   Console.WriteLine(ex.Message);

  19: }

  20:  

  21: container.Dispose();

Dieser Ansatz funktioniert wunderbar, doch ist es allerdings mühevoll hier ein Klasse anlegen zu müssen, die mehr oder weniger eine 1:1 Kopie der Proxy-Klasse ist.

3. Ansatz: Bridging Ansatz

Um weniger Code schreiben zu müssen und trotzdem die Sicherheit zu haben, kann ein Bridge Ansatz gewählt werden. Zunächst kann ein generisches Interface definiert werden, welches das WCF-Interface als generischen Parameter hat, dieses Interface als einzige Property anbietet und von IDisposable ableitet.

   1: public interface IWcfServiceBridge<T> : IDisposable

   2: {

   3:     T Interface { get; }

   4: }

Die Implementierung des Interfaces sieht dann wie folgt aus

   1: public class WcfServiceBrigde<T> : IWcfServiceBridge<T> where T : class

   2: {

   3:   private System.ServiceModel.ClientBase<T> _client;

   4:  

   5:   public WcfServiceBrigde(System.ServiceModel.ClientBase<T> client)

   6:   {

   7:     _client = client;

   8:   }

   9:  

  10:   ~WcfServiceBrigde()

  11:   {

  12:     Dispose(false);

  13:   }

  14:  

  15:   public T Interface

  16:   {

  17:     get { return _client as T; }

  18:   }

  19:  

  20:   public void Dispose()

  21:   {

  22:     Dispose(true);

  23:     GC.SuppressFinalize(this);

  24:   }

  25:  

  26:   private void Dispose(bool isDisposing)

  27:   {

  28:     if (isDisposing)

  29:     {

  30:       if (_client.State == System.ServiceModel.CommunicationState.Faulted)

  31:         _client.Abort();

  32:       else

  33:         _client.Close();

  34:     }

  35:   }

  36: }

Zu beachten ist hier, dass im Konstruktor die Basisklasse ClientBase<T> als Parameter erwartet wird, also eine Service-Proxy Klasse. Das Interface wird über die Proxy-Klasse geliefert und die Dispose Methode des IDisposable Interfaces kann auf den CommunicationState des Proxy reagieren. Eine weitere Anpassung ist am lokalen BusinessService noch notwendig, damit dieser nicht mehr das WCF-Interface als Abhängigkeit erwartet, sondern das Bridge-Interface.

   1: public class LocalBusinessServiceUsingBridge : ILocalBusinessService

   2: {

   3:   ISampleWcfService _service;

   4:  

   5:   public LocalBusinessServiceUsingBridge(IWcfServiceBridge<ISampleWcfService> service)

   6:   {

   7:     _service = service.Interface;

   8:   }

   9:  

  10:   public void Process(string message, bool callFaultException)

  11:   {

  12:

  13:     var result = _service.Method(message);

  14:

  15:   }

  16: }

Bootstrapping und Verwendung dieses Ansatzes sieht wie folgt aus

   1: var container = new Castle.Windsor.WindsorContainer();

   2: container.Register(Component.For<ISampleWcfService>()

   3:   .ImplementedBy<SampleWcfServiceClient>()

   4:   .DependsOn(Dependency.OnValue("endpointConfigurationName", "tcpEndpoint"))

   5:   .Named("SampleService"));

   6: container.Register(Component.For<IWcfServiceBridge<ISampleWcfService>>()

   7:   .ImplementedBy<WcfServiceBrigde<ISampleWcfService>>()

   8:   .DependsOn(Property.ForKey<ClientBase<ISampleWcfService>>().Is("SampleService")));

   9: container.Register(Component.For<ILocalBusinessService>()

  10:   .ImplementedBy<LocalBusinessServiceUsingBridge>());

  11:  

  12: var localService = container.Resolve<ILocalBusinessService>();

  13: try

  14: {

  15:   localService.Process("CastleWindsorCallWithBridge");

  16: }

  17: catch (FaultException<SampleWcfServiceReference.FaultInformation> ex)

  18: {

  19:   Console.WriteLine(ex.Message);

  20:   Console.WriteLine(ex.Detail.Message);

  21: }

  22: catch (Exception ex)

  23: {

  24:   Console.WriteLine(ex.Message);

  25: }

  26:  

  27: container.Dispose();

Fazit

Bei der Verwendung von WCF-Services und dem DI-Pattern ist das Lifecycle Management des DI-Containers zu beachten. Probleme können beim Aufräumen der Komponenten im Container entstehen. Wie sie umgangen werden können, wurde hier gezeigt. Persönlich würde ich den Bridging Ansatz wählen, weil damit kein Nachbau des WCF-Interfaces notwendig ist und dieser Ansatz somit auch stabil gegenüber Änderungen am Interface in der Entwicklungsphase ist.