Foundations

Dienstbuspuffer

Juval Lowy

Beispielcode herunterladen

In meiner Kolumne vom Oktober 2009 zum Thema von Routern im Dienstbus (msdn.microsoft.com/magazine/ee335696) habe ich die Richtung skizziert, die vermutlich für den Windows Azure AppFabric-Dienstbus eingeschlagen wird – und zwar als ultimativer Interceptor. Ich habe die Routerfunktion vorgestellt und versprochen, als Nächstes über Warteschlangen zu schreiben.

Router und Warteschlangen wurden mittlerweile auf die Veröffentlichung der zweiten Version des Dienstbuses verschoben, und stattdessen werden über den Dienstbus bis auf Weiteres Puffer bereitgestellt. Zukünftige Versionen werden wahrscheinlich außerdem Protokollierungs- und Diagnosefunktionen und eine Reihe von Instrumentationsoptionen umfassen. Diese Aspekte werde ich in einem zukünftigen Artikel beleuchten. In diesem Artikel soll es um Puffer gehen. Außerdem erläutere ich einige erweiterte Programmierverfahren für Windows Communication Foundation (WCF).

Dienstbuspuffer

Für den Dienstbus handelt es sich bei den einzelnen URI im Dienstnamespace um adressierbare Messagingverbindungen. Clients können an diese Verbindungen Nachrichten senden, die von der Verbindung an die Dienste weitergeleitet werden. Die einzelnen Verbindungen können jedoch auch die Funktion eines Puffers erfüllen (siehe Abbildung 1).

Figure 1 Buffers in the Service Bus

Abbildung 1 Puffer im Dienstbus

Die Nachrichten werden für eine konfigurierbare Dauer im Puffer gespeichert, auch dann, wenn der Puffer von keinem Dienst überwacht wird. Der Puffer kann von mehreren Diensten überwacht werden, aber wenn die Nachricht nicht ausdrücklich geprüft und gesperrt wird, kann diese nur von einem Dienst abgerufen werden.

Der Client ist nicht an die Dienste für den Puffer gekoppelt, und Client und Dienst müssen nicht zur selben Zeit ausgeführt werden. Da vom Client mit einem Puffer und nicht mit einem echten Dienstendpunkt kommuniziert wird, verläuft der Nachrichtenfluss nur in eine Richtung, und es besteht keine Möglichkeit (zumindest keine standardmäßig vorgesehene Möglichkeit), die Ergebnisse für den Aufruf der Nachricht oder Fehler abzurufen.

Verwechseln Sie Dienstbuspuffer nicht mit Warteschlangen wie für MSMQ-Warteschlangen (Microsoft Message Queuing) oder WCF-Warteschlangendienste, denn es bestehen eine Reihe grundlegender Unterschiede:

  • Dienstbuspuffer sind nicht permanent, und die Nachrichten werden im Speicher gespeichert. Damit besteht das Risiko, dass Nachrichten bei einem (eher unwahrscheinlichen) Totalausfall des Dienstbuses verloren gehen.
  • Dienstbuspuffer sind keine Transaktionsressourcen – Nachrichten können nicht als Teil von Transaktionen gesendet oder abgerufen werden.
  • Nachrichten mit längerer Verweildauer können von den Puffern nicht behandelt werden. Sondern müssen von dem Dienst innerhalb von 10 Minuten aus dem Puffer abgerufen werden. Andernfalls werden die Nachrichten verworfen. MSMQ-basierte WCF-Nachrichten weisen zwar ebenfalls eine bestimmte Gültigkeitsdauer auf, aber diese ist wesentlich länger und standardmäßig auf einen Tag festgelegt. Damit ist eine wesentlich höhere Anzahl wirklich unabhängiger Vorgänge und getrennter Anwendungen möglich.
  • Puffer sind in ihrer Größe begrenzt und können höchstens 50 Nachrichten speichern.
  • Auch die gepufferten Nachrichten sind in der Größe begrenzt und werden jeweils bei 64 KB abgeschnitten. Auch bei MSMQ ist zwar eine maximale Nachrichtengröße festgelegt, aber diese ist wesentlich größer (4 MB pro Nachricht).

Damit werden mithilfe von Puffern über die Cloud keine echten eingereihten Aufrufe bereitgestellt, sondern vielmehr wird die Elastizität der Verbindung erhöht. Die Aufrufe stehen damit zwischen eingereihten Aufrufen und asynchronen Fire-and-Forget-Aufrufen („auslösen und vergessen“).

Puffer sind in zwei Szenarios sinnvoll. Eine Anwendungsmöglichkeit für Puffer sind Situationen, in denen für die Kommunikation zwischen Client und Dienst eine instabile Verbindung verwendet und toleriert wird, dass die Verbindung mehrfach getrennt und wiederhergestellt wird, sofern die Nachrichten für die kurze Zeit gepuffert werden, für die die Verbindung unterbrochen ist. In dem zweiten (und häufigeren) Szenario werden von einem Client asynchrone unidirektionale Aufrufe ausgegeben und ein Antwortpuffer (unten im Abschnitt zum Antwortdienst beschrieben) verwendet, um die Ergebnisse für die Aufrufe zu verarbeiten. Bei solchen Kommunikationsvorgängen funktioniert die Netzwerkverbindung wie ein elastisches Seil und nicht als starrer Draht ohne Speicherkapazität.

Arbeiten mit Puffern

Die Pufferadresse muss eindeutig sein. Einer Adresse kann nur ein Puffer zugeordnet sein, und die Adresse darf nicht bereits von einem Puffer oder Dienst verwendet werden. Es besteht jedoch die Möglichkeit, dass mehrere Parteien aus demselben Puffer Nachrichten abrufen. Darüber hinaus muss in der Pufferadresse für das Schema HTTP oder HTTPS verwendet werden. Zum Senden und Abrufen von Nachrichten über den Puffer umfasst der Dienstbus eine API, die System.Messaging ähnelt. Das heißt, dass Sie mit unformatierten Nachrichten umgehen. Der Dienstbusadministrator verwaltet die Puffer unabhängig von Diensten oder Clients. Für jeden Puffer muss eine Richtlinie vorhanden sein, über die dessen Verhalten und Gültigkeitsdauer festgelegt wird. Standardmäßig muss der Dienstbusadministrator programmgesteuerte Aufrufe ausführen, um Puffer zu erstellen und zu verwalten.

