Kapitel 4. Instanz-Management

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Instanzmanagement ist mein Name für die Techniken, die WCF verwendet, um Client-Anfragen an Service-Instanzen zu binden und zu bestimmen, welche Service-Instanz wann welche Client-Anfrage bearbeitet. Die Instanzverwaltung ist notwendig weil sich Anwendungen in ihren Anforderungen an Skalierbarkeit, Leistung, Durchsatz, Dauerhaftigkeit, Transaktionen und Aufrufen in Warteschlangen unterscheiden - es gibt einfach keine Einheitslösung für alle. Es gibt jedoch ein paar kanonische Techniken für das Instanzmanagement, die für alle Anwendungen anwendbar sind und somit eine Vielzahl von Szenarien und Programmiermodellen ermöglichen. Diese Techniken sind das Thema dieses Kapitels. Sie zu verstehen ist wichtig, um skalierbare und konsistente Anwendungen zu entwickeln. WCF unterstützt drei Arten der Instanzaktivierung: Per-Call-Dienste weisen für jede Client-Anfrage eine neue Dienstinstanz zu (und zerstören sie); Sessionful-Dienste weisen für jede Client-Verbindung eine Dienstinstanz zu; und Singleton-Dienste teilen dieselbe Dienstinstanz für alle Clients, über alle Verbindungen und Aktivierungen hinweg. In diesem Kapitel werden die Gründe für jede dieser Instanzverwaltungsarten erläutert und es werden Richtlinien gegeben, wann und wie man sie am besten einsetzt. Außerdem werden einige verwandte Themen behandelt, wie z. B. Verhaltensweisen, Kontexte, Abgrenzung von Operationen, Deaktivierung von Instanzen, dauerhafte Dienste und Drosselung.1

Verhaltensweisen

Im Großen und Ganzen ist der Modus der Dienstinstanz ein rein dienstseitiges Implementierungsdetail, das sich nicht auf der Client-Seite bemerkbar machen sollte. Um dies und einige andere lokale diensteseitige Aspekte zu unterstützen, definiert die WCF das Konzept der Verhaltensweisen. Ein Verhalten ist ein lokales Attribut des Dienstes oder des Clients, das sich nicht auf seine Kommunikationsmuster auswirkt. Die Clients sollten keine Kenntnis von den Verhaltensweisen der Dienste haben, und die Verhaltensweisen werden nicht in der Bindung oder den veröffentlichten Metadaten des Dienstes angezeigt. In den vorangegangenen Kapiteln hast du bereits zwei Verhaltensweisen für Dienste kennengelernt: In Kapitel 1 wird das Verhalten der Dienstmetadaten verwendet, um den Host anzuweisen, die Metadaten des Dienstes über HTTP-GET zu veröffentlichen oder den MEX-Endpunkt zu implementieren, und in Kapitel 3 wird das Verhalten des Dienstes verwendet, um die Datenobjekt-Erweiterung zu ignorieren. Kein Client kann anhand der Kommunikation und der ausgetauschten Nachrichten feststellen, ob der Dienst die Datenobjekt-Erweiterung ignoriert oder wer seine Metadaten veröffentlicht hat.

WCF definiert zwei Arten von deklarativen dienstseitigen Verhaltensweisen, die durch zwei entsprechende Attribute gesteuert werden. Die ServiceBehaviorAttribute wird verwendet, um das Verhalten von Diensten zu konfigurieren, d.h. Verhaltensweisen, die alle Endpunkte (alle Verträge und Operationen) des Dienstes betreffen. Wenden Sie das Attribut ServiceBehavior direkt auf die Dienstimplementierungsklasse an.

Mit und OperationBehaviorAttribute kannst du das Verhalten von Operationen konfigurieren, d.h. Verhaltensweisen, die nur die Implementierung einer bestimmten Operation betreffen. Das Attribut OperationBehavior kann nur auf eine Methode angewendet werden, die eine Vertragsoperation implementiert, niemals auf die Operationsdefinition im Vertrag selbst. Du wirst die Verwendung des Attributs OperationBehavior später in diesem Kapitel und auch in den folgenden Kapiteln kennenlernen.

Im Kontext dieses Kapitels wird das Attribut ServiceBehavior verwendet, um den Modus der Service-Instanz zu konfigurieren. Wie in Beispiel 4-1 gezeigt, definiert das Attribut die Eigenschaft InstanceContextMode des Enum-Typs InstanceContextMode. Der Wert des InstanceContextMode enum steuert, welcher Instanzmodus für den Dienst verwendet wird.

Beispiel 4-1. ServiceBehaviorAttribute für die Konfiguration des Instanzkontextmodus
public enum InstanceContextMode
{
   PerCall,
   PerSession,
   Single
}
[AttributeUsage(AttributeTargets.Class)]
public sealed class ServiceBehaviorAttribute : Attribute,...
{
   public InstanceContextMode InstanceContextMode
   {get;set;}
   //More members
}

Die Enum heißt richtigerweise InstanceContextMode und nicht InstanceMode, weil sie den Instanzierungsmodus des Kontexts steuert, in dem sich die Instanz befindet, und nicht den der Instanz selbst (wie in Kapitel 1 beschrieben, ist der Instanzkontext der innerste Ausführungsbereich des Dienstes). Standardmäßig werden die Instanz und ihr Kontext jedoch als eine Einheit behandelt, sodass die Enum auch das Leben der Instanz steuert. Du wirst später in diesem Kapitel und in den folgenden Kapiteln sehen, wie (und wann) du die beiden voneinander trennen kannst und zu welchem Zweck.

Per-Call-Dienste

Wenn der Diensttyp für die Aktivierung pro Aufruf konfiguriert ist, existiert eine Instanz des Dienstes (das CLR-Objekt) nur, solange ein Client-Aufruf läuft. Jede Client-Anfrage (d.h. ein Methodenaufruf des WCF-Vertrags) erhält eine neue dedizierte Dienstinstanz. In der folgenden Liste wird erklärt, wie die Per-Call-Aktivierung funktioniert, und die Schritte sind in Abbildung 4-1 dargestellt:

  1. Der Kunde ruft den Proxy an und der Proxy leitet den Anruf an den Dienst weiter.

  2. WCF erstellt einen neuen Kontext mit einer neuen Dienstinstanz und ruft die Methode auf.

  3. Wenn der Methodenaufruf zurückkehrt und das Objekt IDisposable implementiert, ruft WCF IDisposable.Dispose() auf. Dann zerstört WCF den Kontext.

  4. Der Kunde ruft den Proxy an und der Proxy leitet den Anruf an den Dienst weiter.

  5. WCF erstellt ein Objekt und ruft die Methode auf.

Per-call instantiation mode
Abbildung 4-1. Modus der Instanziierung pro Aufruf

Die Entsorgung der Dienstinstanz ist ein interessanter Punkt. Wie bereits erwähnt, ruft WCF automatisch die Methode Dispose() auf, wenn der Dienst die Schnittstelle IDisposable unterstützt, damit der Dienst alle erforderlichen Aufräumarbeiten durchführen kann. Beachte, dass Dispose() auf demselben Thread aufgerufen wird, der den ursprünglichen Methodenaufruf versendet hat, und dass Dispose() einen Operationskontext hat (wird später vorgestellt). Nach dem Aufruf von Dispose() trennt die WCF die Instanz vom Rest der WCF-Infrastruktur und macht sie zu einem Kandidaten für die Speicherbereinigung.

Vorteile von Per-Call-Diensten

Im klassischen Client/Server-Programmiermodell mit Sprachen wie C++ oder C# erhält jeder Client sein eigenes Serverobjekt. Das grundlegende Problem bei diesem Ansatz ist, dass er nicht gut skalierbar ist. Stell dir eine Anwendung vor, die viele Clients bedienen muss. Normalerweise erstellen diese Clients die Objekte, die sie benötigen, wenn die Client-Anwendung startet und entsorgen sie, wenn die Client-Anwendung beendet wird. Die Skalierbarkeit des Client/Server-Modells wird dadurch beeinträchtigt, dass die Client-Anwendungen Objekte über lange Zeiträume hinweg behalten können, obwohl sie sie nur für einen Bruchteil dieser Zeit tatsächlich nutzen. Diese Objekte können teure oder knappe Ressourcen enthalten, wie z. B. Datenbankverbindungen, Kommunikationsports oder Dateien. Wenn du jedem Client ein Objekt zuordnest, bindest du diese wichtigen und/oder begrenzten Ressourcen für lange Zeit und irgendwann gehen dir die Ressourcen aus.

Ein besseres Aktivierungsmodell besteht darin, ein Objekt für einen Kunden nur dann zuzuweisen, wenn gerade ein Aufruf vom Kunden zum Dienst läuft. Auf diese Weise musst du nur so viele Objekte erstellen und im Speicher halten, wie es gleichzeitige Aufrufe gibt, und nicht so viele Objekte, wie es ausstehende Kunden gibt. Meine persönliche Erfahrung zeigt, dass in einem typischen Unternehmenssystem, vor allem in einem System, an dem Benutzer beteiligt sind, höchstens 1 % aller Clients gleichzeitig Aufrufe tätigen (in einem stark ausgelasteten Unternehmenssystem steigt diese Zahl auf 3 %). Wenn dein System also 100 teure Service-Instanzen gleichzeitig aufrechterhalten kann, kann es in der Regel trotzdem bis zu 10.000 Kunden bedienen. Das ist genau der Vorteil, den der Aktivierungsmodus pro Aufruf der Instanz bietet. Zwischen den Aufrufen hält der Kunde einen Verweis auf einen Proxy, der kein tatsächliches Objekt am Ende der Leitung hat. Das bedeutet, dass du die teuren Ressourcen, die die Service-Instanz belegt, entsorgen kannst, lange bevor der Client den Proxy schließt. Aus demselben Grund wird die Beschaffung der Ressourcen aufgeschoben, bis sie tatsächlich von einem Kunden benötigt werden.

Bedenke, dass es viel billiger ist, eine Service-Instanz wiederholt auf der Service-Seite zu erstellen und zu zerstören, ohne die Verbindung zum Client (mit seinem clientseitigen Proxy) abzubauen, als wiederholt eine Instanz und eine Verbindung zu erstellen. Der zweite Vorteil ist, dass die Service-Instanz gezwungen ist, ihre Ressourcen bei jedem Aufruf neu zuzuweisen oder sich mit ihnen zu verbinden, was sehr gut zu transaktionalen Ressourcen und transaktionaler Programmierung passt (siehe Kapitel 7), weil es die Aufgabe erleichtert, die Konsistenz mit dem Zustand der Instanz durchzusetzen. Der dritte Vorteil von Per-Call-Services ist, dass sie in Verbindung mit Disconnected Calls in Warteschlangen verwendet werden können (beschrieben in Kapitel 9), da sie eine einfache Zuordnung von Service-Instanzen zu diskreten Nachrichten in Warteschlangen ermöglichen.

Per-Call-Dienste konfigurieren

Um einen Servicetyp als Per-Call-Service zu konfigurieren, wendest du das Attribut ServiceBehavior an, wobei die Eigenschaft InstanceContextMode auf InstanceContextMode.PerCall gesetzt ist:

[ServiceContract]
interface IMyContract
{...}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{...}

Beispiel 4-2 zeigt einen einfachen Per-Call-Dienst und seinen Client. Wie du in der Programmausgabe sehen kannst, wird für jeden Aufruf einer Client-Methode eine neue Service-Instanz erstellt.

Beispiel 4-2. Pro-Aufruf-Dienst und Client
///////////////////////// Service Code /////////////////////
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract,IDisposable
{
   int m_Counter = 0;

   MyService()
   {
      Trace.WriteLine("MyService.MyService()");
   }
   public void MyMethod()
   {
      m_Counter++;
      Trace.WriteLine("Counter = " + m_Counter);
   }
   public void Dispose()
   {
      Trace.WriteLine("MyService.Dispose()");
   }
}
///////////////////////// Client Code /////////////////////
MyContractClient proxy = new MyContractClient();

proxy.MyMethod();
proxy.MyMethod();

proxy.Close();

//Possible output
MyService.MyService()
Counter = 1
MyService.Dispose()
MyService.MyService()
Counter = 1
MyService.Dispose()

Per-Call-Dienste und Transport-Sitzungen

Die Verwendung eines Dienstes pro Anruf ist unabhängig vom Vorhandensein einer Transportsitzung (beschrieben in Kapitel 1). Eine Transportsitzung ordnet alle Nachrichten von einem bestimmten Kunden einem bestimmten Kanal zu. Wenn der Dienst für die Instanziierung pro Aufruf konfiguriert ist, kann es immer noch eine Transportsitzung geben, aber für jeden Aufruf erstellt die WCF einen neuen Kontext, der nur für diesen Aufruf verwendet wird. Wenn keine Sitzungen auf Transportebene verwendet werden, verhält sich der Dienst, wie du später sehen wirst, unabhängig von seiner Konfiguration immer wie ein Per-Call-Dienst.

Wenn der Per-Call-Service eine Transportsitzung hat, unterliegt die Kommunikation vom Client dem Inaktivitäts-Timeout der Transportsitzung (Standardwert: 10 Minuten). Wenn die Zeitüberschreitung abgelaufen ist, kann der Kunde den Proxy nicht mehr benutzen, um den Per-Call-Service aufzurufen, da die Transportsitzung beendet ist.

Die größte Auswirkung von Transportsitzungen auf Per-Call-Dienste besteht darin, dass, wenn der Dienst für den Single-Thread-Zugriff konfiguriert ist (die WCF-Standardeinstellung, die in Kapitel 8 erläutert wird), die Transportsitzung eine Lock-Step-Ausführung erzwingt, bei der Aufrufe des Per-Call-Dienstes vom selben Proxy aus in Serie geschaltet werden. Das heißt, selbst wenn der Client die Aufrufe gleichzeitig tätigt, werden sie der Reihe nach gegen verschiedene Instanzen ausgeführt. Das hat besondere Auswirkungen auf die Beseitigung der Instanz. WCF blockiert den Client nicht, während er die Instanz des Dienstes entsorgt. Wenn der Kunde jedoch während des Aufrufs an Dispose() einen zweiten Aufruf getätigt hat, darf dieser Aufruf erst auf eine neue Instanz zugreifen, nachdem Dispose() zurückgekehrt ist. Die Ausgabe am Ende von Beispiel 4-2 stellt einen Fall dar, in dem es eine Transportsitzung gibt, da der zweite Aufruf erst ausgeführt werden kann, wenn Dispose() zurückgekehrt ist. Wenn es in Beispiel 4-2 keine Transportsitzung gäbe, könntest du am Ende dieselbe Ausgabe erhalten, aber auch einen Aufruf außerhalb der Reihenfolge, bei dem Dispose() nicht blockiert ist, wie z. B:

MyService.MyService()
Counter = 1
MyService.MyService()
Counter = 1
MyService.Dispose()
MyService.Dispose()

Gestaltung von Per-Call-Diensten

Theoretisch kannst du den Modus der Instanzaktivierung pro Aufruf für jeden Diensttyp verwenden, aber in der Praxis musst du den Dienst und seine Verträge von Grund auf so gestalten, dass sie diesen Modus unterstützen. Das Hauptproblem ist, dass der Client nicht weiß, dass er bei jedem Aufruf eine neue Instanz erhält. Per-Call-Dienste müssen zustandsorientiert sein, d. h. sie müssen ihren Zustand proaktiv verwalten und dem Kunden die Illusion einer kontinuierlichen Sitzung vermitteln. Ein zustandsorientierter Dienst ist nicht dasselbe wie ein zustandsloser Dienst. Wäre der Dienst pro Anruf wirklich zustandslos, wäre eine Aktivierung pro Anruf gar nicht erst nötig. Gerade weil er einen Zustand hat, und zwar einen teuren Zustand, brauchst du den Per-Call-Modus. Eine Instanz eines Per-Call-Dienstes wird kurz vor jedem Methodenaufruf erstellt und sofort nach jedem Aufruf wieder gelöscht. Deshalb sollte das Objekt zu Beginn eines jeden Aufrufs seinen Zustand aus den in einer Speicherung gespeicherten Werten initialisieren und am Ende des Aufrufs seinen Zustand an die Speicherung zurückgeben. Bei dieser Speicherung handelt es sich in der Regel um eine Datenbank oder das Dateisystem, es kann aber auch eine flüchtige Speicherung (z. B. statische Variablen) verwendet werden.