Die einzelnen Pufferrichtlinien werden anhand einer Instanz der MessageBufferPolicy-Klasse ausgedrückt, so wie in Abbildung 2 gezeigt.

Abbildung 2 MessageBufferPolicy-Klasse

[DataContract]
public class MessageBufferPolicy : ...
{
  public MessageBufferPolicy();
  public MessageBufferPolicy(MessageBufferPolicy policyToCopy);

  public DiscoverabilityPolicy Discoverability
  {get;set;}

  public TimeSpan ExpiresAfter
  {get;set;}

  public int MaxMessageCount
  {get;set;}

  public OverflowPolicy OverflowPolicy
  {get;set;}

  public AuthorizationPolicy Authorization
  {get;set;} 
  
  public TransportProtectionPolicy TransportProtection
  {get;set;}
}

Die Erkennbarkeitsrichtlinieneigenschaft ist eine Aufzählung vom Typ DiscoverabilityPolicy, über die Sie steuern, ob der Puffer in der Dienstbusregistrierung (ATOM-Feed) enthalten ist:

public enum DiscoverabilityPolicy
{
  Managers,
  ManagersListeners,
  ManagersListenersSenders,
  Public 
}

Die Standardeinstellung für die Erkennbarkeit ist DiscoverabilityPolicy.Managers. Das heißt, dass ein verwalteter Autorisierungsanspruch erforderlich ist. Wird stattdessen DiscoverabilityPolicy.Public festgelegt, erfolgt die Veröffentlichung im Feed ohne jegliche Autorisierung.

Die ExpiresAfter-Eigenschaft steuert die Gültigkeitsdauer von Nachrichten im Puffer. Diese ist standardmäßig auf 5 Minuten festgelegt. Der kleinste mögliche Wert ist 1 Minute, der größte zulässige Wert beträgt 10 Minuten. Versuche, eine längere Gültigkeitsdauer zu konfigurieren, werden still ignoriert.

Die MaxMessageCount-Eigenschaft begrenzt die Puffergröße. Standardmäßig sind für die Richtlinie 10 Nachrichten festgelegt, und der kleinste mögliche Wert beträgt natürlich 1. Wie bereits erwähnt, beläuft sich die maximal mögliche Puffergröße auf 50 Nachrichten, und Versuche, einen höheren Wert festzulegen, werden wiederum still ignoriert.

Die OverflowPolicy-Eigenschaft ist eine Aufzählung mit einem einzigen Wert, der wie folgt definiert ist:

public enum OverflowPolicy
{
  RejectIncomingMessage
}

OverflowPolicy steuert die Verarbeitung von Nachrichten für volle Puffer, das heißt für den Fall, dass die Pufferkapazität ausgeschöpft ist (von MaxMessageCount festgelegt). Es gibt nur eine Option, und zwar die Nachricht abzulehnen. Das heißt, dass diese mit einem Fehler an den Absender zurückgeleitet wird.

Die Einzelwertaufzählung dient als Platzhalter für spätere Optionen, beispielsweise, um Nachricht zu verwerfen, ohne den Absender zu informieren, oder um eine Nachricht aus dem Puffer zu entfernen und eine neue Nachricht zu akzeptieren.

Die letzten beiden Eigenschaften legen die Sicherheitskonfiguration fest. Die AuthorizationPolicy-Eigenschaft weist den Dienstbus in Hinblick darauf an, ob das Token des Clients akzeptiert werden soll:

public enum AuthorizationPolicy
{
  NotRequired,
  RequiredToSend,
  RequiredToReceive,
  Required
}

Standardmäßig ist für AuthorizationPolicy.Required festgelegt, dass sowohl für sendende als auch empfangende Clients eine Autorisierung erforderlich ist. 

Die TransportProtection-Eigenschaft schließlich legt den Mindestgrad an Sicherheit für die Übertragung von Nachrichten an den Puffer fest. Dafür wird eine Aufzählung vom Typ TransportProtectionPolicy verwendet:

public enum TransportProtectionPolicy
{
  None,
  AllPaths,
}

Für alle Pufferrichtlinien ist als Transportsicherheit standardmäßig TransportProtectionPolicy.AllPaths festgelegt. Dabei muss eine HTTPS-Adresse verwendet werden.

Mit der MessageBufferClient-Klasse können Sie den Puffer verwalten, so wie in Abbildung 3 gezeigt.

Abbildung 3 MessageBufferClient-Klasse

public sealed class MessageBufferClient
{
  public Uri MessageBufferUri
  {get;}

  public static MessageBufferClient CreateMessageBuffer(
    TransportClientEndpointBehavior credential,
    Uri messageBufferUri,MessageBufferPolicy policy);

  public static MessageBufferClient GetMessageBuffer(
    TransportClientEndpointBehavior credential,Uri messageBufferUri);
  public MessageBufferPolicy GetPolicy();
  public void DeleteMessageBuffer();

  // More members   
}

Sie können die statischen Methoden der MessageBufferClient-Klasse verwenden, um eine authentifizierte Instanz der MessageBufferClient-Klasse abzurufen, indem Sie für diese Methoden die Anmeldeinformationen für den Dienstbus bereitstellen (vom Typ TransportClientEndpointBehavior). Wenn Sie MessageBufferClient verwenden, müssen Sie normalerweise überprüfen, ob der Puffer bereits im Dienstbus vorhanden ist, indem Sie die GetMessageBuffer-Methode aufrufen. Ist kein Puffer vorhanden, löst GetMessageBuffer eine Ausnahme aus.

So erstellen Sie Puffer programmgesteuert:

Uri bufferAddress = 
  new Uri(@"https://MyNamespace.servicebus.windows.net/MyBuffer/");

TransportClientEndpointBehavior credential = ...

MessageBufferPolicy bufferPolicy = new MessageBufferPolicy();

bufferPolicy.MaxMessageCount = 12;
bufferPolicy.ExpiresAfter = TimeSpan.FromMinutes(3);
bufferPolicy.Discoverability = DiscoverabilityPolicy.Public;

MessageBufferClient.CreateMessageBuffer(credential,bufferAddress,
  bufferPolicy);

In diesem Beispiel instanziieren Sie ein Pufferrichtlinienobjekt und legen für die Richtlinie die gewünschten Werte fest. Um den Puffer zu installieren, müssen Sie nur die CreateMessageBuffer-Methode der MessageBufferClient-Klasse aufrufen und dabei die Richtlinie und gültige Anmeldeinformationen verwenden.

Statt programmgesteuerter Aufrufe können Sie auch die von mir entwickelte Anwendung Service Bus Explorer verwenden (den ich in meinem Artikel zu Routern vorgestellt habe und der online mit dem Beispielcode für diesen Artikel verfügbar ist), um Puffer anzuzeigen und zu ändern. In Abbildung 4 ist dargestellt, wie ein neuer Puffer erstellt wird, indem die entsprechende Adresse und die Richtlinieneigenschaften angegeben werden. Auf mehr oder weniger dieselbe Weise können Sie auch alle Puffer im Dienstnamespace löschen.

Figure 4 Creating a Buffer Using the Service Bus Explorer
Abbildung 4 Erstellen eines Puffers mit Service Bus Explorer

Darüber hinaus können Sie die Richtlinien für vorhandene Puffer überprüfen und ändern und Nachrichten aus dem Puffer und sogar Puffer selbst löschen, indem Sie die betreffenden Puffer in der Dienstnamespacestruktur löschen und die Puffereigenschaften im rechten Bereich verwenden, so wie in Abbildung 5 gezeigt.

Figure 5 A Buffer in the Service Bus Explorer
Abbildung 5 Ein Puffer in Service Bus Explorer

Optimieren der Verwaltung

Am besten ist es, möglichst große Puffer mit einer möglichst langen Gültigkeitsdauer zu erstellen, damit Clients und Diensten mehr Zeit für die Kommunikation zur Verfügung steht. Außerdem ist von Vorteil, Puffer als erkennbar zu konfigurieren, damit Sie den Puffer in der Dienstbusregistrierung anzeigen können. Was die Verwendung von Puffern betrifft, sollte sowohl vom Client als auch dem Dienst überprüft werden, ob der betreffende Puffer bereits erstellt ist und diesen ggf. erstellen, wenn der Puffer nicht vorhanden ist.

Zur Automatisierung dieser Schritte habe ich die ServiceBusHelper-Klasse erstellt:

public static partial class ServiceBusHelper
{    
  public static void CreateBuffer(string bufferAddress,string secret);
  public static void CreateBuffer(string bufferAddress,string issuer,
    string secret);

  public static void VerifyBuffer(string bufferAddress,string secret);
  public static void VerifyBuffer(string bufferAddress,string issuer,
    string secret);
  public static void PurgeBuffer(Uri bufferAddress,
    TransportClientEndpointBehavior credential);
  public static void DeleteBuffer(Uri bufferAddress,
    TransportClientEndpointBehavior credential); 
}

Die CreateBuffer-Methode erstellt einen neuen erkennbaren Puffer mit einer Höchstkapazität von 50 Nachrichten und einer Dauer von 10 Minuten. Sofern bereits ein älterer Puffer vorhanden ist, löscht CreateBuffer diesen. DieVerifyBuffer-Methode überprüft, ob ein Puffer vorhanden ist, und erstellt einen neuen Puffer, sofern dies nicht der Fall ist. PurgeBuffer ist nützlich, um für einen Diagnose- oder Debugvorgang alle gepufferten Nachrichten zu löschen. DeleteBuffer löscht den Puffer einfach. In Abbildung 6 ist ein Auszug der Implementierung dieser Methoden dargestellt.

Abbildung 6 Auszug aus den Pufferhilfsmethoden.

public static partial class ServiceBusHelper
{    
  public static void CreateBuffer(string bufferAddress,
    string issuer,string secret)
  {
    TransportClientEndpointBehavior credentials = ...;
    CreateBuffer(bufferAddress,credentials);
  }
  static void CreateBuffer(string bufferAddress,
    TransportClientEndpointBehavior credentials)
  {
    MessageBufferPolicy policy = CreateBufferPolicy();
    CreateBuffer(bufferAddress,policy,credentials);
  }
  static internal MessageBufferPolicy CreateBufferPolicy()
  {
    MessageBufferPolicy policy = new MessageBufferPolicy();                
    policy.Discoverability = DiscoverabilityPolicy.Public;
    policy.ExpiresAfter = TimeSpan.Fromminutes(10);
    policy.MaxMessageCount = 50;

    return policy;
  }
   public static void PurgeBuffer(Uri bufferAddress,
     TransportClientEndpointBehavior credentials)
   {
     Debug.Assert(BufferExists(bufferAddress,credentials));
     MessageBufferClient client = 
       MessageBufferClient.GetMessageBuffer(credentials,bufferAddress);
     MessageBufferPolicy policy = client.GetPolicy();
     client.DeleteMessageBuffer();
        
     MessageBufferClient.CreateMessageBuffer(credential,bufferAddress,policy);
   }
   public static void VerifyBuffer(string bufferAddress,
     string issuer,string secret)
   {
     TransportClientEndpointBehavior credentials = ...;
     VerifyBuffer(bufferAddress,credentials);
   }
   internal static void VerifyBuffer(string bufferAddress,
     TransportClientEndpointBehavior credentials)
   {
     if(BufferExists(bufferAddress,credentials))
     {
       return;
     }
     CreateBuffer(bufferAddress,credentials);
   }
   internal static bool BufferExists(Uri bufferAddress,
     TransportClientEndpointBehavior credentials)
   {
     try
     {
       MessageBufferClient client = 
         MessageBufferClient.GetMessageBuffer(credentials,bufferAddress);
       client.GetPolicy();
       return true;
     }
     catch(FaultException)
     {}
      
     return false;
   }
   static void CreateBuffer(string bufferAddress,
     MessageBufferPolicy policy,
     TransportClientEndpointBehavior credentials)
   {   
     Uri address = new Uri(bufferAddress);
     if(BufferExists(address,credentials))
     {
       MessageBufferClient client = 
         MessageBufferClient.GetMessageBuffer(credentials,address);
       client.DeleteMessageBuffer();
     }  
     MessageBufferClient.CreateMessageBuffer(credentials,address,policy);
   }
}

Die BufferExists-Methode verwendet die GetPolicy-Methode der MessageBufferClient-Klasse, um zu überprüfen, ob ein Puffer vorhanden ist, und interpretiert Fehler als Hinweis dafür, dass kein Puffer vorhanden ist. Sie bereinigen Puffer, indem Sie die Richtlinie kopieren, den Puffer löschen und einen neuen Puffer (mit derselben Adresse) mit der alten Richtlinie erstellen.

Senden und Empfangen von Nachrichten