Allerdings können nicht alle Zustände des Objekts in ihrem jetzigen Zustand gespeichert werden. Wenn der Status beispielsweise eine Datenbankverbindung enthält, muss das Objekt die Verbindung bei der Erstellung oder zu Beginn jedes Aufrufs neu anfordern und die Verbindung am Ende des Aufrufs oder in seiner Implementierung von IDisposable.Dispose() entsorgen.

Die Verwendung des Instanzmodus pro Aufruf hat eine wichtige Auswirkung auf das Design von Operationen: Jede Operation muss einen Parameter enthalten, der die Service-Instanz identifiziert, deren Status abgerufen werden muss. Die Instanz verwendet diesen Parameter, um ihren Zustand aus der Speicherung abzurufen, und nicht den Zustand einer anderen Instanz des gleichen Typs. Daher ist die Speicherung des Zustands in der Regel verschlüsselt (z. B. in Form eines statischen Wörterbuchs im Speicher oder einer Datenbanktabelle). Beispiele für solche Zustandsparameter sind die Kontonummer für einen Bankkontodienst, die Auftragsnummer für einen Auftragsabwicklungsdienst und so weiter.

Beispiel 4-3 zeigt ein Templating für die Implementierung eines Per-Call-Service.

Beispiel 4-3. Einen Dienst pro Anruf implementieren
[DataContract]
class Param
{...}