Wie bereits erwähnt, können mit Dienstbuspuffern nur unformatierte WCF-Nachrichten übermittelt werden. Dafür werden die Send- und Retrieve-Methoden der MessageBufferClient-Klasse verwendet (Abruf beim Erstellen oder Abrufen eines Puffers):

public sealed class MessageBufferClient
{
  public void Send(Message message);
  public void Send(Message message,TimeSpan timeout);

  public Message Retrieve();
  public Message Retrieve(TimeSpan timeout);

  // More members
}

Für beide Methoden muss eine Zeitüberschreitung konfiguriert werden, die für die parameterlosen Versionen standardmäßig auf 1 Minute festgelegt ist. Für den Sender gibt die Zeitüberschreitung an, wie lange gewartet werden muss, wenn der Puffer voll ist. Für den Abrufer gibt die Zeitüberschreitung an, wie lange gewartet werden muss, wenn der Puffer leer ist.

Im Folgenden ist der senderseitige Code zum Senden von unformatierten Nachrichten an den Puffer aufgeführt:

TransportClientEndpointBehavior credential = ...;
Uri bufferUri = new Uri(@"sb://MyNamespace.servicebus.windows.net/MyBuffer/");

MessageBufferClient client =   
  MessageBufferClient.GetMessageBuffer(credential,bufferUri);

Message message = Message.CreateMessage(MessageVersion.Default,"Hello");

client.Send(message,TimeSpan.MaxValue);

Vom Sender wird zunächst ein Anmeldeinformationsobjekt erstellt, das zum Abrufen einer Instanz der MessageBufferClient-Klasse verwendet wird. Anschließend wird vom Sender eine WCF-Nachricht erstellt, die an den Puffer gesendet wird. Im Folgenden ist der abrufseitige Code zum Abrufen von unformatierten Nachrichten aus dem Puffer aufgeführt:

TransportClientEndpointBehavior credential = ...;
Uri bufferUri = new Uri(@"sb://MyNamespace.servicebus.windows.net/MyBuffer/");

MessageBufferClient client = 
  MessageBufferClient.GetMessageBuffer(credential,bufferUri);
Message message = client.Retrieve();

Debug.Assert(message.Headers.Action == "Hello");

Gepufferte Dienste

Die Leistung eines Dienstbuses besteht in genau dieser Verwendung unformatierter WCF-Nachrichten, die in den Codeausschnitten gezeigt wird. Ein solches Programmiermodell lässt allerdings viel zu wünschen übrig. Es ist umständlich, aufwendig, unstrukturiert, nicht objektorientiert und nicht typensicher. Es ist ein Rückschritt in die Zeit vor WCF, als für die explizite Programmierung für MSMQ die System.Messaging-API verwendet wurde. Sie müssen den Nachrichteninhalt analysieren und die Elemente aktivieren.

Glücklicherweise lässt sich das Basisangebot optimieren. Statt der Übermittlung unformatierter Nachrichten sollten Sie für die Kommunikation zwischen Clients und Diensten strukturierte Aufrufe verwenden. Dafür ist zwar eine Menge erweiterter Verarbeitung auf niedriger Ebene erforderlich, aber ich konnte diese Schritte mit einer kleinen Gruppe an Hilfsklassen einkapseln.

Zur Bereitstellung strukturierter gepufferter Aufrufe auf Dienstseite habe ich die BufferedServiceBusHost<T>-Klasse geschrieben, die wie folgt definiert ist:

// Generic type parameter based host
public class ServiceHost<T> : ServiceHost
{...}

public class BufferedServiceBusHost<T> : ServiceHost<T>,...
{
  public BufferedServiceBusHost(params Uri[] bufferAddresses);
  public BufferedServiceBusHost(
    T singleton,params Uri[] bufferAddresses);

  /* Additional constructors */
}

Ich habe BufferedServiceBusHost<T> in Anlehnung an die Verwendung von WCF mit der MSMQ-Bindung entworfen. Sie müssen für den Konstruktor die Adressen der Puffer bereitstellen, aus denen Nachrichten abgerufen werden sollen. Alles andere funktioniert genauso wie bei normalen WCF-Diensthosts:

Uri buffer = new Uri(@"https://MyNamespace.servicebus.windows.net/MyBuffer");
ServiceHost host = new BufferedServiceBusHost<MyService>(buffer);
host.Open();

Sie können für die Konstruktoren mehrere Pufferadressen zur Überprüfung bereitstellen, so wie auch WCF-Diensthosts mehrere Endpunkte mit unterschiedlichen Warteschlangen öffnen können. Es ist nicht erforderlich (allerdings auch nicht möglich), diese Pufferadressen im Dienstendpunktabschnitt der Konfigurationsdatei bereitzustellen (wobei die Pufferadressen jedoch aus dem Abschnitt für die Anwendungseinstellungen stammen können, wenn Sie dies in Ihrem Entwurf so vorsehen).

Für die Kommunikation mit dem Dienstbuspuffer werden zwar trotzdem unformatierte WCF-Nachrichten verwendet, aber die entsprechende Verarbeitung ist jetzt eingekapselt. BufferedServiceBusHost<T> überprüft, ob die bereitgestellten Puffer tatsächlich vorhanden sind, und erstellt die Puffer unter Verwendung der Pufferrichtlinie für ServiceBusHelper.VerifyBuffer aus Abbildung 6, wenn keine Puffer vorhanden sind. BufferedServiceBusHost<T> verwendet die standardmäßig festgelegte Übertragungssicherheit mit Sicherung aller Pfade. Darüber hinaus wird überprüft, ob alle Verträge des bereitgestellten dienstunabhängigen Typparameters T unidirektional sind, d. h., ob die Vorgänge für alle Verträge nur in eine Richtung verlaufen (so wie bei der unidirektionale Relaybindung). Es gibt noch eine weitere Funktion: In Debugversionen bereinigt BufferedServiceBusHost<T> beim Schließen des Hosts alle Puffer, damit die nächste Debugsitzung fehlerfrei gestartet werden kann.

BufferedServiceBusHost<T> funktioniert so, dass der angegebene Dienst lokal gehostet wird. Für jeden Dienstvertrag des Typparameters T fügt BufferedServiceBusHost<T> über IPC (Named Pipes) einen Endpunkt hinzu. Die IPC-Bindung an diese Endpunkte wird so konfiguriert, dass keine Zeitüberschreitung erfolgt.

Für IPC gibt es zwar immer eine Transportsitzung, aber zur Nachahmung des MSMQ-Verhaltens werden sogar sitzungsspezifische Dienste als aufrufspezifische Dienste behandelt. Alle WCF-Nachrichten, die aus der Warteschlange entfernt werden, werden an eine neue Instanz des Diensts weitergeleitet, und zwar möglicherweise zusammen mit vorhergehenden Nachrichten, so wie auch bei der MSMQ-Bindung. Wenn es sich bei dem bereitgestellten Diensttyp um ein Singleton handelt, wird dies von BufferedServiceBusHost<T> berücksichtigt, und alle Nachrichten werden für alle Puffer und Endpunkte werden an dieselbe Dienstinstanz gesendet – genau wie bei der MSMQ-Bindung.

BufferedServiceBusHost<T> überwacht die einzelnen angegebenen Puffer auf einem gesonderten Arbeitsthread im Hintergrund. Wenn eine Nachricht im Puffer gespeichert wird, ruft BufferedServiceBusHost<T> diese Nachricht ab, und die unformatierte WCF-Nachricht wird in einen Aufruf des entsprechenden Endpunkts über IPC konvertiert.

Abbildung 7 zeigt einen Auszug aus BufferedServiceBusHost<T>, aus dem Fehlerbehandlung und Sicherheit größtenteils entfernt wurden.

Abbildung 7 Auszug aus BufferedServiceBusHost<T>

public class BufferedServiceBusHost<T> : 
  ServiceHost<T>,IServiceBusProperties 
{
  Uri[] m_BufferAddresses;
  List<Thread> m_RetrievingThreads;
  IChannelFactory<IDuplexSessionChannel>
    m_Factory;
  Dictionary<string,IDuplexSessionChannel> 
    m_Proxies;

  const string CloseAction = 
    "BufferedServiceBusHost.CloseThread";

  public BufferedServiceBusHost(params Uri[] 
    bufferAddresses)
  {
    m_BufferAddresses = bufferAddresses;
    Binding binding = new NetNamedPipeBinding();
    binding.SendTimeout = TimeSpan.MaxValue;

    Type[] interfaces = 
      typeof(T).GetInterfaces();

    foreach(Type interfaceType in interfaces)
    {         
      VerifyOneway(interfaceType);
      string address = 
        @"net.pipe://localhost/" + Guid.NewGuid();
      AddServiceEndpoint(interfaceType,binding,
        address);
    }
    m_Factory = 
      binding.BuildChannelFactory
      <IDuplexSessionChannel>();
    m_Factory.Open();
  }
  protected override void OnOpened()
  {
    CreateProxies();                       
    CreateListeners();
    base.OnOpened();
  }
  protected override void OnClosing()
  {
    CloseListeners();

    foreach(IDuplexSessionChannel proxy in 
      m_Proxies.Values)
    {
      proxy.Close();
    }

    m_Factory.Close();

    PurgeBuffers();

    base.OnClosing();
  }

  // Verify all operations are one-way
  
  void VerifyOneway(Type interfaceType)
  {...}
  void CreateProxies()
  {
    m_Proxies = 
      new Dictionary
      <string,IDuplexSessionChannel>();

    foreach(ServiceEndpoint endpoint in 
      Description.Endpoints)
    {
      IDuplexSessionChannel channel = 
        m_Factory.CreateChannel(endpoint.Address);
      channel.Open();
      m_Proxies[endpoint.Contract.Name] = 
        channel;
    }
  }

  void CreateListeners()
  {
    m_RetrievingThreads = new List<Thread>();

    foreach(Uri bufferAddress in 
      m_BufferAddresses)
    {         ?      ServiceBusHelper.VerifyBuffer(
        bufferAddress.AbsoluteUri,m_Credential);
         
      Thread thread = new Thread(Dequeue);

      m_RetrievingThreads.Add(thread);
      thread.IsBackground = true;
      thread.Start(bufferAddress);
    }
  }

  void Dequeue(object arg)
  {
    Uri bufferAddress = arg as Uri;

    MessageBufferClient bufferClient = ?      MessageBufferClient.GetMessageBuffer(
        m_Credential,bufferAddress);      
    while(true)
    {
      Message message = 
        bufferClient.Retrieve(TimeSpan.MaxValue);
      if(message.Headers.Action == CloseAction)
      {
        return;
      }
      else
      {
        Dispatch(message);
      }      
    }
  }
   
  
  
  void Dispatch(Message message)
  {
    string contract = ExtractContract(message);
    m_Proxies[contract].Send(message);
  }
  string ExtractContract(Message message)
  {
    string[] elements = 
      message.Headers.Action.Split('/');
    return elements[elements.Length-2];         
  }
  protected override void OnClosing()
  {
    CloseListeners();
    foreach(IDuplexSessionChannel proxy in 
      m_Proxies.Values)
    {
      proxy.Close();
    }
    m_Factory.Close();

    PurgeBuffers();
    base.OnClosing();
  }
  void SendCloseMessages()
  {
    foreach(Uri bufferAddress in 
      m_BufferAddresses)
    {
      MessageBufferClient bufferClient =                 ?        MessageBufferClient.GetMessageBuffer(
        m_Credential,bufferAddress);
      Message message =   
        Message.CreateMessage(
        MessageVersion.Default,CloseAction);
      bufferClient.Send(message);
    }   
  }
  void CloseListeners()
  {
    SendCloseMessages();

    foreach(Thread thread in m_RetrievingThreads)
    {
      thread.Join();
    }
  }   

  [Conditional("DEBUG")]
  void PurgeBuffers()
  {
    foreach(Uri bufferAddress in 
      m_BufferAddresses)
    {
      ServiceBusHelper.PurgeBuffer(
        bufferAddress,m_Credential);
    }
  } 
}

BufferedServiceBusHost<T> speichert die Proxys in den lokal gehosteten IPC-Endpunkten in einem Wörterbuch mit der Bezeichnung „m_Proxies“:

Dictionary<string,IDuplexSessionChannel> m_Proxies;

Der Schlüssel für das Wörterbuch entspricht dem Vertragstypnamen der Endpunkte.

Die bereitgestellten Pufferadressen werden von den Konstruktoren gespeichert, die anschließend per Reflexion eine Sammlung sämtlicher Schnittstellen für den Diensttyp abrufen. BufferedServiceBusHost<T> überprüft für jede Schnittstelle, ob ausschließlich unidirektionale Vorgänge ausgeführt werden, und ruft anschließend die AddServiceEndpoint-Basismethode auf, um einen Endpunkt für den betreffenden Vertragstyp hinzuzufügen. Bei der Adresse handelt es sich um eine IPC-Adresse mit einem GUID als Pipenamen. Mit der IPC-Bindung erstellen die Konstruktoren eine Kanalfactory des Typs IChannelFactory<IDuplexSessionChannel>. IChannelFactory<T> wird verwendet, um über die Bindung einen nicht stark typisierten Kanal zu erstellen:

public interface IChannelFactory<T> : IChannelFactory
{
  T CreateChannel(EndpointAddress to);
  // More members
}

Nach dem Öffnen des internen Hosts mit allen zugehörigen IPC-Endpunkten erstellt die OnOpened-Methode die internen Proxys für diese Endpunkte und die gepufferten Listener. Diese beiden Schritte bilden den Kern von BufferedServiceBusHost<T>. Zum Erstellen der Proxys wird die Sammlung der Endpunkte durchlaufen. Die Adressen der einzelnen Endpunkte werden abgerufen, und IChannelFactory<IDuplexSessionChannel> erstellt für die einzelnen Adressen einen Kanal. Dieser Kanal (oder Proxy) wird anschließend im Wörterbuch gespeichert. Die CreateListeners-Methode durchläuft die angegebenen Pufferadressen. Für jede Adresse wird der Puffer überprüft und ein Arbeitsthread erstellt, um die Nachrichten aus der Warteschlange zu entfernen.

Die Dequeue-Methode verwendet MessageBufferClient, um die Nachrichten in einer Endlosschleife abzurufen und mithilfe der Dispatch-Methode zu verteilen. Dispatch extrahiert den Zielvertragsnamen aus der Nachricht verwendet diesen, um IDuplexChannel im Proxy-Wörterbuch zu suchen und die Nachricht über IPC zu senden. IDuplexChannel wird von dem zugrunde liegenden IPC-Kanal unterstützt und bietet eine Möglichkeit, unformatierte Nachrichten zu senden:

public interface IOutputChannel : ...
{
  void Send(Message message,TimeSpan timeout);
  // More members
}
public interface IDuplexSessionChannel : IOutputChannel,...
{}

Wenn bei dem IPC-Aufruf ein Fehler auftritt, erstellt BufferedServiceBusHost<T> den Kanal für den entsprechenden verwalteten Endpunkt erneut (in Abbildung 7 nicht dargestellt). Beim Schließen des Hosts müssen auch die Proxys geschlossen werden. Diese warten geduldig, dass die Aufrufe in Verarbeitung abgeschlossen werden. Die Herausforderung besteht darin, alle Abrufthreads elegant zu schließen, da es sich bei MessageBufferClient.Retrieve um einen blockierenden Vorgang handelt, der nicht direkt abgebrochen werden kann. Die Lösung besteht darin, an alle überwachten Puffer eine besondere private Nachricht zu senden, mit deren Aktion das Beenden des Abrufthreads signalisiert wird. Darin besteht der Zweck der SendCloseMessages-Methode. Die CloseListeners-Methode übermittelt diese private Nachricht an die Puffer und wartet dann darauf, dass alle Abrufthreads beendet werden, indem diese zusammengeführt werden. Bei geschlossenen Abrufthreads werden keine Nachrichten mehr an die internen Proxys übermittelt, und sobald die Proxys geschlossen sind (nachdem alle Aufrufe in Verarbeitung zurückgegeben wurden), kann der Host geschlossen werden. BufferedServiceBusHost<T> unterstützt auch die eher plumpe Abort-Methode, bei der sämtliche Threads einfach abgebrochen werden (in Abbildung 7 nicht dargestellt).

Beachten Sie außerdem, dass BufferedServiceBusHost<T> die IserviceBusProperties-Schnittstelle unterstützt, die ich wie folgt definiert habe:

public interface IServiceBusProperties
{
  TransportClientEndpointBehavior Credential
  {get;set;}

  Uri[] Addresses
  {get;}
}

Eine solche Schnittstelle war bei der Entwicklung meines Frameworks an bestimmten Stellen erforderlich, insbesondere zur Optimierung der Pufferung. Für den Client habe ich die BufferedServiceBusClient<T>-Klasse geschrieben, die wie folgt definiert ist:

public abstract class BufferedServiceBusClient<T> :                          
  HeaderClientBase<T,ResponseContext>,IServiceBusProperties 
{
  // Buffer address from config
  public BufferedServiceBusClient() 
  {}
  // No need for config file
  public BufferedServiceBusClient(Uri bufferAddress);


  /* Additional constructors with different credentials */  
  protected virtual void Enqueue(Action action);
}

BufferedServiceBusClient<T> habe ich aus meiner HeaderClientBase<T,H>-Klasse abgeleitet (ein Hilfsproxy für die Übermittlung von Informationen in den Nachrichtenheadern; ausführliche Informationen dazu finden Sie in meinem Artikel vom November 2007 mit dem Titel „Synchronisierungskontexte in WCF“, der unter msdn.microsoft.com/de-de/magazine/cc163321) verfügbar ist): 

public abstract class HeaderClientBase<T,H> : InterceptorClientBase<T> 
                                              where T : class
{
  protected H Header
  {get;set;}

  // More members
}

Der Zweck dieser Basisklasse besteht in der Unterstützung eines Antwortdiensts, dem Thema des nächsten Abschnitts. Für einfache Clients in einem gepufferten Dienst ist diese Ableitung nicht von Bedeutung.

Sie können BufferedServiceBusClient<T> mit oder ohne Clientkonfigurationsdatei verwenden. Für Konstruktoren, von denen die Pufferadresse akzeptiert wird, ist keine Konfigurationsdatei erforderlich. Der parameterlose Konstruktor und Konstruktoren, die den Endpunktnamen akzeptieren, erwarten, dass die Konfigurationsdatei einen entsprechenden Endpunkt für den Vertragstyp mit der unidirektionalen Bindung enthält (auch wenn diese Bindung von BufferedServiceBusClient<T> vollständig ignoriert wird).

Wenn Sie einen Proxy aus BufferedServiceBusClient<T> ableiten, müssen Sie die geschützte Enqueue-Methode verwenden statt direkt die Channel-Eigenschaft verwenden:

[ServiceContract]
interface IMyContract
{
  [OperationContract(IsOneWay = true)]
  void MyMethod(int number);
}

class MyContractClient : BufferedServiceBusClient<IMyContract>,IMyContract
{
  public void MyMethod(int number)
  {
    Enqueue(()=>Channel.MyMethod(number));
  }
}