[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod(Param stateIdentifier);
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyPerCallService : IMyContract,IDisposable
{
   public void MyMethod(Param stateIdentifier)
   {
      GetState(stateIdentifier);
      DoWork();
      SaveState(stateIdentifier);
   }
   void GetState(Param stateIdentifier)
   {...}
   void DoWork()
   {...}
   void SaveState(Param stateIdentifier)
   {...}
   public void Dispose()
   {...}
}

Die Klasse implementiert die Operation MyMethod(), die einen Parameter vom Typ Param (ein für dieses Beispiel erfundener Pseudotyp) akzeptiert, der die Instanz identifiziert:

public void MyMethod(Param stateIdentifier);

Die Instanz verwendet dann den Bezeichner, um ihren Zustand abzurufen und am Ende des Methodenaufrufs wieder zu speichern. Jeder Status, der für alle Clients gleich ist, kann im Konstruktor zugewiesen und unter Dispose() entsorgt werden.

Der Aktivierungsmodus "pro Aufruf" funktioniert am besten, wenn der Arbeitsaufwand pro Methodenaufruf endlich ist und es keine weiteren Aktivitäten im Hintergrund gibt, die nach der Rückkehr einer Methode abgeschlossen werden müssen. Da das Objekt verworfen wird, sobald die Methode zurückkehrt, solltest du keine Hintergrund-Threads starten oder asynchrone Aufrufe an die Instanz zurückschicken.

Da der Per-Call-Dienst bei jedem Methodenaufruf seinen Zustand aus einer Speicherung abruft, funktionieren Per-Call-Dienste sehr gut in Verbindung mit einer Load-Balancing-Maschine, solange der Zustandsspeicher eine globale Ressource ist, auf die alle Maschinen Zugriff haben. Der Lastausgleichsrechner kann Aufrufe nach Belieben an verschiedene Rechner weiterleiten, da er weiß, dass jeder Per-Call-Dienst den Aufruf ausführen kann, nachdem er seinen Status abgerufen hat.

Dienstleistungen und Leistung pro Anruf

Per-Call-Services bieten einen klaren Kompromiss zwischen Leistung (der Aufwand für das Abrufen und Speichern des Instanzstatus bei jedem Methodenaufruf) und Skalierbarkeit (Beibehaltung des Status und der damit verbundenen Ressourcen). Es gibt keine festen Regeln dafür, wann und in welchem Umfang du Leistung gegen Skalierbarkeit eintauschen solltest. Möglicherweise musst du ein Profil deines Systems erstellen und einige Dienste so konzipieren, dass sie die Aktivierung pro Aufruf verwenden, während andere sie nicht verwenden.

Aufräumarbeiten

Ob der Diensttyp IDisposable unterstützt oder nicht, ist ein Implementierungsdetail und für den Kunden nicht relevant. Tatsächlich hat der Kunde ohnehin keine Möglichkeit, die Methode Dispose() aufzurufen. Wenn du einen Vertrag für einen Dienst pro Aufruf entwirfst, vermeide es, Operationen zu definieren, die für die Bereinigung von Zuständen oder Ressourcen bestimmt sind, wie z. B. diese:

//Avoid
[ServiceContract]
interface IMyContract
{
   void DoSomething();
   void Cleanup();
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyPerCallService : IMyContract,IDisposable
{
   public void DoSomething()
   {...}
   public void Cleanup()
   {...}
   public void Dispose()
   {
      Cleanup();
   }
}

Die Torheit eines solchen Designs liegt auf der Hand: Wenn der Client die Cleanup-Methode aufruft, hat das den nachteiligen Effekt, dass ein Objekt erstellt wird, nur damit der Client Cleanup() aufrufen kann, gefolgt von einem Aufruf von IDisposable.Dispose() (falls vorhanden) durch WCF, um die Bereinigung erneut durchzuführen.

Die Wahl von Per-Call-Diensten

Auch wenn das Programmiermodell der Per-Call-Services für Anwendungsentwickler, die zu Service-Entwicklern werden, etwas befremdlich erscheinen mag, sind Per-Call-Services tatsächlich der bevorzugte Instanzmanagement-Modus für viele WCF-Services. Das liegt ganz einfach daran, dass Per-Call-Dienste besser skalieren oder zumindest nicht skalierbar sind. Wenn ich einen Dienst entwerfe, lautet meine goldene Regel für Skalierbarkeit 10X. Das heißt, jeder Dienst sollte so konzipiert sein, dass er eine Last bewältigen kann, die mindestens eine Größenordnung größer ist als die, die er benötigt. In jeder anderen technischen Disziplin entwerfen Ingenieure ein System nie so, dass es genau die angegebene Nennlast bewältigen kann. Du würdest kein Gebäude betreten wollen, dessen Balken nur die exakte Last tragen können, für die sie ausgelegt sind, oder in einem Aufzug fahren, dessen Kabel nur die exakte Anzahl von Fahrgästen tragen können, für die er ausgelegt ist, usw. Mit Softwaresystemen verhält es sich nicht anders - warum sollte man ein System für eine bestimmte aktuelle Belastung entwerfen, während alle anderen im Unternehmen daran arbeiten, das Geschäft zu steigern und die implizierte Belastung zu erhöhen? Du solltest Softwaresysteme so konzipieren, dass sie jahrelang halten und den aktuellen und zukünftigen Belastungen standhalten. Wenn du die 10-fache goldene Regel anwendest, wirst du sehr schnell die Skalierbarkeit des Pro-Call-Service benötigen.

Pro-Sitzung-Dienste

WCF kann eine logische Sitzung zwischen einem Client und einer bestimmten Dienstinstanz aufrechterhalten. Wenn der Kunde einen neuen Proxy für einen Dienst erstellt, der als sitzungsfähiger Dienst konfiguriert ist, erhält der Kunde eine neue dedizierte Dienstinstanz, die unabhängig von allen anderen Instanzen desselben Dienstes ist. Diese Instanz bleibt in der Regel so lange in Betrieb, bis der Kunde sie nicht mehr benötigt. Dieser Aktivierungsmodus (manchmal auch als Private-Session-Modus bezeichnet) ist dem klassischen Client/Server-Modell sehr ähnlich: Jede Private Session bindet einen Proxy und seinen Satz an client- und serviceseitigen Kanälen eindeutig an eine bestimmte Dienstinstanz, genauer gesagt an ihren Kontext. Daraus folgt, dass für den Private-Session-Instanziierungsmodus eine Transportsitzung erforderlich ist, wie später in diesem Abschnitt erläutert wird.

Da die Service-Instanz während der gesamten Sitzung im Speicher verbleibt, kann sie den Status im Speicher halten, und das Programmiermodell ähnelt sehr dem des klassischen Client/Server-Modells. Das Programmiermodell ist dem klassischen Client/Server-Modell sehr ähnlich. Folglich leidet es unter denselben Skalierbarkeits- und Transaktionsproblemen wie das klassische Client/Server-Modell. Ein Dienst, der für private Sitzungen konfiguriert ist, kann in der Regel nicht mehr als ein paar Dutzend (oder vielleicht bis zu ein- oder zweihundert) ausstehende Kunden unterstützen, da die Kosten für jede einzelne Instanz eines solchen Dienstes zu hoch sind.

Hinweis

Die Kundensitzung ist pro Service-Endpunkt und Proxy. Wenn der Kunde einen weiteren Proxy für denselben oder einen anderen Endpunkt erstellt, wird dieser zweite Proxy mit einer neuen Instanz und Sitzung verknüpft.

Private Sitzungen konfigurieren

Es gibt drei Elemente zur Unterstützung einer Sitzung: Verhalten, Bindung und Vertrag. Der Verhaltensteil ist erforderlich, damit WCF den Kontext der Dienstinstanz während der gesamten Sitzung aufrechterhält und die Nachrichten des Clients an ihn weiterleitet. Diese lokale Verhaltensfacette wird erreicht, indem die Eigenschaft InstanceContextMode des Attributs ServiceBehavior auf InstanceContextMode.PerSession gesetzt wird:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
class MyService : IMyContract
{...}

Da InstanceContextMode.PerSession der Standardwert für die Eigenschaft InstanceContextMode ist, sind diese Definitionen gleichwertig:

class MyService : IMyContract
{...}

[ServiceBehavior]
class MyService : IMyContract
{...}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
class MyService : IMyContract
{...}

Die Sitzung wird in der Regel beendet, wenn der Kunde den Proxy schließt, woraufhin der Proxy dem Dienst mitteilt, dass die Sitzung beendet ist. Wenn der Dienst IDisposable unterstützt, wird die Methode Dispose() asynchron zum Client aufgerufen. Allerdings wird Disposed() in einem Worker-Thread ohne Operationskontext aufgerufen.

Um alle Nachrichten von einem bestimmten Client einer bestimmten Instanz zuordnen zu können, muss WCF den Client identifizieren können. Wie in Kapitel 1 erklärt, ist das genau das, was die Transportsitzung erreicht. Wenn dein Dienst als Sessionful Service genutzt werden soll, musst du diese Erwartung auf Vertragsebene zum Ausdruck bringen können. Das Vertragselement ist über die Dienstgrenze hinweg erforderlich, weil die clientseitige WCF-Laufzeit wissen muss, dass sie eine Sitzung verwenden soll. Zu diesem Zweck bietet das Attribut ServiceContract die Eigenschaft SessionMode vom Enum-Typ SessionMode:

public enum SessionMode
{
   Allowed,
   Required,
   NotAllowed
}
[AttributeUsage(AttributeTargets.Interface|AttributeTargets.Class,
                Inherited=false)]
public sealed class ServiceContractAttribute : Attribute
{
   public SessionMode SessionMode
   {get;set;}
   //More members
}

SessionMode ist standardmäßig auf SessionMode.Allowed eingestellt. Der konfigurierte Wert SessionMode ist in den Metadaten des Dienstes enthalten und wird korrekt wiedergegeben, wenn der Kunde die Vertragsmetadaten importiert. Der Enum-Wert SessionMode hat nichts mit der Service-Sitzung zu tun. Sein richtiger Name sollte TransportSessionMode lauten, da er sich auf die Transport-Sitzung bezieht und nicht auf die logische Sitzung zwischen dem Kunden und der Instanz.

SessionMode.Allowed

SessionMode.Allowed ist der Standardwert der Eigenschaft , daher sind diese Definitionen gleichwertig: SessionMode

[ServiceContract]
interface IMyContract
{...}

[ServiceContract(SessionMode = SessionMode.Allowed)]
interface IMyContract
{...}

Alle Bindungen unterstützen die Konfiguration des Vertrags auf dem Endpunkt mit SessionMode.Allowed. Wenn die Eigenschaft SessionMode mit diesem Wert konfiguriert ist, werden Transportsitzungen zugelassen, aber nicht erzwungen. Das genaue Verhalten hängt von der Konfiguration des Dienstes und der verwendeten Bindung ab. Wenn der Dienst für die Aktivierung pro Anruf konfiguriert ist, verhält er sich wie in Beispiel 4-2 als Dienst pro Anruf. Wenn der Dienst für die Aktivierung pro Sitzung konfiguriert ist, verhält er sich nur dann wie ein Sitzungsdienst, wenn die verwendete Bindung eine Sitzung auf Transportebene aufrechterhält. Zum Beispiel kann die BasicHttpBinding aufgrund der verbindungslosen Natur des HTTP-Protokolls niemals eine Sitzung auf Transportebene haben. WSHttpBinding ohne Message Security und ohne zuverlässiges Messaging wird ebenfalls keine Sitzung auf Transportebene aufrechterhalten. In beiden Fällen verhält sich der Dienst, obwohl er mit InstanceContextMode.PerSession und der Vertrag mit SessionMode.Allowed konfiguriert ist, wie ein Dienst pro Aufruf.

Wenn du jedoch WSHttpBinding mit Nachrichtensicherheit (die Standardkonfiguration) oder mit zuverlässigem Messaging verwendest, oder wenn du NetTcpBinding oder NetNamedPipeBinding verwendest, verhält sich der Dienst wie ein sitzungsbezogener Dienst. Wenn du z. B. NetTcpBinding verwendest, verhält sich dieser Dienst wie ein sitzungsbezogener Dienst:

[ServiceContract]
interface IMyContract
{...}

class MyService : IMyContract
{...}

Beachte, dass der vorherige Codeschnipsel einfach die Standardwerte für die Eigenschaften SessionMode und InstanceContextMode übernimmt.

SessionMode.Required

Der Wert SessionMode.Required schreibt die Verwendung einer Sitzung auf Transportebene vor, aber nicht unbedingt eine Sitzung auf Anwendungsebene. Es ist nicht möglich, einen Vertrag mit SessionMode.Required mit einem Service-Endpunkt zu konfigurieren, dessen Bindung keine Session auf Transportebene vorsieht, und diese Einschränkung wird beim Laden des Service überprüft. Du kannst den Dienst aber trotzdem so konfigurieren, dass er bei jedem Client-Aufruf eine Instanz erzeugt und zerstört. Nur wenn der Dienst als sitzungsbezogener Dienst konfiguriert ist, bleibt die Dienstinstanz während der gesamten Sitzung des Kunden bestehen:

[ServiceContract(SessionMode = SessionMode.Required)]
interface IMyContract
{...}

class MyService : IMyContract
{...}
Hinweis

Wenn du einen sitzungsbasierten Vertrag entwirfst, empfehle ich dir, SessionMode.Required explizit zu verwenden und dich nicht auf die Vorgabe von SessionMode.Allowed zu verlassen. Die restlichen Codebeispiele in diesem Buch wenden SessionMode.Required aktiv an, wenn sitzungsbasierte Interaktion geplant ist.

Beispiel 4-4 listet denselben Dienst und denselben Kunden wie in Beispiel 4-2 auf, nur dass der Vertrag und der Dienst so konfiguriert sind, dass sie eine private Sitzung erfordern. Wie du in der Ausgabe sehen kannst, hat der Kunde eine eigene Instanz erhalten.

Beispiel 4-4. Pro-Sitzung-Dienst und Client
///////////////////////// Service Code /////////////////////
[ServiceContract(SessionMode = SessionMode.Required)]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
class MyService : IMyContract,IDisposable
{
   int m_Counter = 0;

   MyService()
   {
      Trace.WriteLine("MyService.MyService()");
   }
   public void MyMethod()
   {
      m_Counter++;
      Trace.WriteLine("Counter = " + m_Counter);
   }
   public void Dispose()
   {
      Trace.WriteLine("MyService.Dispose()");
   }
}
///////////////////////// Client Code /////////////////////
MyContractClient proxy = new MyContractClient();

proxy.MyMethod();
proxy.MyMethod();

proxy.Close();

//Output
MyService.MyService()
Counter = 1
Counter = 2
MyService.Dispose()

SessionMode.NotAllowed

SessionMode.NotAllowed verbietet die Verwendung einer Sitzung auf Transportebene, wodurch eine Sitzung auf Anwendungsebene ausgeschlossen wird. Unabhängig von der Konfiguration des Dienstes verhält sich der Dienst bei Verwendung dieses Wertes immer wie ein Dienst pro Aufruf.

Da sowohl das TCP- als auch das IPC-Protokoll eine Sitzung auf der Transportebene aufrechterhalten, kannst du keinen Dienstendpunkt konfigurieren, der NetTcpBinding oder NetNamedPipeBinding verwendet, um einen mit SessionMode.NotAllowed gekennzeichneten Vertrag zu exponieren, was beim Laden des Dienstes überprüft wird. Die Verwendung von WSHttpBinding mit einer emulierten Transportsitzung ist jedoch weiterhin erlaubt. Im Interesse der Lesbarkeit empfehle ich, dass du bei der Auswahl von SessionMode.NotAllowed den Dienst immer auch als per-call konfigurierst:

[ServiceContract(SessionMode = SessionMode.NotAllowed)]
interface IMyContract
{...}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{...}

Da BasicHttpBinding keine Sitzung auf Transportebene haben kann, verhalten sich Endpunkte, die es verwenden, so, als ob der Vertrag immer mit SessionMode.NotAllowed konfiguriert ist. Ich betrachte SessionMode.NotAllowed als eine Einstellung, die eher der Vollständigkeit halber zur Verfügung steht, und würde sie nicht explizit auswählen.

Bindungen, Verträge und Dienstverhalten

Tabelle 4-1 fasst den resultierenden Instanzmodus als Produkt aus der verwendeten Bindung, dem Sitzungsmodus im Vertrag und dem konfigurierten Instanzkontextmodus im Dienstverhalten zusammen. In der Tabelle sind keine ungültigen Konfigurationen aufgeführt, wie z. B. SessionMode.Required mit dem BasicHttpBinding.

Tabelle 4-1. Instanzmodus als Produkt aus Bindung, Vertragskonfiguration und Serviceverhalten
Binden Sitzungsmodus Kontext-Modus Instanz-Modus
Basic Allowed/NotAllowed PerCall/PerSession PerCall
TCP, IPC Allowed/Required PerCall PerCall
TCP, IPC Allowed/Required PerSession PerSession
WS (keine Nachrichtensicherheit, keine Zuverlässigkeit) NotAllowed/Allowed PerCall/PerSession PerCall
WS (mit Nachrichtensicherheit oder Zuverlässigkeit) Allowed/Required PerSession PerSession
WS (mit Nachrichtensicherheit oder Zuverlässigkeit) NotAllowed PerCall/PerSession PerCall

Konsistente Konfiguration

Ich empfehle dringend, dass, wenn ein Vertrag, den der Dienst implementiert, ein sessionful Vertrag ist, alle Verträge sessionful sein sollten, und dass du es vermeiden solltest, per-call und sessionful Verträge auf demselben per-session Diensttyp zu mischen (auch wenn WCF dies erlaubt):

[ServiceContract(SessionMode = SessionMode.Required)]
interface IMyContract
{...}

[ServiceContract(SessionMode = SessionMode.NotAllowed)]
interface IMyOtherContract
{...}

//Avoid
class MyService : IMyContract,IMyOtherContract
{...}

Der Grund dafür liegt auf der Hand: Per-Call-Dienste müssen ihren Status proaktiv verwalten, während dies bei Per-Session-Diensten nicht der Fall ist. Während die beiden Verträge an zwei verschiedenen Endpunkten offengelegt werden und unabhängig voneinander von zwei verschiedenen Kunden genutzt werden können, erfordert diese Dualität eine umständliche Implementierung für die zugrunde liegende Serviceklasse.

Sitzungen und Verlässlichkeit

Die Sitzung zwischen dem Kunden und der Dienstinstanz ist nur so zuverlässig wie die zugrunde liegende Transportsitzung. Daher sollten alle Endpunkte eines Dienstes, der einen sitzungsbasierten Vertrag implementiert, Bindungen verwenden, die zuverlässige Transportsitzungen ermöglichen. Achte darauf, dass du immer eine Bindung verwendest, die Zuverlässigkeit unterstützt, und dass du sie sowohl auf dem Client als auch auf dem Dienst explizit aktivierst, entweder programmatisch oder administrativ, wie in Beispiel 4-5 gezeigt.

Beispiel 4-5. Aktivieren der Zuverlässigkeit für sitzungsbezogene Dienste
<!—Host configuration:—>
<system.serviceModel>
   <services>
      <service name = "MyPerSessionService">
         <endpoint
            address  = "net.tcp://localhost:8000/MyPerSessionService"
            binding  = "netTcpBinding"
            bindingConfiguration = "TCPSession"
            contract = "IMyContract"
         />
      </service>
   </services>
   <bindings>
      <netTcpBinding>
         <binding name = "TCPSession">
            <reliableSession enabled = "true"/>
         </binding>
      </netTcpBinding>
   </bindings>
</system.serviceModel>

<!—Client configuration:—>
<system.serviceModel>
   <client>
      <endpoint
         address  = "net.tcp://localhost:8000/MyPerSessionService/"
         binding  = "netTcpBinding"
         bindingConfiguration = "TCPSession"
         contract = "IMyContract"
      />
   </client>
   <bindings>
      <netTcpBinding>
         <binding name = "TCPSession">
            <reliableSession enabled = "true"/>
         </binding>
      </netTcpBinding>
   </bindings>
</system.serviceModel>

Die einzige Ausnahme von dieser Regel ist die IPC-Verbindung. Diese Bindung benötigt kein zuverlässiges Messaging-Protokoll (alle Aufrufe erfolgen ohnehin auf demselben Rechner) und gilt als inhärent zuverlässiger Transport.

Genauso wie eine zuverlässige Transportsitzung optional ist, ist auch die geordnete Zustellung von Nachrichten optional, und WCF sorgt für eine Sitzung, auch wenn die geordnete Zustellung deaktiviert ist. Es liegt jedoch in der Natur einer Anwendungssitzung, dass ein Client, der mit einem sitzungsbasierten Dienst interagiert, erwartet, dass alle Nachrichten in der Reihenfolge zugestellt werden, in der sie gesendet werden. Glücklicherweise ist die geordnete Zustellung standardmäßig aktiviert, wenn zuverlässige Transportsitzungen aktiviert sind, so dass keine zusätzliche Einstellung erforderlich ist.

Die Sitzungs-ID

Jede Sitzung hat eine eindeutige ID, die sowohl der Client als auch der Dienst erhalten können. Die Sitzungs-ID hat meist die Form einer GUID und kann für die Protokollierung und Diagnose verwendet werden. Der Dienst kann auf die Sitzungs-ID über den Operationsaufrufkontext zugreifen. Dabei handelt es sich um eine Reihe von Eigenschaften (einschließlich der Sitzungs-ID), die für Callbacks, Nachrichtenheader, Transaktionsmanagement, Sicherheit, Host-Zugriff und den Zugriff auf das Objekt, das den Ausführungskontext selbst darstellt, verwendet werden. Jede Serviceoperation hat einen Operationsaufrufkontext, auf den über die Klasse OperationContext zugegriffen werden kann. Ein Dienst kann einen Verweis auf den Ausführungskontext der aktuellen Methode über die statische Methode Current der Klasse OperationContext erhalten:

public sealed class OperationContext : ...
{
   public static OperationContext Current
   {get;set;}
   public string SessionId
   {get;}
}

Um auf die Sitzungs-ID zuzugreifen, muss der Dienst den Wert der Eigenschaft SessionId lesen, die (fast) eine GUID in Form eines Strings zurückgibt. Im Fall der TCP-Verbindung ohne Zuverlässigkeit folgt darauf die Ordnungszahl der Sitzung von diesem Host:

string sessionID = OperationContext.Current.SessionId;
Trace.WriteLine(sessionID);
//Traces:
//uuid:8a0480da-7ac0-423e-9f3e-b2131bcbad8d;id=1

Wenn ein Per-Call-Service ohne Transportsitzung auf die Eigenschaft SessionId zugreift, lautet die Sitzungs-ID null, da es keine Sitzung und somit keine ID gibt.

Der Client kann über den Proxy auf die Sitzungs-ID zugreifen. Wie in Kapitel 1 eingeführt, ist die Klasse ClientBase<T> die Basisklasse des Proxys. ClientBase<T> bietet die schreibgeschützte Eigenschaft InnerChannel vom Typ IClientChannel. IClientChannel leitet sich von der Schnittstelle IContextChannel ab, die eine Eigenschaft SessionId bietet, die die Sitzungs-ID in Form eines Strings zurückgibt:

public interface IContextChannel : ...
{
   string SessionId
   {get;}
   //More members
}
public interface IClientChannel : IContextChannel,...
{...}
public abstract class ClientBase<T> : ...
{
   public IClientChannel InnerChannel
   {get;}
   //More members
}

Ausgehend von den Definitionen in Beispiel 4-4 könnte der Client die Sitzungs-ID wie folgt erhalten:

MyContractClient proxy = new MyContractClient();
proxy.MyMethod();

string sessionID = proxy.InnerChannel.SessionId;
Trace.WriteLine(sessionID);

Inwieweit die client-seitige Sitzungs-ID mit der des Dienstes übereinstimmt (und sogar, wann der Client auf die Eigenschaft SessionId zugreifen darf), hängt jedoch von der verwendeten Bindung und ihrer Konfiguration ab. Was die Sitzungs-IDs auf Client- und Service-Seite miteinander verbindet, ist die zuverlässige Sitzung auf der Transportebene. Wenn die TCP-Bindung verwendet wird und eine verlässliche Sitzung aktiviert ist (so wie es sein sollte), kann der Client eine gültige Sitzungs-ID erst nach dem ersten Methodenaufruf an den Dienst zum Aufbau der Sitzung oder nach dem expliziten Öffnen des Proxys erhalten. In diesem Fall stimmt die Sitzungs-ID, die der Kunde erhält, mit der des Dienstes überein. (Greift der Client vor dem ersten Aufruf auf die Sitzungs-ID zu, wird die Eigenschaft SessionId auf null gesetzt). Wenn die TCP-Bindung verwendet wird, aber zuverlässige Sitzungen deaktiviert sind, kann der Client vor dem ersten Aufruf auf die Sitzungs-ID zugreifen, aber die erhaltene ID wird sich von der des Dienstes unterscheiden. Bei der WS-Bindung und aktivierter zuverlässiger Nachrichtenübermittlung wird die Sitzungs-ID bis nach dem ersten Aufruf (oder nachdem der Client den Proxy geöffnet hat) unter null angezeigt, aber danach haben der Client und der Dienst immer dieselbe Sitzungs-ID. Ohne zuverlässiges Messaging muss der Client zuerst den Proxy verwenden (oder ihn einfach öffnen), bevor er auf die Sitzungs-ID zugreifen kann, oder er riskiert eine InvalidOperationException. Nach dem Öffnen des Proxys haben der Client und der Dienst eine korrelierte Sitzungs-ID. Mit der IPC-Bindung kann der Client auf die Eigenschaft SessionId zugreifen, bevor er den ersten Aufruf tätigt, aber der Client wird immer eine andere Sitzungs-ID erhalten als der Dienst. Bei der Verwendung dieser Bindung ist es daher besser, die Sitzungs-ID ganz zu ignorieren.

Beendigung der Sitzung

Normalerweise wird die Sitzung beendet, wenn der Kunde den Proxy schließt. Wenn der Kunde es jedoch versäumt, den Proxy zu schließen, oder wenn der Kunde unfreiwillig aufhört oder ein Kommunikationsproblem auftritt, wird die Sitzung ebenfalls beendet, sobald der Inaktivitäts-Timeout der Transportsitzung überschritten wird.

Singleton Service

Der Singleton-Dienst ist der ultimative gemeinsam nutzbare Dienst. Wenn du einen Dienst als Singleton konfigurierst, sind alle Clients unabhängig voneinander mit demselben bekannten Instanzkontext und implizit mit derselben Instanz innerhalb des Dienstes verbunden, unabhängig davon, mit welchem Endpunkt des Dienstes sie sich verbinden. Das Singleton wird genau einmal erstellt, wenn der Host erstellt wird, und lebt für immer: Es wird nur entsorgt, wenn der Host heruntergefahren wird.

Hinweis

Der IIS oder der WAS erstellen nur dann ein Singleton, wenn die erste Anfrage an einen Dienst in diesem Prozess gestellt wird. Um die Semantik des Singletons zu erhalten, musst du Self-Hosting verwenden.

Die Verwendung eines Singletons erfordert nicht, dass der Kunde eine logische Sitzung mit der Singleton-Instanz unterhält oder eine Bindung verwendet, die eine Sitzung auf Transportebene unterstützt. Wenn der Vertrag, den der Kunde konsumiert, eine Sitzung hat, hat das Singleton während des Aufrufs dieselbe Sitzungs-ID wie der Kunde (sofern die Bindung dies zulässt), aber das Schließen des Client-Proxys beendet nur die Transportsitzung, nicht aber den Singleton-Kontext und die Instanz darin. Wenn der Singleton-Dienst Verträge ohne eine Sitzung unterstützt, sind diese Verträge nicht aufrufbar: Sie werden ebenfalls mit derselben Instanz verbunden. Es liegt in der Natur der Sache, dass das Singleton gemeinsam genutzt wird und jeder Kunde einfach seinen eigenen Proxy oder Proxys dafür erstellen sollte.

Du konfigurierst einen Singleton-Dienst, indem du die Eigenschaft InstanceContextMode auf InstanceContextMode.Single setzt:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class MySingleton : ...
{...}

Beispiel 4-6 demonstriert einen Singleton-Dienst mit zwei Verträgen, von denen einer eine Sitzung erfordert und der andere nicht. Wie du aus dem Client-Aufruf ersehen kannst, wurden die Aufrufe an den beiden Endpunkten an dieselbe Instanz weitergeleitet und das Schließen der Proxys hat das Singleton nicht beendet.

Beispiel 4-6. Ein Singleton Service und Client
///////////////////////// Service Code /////////////////////
[ServiceContract(SessionMode = SessionMode.Required)]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
[ServiceContract(SessionMode = SessionMode.NotAllowed)]
interface IMyOtherContract
{
   [OperationContract]
   void MyOtherMethod();
}
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
class MySingleton : IMyContract,IMyOtherContract,IDisposable
{
   int m_Counter = 0;

   public MySingleton()
   {
      Trace.WriteLine("MySingleton.MySingleton()");
   }
   public void MyMethod()
   {
      m_Counter++;
      Trace.WriteLine("Counter = " + m_Counter);
   }
   public void MyOtherMethod()
   {
      m_Counter++;
      Trace.WriteLine("Counter = " + m_Counter);
   }
   public void Dispose()
   {
      Trace.WriteLine("Singleton.Dispose()");
   }
}
///////////////////////// Client Code /////////////////////
MyContractClient proxy1 = new MyContractClient();
proxy1.MyMethod();
proxy1.Close();

MyOtherContractClient proxy2 = new MyOtherContractClient();
proxy2.MyOtherMethod();
proxy2.Close();

//Output
MySingleton.MySingleton()
Counter = 1
Counter = 2

Initialisierung eines Singletons

Manchmal möchtest du das Singleton vielleicht nicht nur mit dem Standardkonstruktor erstellen und initialisieren. Vielleicht erfordert die Initialisierung des Status einige benutzerdefinierte Schritte oder spezielles Wissen, mit dem die Kunden nicht behelligt werden sollten oder das den Kunden nicht zur Verfügung steht. Was auch immer der Grund sein mag, du möchtest das Singleton vielleicht über einen anderen Mechanismus als den WCF Service Host erstellen. Um solche Szenarien zu unterstützen, bietet WCF die Möglichkeit, die Singleton-Instanz vorher direkt mit der normalen CLR-Instanziierung zu erstellen, sie zu initialisieren und dann den Host mit dieser Instanz als Singleton-Dienst zu öffnen. Die Klasse ServiceHost bietet einen eigenen Konstruktor, der eine object akzeptiert:

public class ServiceHost : ServiceHostBase,...
{
   public ServiceHost(object singletonInstance,params Uri[] baseAddresses);
   public object SingletonInstance
   {get;}
   //More members
}

Beachte, dass die object als Singleton konfiguriert werden muss. Betrachte zum Beispiel den Code in Beispiel 4-7. Die Klasse MySingleton wird zunächst initialisiert und dann als Singleton gehostet.

Beispiel 4-7. Initialisierung und Hosting eines Singletons
//Service code
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
class MySingleton : IMyContract
{
   public int Counter
   {get;set;}

   public void MyMethod()
   {
      Counter++;
      Trace.WriteLine("Counter = " + Counter);
   }
}
//Host code
MySingleton singleton = new MySingleton();
singleton.Counter = 287;

ServiceHost host = new ServiceHost(singleton);
host.Open();

//Client code
MyContractClient proxy = new MyContractClient();
proxy.MyMethod();
proxy.Close();

//Output:
Counter = 288

Wenn du ein Singleton auf diese Weise initialisierst und bereitstellst, möchtest du vielleicht auch die Möglichkeit haben, direkt auf der Host-Seite darauf zuzugreifen. WCF ermöglicht es nachgelagerten Objekten, über die Eigenschaft SingletonInstance von ServiceHost direkt auf das Singleton zuzugreifen. Jede Partei in der Aufrufkette, die von einem Operationsaufruf auf dem Singleton nach unten führt, kann immer über die schreibgeschützte Eigenschaft Host des Operationskontexts auf den Host zugreifen:

public sealed class OperationContext : ...
{
   public ServiceHostBase Host
   {get;}
   //More members
}

Sobald du die Singleton-Referenz hast, kannst du direkt mit ihr interagieren:

ServiceHost host = OperationContext.Current.Host as ServiceHost;
Debug.Assert(host != null);
MySingleton singleton = host.SingletonInstance as MySingleton;
Debug.Assert(singleton != null);
singleton.Counter = 388;

Wenn dem Host keine Singleton-Instanz zur Verfügung gestellt wurde, gibt SingletonInstance null zurück.

Rationalisierung mit ServiceHost<T>

Die in Kapitel 1 vorgestellte Klasse ServiceHost<T> kann so erweitert werden, dass sie typsichere Singleton-Initialisierung und -Zugriff bietet:

public class ServiceHost<T> : ServiceHost
{
   public ServiceHost(T singleton,params Uri[] baseAddresses) : 
                      base(singleton,baseAddresses)
   {}
   public virtual T Singleton
   {
      get
      {
         if(SingletonInstance == null)
         {
            return default(T);
         }
         return (T)SingletonInstance;
      }
   }
   //More members
}

Der Typ-Parameter bietet eine typsichere Bindung für das Objekt, das für die Konstruktion verwendet wird:

MySingleton singleton = new MySingleton();
singleton.Counter = 287;

ServiceHost<MySingleton> host = new ServiceHost<MySingleton>(singleton);
host.Open();

und das Objekt, das von der Eigenschaft Singleton zurückgegeben wird:

ServiceHost<MySingleton> host = OperationContext.Current.Host as 
                                ServiceHost<MySingleton>;
Debug.Assert(host != null);
host.Singleton.Counter = 388;
Hinweis

Die InProcFactory<T> (vorgestellt in Kapitel 1) wird auf ähnliche Weise erweitert, um eine Singleton-Instanz zu initialisieren.

Ein Singleton auswählen

Der Singleton-Dienst ist der Erzfeind der Skalierbarkeit. Der Grund dafür liegt in der Synchronisierung des Singleton-Status und nicht in den Kosten für die einzelne Instanz. Ein Singleton setzt voraus, dass das Singleton einen wertvollen Status hat, den du mit mehreren Clients teilen möchtest. Wenn der Zustand des Singletons veränderbar ist und sich mehrere Clients mit dem Singleton verbinden, kann es sein, dass sie dies alle gleichzeitig tun und die eingehenden Client-Aufrufe auf mehreren Worker-Threads laufen. Das Singleton muss daher den Zugriff auf seinen Zustand synchronisieren, um eine Beschädigung des Zustands zu vermeiden. Das wiederum bedeutet, dass immer nur ein Client gleichzeitig auf das Singleton zugreifen kann. Diese Einschränkung kann den Durchsatz, die Reaktionsfähigkeit und die Verfügbarkeit so weit verschlechtern, dass das Singleton in einem großen System unbrauchbar wird. Wenn zum Beispiel eine Operation an einem Singleton eine Zehntelsekunde dauert, kann das Singleton nur 10 Kunden pro Sekunde bedienen. Wenn es viel mehr Kunden gibt (z. B. 20 oder 100), ist die Leistung des Systems unzureichend.

Im Allgemeinen solltest du ein Singleton nur dann verwenden, wenn es einem natürlichen Singleton in der Anwendungsdomäne gut entspricht. Ein natürliches Singleton ist eine Ressource, die von Natur aus einmalig und einzigartig ist. Beispiele für natürliche Singletons sind ein globales Logbuch, in das alle Dienste ihre Aktivitäten eintragen sollen, ein einzelner Kommunikationsport oder ein einzelner mechanischer Motor. Vermeide es, ein Singleton zu verwenden, wenn auch nur die geringste Chance besteht, dass die Geschäftslogik in Zukunft mehr als einen solchen Dienst zulässt (z. B. das Hinzufügen eines weiteren Motors oder einer zweiten Kommunikationsschnittstelle). Der Grund dafür liegt auf der Hand: Wenn alle deine Clients darauf angewiesen sind, mit der bekannten Instanz verbunden zu sein, und mehr als eine Service-Instanz verfügbar ist, müssen die Clients plötzlich eine Möglichkeit haben, sich an die richtige Instanz zu binden. Das kann schwerwiegende Auswirkungen auf das Programmiermodell der Anwendung haben. Aufgrund dieser Einschränkungen empfehle ich, Singletons im Allgemeinen zu vermeiden und Wege zu finden, den Zustand des Singletons und nicht die Singleton-Instanz selbst zu teilen. Es gibt jedoch Fälle, in denen die Verwendung eines Singletons akzeptabel ist, wie bereits erwähnt.

Abgrenzungsoperationen

Manchmal enthält ein sitzungsbezogener Vertrag eine implizite Reihenfolge der Operationsaufrufe. Einige Operationen können nicht zuerst aufgerufen werden, während andere Operationen zuletzt aufgerufen werden müssen. Nehmen wir zum Beispiel diesen Vertrag, der für die Verwaltung von Kundenbestellungen verwendet wird:

[ServiceContract(SessionMode = SessionMode.Required)]
interface IOrderManager
{
   [OperationContract]
   void SetCustomerId(int customerId);

   [OperationContract]
   void AddItem(int itemId);

   [OperationContract]
   decimal GetTotal();

   [OperationContract]
   bool ProcessOrders();
}

Der Vertrag hat folgende Einschränkungen: Der Kunde muss die Kunden-ID als erste Operation in der Sitzung angeben, sonst können keine anderen Operationen stattfinden; Artikel können hinzugefügt und die Gesamtsumme berechnet werden, so oft der Kunde es wünscht; die Bearbeitung der Bestellung beendet die Sitzung und muss daher zuletzt erfolgen. In klassischem .NET zwangen solche Anforderungen die Entwickler oft dazu, einen Zustandsautomaten oder Zustandsflags zu unterstützen und den Zustand bei jeder Operation zu überprüfen.

WCF ermöglicht es Vertragsgestaltern jedoch, Vertragsoperationen als Operationen zu kennzeichnen, die die Sitzung starten oder beenden können, indem sie die Eigenschaften IsInitiating und IsTerminating des Attributs OperationContract verwenden:

[AttributeUsage(AttributeTargets.Method)]
public sealed class OperationContractAttribute : Attribute
{
   public bool IsInitiating
   {get;set;}
   public bool IsTerminating
   {get;set;}
   //More members
}

Diese Eigenschaften können verwendet werden, um die Grenze der Sitzung abzugrenzen; daher nenne ich diese Technik Abgrenzungsoperationen. Wenn diese Eigenschaften beim Laden des Dienstes (oder während der Nutzung des Proxys auf der Client-Seite) auf einen anderen Wert als den Standardwert gesetzt werden, überprüft die WCF, ob die Abgrenzungsoperationen Teil eines Vertrags sind, der Sitzungen vorschreibt (d.h., dass SessionMode auf SessionMode.Required gesetzt ist), und gibt andernfalls einen InvalidOperationException aus. Sowohl ein sessionful Service als auch ein Singleton können Verträge implementieren, die abgrenzende Operationen zur Verwaltung ihrer Client-Sitzungen verwenden.

Die Standardwerte für diese Eigenschaften sind true für IsInitiating und false für IsTerminating. Daher sind diese beiden Definitionen gleichwertig:

[OperationContract]
void MyMethod();

[OperationContract(IsInitiating = true,IsTerminating = false)]
void MyMethod();

Wie du siehst, kannst du beide Eigenschaften für dieselbe Methode festlegen. Außerdem grenzen Operationen nicht standardmäßig die Sitzung ab - Operationen können zuerst, zuletzt oder zwischen anderen Operationen in der Sitzung aufgerufen werden. Durch die Verwendung von Nicht-Standardwerten kannst du festlegen, dass eine Methode nicht als erstes oder als letztes oder beides aufgerufen wird:

[ServiceContract(SessionMode = SessionMode.Required)]
interface IMyContract
{
   [OperationContract]
   void StartSession();

   [OperationContract(IsInitiating = false)]
   void CannotStart();

   [OperationContract(IsTerminating = true)]
   void EndSession();

   [OperationContract(IsInitiating = false,IsTerminating = true)]
   void CannotStartCanEndSession();
}

Um auf den Vertrag zur Auftragsverwaltung zurückzukommen, kannst du Abgrenzungsoperationen verwenden, um die Interaktionsbeschränkungen durchzusetzen:

[ServiceContract(SessionMode = SessionMode.Required)]
interface IOrderManager
{
   [OperationContract]
   void SetCustomerId(int customerId);

   [OperationContract(IsInitiating = false)]
   void AddItem(int itemId);

   [OperationContract(IsInitiating = false)]
   decimal GetTotal();

   [OperationContract(IsInitiating = false,IsTerminating = true)]
   bool ProcessOrders();
}
//Client code
OrderManagerClient proxy = new OrderManagerClient();

proxy.SetCustomerId(123);
proxy.AddItem(4);
proxy.AddItem(5);
proxy.AddItem(6);
proxy.ProcessOrders();

proxy.Close();

Wenn IsInitiating auf true gesetzt ist (Standardeinstellung), bedeutet das, dass die Operation eine neue Sitzung beginnt, wenn sie die erste Methode ist, die der Kunde aufruft, aber Teil der laufenden Sitzung ist, wenn eine andere Operation zuerst aufgerufen wird. Wenn IsInitiating auf false gesetzt ist, bedeutet dies, dass ein Kunde diesen Vorgang nie als ersten Vorgang in einer neuen Sitzung aufrufen kann und dass die Methode nur Teil einer laufenden Sitzung sein kann.

Wenn IsTerminating auf false gesetzt ist (Standardeinstellung), bedeutet dies, dass die Sitzung nach der Rückkehr der Operation fortgesetzt wird. Wenn IsTerminating auf true gesetzt ist, bedeutet dies, dass die Sitzung beendet wird, sobald die Methode zurückkehrt, und die WCF die Dienstinstanz asynchron entsorgt. Der Client kann dann keine weiteren Aufrufe an den Proxy richten. Beachte, dass der Client den Proxy trotzdem schließen sollte.

Hinweis

Wenn du einen Proxy für einen Dienst generierst, der Abgrenzungsoperationen verwendet, enthält die importierte Vertragsdefinition die Eigenschaftseinstellungen. Außerdem erzwingt WCF die Abgrenzung getrennt auf der Client- und der Service-Seite, so dass du sie eigentlich unabhängig voneinander einsetzen könntest.

Instanz Deaktivierung

Konzeptionell verbindet die bisher beschriebene Technik der sitzungsbasierten Verwaltung von Dienstinstanzen einen oder mehrere Clients mit einer Dienstinstanz. In Wirklichkeit ist das Bild jedoch komplexer. Wie in Kapitel 1 beschrieben, wird jede Serviceinstanz in einem Kontext gehostet, wie in Abbildung 4-2 dargestellt.

Contexts and instances
Abbildung 4-2. Kontexte und Instanzen

Bei Sitzungen werden die Nachrichten des Clients nicht mit der Instanz, sondern mit dem Kontext, der sie hostet, verknüpft. Wenn die Sitzung beginnt, erstellt der Host einen neuen Kontext. Wenn die Sitzung endet, wird der Kontext beendet. Standardmäßig ist die Lebensdauer des Kontexts die gleiche wie die der Instanz, die er hostet. Aus Gründen der Optimierung und Erweiterbarkeit bietet WCF dem Dienstentwickler jedoch die Möglichkeit, die beiden Lebensdauern zu trennen und die Instanz getrennt von ihrem Kontext zu deaktivieren. WCF ermöglicht es auch, dass ein Kontext ohne eine zugehörige Instanz existiert, wie in Abbildung 4-2 dargestellt. Ich nenne diese Technik der Instanzverwaltung Kontextdeaktivierung. Die übliche Methode zur Steuerung der Kontextdeaktivierung ist die Eigenschaft ReleaseInstanceMode des Attributs OperationBehavior:

public enum ReleaseInstanceMode
{
   None,
   BeforeCall,
   AfterCall,
   BeforeAndAfterCall,
}
[AttributeUsage(AttributeTargets.Method)]
public sealed class OperationBehaviorAttribute : Attribute,...
{
   public ReleaseInstanceMode ReleaseInstanceMode
   {get;set;}
   //More members
}

ReleaseInstanceMode ist vom Enum-Typ ReleaseInstanceMode. Die verschiedenen Werte von ReleaseInstanceMode bestimmen, wann die Instanz in Bezug auf den Methodenaufruf freigegeben wird: vor, nach, vor und nach oder gar nicht. Bei der Freigabe der Instanz wird, wenn der Dienst IDisposable unterstützt, die Methode Dispose() aufgerufen und Dispose() hat einen Operationskontext.

Normalerweise wendest du die Instanzdeaktivierung auf einige, aber nicht auf alle Dienstmethoden an, oder mit unterschiedlichen Werten auf verschiedenen Methoden:

[ServiceContract(SessionMode = SessionMode.Required)]
interface IMyContract
{
   [OperationContract]
   void MyMethod();

   [OperationContract]
   void MyOtherMethod();
}
class MyService : IMyContract,IDisposable
{
   [OperationBehavior(ReleaseInstanceMode = ReleaseInstanceMode.AfterCall)]
   public void MyMethod()
   {...}
   public void MyOtherMethod()
   {...}
   public void Dispose()
   {...}
}

Der Grund dafür, dass du sie normalerweise sporadisch anwendest, ist, dass du bei einer einheitlichen Anwendung einen anrufbezogenen Dienst erhalten würdest, der genauso gut als anrufbezogen konfiguriert werden könnte.

Wenn die Deaktivierung von Instanzen eine bestimmte Aufrufreihenfolge voraussetzt, kannst du versuchen, diese Reihenfolge mit Abgrenzungsoperationen durchzusetzen.

Konfigurieren mit ReleaseInstanceMode.None

Der Standardwert für die Eigenschaft ReleaseInstanceMode ist ReleaseInstanceMode.None, sodass diese beiden Definitionen gleichwertig sind:

[OperationBehavior(ReleaseInstanceMode = ReleaseInstanceMode.None)]
public void MyMethod()
{...}

public void MyMethod()
{...}

ReleaseInstanceMode.None bedeutet, dass die Lebensdauer der Instanz durch den Aufruf nicht beeinflusst wird, wie in Abbildung 4-3 dargestellt.

Instance lifetime with methods configured with ReleaseInstanceMode.None
Abbildung 4-3. Instanzlebensdauer mit Methoden, die mit ReleaseInstanceMode.None konfiguriert sind

Konfigurieren mit ReleaseInstanceMode.BeforeCall

Wenn eine Methode mit ReleaseInstanceMode.BeforeCall konfiguriert ist und bereits eine Instanz in der Sitzung vorhanden ist, deaktiviert WCF diese vor der Weiterleitung des Aufrufs, erstellt eine neue Instanz an ihrer Stelle und lässt diese neue Instanz den Aufruf bedienen, wie in Abbildung 4-4 dargestellt.

Instance lifetime with methods configured with ReleaseInstanceMode.BeforeCall
Abbildung 4-4. Instanzlebensdauer mit Methoden, die mit ReleaseInstanceMode.BeforeCall konfiguriert sind

WCF deaktiviert die Instanz und ruft Dispose() auf, bevor der Aufruf im Thread des eingehenden Aufrufs erfolgt, während der Client blockiert. Dadurch wird sichergestellt, dass die Deaktivierung tatsächlich vor dem Aufruf erfolgt und nicht gleichzeitig mit diesem. ReleaseInstanceMode.BeforeCall wurde entwickelt, um Methoden wie Create() zu optimieren, die wertvolle Ressourcen erwerben und die zuvor zugewiesenen Ressourcen wieder freigeben möchten. Anstatt die Ressourcen beim Start der Sitzung zu erwerben, wartest du bis zum Aufruf der Methode Create() und gibst dann sowohl die zuvor zugewiesenen Ressourcen frei als auch die neuen zuweisen. Nachdem Create() aufgerufen wurde, kannst du mit dem Aufruf anderer Methoden auf der Instanz beginnen, die normalerweise mit ReleaseInstanceMode.None konfiguriert werden.

Konfigurieren mit ReleaseInstanceMode.AfterCall

Wenn eine Methode mit ReleaseInstanceMode.AfterCall konfiguriert ist, deaktiviert die WCF die Instanz nach dem Aufruf, wie in Abbildung 4-5 dargestellt.

Damit soll eine Methode wie oder Cleanup() optimiert werden, die wertvolle Ressourcen der Instanz aufräumt, ohne auf die Beendigung der Sitzung zu warten. ReleaseInstanceMode.AfterCall wird normalerweise auf Methoden angewendet, die nach Methoden aufgerufen werden, die mit ReleaseInstanceMode.None konfiguriert wurden.

Instance lifetime with methods configured with ReleaseInstanceMode.AfterCall
Abbildung 4-5. Instanzlebensdauer mit Methoden, die mit ReleaseInstanceMode.AfterCall konfiguriert sind

Konfigurieren mit ReleaseInstanceMode.BeforeAndAfterCall

Wie der Name schon sagt, hat die Konfiguration einer Methode mit ReleaseInstanceMode.BeforeAndAfterCall die gleiche Wirkung wie die Verwendung von ReleaseInstanceMode.BeforeCall und ReleaseInstanceMode.AfterCall. Wenn der Kontext vor dem Aufruf eine Instanz hat, deaktiviert WCF diese Instanz kurz vor dem Aufruf und erstellt eine neue Instanz, um den Aufruf zu bedienen. Die neue Instanz wird dann nach dem Aufruf deaktiviert, wie in Abbildung 4-6 dargestellt.

Instance lifetime with methods configured with ReleaseInstanceMode.BeforeAndAfterCall
Abbildung 4-6. Instanzlebensdauer mit Methoden, die mit ReleaseInstanceMode.BeforeAndAfterCall konfiguriert sind

ReleaseInstanceMode.BeforeAndAfterCall mag auf den ersten Blick überflüssig erscheinen, aber er ergänzt die anderen Werte tatsächlich. Er ist dafür gedacht, auf Methoden angewendet zu werden, die nach den mit ReleaseInstanceMode.BeforeCall oder None gekennzeichneten Methoden oder vor den mit ReleaseInstanceMode.AfterCall oder None gekennzeichneten Methoden aufgerufen werden. Stellen Sie sich eine Situation vor, in der ein sitzungsbezogener Dienst von zustandsorientiertem Verhalten profitieren möchte (wie ein Dienst pro Aufruf), während er Ressourcen nur bei Bedarf zurückhält, um die Ressourcenzuweisung und Sicherheitsabfrage zu optimieren. Wäre ReleaseInstanceMode.BeforeCall die einzige verfügbare Option, gäbe es nach dem Aufruf eine Zeitspanne, in der die Ressourcen dem Objekt noch zugewiesen wären, aber nicht genutzt würden. Ähnlich verhält es sich, wenn ReleaseInstanceMode.AfterCall die einzige verfügbare Option wäre, denn dann gäbe es eine Zeitspanne vor dem Aufruf, in der die Ressourcen verschwendet werden würden.

Explizite Deaktivierung

Anstatt zur Entwurfszeit zu entscheiden, welche Methoden zur Deaktivierung einer Instanz verwendet werden sollen, kannst du zur Laufzeit entscheiden, die Instanz zu deaktivieren, nachdem die Methode zurückgekehrt ist. Dazu rufst du unter die Methode ReleaseServiceInstance() für den Instanzkontext auf. Den Instanzkontext erhältst du über die InstanceContext Eigenschaft des Operationskontexts:

public sealed class InstanceContext : ...
{
   public void ReleaseServiceInstance();
   //More members
}
public sealed class OperationContext : ...
{
   public InstanceContext InstanceContext
   {get;}
   //More members
}

Beispiel 4-8 demonstriert die Verwendung der expliziten Deaktivierung zur Implementierung einer benutzerdefinierten Instanzverwaltungstechnik, die vom Wert eines Zählers abhängig ist.

Beispiel 4-8. ReleaseServiceInstance() verwenden
[ServiceContract(SessionMode = SessionMode.Required)]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
class MyService : IMyContract,IDisposable
{
   int m_Counter = 0;

   public void MyMethod()
   {
      m_Counter++;

      if(m_Counter > 4)
      {
         OperationContext.Current.InstanceContext.ReleaseServiceInstance();
      }
   }
   public void Dispose()
   {...}
}

Der Aufruf von ReleaseServiceInstance() hat eine ähnliche Wirkung wie die Verwendung von ReleaseInstanceMode.AfterCall. Wenn er in einer Methode verwendet wird, die mit ReleaseInstanceMode.BeforeCall dekoriert ist,hat er eine ähnliche Wirkung wie ReleaseInstanceMode.BeforeAndAfterCall .

Hinweis

Die Deaktivierung einer Instanz wirkt sich auch auf ein Singleton aus, obwohl es wenig Sinn macht, beides zu kombinieren - denn es ist zulässig und sogar wünschenswert, das Singleton niemals zu deaktivieren.

Instanz-Deaktivierung verwenden

Die Deaktivierung von Instanzen ist eine Optimierungstechnik, und wie alle Optimierungstechniken solltest du sie im Allgemeinen vermeiden. Sie erhöht die Komplexität der Anwendung und macht den Code für alle außer WCF-Experten weniger zugänglich und wartbar. Ziehe die Instanzdeaktivierung nur dann in Erwägung, wenn sowohl deine Leistungs- als auch deine Skalierbarkeitsziele fehlgeschlagen sind und wenn eine sorgfältige Prüfung und ein Profiling zweifelsfrei ergeben haben, dass die Instanzdeaktivierung die Situation verbessern wird. Wenn dir Skalierbarkeit und Durchsatz wichtig sind, solltest du die Einfachheit des Instanzierungsmodus pro Aufruf nutzen und die Instanzdeaktivierung vermeiden. Der Hauptgrund, warum ich diese Technik mit dir teile, ist, dass die WCF selbst ausgiebig von der Instanzdeaktivierung Gebrauch macht; daher ist das Wissen darüber wichtig, um andere Aspekte der WCF zu entmystifizieren, z. B. dauerhafte Dienste und Transaktionen.

Langlebige Dienstleistungen

Nehmen wir den Fall eines langlaufenden Geschäftsprozesses oder Workflows, der aus mehreren Ausführungssequenzen besteht und viele Tage oder sogar Wochen dauert.

Hinweis

Ich verwende den Begriff Workflow, um einen Geschäftsworkflow im Allgemeinen zu bezeichnen, nicht einen, der notwendigerweise von dem Produkt namens Windows Workflow unterstützt wird oder damit zusammenhängt.

Bei solchen lang andauernden Prozessen können sich Clients (oder sogar Endnutzer) mit der Anwendung verbinden, eine begrenzte Menge an Arbeit erledigen, den Workflow in einen neuen Zustand überführen und dann die Verbindung für eine unbestimmte Zeit unterbrechen, bevor sie sich erneut verbinden und den Workflow weiter ausführen. Die Clients können jederzeit beschließen, den Workflow zu beenden und einen neuen zu starten, oder der Backend-Dienst, der den Workflow unterstützt, kann ihn beenden. Es macht natürlich wenig Sinn, Proxys und Dienste im Speicher zu halten und darauf zu warten, dass die Kunden sie aufrufen. Ein solcher Ansatz ist nicht stabil genug, um den Test der Zeit zu bestehen; zumindest werden Timeout-Probleme die Verbindung unweigerlich beenden, und es gibt keine einfache Möglichkeit, den Rechnern auf beiden Seiten zu erlauben, neu zu starten oder sich abzumelden. Die Notwendigkeit, den Clients und den Diensten unabhängige Lebenszyklen zu ermöglichen, ist in einem langlaufenden Geschäftsprozess sehr wichtig, denn ohne sie gibt es keine Möglichkeit, den Clients zu ermöglichen, sich zu verbinden, eine Arbeit im Rahmen des Workflows auszuführen und die Verbindung zu trennen. Auf der Hostseite möchtest du im Laufe der Zeit vielleicht sogar Anrufe zwischen Rechnern umleiten.

Die Lösung für lang laufende Dienste besteht darin, den Zustand des Dienstes nicht im Speicher zu halten, sondern jeden Aufruf einer neuen Instanz mit einem eigenen temporären Zustand im Speicher zu behandeln. Für jede Operation sollte der Dienst seinen Zustand aus einer dauerhaften Speicherung (z. B. einer Datei oder einer Datenbank) abrufen, die angeforderte Arbeitseinheit für diese Operation ausführen und den Zustand am Ende des Aufrufs wieder in der dauerhaften Speicherung speichern. Dienste, die diesem Modell folgen, werden als dauerhafte Dienste bezeichnet. Da die dauerhafte Speicherung von mehreren Rechnern gemeinsam genutzt werden kann, kannst du mit dauerhaften Diensten Aufrufe zu unterschiedlichen Zeiten an verschiedene Rechner weiterleiten, sei es aus Gründen der Skalierbarkeit, der Redundanz oder der Wartung.

Dauerhafte Dienste und Instanzmanagement-Modi

Dieser Ansatz für die Zustandsverwaltung von dauerhaften Diensten ähnelt sehr dem zuvor vorgeschlagenen Ansatz für Per-Call-Dienste, die ihren Zustand proaktiv verwalten. Die Verwendung von Per-Call-Diensten ist auch deshalb sinnvoll, weil es keinen Sinn macht, die Instanz zwischen den Aufrufen zu behalten, wenn ihr Zustand aus einer dauerhaften Speicherung stammt. Der einzige Unterschied zwischen einem dauerhaften Dienst und einem klassischen Per-Call-Dienst besteht darin, dass der Zustandsspeicher dauerhaft sein muss.

Theoretisch spricht zwar nichts dagegen, einen dauerhaften Dienst auf einen Sessionful- oder sogar einen Singleton-Dienst zu stützen und diesen Dienst seinen Zustand in und aus der dauerhaften Speicherung verwalten zu lassen, aber in der Praxis wäre das kontraproduktiv. Im Falle eines Sessionful-Dienstes müsstest du den Proxy auf der Client-Seite für lange Zeit offen halten und damit Clients ausschließen, die ihre Verbindung beenden und sich dann neu verbinden. Im Falle eines Singleton-Dienstes suggeriert schon der Begriff Singleton eine unendliche Lebensdauer mit Clients, die kommen und gehen, so dass es keinen Bedarf für Dauerhaftigkeit gibt. Daher ist der Modus der Instanziierung pro Aufruf die beste Wahl. Da bei dauerhaften Diensten pro Aufruf das Hauptaugenmerk auf lang andauernden Workflows und nicht auf Skalierbarkeit oder Ressourcenmanagement liegt, ist die Unterstützung von IDisposable optional. Es ist auch erwähnenswert, dass das Vorhandensein einer Transportsitzung für einen dauerhaften Dienst optional ist, da es keine Notwendigkeit gibt, eine logische Sitzung zwischen dem Kunden und dem Dienst aufrechtzuerhalten. Die Transportsitzung ist eine Facette des verwendeten Transportkanals und wird nicht verwendet, um die Lebensdauer der Instanz zu bestimmen.

Einleiten und Beenden

Wenn ein lang laufender Arbeitsablauf beginnt, muss der Dienst zunächst seinen Status in die dauerhafte Speicherung schreiben, damit nachfolgende Operationen den Status in der Speicherung finden können. Am Ende des Workflows muss der Dienst seinen Status aus der Speicherung entfernen, da die Speicherung sonst im Laufe der Zeit mit Instanzstatus aufgebläht wird, die niemand braucht.

Instanz-IDs und dauerhafte Speicherung

Da für jeden Vorgang eine neue Service-Instanz erstellt wird, muss eine Instanz eine Möglichkeit haben, ihren Zustand aus der dauerhaften Speicherung abzurufen und zu laden. Der Kunde muss daher einen Zustandsbezeichner für die Instanz angeben. Diese Kennung wird Instanz-ID genannt. Um Clients zu unterstützen, die sich nur gelegentlich mit dem Dienst verbinden, und Client-Anwendungen oder sogar Maschinen, die zwischen Aufrufen recyceln, speichert der Client die Instanz-ID in der Regel in einer dauerhaften Speicherung auf der Client-Seite (z. B. in einer Datei) und stellt diese ID bei jedem Aufruf zur Verfügung, solange der Workflow läuft. Wenn der Arbeitsablauf endet, kann der Kunde diese ID löschen. Für eine Instanz-ID ist es wichtig, einen Typ zu wählen, der serialisierbar und gleichwertig ist. Eine serialisierbare ID ist wichtig, weil der Dienst die ID zusammen mit ihrem Status in der dauerhaften Speicherung speichern muss. Eine gleichsetzbare ID ist erforderlich, damit der Dienst den Status aus der Speicherung abrufen kann. Alle .NET-Primitive (wie int, string und Guid) können als Instanz-IDs verwendet werden.

Die dauerhafte Speicherung ist in der Regel eine Art Wörterbuch, das die Instanz-ID mit dem Zustand der Instanz verbindet. In der Regel verwendet der Dienst eine einzige ID, um seinen gesamten Status zu repräsentieren, obwohl auch komplexere Beziehungen mit mehreren Schlüsseln und sogar Hierarchien von Schlüsseln möglich sind. Der Einfachheit halber beschränke ich mich hier auf eine einzige ID. Außerdem verwendet der Dienst oft eine spezielle Hilfsklasse oder eine Struktur, um alle Mitgliedsvariablen zusammenzufassen, und speichert diesen Typ in der dauerhaften Speicherung und ruft ihn von dort ab. Schließlich muss der Zugriff auf die dauerhafte Speicherung selbst thread-sicher und synchronisiert sein. Dies ist erforderlich, weil mehrere Instanzen gleichzeitig versuchen können, auf den Speicher zuzugreifen und ihn zu verändern.

Um dir bei der Implementierung und Unterstützung einfacher dauerhafter Dienste zu helfen, habe ich die Klasse FileInstanceStore<ID,T> geschrieben:

public interface IInstanceStore<ID,T> where ID : IEquatable<ID>
{
   void RemoveInstance(ID instanceId);
   bool ContainsInstance(ID instanceId);
   T this[ID instanceId]
   {get;set;}
}

public class FileInstanceStore<ID,T> : IInstanceStore<ID,T> where ID :
                                                                  IEquatable<ID>
{
   protected readonly string Filename;

   public FileInstanceStore(string fileName);

   //Rest of the implementation
}

FileInstanceStore<ID,T> ist ein allgemeiner dateibasierter Instanzspeicher. FileInstanceStore<ID,T> nimmt zwei Typparameter entgegen: Der Typparameter ID muss ein gleichwertiger Typ sein, und der Typparameter T repräsentiert den Instanzstatus. FileInstanceStore<ID,T> überprüft zur Laufzeit in einem statischen Konstruktor, dass sowohl T als auch ID serialisierbare Typen sind.

FileInstanceStore<ID,T> bietet einen einfachen Indexer, mit dem du den Instanzstatus in der Datei lesen und schreiben kannst. Du kannst auch einen Instanzstatus aus der Datei entfernen und überprüfen, ob die Datei den Instanzstatus enthält. Diese Vorgänge sind in der Schnittstelle IInstanceStore<ID,T> definiert. Die Implementierung von FileInstanceStore<ID,T> kapselt ein Wörterbuch und serialisiert und deserialisiert das Wörterbuch bei jedem Zugriff auf die Datei. Wenn FileInstanceStore<ID,T> zum ersten Mal verwendet wird und die Datei leer ist, initialisiert FileInstanceStore<ID,T> sie mit einem leeren Wörterbuch.

Explizite Instanz-IDs

Die einfachste Art und Weise, wie ein Client dem Service die Instanz-ID mitteilen kann, ist als expliziter Parameter für jede Operation, die auf den Status zugreifen soll. Beispiel 4-9 zeigt einen solchen Client und Service mit den entsprechenden Typdefinitionen.

Beispiel 4-9. Übergabe expliziter Instanz-IDs
[DataContract]
class SomeKey : IEquatable<SomeKey>
{...}

[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod(SomeKey instanceId);
}

//Helper type used by the service to capture its state
[Serializable]
struct MyState
{...}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{
   public void MyMethod(SomeKey instanceId)
   {
      GetState(instanceId);
      DoWork();
      SaveState(instanceId);
   }
   void DoWork()
   {...}

   //Get and set MyState from durable storage
   void GetState(SomeKey instanceId)
   {...}

   void SaveState(SomeKey instanceId)
   {...}
}

Um Beispiel 4-9 konkreter zu machen, betrachte Beispiel 4-10, das einen Taschenrechner mit einem dauerhaften Speicher in einer Datei unterstützt.

Beispiel 4-10. Rechner mit expliziter Instanz-ID
[ServiceContract]
interface ICalculator
{
   [OperationContract]
   double Add(double number1,double number2);

   /* More arithmetic operations */

   //Memory management operations

   [OperationContract]
   void MemoryStore(string instanceId,double number);

   [OperationContract]
   void MemoryClear(string instanceId);
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   static IInstanceStore<string,double> Memory =
             new FileInstanceStore<string,double>(Settings.Default.MemoryFileName);

   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(string instanceId,double number)
   {
      lock(typeof(MyCalculator))
      {
         Memory[instanceId] = number;
      }
   }
   public void MemoryClear(string instanceId)
   {
      lock(typeof(MyCalculator))
      {
         Memory.RemoveInstance(instanceId);
      }
   }
   //Rest of the implementation
}

In Beispiel 4-10 ist der Dateiname in den Eigenschaften des Projekts in der Klasse Settings verfügbar. Alle Instanzen des Rechners verwenden denselben statischen Speicher in Form eines FileInstanceStore<string,double>. Der Rechner synchronisiert den Zugriff auf den Speicher bei jeder Operation über alle Instanzen hinweg, indem er auf den Diensttyp sperrt. Das Löschen des Speichers signalisiert dem Rechner das Ende des Arbeitsablaufs, sodass er seinen Zustand aus der Speicherung löscht.

Instanz-IDs in Kopfzeilen

Anstatt die Instanz-ID explizit zu übergeben, kann der Client die Instanz-ID in den Nachrichten-Headern angeben. Die Verwendung von Nachrichtenköpfen als Technik zur Übergabe von Out-of-Band-Parametern für benutzerdefinierte Kontexte wird in Anhang B ausführlich beschrieben. In diesem Fall kann der Kunde meine HeaderClientBase<T,H> Proxy-Klasse verwenden, und der Dienst kann die ID in den entsprechenden Operationen mit meiner GenericContext<H> Helper-Klasse auslesen. Der Dienst kann GenericContext<H> so verwenden, wie er ist, oder ihn in einen eigenen Kontext verpacken.

Das allgemeine Muster für diese Technik ist in Beispiel 4-11 dargestellt.

Beispiel 4-11. Übergabe von Instanz-IDs in Nachrichtenköpfen
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
//Client-side
class MyContractClient : HeaderClientBase<IMyContract,SomeKey>,IMyContract
{
   public MyContractClient(SomeKey instanceId)
   {}
   public MyContractClient(SomeKey instanceId,string endpointName) :
                                                      base(instanceId,endpointName)
   {}

   //More constructors

   public void MyMethod()
   {
      Channel.MyMethod();
   }
}
//Service-side
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{
   public void MyMethod()
   {
      SomeKey instanceId = GenericContext<SomeKey>.Current.Value;
      ...
   }
   //Rest same as Example 4-9
}

Um Beispiel 4-11 weniger abstrakt zu machen, zeigt Beispiel 4-12 den Rechner, der die Technik der Nachrichtenköpfe verwendet.

Beispiel 4-12. Rechner mit Instanz-ID im Header
[ServiceContract]
interface ICalculator
{
   [OperationContract]
   double Add(double number1,double number2);

   /* More arithmetic operations */

   //Memory management operations

   [OperationContract]
   void MemoryStore(double number);

   [OperationContract]
   void MemoryClear();
}
//Client-side
class MyCalculatorClient : HeaderClientBase<ICalculator,string>,ICalculator
{
   public MyCalculatorClient(string instanceId)
   {}

   public MyCalculatorClient(string instanceId,string endpointName) :
                                                      base(instanceId,endpointName)
   {}

   //More constructors

   public double Add(double number1,double number2)
   {
      return Channel.Add(number1,number2);
   }

   public void MemoryStore(double number)
   {
      Channel.MemoryStore(number);
   }

   //Rest of the implementation
}
//Service-side
//If using GenericContext<T> is too raw, can encapsulate:
static class CalculatorContext
{
   public static string Id
   {
      get
      {
         return GenericContext<string>.Current.Value ?? String.Empty;
      }
   }
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   static IInstanceStore<string,double> Memory =
             new FileInstanceStore<string,double>(Settings.Default.MemoryFileName);

   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(double number)
   {
      lock(typeof(MyCalculator))
      {
         Memory[CalculatorContext.Id] = number;
      }
   }
   public void MemoryClear()
   {
      lock(typeof(MyCalculator))
      {
         Memory.RemoveInstance(CalculatorContext.Id);
      }
   }
   //Rest of the implementation
}

Kontext-Bindungen für Instanz-IDs

WCF bietet spezielle Bindungen für die Übergabe benutzerdefinierter Kontextparameter. Diese Bindungen, die sogenannten Kontextbindungen, werden auch in Anhang B erläutert. Clients können die Klasse ContextClientBase<T> verwenden, um die Instanz-ID über das Kontextbindungsprotokoll zu übergeben. Da die Kontextbindungen einen Schlüssel und einen Wert für jeden Kontextparameter erfordern, müssen die Kunden dem Proxy beides zur Verfügung stellen. Unter Verwendung der gleichen IMyContract wie in Beispiel 4-11 sieht ein solcher Proxy wie folgt aus:

class MyContractClient : ContextClientBase<IMyContract>,IMyContract
{
   public MyContractClient(string key,string instanceId) : base(key,instanceId)
   {}
   public MyContractClient(string key,string instanceId,string endpointName) :
                                                 base(key,instanceId,endpointName)
   {}

   //More constructors

   public void MyMethod()
   {
      Channel.MyMethod();
   }
}

Beachte, dass das Kontextprotokoll nur Strings für Schlüssel und Werte unterstützt. Da der Wert des Schlüssels dem Dienst im Voraus bekannt sein muss, kann der Kunde denselben Schlüssel auch im Proxy selbst codieren. Der Dienst kann dann die Instanz-ID mit meiner Hilfsklasse ContextManager (beschrieben in Anhang B) abrufen. Wie bei den Nachrichtenköpfen kann der Dienst auch die Interaktion mit ContextManager in einer eigenen Kontextklasse kapseln.

Beispiel 4-13 zeigt das allgemeine Muster für die Übergabe einer Instanz-ID über die Kontextbindungen. Beachte, dass der Proxy den Schlüssel für die Instanz-ID fest einträgt und dass diese ID auch dem Dienst bekannt ist.

Beispiel 4-13. Übergabe der Instanz-ID über eine Kontextbindung
//Client-side
class MyContractClient : ContextClientBase<IMyContract>,IMyContract
{
   public MyContractClient(string instanceId) : base("MyKey",instanceId)
   {}

   public MyContractClient(string instanceId,string endpointName) :
                                              base("MyKey",instanceId,endpointName)
   {}

   //More constructors

   public void MyMethod()
   {
      Channel.MyMethod();
   }
}
//Service-side
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyService : IMyContract
{
   public void MyMethod()
   {
      string instanceId = ContextManager.GetContext("MyKey");

      GetState(instanceId);
      DoWork();
      SaveState(instanceId);
   }
   void DoWork()
   {...}

   //Get and set state from durable storage
   void GetState(string instanceId)
   {...}

   void SaveState(string instanceId)
   {...}
}

Beispiel 4-14 zeigt das passende Beispiel für einen konkreten Rechner.

Beispiel 4-14. Rechner mit Instanz-ID über Kontextbindung
//Client-side
class MyCalculatorClient : ContextClientBase<ICalculator>,ICalculator
{
   public MyCalculatorClient(string instanceId) : base("CalculatorId",instanceId)
   {}
   public MyCalculatorClient(string instanceId,string endpointName) :
                                       base("CalculatorId",instanceId,endpointName)
   {}

   //More constructors

   public double Add(double number1,double number2)
   {
      return Channel.Add(number1,number2);
   }
   public void MemoryStore(double number)
   {
      Channel.MemoryStore(number);
   }

   //Rest of the implementation
}
//Service-side
static class CalculatorContext
{
   public static string Id
   {
      get
      {
         return ContextManager.GetContext("CalculatorId") ?? String.Empty;
      }
   }
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   //Same as Example 4-12
}

Verwendung der Standard-ID für die Kontextbindung

Die Notwendigkeit, den für die Instanz-ID verwendeten Schlüssel im Voraus zu kennen, ist eine Belastung. Die Kontextbindungen wurden mit Blick auf dauerhafte Dienste entwickelt, daher enthält jede Kontextbindung immer eine automatisch generierte Instanz-ID in Form von Guid (im String-Format), die über den reservierten Schlüssel instanceId zugänglich ist. Der Client und der Dienst sehen denselben Wert für die Instanz-ID. Der Wert wird initialisiert, sobald der erste Aufruf des Proxys zurückkehrt, nachdem die Bindung die Möglichkeit hatte, ihn zwischen dem Client und dem Dienst zuzuordnen. Wie jeder andere Parameter, der über eine Kontextbindung übergeben wird, ist auch der Wert der Instanz-ID während der gesamten Lebensdauer des Proxys unveränderlich.

Um die Interaktion mit der Standardinstanz-ID zu vereinfachen, habe ich ContextManager um ID-Verwaltungsmethoden, Eigenschaften und Proxy-Erweiterungsmethoden erweitert, wie in Beispiel 4-15 gezeigt.

Beispiel 4-15. Standard-Instanz-ID-Verwaltung mit ContextManager
public static class ContextManager
{
   public const string InstanceIdKey = "instanceId";

   public static Guid InstanceId
   {
      get
      {
         string id = GetContext(InstanceIdKey) ?? Guid.Empty.ToString();
         return new Guid(id);
      }
   }
   public static Guid GetInstanceId(IClientChannel innerChannel)
   {
      try
      {
         string instanceId =
           innerChannel.GetProperty<IContextManager>().GetContext()[InstanceIdKey];
         return new Guid(instanceId);
      }
      catch(KeyNotFoundException)
      {
         return Guid.Empty;
      }
   }
   public static void SetInstanceId(IClientChannel innerChannel,Guid instanceId)
   {
      SetContext(innerChannel,InstanceIdKey,instanceId.ToString());
   }
   public static void SaveInstanceId(Guid instanceId,string fileName)
   {
      using(Stream stream =
                   new FileStream(fileName,FileMode.OpenOrCreate,FileAccess.Write))
      {
         IFormatter formatter = new BinaryFormatter();
         formatter.Serialize(stream,instanceId);
      }
   }

   public static Guid LoadInstanceId(string fileName)
   {
      try
      {
         using(Stream stream = new FileStream(fileName,FileMode.Open,
                                              FileAccess.Read))
         {
            IFormatter formatter = new BinaryFormatter();
            return (Guid)formatter.Deserialize(stream);
         }
      }
      catch
      {
         return Guid.Empty;
      }
   }
   //More members
}

ContextManager bietet die Methoden GetInstanceId() und SetInstanceId(), damit der Client eine Instanz-ID aus dem Kontext lesen und in den Kontext schreiben kann. Der Dienst verwendet die Read-Only-Eigenschaft InstanceId, um die ID zu erhalten. ContextManager fügt Typsicherheit hinzu, indem es die Instanz-ID als Guid und nicht als string behandelt. Es fügt auch Fehlerbehandlung hinzu.

Schließlich bietet ContextManager die Methoden , LoadInstanceId() und SaveInstanceId(), um die Instanz-ID auszulesen und in eine Datei zu schreiben. Diese Methoden sind auf der Client-Seite nützlich, um die ID zwischen den Sitzungen der Client-Anwendung mit dem Dienst zu speichern.

Der Client kann zwar ContextClientBase<T> (wie in Beispiel 4-13) verwenden, um die Standard-ID zu übergeben, aber es ist besser, sie zu verschärfen und eine integrierte Unterstützung für die Standard-Instanz-ID bereitzustellen, wie in Beispiel 4-16 gezeigt.

Beispiel 4-16. Erweitern von ContextClientBase<T> zur Unterstützung von Standard-IDs
public abstract class ContextClientBase<T> : ClientBase<T> where T : class
{
   public Guid InstanceId
   {
      get
      {
         return ContextManager.GetInstanceId(InnerChannel);
      }
   }
   protected ContextClientBase(Guid instanceId) :
                           this(ContextManager.InstanceIdKey,instanceId.ToString())
   {}

   public ContextClientBase(Guid instanceId,string endpointName) :
              this(ContextManager.InstanceIdKey,instanceId.ToString(),endpointName)
   {}

   //More constructors
}

Beispiel 4-17 zeigt den Taschenrechner-Client und den Dienst, der die Standard-ID verwendet.

Beispiel 4-17. Rechner mit Standard-ID
//Client-side
class MyCalculatorClient : ContextClientBase<ICalculator>,ICalculator
{
   public MyCalculatorClient()
   {}
   public MyCalculatorClient(Guid instanceId) : base(instanceId)
   {}
   public MyCalculatorClient(Guid instanceId,string endpointName) :
                                                      base(instanceId,endpointName)
   {}

   //Rest same as Example 4-14
}
//Service-side
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
class MyCalculator : ICalculator
{
   static IInstanceStore<Guid,double> Memory =
               new FileInstanceStore<Guid,double>(Settings.Default.MemoryFileName);

   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(double number)
   {
      lock(typeof(MyCalculator))
      {
         Memory[ContextManager.InstanceId] = number;
      }
   }
   public void MemoryClear()
   {
      lock(typeof(MyCalculator))
      {
         Memory.RemoveInstance(ContextManager.InstanceId);
      }
   }
   //Rest of the implementation
}

Automatisches dauerhaftes Verhalten

Alle bisher vorgestellten Techniken für dauerhafte Dienste erfordern einen nicht unerheblichen Aufwand für den Dienst - insbesondere die Bereitstellung einer dauerhaften Speicherung und die explizite Verwaltung des Instanzzustands in jeder Operation. Da sich diese Arbeit ständig wiederholt, kann WCF sie für dich automatisieren und den Zustand des Dienstes bei jeder Operation aus einem angegebenen Zustandsspeicher unter Verwendung der Standardinstanz-ID serialisieren und deserialisieren.

Wenn du WCF deinen Instanzstatus verwalten lässt, folgt es diesen Regeln:

  • Wenn der Kunde keine ID angibt, erstellt die WCF eine neue Dienstinstanz, indem sie ihren Konstruktor verwendet. Nach dem Aufruf serialisiert die WCF die Instanz in den State Store.

  • Wenn der Kunde dem Proxy eine ID mitteilt und der Speicher bereits einen Zustand enthält, der dieser ID entspricht, ruft WCF den Instanzkonstruktor nicht auf. Stattdessen wird der Aufruf mit einer neuen Instanz bedient, die aus dem State Store deserialisiert wurde.

  • Wenn der Kunde eine gültige ID angibt, deserialisiert WCF für jede Operation eine Instanz aus dem Store, ruft die Operation auf und serialisiert den neuen Zustand, der durch die Operation verändert wurde, zurück in den Store.

  • Wenn der Kunde eine ID angibt, die nicht im State Store gefunden wird, löst WCF eine Ausnahme aus.

Das Attribut für das dauerhafte Dienstverhalten

Um dieses automatische dauerhafte Verhalten zu aktivieren, bietet WCF das Attribut DurableService behavior, das wie folgt definiert ist:

public sealed class DurableServiceAttribute : Attribute,IServiceBehavior,...
{...}

Du wendest dieses Attribut direkt auf die Serviceklasse an. Am wichtigsten ist, dass die Serviceklasse entweder als serialisierbar oder als Datenkontrakt mit dem Attribut DataMember für alle Member, die eine dauerhafte Zustandsverwaltung benötigen, gekennzeichnet wird:

[Serializable]
[DurableService]
class MyService : IMyContract
{
   /* Serializable member variables only  */

   public void MyMethod()
   {
      //Do work
   }
}

Die Instanz kann nun ihren Zustand in Mitgliedsvariablen verwalten, als wäre sie eine normale Instanz, und vertraut darauf, dass WCF diese Mitglieder für sie verwaltet. Wenn der Dienst nicht als serialisierbar (oder als Datenvertrag) gekennzeichnet ist, schlägt der erste Aufruf fehl, sobald WCF versucht, ihn in den Store zu serialisieren. Jeder Dienst, der auf eine automatische dauerhafte Zustandsverwaltung angewiesen ist, muss als "per-session" konfiguriert werden, verhält sich aber immer wie ein "per-call"-Dienst (WCF verwendet die Kontextdeaktivierung nach jedem Aufruf). Außerdem muss der Dienst eine der Kontextbindungen mit jedem Endpunkt verwenden, um die Standardinstanz-ID zu aktivieren, und der Vertrag muss eine Transportsitzung zulassen oder erfordern, kann sie aber nicht verbieten. Diese beiden Beschränkungen werden beim Laden des Dienstes überprüft.

Das Attribut für das dauerhafte Betriebsverhalten

Ein Dienst kann optional das Attribut DurableOperation behavior verwenden, um WCF anzuweisen, seinen Status am Ende des Workflows aus dem Store zu löschen:

[AttributeUsage(AttributeTargets.Method)]
public sealed class DurableOperationAttribute : Attribute,...
{
   public bool CanCreateInstance
   {get;set;}

   public bool CompletesInstance
   {get;set;}
}

Wenn du die Eigenschaft CompletesInstance auf true setzt, weist du WCF an, die Instanz-ID aus dem Speicher zu entfernen, sobald der Aufruf der Operation zurückkehrt. Der Standardwert der Eigenschaft CompletesInstance ist false. Falls der Kunde keine Instanz-ID angibt, kannst du auch verhindern, dass eine Operation eine neue Instanz erzeugt, indem du die Eigenschaft CanCreateInstance auf false setzt. Beispiel 4-18 zeigt die Verwendung der Eigenschaft CompletesInstance für die Operation MemoryClear() des Taschenrechners.

Beispiel 4-18. CompletesInstance verwenden, um den Zustand zu entfernen
[Serializable]
[DurableService]
class MyCalculator : ICalculator
{
   double Memory
   {get;set;}

   public double Add(double number1,double number2)
   {
      return number1 + number2;
   }
   public void MemoryStore(double number)
   {
      Memory = number;
   }
   [DurableOperation(CompletesInstance = true)]
   public void MemoryClear()
   {
      Memory = 0;
   }
   //Rest of the implementation
}

Das Problem bei der Verwendung von CompletesInstance ist, dass die Kontext-ID nicht veränderbar ist. Das bedeutet, dass, wenn der Kunde versucht, nach dem Aufruf eines Vorgangs, für den CompletesInstance auf true gesetzt ist, weitere Aufrufe an den Proxy zu tätigen, alle diese Aufrufe fehlschlagen werden, da der Speicher die Instanz-ID nicht mehr enthält. Der Kunde muss sich also darüber im Klaren sein, dass er denselben Proxy nicht weiter verwenden kann: Wenn der Kunde weitere Aufrufe an den Dienst richten will, muss er dies mit einem neuen Proxy tun, der noch keine Instanz-ID hat, und damit startet er einen neuen Arbeitsablauf. Eine Möglichkeit, dies zu erzwingen, besteht darin, das Client-Programm nach Beendigung des Workflows einfach zu schließen (oder eine neue Proxy-Referenz zu erstellen). Beispiel 4-19 zeigt anhand der Proxy-Definition aus Beispiel 4-17, wie der Proxy des Taschenrechners nach dem Löschen des Speichers verwaltet wird, während der Proxy nahtlos weiter verwendet wird.

Beispiel 4-19. Zurücksetzen des Proxys nach Beendigung eines Workflows
class CalculatorProgram
{
   MyCalculatorClient m_Proxy;

   public CalculatorProgram()
   {
      Guid calculatorId =
              ContextManager.LoadInstanceId(Settings.Default.CalculatorIdFileName);

      m_Proxy = new MyCalculatorClient(calculatorId);
   }
   public void Add()
   {
      m_Proxy.Add(2,3);
   }
   public void MemoryClear()
   {
      m_Proxy.MemoryClear();

      ResetDurableSession(ref m_Proxy);
   }
   public void Close()
   {
      ContextManager.SaveInstanceId(m_Proxy.InstanceId,
                                    Settings.Default.CalculatorIdFileName);
      m_Proxy.Close();
   }
   void ResetDurableSession(ref MyCalculatorClient proxy)
   {
      ContextManager.SaveInstanceId(Guid.Empty,
                                    Settings.Default.CalculatorIdFileName);
      Binding binding = proxy.Endpoint.Binding;
      EndpointAddress address = proxy.Endpoint.Address;

      proxy.Close();

      proxy = new MyCalculatorClient(binding,address);
   }
}

Beispiel 4-19 verwendet meine Hilfsklasse ContextManager, um eine Instanz-ID zu laden und sie in einer Datei zu speichern. Der Konstruktor des Client-Programms erstellt einen neuen Proxy mit der in der Datei gefundenen ID. Wie in Beispiel 4-15 gezeigt, gibt LoadInstanceId() Guid.Empty zurück, wenn die Datei keine Instanz-ID enthält. Mein ContextClientBase<T> ist so konzipiert, dass es eine leere GUID für die Kontext-ID erwartet: Wenn eine leere GUID angegeben wird, konstruiert sich ContextClientBase<T> ohne Instanz-ID und stellt so einen neuen Arbeitsablauf sicher. Nachdem der Speicher des Rechners geleert wurde, ruft der Client die Hilfsmethode ResetDurableSession() auf. ResetDurableSession() speichert zunächst eine leere GUID in der Datei und dupliziert dann den bestehenden Proxy. Er kopiert die Adresse und die Bindung des alten Proxys, schließt den alten Proxy und setzt die Proxy-Referenz auf einen neuen Proxy, der mit derselben Adresse und Bindung wie der alte Proxy und mit einer impliziten leeren GUID für die Instanz-ID erstellt wurde.

Programmatische Instanzverwaltung

WCF bietet eine einfache Hilfsklasse für dauerhafte Dienste namens DurableOperationContext:

public static class DurableOperationContext
{
   public static void AbortInstance();
   public static void CompleteInstance();
   public static Guid InstanceId
   {get;}
}

Mit der Methode CompleteInstance() kann der Dienst die Instanz programmatisch (statt deklarativ über das Attribut DurableOperation ) abschließen und den Zustand aus dem Speicher entfernen, sobald der Aufruf zurückkehrt. AbortInstance() Die Methode hingegen macht alle Änderungen, die während des Aufrufs am Speicher vorgenommen wurden, rückgängig, so als ob die Operation nie aufgerufen worden wäre. Die Eigenschaft InstanceId ist ähnlich wie ContextManager.InstanceId.

Persistenzanbieter

Das Attribut DurableService weist WCF zwar an, wann die Instanz serialisiert und deserialisiert werden soll, sagt aber nichts darüber aus, wo dies geschehen soll, und liefert auch keine Informationen über die Speicherung des Zustands. WCF verwendet ein Brückenmuster in Form eines Providermodells, das es dir ermöglicht, den Statusspeicher getrennt vom Attribut anzugeben. Das Attribut ist somit von der Speicherung entkoppelt, so dass du dich auf das automatische dauerhafte Verhalten für jede kompatible Speicherung verlassen kannst.

Wenn ein Dienst mit dem Attribut DurableService konfiguriert wird, musst du seinen Host mit einer Persistenzanbieter-Fabrik konfigurieren. Die Fabrik leitet sich von der abstrakten Klasse PersistenceProviderFactory ab und erstellt eine Unterklasse der abstrakten Klasse PersistenceProvider:

public abstract class PersistenceProviderFactory : CommunicationObject
{
   protected PersistenceProviderFactory();
   public abstract PersistenceProvider CreateProvider(Guid id);
}

public abstract class PersistenceProvider : CommunicationObject
{
   protected PersistenceProvider(Guid id);

   public Guid Id
   {get;}

   public abstract object Create(object instance,TimeSpan timeout);
   public abstract void   Delete(object instance,TimeSpan timeout);
   public abstract object Load(TimeSpan timeout);
   public abstract object Update(object instance,TimeSpan timeout);

   //Additional members
}

Die gebräuchlichste Art, die Persistenz-Provider-Fabrik zu spezifizieren, ist, sie als Serviceverhalten in die Host-Konfigurationsdatei aufzunehmen und dieses Verhalten in der Service-Definition zu referenzieren:

<behaviors>
   <serviceBehaviors>
      <behavior name = "DurableService">
         <persistenceProvider
            type = "...type...,...assembly ..."
            <!--  Provider-specific parameters -->
         />
      </behavior>
   </serviceBehaviors>
</behaviors>

Sobald der Host mit der Persistenzproviderfabrik konfiguriert ist, verwendet WCF die erstellte PersistenceProvider für jeden Aufruf zur Serialisierung und Deserialisierung der Instanz. Wenn keine Persistenzprovider-Fabrik angegeben wird, bricht WCF die Erstellung des Service-Hosts ab.

Benutzerdefinierte Persistenzanbieter

Eine schöne Art zu demonstrieren, wie man einen einfachen benutzerdefinierten Persistenzanbieter schreibt, ist mein FilePersistenceProviderFactory, definiert als:

public class FilePersistenceProviderFactory : PersistenceProviderFactory
{
   public FilePersistenceProviderFactory();
   public FilePersistenceProviderFactory(string fileName);
   public FilePersistenceProviderFactory(NameValueCollection parameters);
}
public class FilePersistenceProvider : PersistenceProvider
{
   public FilePersistenceProvider(Guid id,string fileName);
}

FilePersistenceProvider umhüllt meine Klasse FileInstanceStore<ID,T>. Im Konstruktor von FilePersistenceProviderFactory musst du den gewünschten Dateinamen angeben. Wenn kein Dateiname angegeben wird, setzt FilePersistenceProviderFactory den Dateinamen standardmäßig auf Instances.bin.

Der Schlüssel zur Verwendung einer benutzerdefinierten Persistenzfabrik in einer Konfigurationsdatei besteht darin, einen Konstruktor zu definieren, der eine NameValueCollection von Parametern benötigt. Bei diesen Parametern handelt es sich um einfache textformatierte Paare von Schlüsseln und Werten, die in der Konfigurationsdatei im Abschnitt über das Verhalten der Anbieterfabrik angegeben sind. Praktisch alle frei formulierten Schlüssel und Werte sind möglich. So kannst du zum Beispiel den Dateinamen angeben:

<behaviors>
   <serviceBehaviors>
      <behavior name = "Durable">
         <persistenceProvider
            type = "FilePersistenceProviderFactory,ServiceModelEx"
            fileName = "MyService.bin"
         />
      </behavior>
   </serviceBehaviors>
</behaviors>

Der Konstruktor kann dann die parameters Sammlung verwenden, um auf diese Parameter zuzugreifen:

string fileName = parameters["fileName"];

Der SQL Server Persistenzanbieter

WCF wird mit einem Persistenz-Provider ausgeliefert, der den Zustand der Instanz in einer speziellen SQL Server-Tabelle speichert. Nach einer Standardinstallation befinden sich die Installationsskripte für die Datenbank in C:\Windows\Microsoft.NET\Framework\<.NET Version>\SQL\EN. Beachte, dass du mit dem von WCF bereitgestellten SQL-Persistenzanbieter nur SQL Server 2005 oder höher für die Speicherung des Zustands verwenden kannst. Der SQL-Anbieter wird in Form von SqlPersistenceProviderFactory und SqlPersistenceProvider bereitgestellt, die sich in der Assembly System.WorkflowServices im Namensraum System.ServiceModel.Persistence befinden.

Du musst nur die SQL-Provider-Factory und den Namen der Verbindungszeichenfolge angeben:

<connectionStrings>
   <add name = "DurableServices"
      connectionString = "..."
      providerName = "System.Data.SqlClient"
   />
</connectionStrings>

<behaviors>
   <serviceBehaviors>
      <behavior name = "Durable">
         <persistenceProvider type = 
                 "System.ServiceModel.Persistence.SqlPersistenceProviderFactory,
                 System.WorkflowServices,Version=4.0.0.0,Culture=neutral,
                 PublicKeyToken=31bf3856ad364e35"
            connectionStringName = "DurableServices"
         />
      </behavior>
   </serviceBehaviors>
</behaviors>

Du kannst WCF auch anweisen, die Instanzen als Text zu serialisieren (anstelle der standardmäßigen binären Serialisierung), z. B. für Diagnose- oder Analysezwecke:

<persistenceProvider
   type = "System.ServiceModel.Persistence.SqlPersistenceProviderFactory,
           System.WorkflowServices,Version=4.0.0.0,Culture=neutral,
           PublicKeyToken=31bf3856ad364e35"
   connectionStringName = "DurableServices"
   serializeAsText = "true"
/>
Warnung

Mit .NET 4.5 verlagerte Microsoft die verschiedenen Workflow Foundation V3.0-Typen, die offiziell in der Assembly System.WorkflowServices gepflegt wurden. Die Verschiebung beinhaltete auch eine Verfallswarnung, die besagt, dass die Workflow Foundation System.Activities Assembly in .NET 4.5 Ersatztypen bereitstellen sollte. Die Assembly enthält jedoch keine Ersatztypen.

Normalerweise ist es keine gute Praxis, Compiler-Direktiven zu verwenden, um Kompilierungswarnungen in deinem Code zu überdecken, es sei denn, du kompensierst ein Versehen wie in diesem Fall. Du kannst deinen Build bereinigen und die Warnmeldungen entfernen, indem du die folgende Compilerdirektive anwendest: [füge das Wiederherstellungspragma nach deinem Code ein]

#pragma warning disable 618

Drosselung

Auch wenn es sich nicht um eine direkte Instanzmanagement-Technik handelt, kannst du mit der Drosselung die Client-Verbindungen und die damit verbundene Belastung deines Dienstes begrenzen. Du brauchst die Drosselung, weil Softwaresysteme nicht elastisch sind, wie in Abbildung 4-7 dargestellt.

The inelastic nature of all software systems
Abbildung 4-7. Die unelastische Natur aller Softwaresysteme

Mit anderen Worten: Du kannst die Belastung des Systems nicht immer weiter erhöhen und erwarten, dass seine Leistung allmählich abnimmt, so als ob du einen Kaugummi dehnst. Die meisten Systeme werden anfangs gut mit der steigenden Belastung umgehen können, aber dann beginnen sie nachzugeben und brechen abrupt zusammen. Alle Softwaresysteme verhalten sich so, aus Gründen, die über den Rahmen dieses Buches hinausgehen und mit der Warteschlangentheorie und dem mit der Verwaltung von Ressourcen verbundenen Aufwand zusammenhängen. Dieses unelastische Verhalten ist besonders besorgniserregend, wenn es zu Lastspitzen kommt, wie in Abbildung 4-8 dargestellt.

A spike in load may push the system beyond its design limit
Abbildung 4-8. Eine Lastspitze kann das System über seine Auslegungsgrenze hinaus belasten

Selbst wenn ein System eine Nennlast gut bewältigt (die horizontale Linie in Abbildung 4-8), kann eine Spitze das System über seine Auslegungsgrenze hinaus belasten, so dass es zusammenbricht und die Kunden eine erhebliche Verschlechterung ihres Servicegrades erfahren. Auch die Geschwindigkeit, mit der die Last ansteigt, kann eine Herausforderung darstellen, selbst wenn die erreichte absolute Höhe dem System sonst keine Probleme bereiten würde.

Mit der Drosselung kannst du verhindern, dass dein Dienst und die ihm zugrunde liegenden Ressourcen, die er zuweist und nutzt, ausgereizt werden. Wenn die Drosselung aktiviert ist und die von dir konfigurierten Einstellungen überschritten werden, stellt die WCF die anstehenden Anrufer automatisch in eine Warteschlange und bedient sie der Reihe nach aus der Warteschlange. Wenn das Zeitlimit für den Anruf eines Kunden abläuft, während sein Anruf in der Warteschlange steht, erhält der Kunde eine TimeoutException. Die Drosselung ist von Natur aus eine unfaire Technik, da die Kunden, deren Anfragen gepuffert werden, eine Verschlechterung ihres Servicelevels erfahren. In diesem Fall ist es jedoch besser, intelligent zu sein: Wenn alle Anrufer in der Warteschlange zugelassen werden, ist das zwar fair, aber alle Anrufer werden eine deutliche Verschlechterung des Leistungsniveaus erleben, wenn das System einbricht. Drosselung ist daher sinnvoll, wenn der Bereich unter der Spitze im Vergleich zum Bereich unter der gesamten Lastkurve relativ klein ist, was bedeutet, dass die Wahrscheinlichkeit, dass derselbe Anrufer nacheinander in die Warteschlange kommt, sehr gering ist. Hin und wieder werden als Reaktion auf einen Spike einige Anrufer gepuffert, aber das System als Ganzes wird trotzdem gut funktionieren. Die Drosselung funktioniert nicht gut, wenn die Last auf ein neues Niveau ansteigt und lange Zeit konstant auf diesem Niveau bleibt (wie in Abbildung 4-9 gezeigt). In diesem Fall werden die Probleme nur ein wenig hinausgezögert, so dass schließlich alle Anrufer einen Timeout erleiden. Ein solches System sollte von Grund auf so konzipiert sein, dass es die höhere Last bewältigen kann.

Inadequate justification for throttling
Abbildung 4-9. Unzureichende Rechtfertigung für die Drosselung

Die Drosselung erfolgt pro Diensttyp, d.h. sie betrifft alle Instanzen des Dienstes und alle seine Endpunkte. Dazu wird die Drosselung mit jedem Channel Dispatcher verknüpft, den der Dienst verwendet.

Mit WCF kannst du einige oder alle der folgenden Parameter für die Nutzung von Diensten steuern:

Maximale Anzahl von gleichzeitigen Sitzungen
Gibt die Gesamtzahl der ausstehenden Clients an, die eine Transportsitzung mit dem Dienst haben können. Im Klartext bedeutet dies die maximale Gesamtzahl der ausstehenden Clients, die TCP, IPC oder eine der WS-Bindungen (mit Zuverlässigkeit, Sicherheit oder beidem) nutzen. Da die verbindungslose Natur einer einfachen HTTP-Verbindung eine sehr kurze Transportsitzung impliziert, die nur für die Dauer des Aufrufs besteht, hat diese Zahl in der Regel keine Auswirkung auf Clients, die die einfache Bindung oder eine WS-Bindung ohne Transportsitzung verwenden; solche Clients werden stattdessen durch die maximal zulässige Anzahl gleichzeitiger Aufrufe begrenzt. Der Standardwert ist das 100-fache der Anzahl der Prozessoren (oder Kerne).
Maximale Anzahl von gleichzeitigen Anrufen
Begrenzt die Gesamtzahl der Anrufe, die derzeit in allen Service-Instanzen laufen können. Diese Zahl sollte in der Regel bei 1%-3% der maximalen Anzahl gleichzeitiger Sitzungen liegen. Der Standardwert ist das 16-fache der Anzahl der Prozessoren (oder Kerne).
Maximale Anzahl von gleichzeitigen Instanzen
Steuert die Gesamtzahl der gleichzeitig aktiven Kontexte. Wenn du diesen Wert nicht explizit festlegst, entspricht er implizit der Summe der maximalen gleichzeitigen Aufrufe und der maximalen gleichzeitigen Sitzungen (116 mal die Prozessoranzahl). Einmal festgelegt, behält er seinen Wert unabhängig von den anderen beiden Eigenschaften bei. Wie Instanzen auf Kontexte abgebildet werden, hängt von der Art der Verwaltung des Instanzkontexts sowie der Deaktivierung von Kontext und Instanz ab. Bei einem Sitzungsdienst ist die maximale Anzahl der Instanzen sowohl die Gesamtzahl der gleichzeitig aktiven Instanzen als auch die Gesamtzahl der gleichzeitigen Sitzungen. Bei der Deaktivierung von Instanzen kann es viel weniger Instanzen als Kontexte geben, und dennoch werden die Kunden blockiert, wenn die Anzahl der Kontexte die maximale Anzahl der gleichzeitig aktiven Instanzen erreicht hat. Bei einem Dienst pro Aufruf ist die Anzahl der Instanzen genau so hoch wie die Anzahl der gleichzeitigen Aufrufe. Folglich ist die maximale Anzahl von Instanzen bei einem Dienst pro Anruf der kleinere Wert aus der konfigurierten maximalen Anzahl gleichzeitiger Instanzen und der maximalen Anzahl gleichzeitiger Anrufe. Der Wert dieses Parameters wird bei einem Singleton-Dienst ignoriert, da dieser ohnehin nur eine einzige Instanz haben kann.
Warnung

Drosselung ist ein Aspekt des Hostings und der Bereitstellung. Wenn du einen Dienst entwirfst, solltest du keine Annahmen über die Drosselungskonfiguration treffen - gehe immer davon aus, dass dein Dienst die volle Last des Clients tragen wird. Obwohl es relativ einfach ist, ein Attribut für das Drosselverhalten zu schreiben, bietet WCF kein solches Attribut.

Konfigurieren der Drosselung

Administratoren konfigurieren die Drosselung normalerweise in der Konfigurationsdatei. So kannst du denselben Servicecode im Laufe der Zeit oder an verschiedenen Einsatzorten unterschiedlich drosseln. Der Host kann die Drosselung auch programmatisch auf der Grundlage bestimmter Laufzeitentscheidungen konfigurieren.

Administrative Drosselung

Beispiel 4-20 zeigt , wie du die Drosselung in der Host-Konfigurationsdatei konfigurierst. Mit dem Tag behaviorConfiguration fügst du deinem Dienst ein benutzerdefiniertes Verhalten hinzu, das die Drosselungswerte festlegt.

Beispiel 4-20. Administrative Drosselung
<system.serviceModel>
   <services>
      <service name = "MyService" behaviorConfiguration = "ThrottledBehavior">
         ...
      </service>
   </services>
   <behaviors>
      <serviceBehaviors>
         <behavior name = "ThrottledBehavior">
            <serviceThrottling
               maxConcurrentCalls     = "500"
               maxConcurrentSessions  = "10000"
               maxConcurrentInstances = "100"
            />
         </behavior>
      </serviceBehaviors>
   </behaviors>
</system.serviceModel>

Programmatische Drosselung

Der Hostprozess kann den Dienst auf der Grundlage einiger Laufzeitparameter programmatisch drosseln. Du kannst die Drosselung nur programmatisch konfigurieren, bevor der Host geöffnet wird. Obwohl der Host das Drosselverhalten in der Konfigurationsdatei außer Kraft setzen kann, indem er es entfernt und sein eigenes hinzufügt, solltest du normalerweise nur dann ein programmatisches Drosselverhalten bereitstellen, wenn in der Konfigurationsdatei kein Drosselverhalten vorhanden ist.

Die Klasse ServiceHostBase bietet die Eigenschaft Description vom Typ ServiceDescription:

public abstract class ServiceHostBase : ...
{
   public ServiceDescription Description
   {get;}
   //More members
}

Die Dienstbeschreibung ist, wie der Name schon sagt, eine Beschreibung des Dienstes mit all seinen Aspekten und Verhaltensweisen. ServiceDescription enthält eine Eigenschaft namens Behaviors vom Typ KeyedByTypeCollection<T>, mit IServiceBehavior als allgemeinem Parameter.

Beispiel 4-21 zeigt, wie du das gedrosselte Verhalten programmatisch einstellen kannst.

Beispiel 4-21. Programmatische Drosselung
ServiceHost host = new ServiceHost(typeof(MyService));

ServiceThrottlingBehavior throttle;
throttle = host.Description.Behaviors.Find<ServiceThrottlingBehavior>();
if(throttle == null)
{
   throttle = new ServiceThrottlingBehavior();
   throttle.MaxConcurrentCalls     = 500;
   throttle.MaxConcurrentSessions  = 10000;
   throttle.MaxConcurrentInstances = 100;
   host.Description.Behaviors.Add(throttle);
}

host.Open();

Zuerst überprüft der Hosting-Code, dass in der Konfigurationsdatei kein Verhalten zur Dienstdrosselung angegeben wurde. Dies geschieht durch den Aufruf der Methode Find<T>() von KeyedByTypeCollection<T>, wobei ServiceThrottlingBehavior als Typparameter verwendet wird:

public class ServiceThrottlingBehavior : IServiceBehavior
{
   public int MaxConcurrentCalls
   {get;set;}
   public int MaxConcurrentSessions
   {get;set;}
   public int MaxConcurrentInstances
   {get;set;}
   //More members
}

Wenn die zurückgegebene Drossel null ist, erstellt der Hosting-Code eine neue ServiceThrottlingBehavior, setzt ihre Werte und fügt sie zu den Verhaltensweisen in der Servicebeschreibung hinzu.

Rationalisierung mit ServiceHost<T>

Mit den C# 3.0-Erweiterungen kannst du ServiceHost (oder eine beliebige Unterklasse davon, wie ServiceHost<T>) erweitern, um den Code in Beispiel 4-21 zu automatisieren, wie in Beispiel 4-22 gezeigt.

Beispiel 4-22. Erweitern von ServiceHost für die Drosselung
public static class ServiceThrottleHelper
{
   public static void SetThrottle(this ServiceHost host,
                                  int maxCalls,int maxSessions,int maxInstances)
   {
      ServiceThrottlingBehavior throttle = new ServiceThrottlingBehavior();
      throttle.MaxConcurrentCalls     = maxCalls;
      throttle.MaxConcurrentSessions  = maxSessions;
      throttle.MaxConcurrentInstances = maxInstances;
      host.SetThrottle(throttle);
   }
   public static void SetThrottle(this ServiceHost host,
                                  ServiceThrottlingBehavior serviceThrottle,
                                  bool overrideConfig)
   {
      if(host.State == CommunicationState.Opened)
      {
         throw new InvalidOperationException("Host is already opened");
      }
      ServiceThrottlingBehavior throttle =
                   host.Description.Behaviors.Find<ServiceThrottlingBehavior>();
      if(throttle == null)
      {
         host.Description.Behaviors.Add(serviceThrottle);
         return;
      }
      if(overrideConfig == false)
      {
         return;
      }
      host.Description.Behaviors.Remove(throttle);
      host.Description.Behaviors.Add(serviceThrottle);
   }
   public static void SetThrottle(this ServiceHost host,
                                  ServiceThrottlingBehavior serviceThrottle)
   {
      host.SetThrottle(serviceThrottle,false);
   }
}

ServiceThrottleHelper bietet die Methode , die die zu verwendende Drossel und ein boolesches Flag akzeptiert, das angibt, ob die konfigurierten Werte überschrieben werden sollen oder nicht, falls vorhanden. Der Standardwert (mit einer überladenen Version von ) ist . überprüft mit der Eigenschaft der Basisklasse , ob der Host noch nicht geöffnet wurde. Wenn es erforderlich ist, die konfigurierte Drossel außer Kraft zu setzen, entfernt sie aus der Beschreibung. Der Rest von SetThrottle() SetThrottle() false SetThrottle() State CommunicationObject SetThrottle() Beispiel 4-22 ist ähnlich wie in Beispiel 4-21. Hier wird gezeigt, wie du verwendest, um eine Drossel programmatisch zu setzen: ServiceHost<T>

ServiceHost<MyService> host = new ServiceHost<MyService>();
host.SetThrottle(12,34,56);
host.Open();
Hinweis

Die in Kapitel 1 vorgestellte Klasse InProcFactory<T> wurde in ähnlicher Weise erweitert, um die Drosselung zu vereinfachen.

Drosselwerte ablesen

Dienstentwickler können die Drosselwerte zur Laufzeit für Diagnose- und Analysezwecke auslesen. Damit eine Service-Instanz zur Laufzeit von ihrem Dispatcher aus auf ihre Throttle-Eigenschaften zugreifen kann, muss sie zunächst aus dem Operationskontext eine Referenz auf den Host erhalten.

Die Host-Basisklasse ServiceHostBase bietet die schreibgeschützte Eigenschaft ChannelDispatchers:

public abstract class ServiceHostBase : CommunicationObject,...
{
   public ChannelDispatcherCollection ChannelDispatchers
   {get;}
   //More members
}

ChannelDispatchers ist eine stark typisierte Sammlung von ChannelDispatcherBase Objekten:

public class ChannelDispatcherCollection :
                                 SynchronizedCollection<ChannelDispatcherBase>
{...}

Jedes Element in der Sammlung ist vom Typ ChannelDispatcher. ChannelDispatcher bietet die Eigenschaft ServiceThrottle:

public class ChannelDispatcher : ChannelDispatcherBase
{
   public ServiceThrottle ServiceThrottle
   {get;set;}
   //More members
}
public sealed class ServiceThrottle
{
   public int MaxConcurrentCalls
   {get;set;}
   public int MaxConcurrentSessions
   {get;set;}
   public int MaxConcurrentInstances
   {get;set;}
}

ServiceThrottle enthält die konfigurierten Drosselwerte:

class MyService : ...
{
   public void MyMethod() //Contract operation
   {
      ChannelDispatcher dispatcher = OperationContext.Current.Host.
                                     ChannelDispatchers[0] as ChannelDispatcher;

      ServiceThrottle serviceThrottle = dispatcher.ServiceThrottle;

      Trace.WriteLine("Max Calls = " + serviceThrottle.MaxConcurrentCalls);
      Trace.WriteLine("Max Sessions = " + 
                      serviceThrottle.MaxConcurrentSessions);
      Trace.WriteLine("Max Instances = " + 
                      serviceThrottle.MaxConcurrentInstances);
   }
}

Beachte, dass der Dienst die Drosselwerte nur lesen kann und keine Möglichkeit hat, sie zu beeinflussen. Wenn der Dienst versucht, die Drosselwerte zu setzen, erhält er eine InvalidOperationException.

Auch hier kannst du den Drossel-Lookup über ServiceHost<T> vereinfachen. Zuerst fügst du eine ServiceThrottle Eigenschaft hinzu:

public class ServiceHost<T> : ServiceHost
{
   public ServiceThrottle Throttle
   {
      get
      {
         if(State == CommunicationState.Created)
         {
            throw new InvalidOperationException("Host is not opened");
         }

         ChannelDispatcher dispatcher = OperationContext.Current.Host.
                                     ChannelDispatchers[0] as ChannelDispatcher;
         return dispatcher.ServiceThrottle;
      }
   }
   //More members
}

Verwende dann ServiceHost<T>, um den Dienst zu hosten und verwende die Eigenschaft ServiceThrottle, um auf die konfigurierte Drossel zuzugreifen:

//Hosting code
ServiceHost<MyService> host = new ServiceHost<MyService>();
host.Open();

class MyService : ...
{
   public void MyMethod()
   {
     ServiceHost<MyService> host = OperationContext.Current.Host as 
                                   ServiceHost<MyService>;

     ServiceThrottle serviceThrottle = host.Throttle;
     ...
   }
}
Hinweis

Du kannst nur auf die Eigenschaft Throttle von ServiceHost<T> zugreifen, nachdem der Host geöffnet wurde, denn die Dispatcher-Sammlung wird erst nach diesem Punkt initialisiert.

1 Dieses Kapitel enthält Auszüge aus meinen Artikeln "WCF Essentials: Discover Mighty Instance Management Techniques for Developing WCF Apps"(MSDN Magazine, Juni 2006) und "Managing State with Durable Services"(MSDN Magazine, Oktober 2008).

Get Programmierung von WCF-Diensten, 4. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.