Enqueue akzeptiert Delegaten (oder Lambda-Ausdrücke), die die Verwendung der Channel-Eigenschaft umschließen. Das Ergebnis ist weiterhin typensicher. Abbildung 8 zeigt einen Auszug aus der BufferedServiceBusClient<T>-Klasse.

Abbildung 8 Auszug aus BufferedServiceBusClient<T>

public abstract class BufferedServiceBusClient<T> :                               
  HeaderClientBase<T,ResponseContext>,IServiceBusProperties where T : class
{
  MessageBufferClient m_BufferClient;

  public BufferedServiceBusClient(Uri bufferAddress) : 
    base(new NetOnewayRelayBinding(),new EndpointAddress(bufferAddress)) 
  {}

  protected virtual void Enqueue(Action action) 
  {
    try
    {
      action();
    }
    catch(InvalidOperationException exception)
    {
      Debug.Assert(exception.Message ==
        "This message cannot support the operation " +
        "because it has been written.");
    }
  }
  protected override T CreateChannel()
  {    
    ServiceBusHelper.VerifyBuffer(Endpoint.Address.Uri.AbsoluteUri,Credential);
    m_BufferClient =  ?      MessageBufferClient.GetMessageBuffer(Credential,m_BufferAddress);

    return base.CreateChannel();   
  }
  protected override void PreInvoke(ref Message request)
  {
    base.PreInvoke(ref request);       
           
    m_BufferClient.Send(request);
  }
  protected TransportClientEndpointBehavior Credential
  {
    get
    {...}
    set
    {...}
  }
}

Pufferadresse und Bindung werden dem Basiskonstruktor über die Konstruktoren der BufferedServiceBusClient<T>-Klasse bereitgestellt. Die Bindung ist dabei immer unidirektional, um die Validierung von unidirektionalen Vorgängen zu erzwingen. Die CreateChannel-Methode überprüft, ob der Zielpuffer vorhanden ist, und ruft eine MessageBufferClient-Klasse ab, die den Zielpuffer darstellt. Den Kern von BufferedServiceBusClient<T> bildet die PreInvoke-Methode. Bei PreInvoke handelt es sich um eine virtuelle Methode, die von InterceptorClientBase<T> bereitgestellt wird, der Basisklasse für HeaderClientBase<T,H>:

public abstract class InterceptorClientBase<T> : ClientBase<T> where T : class
{
  protected virtual void PreInvoke(ref Message request);
  // Rest of the implementation 
}

Mit PreInvoke können Sie WCF-Nachrichten vor der Verteilung durch den Client auf einfache Weise verarbeiten. BufferedServiceBusClient<T> setzt PreInvoke außer Kraft, und stattdessen wird der Pufferclient verwendet, um die Nachricht an den Puffer zu senden. Auf diese Weise wird für den Client das Modell einer strukturierten Programmierung aufrechterhalten, und BufferedServiceBusClient<T> kapselt den Kommunikationsvorgang für die WCF-Nachricht ein. Ein Nachteil besteht allerdings darin, dass die Nachricht nur ein Mal gesendet werden kann, und wenn die Basisklasse für ClientBase versucht, die Nachricht zu senden, wird InvalidOperationException ausgelöst. An diesem Punkt ist Enqueue praktisch: Die Ausnahme wird einfach gelöscht. 

Antwortdienst

In meiner Kolumne vom Februar 2007 mit dem Titel „Erstellen eines WCF-Antwortdiensts in einer Warteschlange“ (msdn.microsoft.com/de-de/magazine/cc163482) habe ich erläutert, dass die einzige Möglichkeit, die Ergebnisse (oder Fehler) für einen eingereihten Aufruf abzurufen, in der Verwendung eines eingereihten Antwortdiensts besteht. Ich habe gezeigt, wie die Nachrichtenheader in einem Antwortkontextobjekt übergeben werden, das die ID für die logische Methode und die Antwortadresse enthält:

[DataContract]
public class ResponseContext
{
  [DataMember]
  public readonly string ResponseAddress;

  [DataMember]
  public readonly string MethodId;

  public ResponseContext(string responseAddress,string methodId);

  public static ResponseContext Current
  {get;set;}

  // More members 
}

Für Puffer gilt dasselbe Entwurfsmuster. Der Client muss einen dedizierten Antwortpuffer für den Dienst bereitstellen, in dem die Antwort gepuffert werden kann. Außerdem muss der Client die Antwortadresse und die Methoden-ID in den Nachrichtenheadern übergeben, genau wie bei MSMQ-basierten Aufrufen. Der zentrale Unterschied zwischen dem MSMQ-basierten Antwortdienst und dem Dienstbus besteht darin, dass sich der Antwortpuffer ebenfalls im Dienstbus befinden muss, so wie in Abbildung 9 gezeigt.

Figure 9 Service Bus Buffered Response Service Abbildung 9 Über einen Dienstbus gepufferter Antwortdienst

Zur Optimierung der Clientseite habe ich die ClientBufferResponseBase<T>-Klasse geschrieben, die wie folgt definiert ist:

 

public abstract class ClientBufferResponseBase<T> : 
  BufferedServiceBusClient<T> where T : class
{
  protected readonly Uri ResponseAddress;

  public ClientBufferResponseBase(Uri responseAddress);

  /* Additional constructors with different credentials */
     
  protected virtual string GenerateMethodId();
}

Bei ClientBufferResponseBase<T> handelt es sich um eine spezialisierte Unterklasse der BufferedServiceBusClient<T>-Klasse, mit der den Nachrichtenheadern der Antwortkontext hinzugefügt wird. Aus diesem Grund wird BufferedServiceBusClient<T> von HeaderClientBase<T,H> abgeleitet und nicht einfach von InterceptorClientBase<T>. Sie können ClientBufferResponseBase<T> auf dieselbe Weise verwenden wie BufferedServiceBusClient, so wie in Abbildung 10 gezeigt.

Abbildung 10 Optimierung der Clientseite

[ServiceContract]
interface ICalculator
{
  [OperationContract(IsOneWay = true)]
  void Add(int number1,int number2);
}

class CalculatorClient : ClientBufferResponseBase<ICalculator>,ICalculator
{
  public CalculatorClient(Uri responseAddress) : base(responseAddress)
  {}
   
  public void Add(int number1,int number2)
  {
     Enqueue(()=>Channel.Add(number1,number2));
  }
}

Die Verwendung der ClientBufferResponseBase<T>-Unterklasse ist unkompliziert:

Uri resposeAddress = 
  new Uri(@"sb://MyNamespace.servicebus.windows.net/MyResponseBuffer/");

CalculatorClient proxy = new CalculatorClient(responseAddress);
proxy.Add(2,3);
proxy.Close();

Sie ist eine bequeme Möglichkeit, die Antworten auf Clientseite zu verwalten. Der aufrufende Client ruft dabei die Methoden-ID ab, die für die Verteilung des Aufrufs verwendet wird. Dies lässt sich ganz einfach mit der Header-Eigenschaft erreichen:

CalculatorClient proxy = new CalculatorClient(responseAddress);
proxy.Add(2,3);
string methodId = proxy.Header.MethodId;

In Abbildung 11 ist die Implementierung von ClientBufferResponseBase<T> aufgeführt. ClientBufferResponseBase<T> setzt die PreInvoke-Methode der HeaderClientBase<T,H>-Klasse außer Kraft, damit für jeden Aufruf eine neue Methoden-ID generiert und in die Header eingefügt werden kann.

Abbildung 11 Implementierung von ClientBufferResponseBase<T>

public abstract class ClientBufferResponseBase<T> : 
  BufferedServiceBusClient<T> where T : class
{
  public readonly Uri ResponseAddress;

  public ClientBufferResponseBase(Uri responseAddress)
  {
    ResponseAddress = responseAddress;
  }
   
  /* More Constructors */

  protected override void PreInvoke(ref Message request)
  {
    string methodId = GenerateMethodId();
    Header = new ResponseContext(ResponseAddress.AbsoluteUri,methodId);         
    base.PreInvoke(ref request);
  }

  protected virtual string GenerateMethodId()
  {
    return Guid.NewGuid().ToString();
  }

  // Rest of the implementation 
}

Zur Optimierung der Verarbeitung, die durch den gepufferten Dienst zum Aufrufen des Antwortdiensts erforderlich ist, habe ich die in Abbildung 12 gezeigte ServiceBufferResponseBase<T>-Klasse geschrieben.

Abbildung 12 ServiceBufferResponseBase<T>-Klasse

public abstract class ServiceBufferResponseBase<T> : 
  BufferedServiceBusClient<T> where T : class 
{
  public ServiceBufferResponseBase() : 
   base(new Uri(ResponseContext.Current.ResponseAddress))
 {
   Header = ResponseContext.Current;
               
   // Grab the credentials the host was using 

   IServiceBusProperties properties = 
     OperationContext.Current.Host as IServiceBusProperties;
   Credential = properties.Credential;
  }
}

Zum Einreihen der Antwort kann zwar auch die einfache BufferedServiceBusClient<T>-Klasse verwendet werden, aber Sie müssen die Antwortpufferadressen aus den Headern extrahieren und irgendwie die Informationen für die Anmeldung beim Dienstbuspuffer abrufen. Außerdem müssen Sie für die Header ausgehender Aufrufe auch den Antwortkontext bereitstellen. All diese Schritte können Sie mithilfe von ServiceBufferResponseBase<T> vereinfachen. ServiceBufferResponseBase<T> stellt dem Basiskonstruktor die Adresse aus dem Antwortkontext bereit und fügt diesen Kontext außerdem in die ausgehenden Header ein.

Eine weitere Annahme von ServiceBufferResponseBase<T>, die die Dinge vereinfacht, besteht darin, dass der Antwortdienst zum Senden von Nachrichten an den Antwortpuffer dieselben Anmeldeinformationen verwenden kann, mit denen der entsprechende Host Nachrichten aus dem eigenen Puffer abruft. Zu diesem Zweck ruft ServiceBufferResponseBase<T> aus dem Vorgangskontext einen Verweis auf den eigenen Host ab und liest die Anmeldeinformationen mithilfe der IserviceBusProperties-Implementierung des Hosts. ServiceBufferResponseBase<T> kopiert die Anmeldeinformationen und verwendet diese (in BufferedServiceBusClient<T>). Das setzt allerdings voraus, dass zum Hosten des Diensts von vornherein BufferedServiceBusHost<T> verwendet wird. Der Dienst muss von ServiceBufferResponseBase<T> eine Proxyklasse ableiten und zum Antworten verwenden. Sehen Sie sich beispielsweise den folgenden Antwortvertrag an:

[ServiceContract]
interface ICalculatorResponse
{
  [OperationContract(IsOneWay = true)]
  void OnAddCompleted(int result,ExceptionDetail error);
}
This would be the definition of the proxy to the response service:
class CalculatorResponseClient :    
  ServiceBufferResponseBase<ICalculatorResponse>,ICalculatorResponse
{
  public void OnAddCompleted(int result,ExceptionDetail error)
  {
    Enqueue(()=>Channel.OnAddCompleted(result,error));
  }
}

Abbildung 13 zeigt einen einfachen gepufferten Dienst mit Antwort an den Client.

Abbildung 13 Verwendung von ServiceBufferResponseBase<T>

class MyCalculator : ICalculator
{
  [OperationBehavior(TransactionScopeRequired = true)]
  public void Add(int number1,int number2)
  {
    int result = 0;
    ExceptionDetail error = null;
    try
    {
      result = number1 + number2;
    }
     // Don’t rethrow 
    catch(Exception exception)
    {
      error = new ExceptionDetail(exception);
    }
    finally
    {
      CalculatorResponseClient proxy = new CalculatorResponseClient();
      proxy.OnAddCompleted(result,error);
      proxy.Close();
    }
  }
}

Der Antwortdienst muss einfach nur auf die Methoden-ID in den Nachrichtenheadern zugreifen, so wie hier gezeigt:

class MyCalculatorResponse : ICalculatorResponse
{
  public void OnAddCompleted(int result,ExceptionDetail error)
  {
    string methodId = ResponseContext.Current.MethodId;
    ...
  }
}

Bleiben Sie dabei für weitere Erläuterungen zum Dienstbus.  

Juval Lowy ist als Softwarearchitekt bei IDesign tätig. Er bietet WCF-Training und Beratung zur WCF-Architektur an. Dieser Artikel enthält Auszüge aus seinem neuen Buch „Programming WCF Services“ (O'Reilly, 2010. 3. Auflage). Er ist außerdem Microsoft Regional Director für das Silicon Valley. Sie erreichen Juval Lowy unter idesign.net.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Jeanne Baker