Kapitel 1. WCF Grundlagen

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

Dieses Kapitel beschreibt die grundlegenden Konzepte und Bausteine der Windows Communication Foundation (WCF) und ihre Architektur, mit der du einfache Dienste erstellen kannst. Du lernst die grundlegenden Begriffe zu Adressen, Bindungen, Verträgen und Endpunkten kennen, erfährst, wie man einen Dienst hostet, wie man einen Client schreibt, verstehst verwandte Themen wie In-Process-Hosting-Zuverlässigkeit und Transport-Sessions und siehst, wie du WCF in Visual Studio nutzen kannst. Auch wenn du bereits mit den grundlegenden Konzepten von WCF vertraut bist, empfehle ich dir, dieses Kapitel zumindest flüchtig zu lesen, nicht nur um sicherzustellen, dass du eine solide Grundlage hast, sondern auch, weil einige der hier vorgestellten Hilfsklassen und Begriffe im Laufe des Buches verwendet und erweitert werden.

Was ist WCF?

WCF ist ein Software Development Kit für die Entwicklung und Bereitstellung von Diensten unter Windows (was ein Dienst ist, beschreibe ich im nächsten Abschnitt). Aber WCF ist noch viel mehr - es ist buchstäblich ein besseres .NET. WCF bietet eine Laufzeitumgebung für deine Dienste, die es dir ermöglicht, Common Language Runtime (CLR)-Typen als Dienste darzustellen und andere Dienste als CLR-Typen zu nutzen. Obwohl du theoretisch auch ohne WCF Dienste erstellen könntest, ist es in der Praxis wesentlich einfacher, Dienste mit WCF zu erstellen. WCF ist Microsofts Implementierung einer Reihe von Industriestandards, die Dienstinteraktionen, Typkonvertierungen, Marshalling und die Verwaltung verschiedener Protokolle definieren. WCF sorgt also für Interoperabilität zwischen Diensten.

WCF bietet Entwicklern die wesentlichen Standardfunktionen, die für fast alle Anwendungen benötigt werden, und steigert so die Produktivität erheblich. Die erste Version von WCF (als Teil von .NET 3.0) bot viele nützliche Funktionen für die Entwicklung von Diensten, wie z. B. Hosting, Verwaltung von Dienstinstanzen, asynchrone Aufrufe, Zuverlässigkeit, Transaktionsmanagement, getrennte Aufrufe in Warteschlangen und Sicherheit. Die zweite Version von WCF (als Teil von .NET 3.5) bot zusätzliche Werkzeuge und erweiterte das ursprüngliche Angebot um zusätzliche Kommunikationsoptionen. Die dritte Version (als Teil von .NET 4.0) enthielt Konfigurationsänderungen, einige Erweiterungen und die neuen Funktionen Discovery (wird in Anhang C behandelt) und Router (wird in diesem Buch nicht behandelt). Mit der vierten Version von WCF (als Teil von .NET 4.5) verfügt WCF über mehrere neue Vereinfachungsfunktionen und zusätzliche Bindungen, darunter UDP- und WebSocket-Bindungen. WCF hat ein elegantes Erweiterungsmodell, mit dem du das Basisangebot erweitern kannst. Die WCF selbst wurde mit diesem Erweiterungsmodell geschrieben. Dieses Buch widmet sich der Erkundung dieser Aspekte und Funktionen.

WCF ist Teil von .NET 4.5 und kann daher nur auf Betriebssystemen ausgeführt werden, die es unterstützen. Zurzeit sind das Windows XP und höher sowie Windows Server 2003 und höher.

Der Großteil der WCF-Funktionen ist in einer einzigen Assembly namens System.ServiceModel.dll enthalten, die sich im Namensraum System.ServiceModel befindet.

Dienstleistungen

Ein Dienst ist eine Einheit von Funktionen, die der Welt zur Verfügung gestellt wird. In dieser Hinsicht ist er der nächste Evolutionsschritt auf dem langen Weg von Funktionen über Objekte und Komponenten zu Diensten. Serviceorientierung (SO) ist ein abstrakter Satz von Prinzipien und bewährten Methoden für die Entwicklung serviceorientierter Anwendungen. Anhang A gibt einen kurzen Überblick und erläutert die Motivation für den Einsatz dieser Methodik. Im Rest dieses Buches wird davon ausgegangen, dass du mit diesen Prinzipien vertraut bist. Eine serviceorientierte Anwendung fasst Dienste zu einer einzigen logischen Anwendung zusammen, ähnlich wie eine komponentenorientierte Anwendung Komponenten und eine objektorientierte Anwendung Objekte zusammenfasst, wie in Abbildung 1-1 dargestellt.

A service-oriented application
Abbildung 1-1. Eine serviceorientierte Anwendung

Die Dienste können lokal oder dezentral sein, von mehreren Parteien mit beliebigen Technologien entwickelt werden, unabhängig voneinander versioniert werden und sogar auf unterschiedlichen Zeitschienen ausgeführt werden. Innerhalb eines Dienstes findest du Konzepte wie Sprachen, Technologien, Plattformen, Versionen und Frameworks, doch zwischen den Diensten sind nur vorgeschriebene Kommunikationsmuster erlaubt.

Der Client eines Dienstes ist lediglich derjenige, der seine Funktionen nutzt. Der Client kann buchstäblich alles sein - zum Beispiel eine Windows Forms-, WPF-, Silverlight- oder Windows Store App-Klasse, eine ASP.NET-Seite oder ein anderer Dienst.

Clients und Dienste interagieren, indem sie Nachrichten senden und empfangen. Die Nachrichten können direkt vom Client an den Dienst übertragen werden oder über einen Vermittler wie den Azure Service Bus gesendet werden. Bei WCF handelt es sich bei den Nachrichten um SOAP-Nachrichten. Diese Nachrichten sind unabhängig von Transportprotokollen - im Gegensatz zu Webdiensten können WCF-Dienste über eine Vielzahl von Transporten (nicht nur HTTP) kommunizieren. WCF-Clients können mit Nicht-WCF-Diensten zusammenarbeiten, und WCF-Dienste können mit Nicht-WCF-Clients interagieren. Wenn du also sowohl den Client als auch den Dienst entwickelst, kannst du die Anwendung in der Regel so konstruieren, dass beide Seiten WCF benötigen, um die WCF-spezifischen Vorteile zu nutzen.

Da die Erstellung des Dienstes nach außen hin undurchsichtig ist, legt ein WCF-Dienst in der Regel Metadaten offen, die die verfügbare Funktionalität und mögliche Wege der Kommunikation mit dem Dienst beschreiben. Die Metadaten werden auf eine vordefinierte, technologieneutrale Weise veröffentlicht, z. B. mit der Web Services Description Language (WSDL) über HTTP-GET oder einem Industriestandard für den Austausch von Metadaten über ein beliebiges Protokoll. Ein Nicht-WCF-Client kann die Metadaten in seine native Umgebung als native Typen importieren. Ebenso kann ein WCF-Client die Metadaten eines Nicht-WCF-Dienstes importieren und sie als native CLR-Klassen und -Schnittstellen konsumieren.

Grenzen der Dienstausführung

Bei WCF interagiert der Client nie direkt mit einem Dienst, selbst wenn es sich um einen lokalen In-Memory-Dienst handelt. Stattdessen verwendet der Kunde immer einen Proxy, um Aufrufe an den Dienst weiterzuleiten. Der Proxy stellt die gleichen Operationen wie der Dienst zur Verfügung und bietet zusätzlich einige Methoden zur Verwaltung des Proxys.

WCF ermöglicht es dem Client, über alle Ausführungsgrenzen hinweg mit einem Dienst zu kommunizieren. Auf demselben Rechner kann der Client Dienste in derselben App-Domäne, über App-Domänen hinweg im selben Prozess oder prozessübergreifend in Anspruch nehmen (siehe Abbildung 1-2).

Same-machine communication using WCF
Abbildung 1-2. Maschinengleiche Kommunikation mit WCF

Über Rechnergrenzen hinweg(Abbildung 1-3) kann der Client mit Diensten in seinem Intranet oder über das Internet interagieren.

Cross-machine communication using WCF
Abbildung 1-3. Maschinenübergreifende Kommunikation mit WCF

WCF und Standorttransparenz

In der Vergangenheit wollten verteilte Computertechnologien wie DCOM und .NET Remoting dem Client dasselbe Programmiermodell bieten, unabhängig davon, ob es sich um ein lokales oder entferntes Objekt handelt. Bei einem lokalen Aufruf verwendete der Client eine direkte Referenz, bei einem entfernten Objekt einen Proxy. Das Problem bei dem Versuch, das lokale Programmiermodell als Fernprogrammiermodell zu verwenden, war, dass ein Fernaufruf viel mehr beinhaltet als ein Objekt mit einer Leitung. Komplexe Aspekte wie Lebenszyklusmanagement, Zuverlässigkeit, Zustandsverwaltung und Sicherheit kamen zum Vorschein und machten das Fernprogrammiermodell deutlich komplexer. Es traten zahlreiche Probleme auf, weil das entfernte Objekt versuchte, etwas zu sein, was es nicht ist - ein lokales Objekt.

WCF strebt außerdem an, dem Kunden unabhängig vom Standort des Dienstes dasselbe Programmiermodell zu bieten. Der WCF-Ansatz ist jedoch das genaue Gegenteil: Er nutzt das Fernprogrammiermodell der Instanziierung und Verwendung eines Proxys und verwendet es auch im lokalsten Fall. Da alle Interaktionen über einen Proxy abgewickelt werden, der die gleiche Konfiguration und das gleiche Hosting erfordert, behält WCF das gleiche Programmiermodell für den lokalen und den entfernten Fall bei; dadurch können Sie nicht nur den Standort wechseln, ohne dass sich dies auf den Client auswirkt, sondern auch das Programmiermodell der Anwendung erheblich vereinfachen. Ein weiterer wichtiger Vorteil der Verwendung eines Proxys ist, dass WCF die Aufrufe abfangen und seinen Wert hinzufügen kann, wie du später sehen wirst.

Adressen

In WCF ist jeder Dienst mit einer eindeutigen Adresse verbunden. Die Adresse enthält zwei wichtige Elemente: den Standort des Dienstes und das Transportprotokoll oder Transportschema, das für die Kommunikation mit dem Dienst verwendet wird. Der Standortteil der Adresse enthält den Namen des Zielrechners, des Standorts oder des Netzwerks, einen Kommunikationsport, eine Pipe oder eine Warteschlange und optional einen bestimmten Pfad, den Uniform Resource Identifier (URI). Ein URI kann eine beliebige eindeutige Zeichenkette sein, z. B. der Name des Dienstes oder ein globaler eindeutiger Bezeichner (GUID).

WCF unterstützt standardmäßig die folgenden Transportverfahren:

  • HTTP/HTTPS

  • TCP

  • IPC

  • MSMQ

  • Service Bus

  • WebSocket

  • UDP

Die Adressen haben immer das folgende Format:

[base address]/[optional URI]

Die Basisadresse ist immer in diesem Format:

[transport]://[machine or domain][:optional port]

Hier sind ein paar Beispieladressen:

http://localhost:8001
http://localhost:8001/MyService
net.tcp://localhost:8002/MyService
net.pipe://localhost/MyPipe
net.msmq://localhost/private/MyQueue
net.msmq://localhost/MyQueue
ws://localhost/MyService
soap.udp://localhost:8081/MyService

Die Art und Weise, wie man eine Adresse wie die folgende liest:

http://localhost:8001

sieht folgendermaßen aus: "Gehe über HTTP zu dem Rechner namens localhost, wo auf Port 8001 jemand auf meine Anrufe wartet."

Wenn es auch eine URI gibt, wie in:

http://localhost:8001/MyService

wird die Adresse wie folgt lauten: "Gehe über HTTP zu dem Rechner namens localhost, wo auf Port 8001 jemand namens MyService auf meine Anrufe wartet."

TCP-Adressen

TCP-Adressen verwenden net.tcp für den Transport und enthalten in der Regel eine Portnummer, wie in:

net.tcp://localhost:8002/MyService

Wenn keine Portnummer angegeben wird, wird die TCP-Adresse standardmäßig auf Port 808 gesetzt:

net.tcp://localhost/MyService

Es ist möglich, dass sich zwei TCP-Adressen (von demselben Host, wie später in diesem Kapitel beschrieben) einen Port teilen:

net.tcp://localhost:8002/MyService
net.tcp://localhost:8002/MyOtherService

In diesem Buch werden ausschließlich TCP-basierte Adressen verwendet.

Hinweis

Du kannst TCP-basierte Adressen von verschiedenen Diensthosts so konfigurieren, dass sie sich einen Port teilen.

HTTP-Adressen

HTTP-Adressen verwenden http für den Transport und können auch https für den sicheren Transport verwenden. Du verwendest HTTP-Adressen in der Regel für nach außen gerichtete internetbasierte Dienste und kannst einen Port angeben, wie hier gezeigt:

http://localhost:8001

Wenn du die Portnummer nicht angibst, wird sie standardmäßig auf 80 (und Port 443 für HTTPS) gesetzt. Wie bei den TCP-Adressen können sich zwei HTTP-Adressen desselben Hosts einen Port teilen, sogar auf demselben Rechner.

In diesem Buch werden auch HTTP-basierte Adressen verwendet.

IPC-Adressen

IPC-Adressen (Inter-Process-Communication) verwenden net.pipe für den Transport, um die Verwendung des Windows Named Pipe-Mechanismus anzuzeigen. In der WCF können Dienste, die IPC nutzen, nur Anrufe vom selben Rechner annehmen. Daher musst du entweder den expliziten lokalen Rechnernamen oder localhost für den Rechnernamen angeben, gefolgt von einem eindeutigen String für den Pipe-Namen:

net.pipe://localhost/MyPipe

Du kannst eine Named Pipe nur einmal pro Computer öffnen. Es ist also nicht möglich, dass sich zwei Named Pipe-Adressen auf demselben Computer einen Pipe-Namen teilen.

In diesem Buch werden durchgängig IPC-basierte Adressen verwendet.

Hinweis

Das von Microsoft angegebene IPC-Adressformat ist falsch und gibt den Mechanismus anstelle des Protokolls an. Das korrekte Schemaformat hätte net.ipc statt net.pipe lauten müssen, so wie die TCP-Adresse net.tcp statt net.socket verwendet.

MSMQ-Adressen

MSMQ-Adressen verwenden net.msmq für den Transport, um die Verwendung der Microsoft Message Queue (MSMQ) anzuzeigen. Du musst den Namen der Warteschlange angeben. Bei privaten Warteschlangen musst du auch den Typ der Warteschlange angeben, aber bei öffentlichen Warteschlangen kannst du das weglassen:

net.msmq://localhost/private/MyService
net.msmq://localhost/MyService

In Kapitel 9 geht es um Anrufe in der Warteschlange.

WebSocket-Adressen

WebSocket-Adressen sind einzigartig, da sie zwischen Client und Dienst asymmetrisch sind. Der Client verwendet ws für den Transport und wss für den sicheren Transport, während der Dienst immer http bzw. https verwendet. WebSocket-Adressen werden benötigt, wenn du Rückrufe über das Internet benötigst, und du kannst einen Port angeben, wie hier gezeigt:

ws://localhost:8080

Wenn du die Portnummer nicht angibst, wird für einen WebSocket standardmäßig der HTTP-Port 80 (und Port 443 für wss oder HTTPS) verwendet. Wie bei TCP-Adressen können sich auch zwei WebSocket-Adressen auf demselben Rechner einen Port teilen.

UDP-Adressen

UDP-Adressen verwenden soap.udp für den Transport, um anzuzeigen, dass SOAP über UDP verwendet wird. Du kannst auch einen Port angeben, wie hier gezeigt:

soap.udp://localhost:8081

Verträge

In WCF stellen alle Dienste Verträge zur Verfügung. Der Vertrag ist eine plattformneutrale und standardisierte Methode, um zu beschreiben, was der Dienst tut. WCF definiert vier Arten von Verträgen:

Dienstleistungsverträge
Beschreibe welche Operationen der Kunde mit dem Dienst durchführen kann. Servicekontrakte sind das Thema von Kapitel 2, aber sie werden in jedem Kapitel dieses Buches ausgiebig verwendet.
Daten Verträge
Definiere , welche Datentypen an den und vom Dienst übergeben werden. WCF definiert implizite Verträge für eingebaute Typen wie int und string, aber du kannst ganz einfach explizite Opt-in-Datenverträge für benutzerdefinierte Typen definieren. Kapitel 3 ist der Definition und Verwendung von Datenverträgen gewidmet, und in den folgenden Kapiteln werden Datenverträge je nach Bedarf verwendet.
Fehlerhafte Verträge
Definiere welche Fehler vom Dienst ausgelöst werden und wie der Dienst Fehler behandelt und an seine Kunden weitergibt. Kapitel 6 widmet sich der Definition und Verwendung von Fehlerverträgen.
Verträge für Nachrichten
Erlauben dem Dienst, direkt mit Nachrichten zu interagieren. Nachrichtenverträge können typisiert oder untypisiert sein und sind in Fällen von Interoperabilität nützlich, wenn eine andere Partei bereits ein explizites (typischerweise proprietäres) Nachrichtenformat vorgeschrieben hat. Dies ist jedoch keineswegs der übliche Fall für gewöhnliche WCF-Anwendungen, so dass in diesem Buch kein Gebrauch von Nachrichtenverträgen gemacht wird. Wenn du nicht unbedingt die Flexibilität, Leistungsfähigkeit und Erweiterbarkeit von Nachrichtenverträgen nutzen musst, solltest du sie vermeiden, da sie keinen Mehrwert bieten, sondern die Komplexität erhöhen. In vielen Fällen deutet der Wunsch, Message Contracts zu verwenden, darauf hin, dass ein benutzerdefinierter Anwendungskontext erforderlich ist, den du mit benutzerdefinierten Headern (eine nützliche alternative Technik, die in diesem Buch durchgängig verwendet wird) erstellen kannst. Weitere Informationen zu Nachrichten-Headern findest du in Anhang B.

Der Dienstleistungsvertrag

Die ServiceContractAttribute ist definiert als:

[AttributeUsage(AttributeTargets.Interface|AttributeTargets.Class,
                Inherited = false)]
public sealed class ServiceContractAttribute : Attribute
{
   public string Name
   {get;set;}
   public string Namespace
   {get;set;}
   //More members
}

Mit diesem Attribut kannst du einen Servicevertrag definieren. Du kannst das Attribut auf eine Schnittstelle oder eine Klasse anwenden, wie in Beispiel 1-1 gezeigt.

Beispiel 1-1. Definition und Implementierung eines Servicevertrags
[ServiceContract]
interface IMyContract
{
   [OperationContract]
   string MyMethod(string text);

   //Will not be part of the contract
   string MyOtherMethod(string text);
}
class MyService : IMyContract
{
   public string MyMethod(string text)
   {
      return "Hello " + text;
   }
   public string MyOtherMethod(string text)
   {
      return "Cannot call this method over WCF";
   }
}

Das Attribut ServiceContract bildet eine CLR-Schnittstelle (oder eine abgeleitete Schnittstelle, wie du später sehen wirst) auf einen technologieneutralen Servicevertrag ab. Das Attribut ServiceContract stellt eine CLR-Schnittstelle (oder eine Klasse) als WCF-Vertrag dar, unabhängig von der Sichtbarkeit des Typs. Die Sichtbarkeit des Typs hat keinen Einfluss auf WCF, denn Sichtbarkeit ist ein CLR-Konzept. Wenn du das Attribut ServiceContract auf eine interne Schnittstelle anwendest, wird diese Schnittstelle als öffentlicher Dienstvertrag dargestellt und kann über die Assembly-Grenze hinweg konsumiert werden. Ohne das Attribut ServiceContract ist die Schnittstelle für WCF-Clients nicht sichtbar, was dem serviceorientierten Grundsatz entspricht, dass Servicegrenzen explizit sein sollten. Um diesen Grundsatz durchzusetzen, müssen sich alle Verträge explizit dafür entscheiden: Nur Schnittstellen (oder Klassen), die mit dem Attribut ServiceContract versehen sind, werden als WCF-Verträge betrachtet; andere Typen werden nicht berücksichtigt.

Außerdem wird keines der Member des Typs jemals Teil des Vertrags sein, wenn du das Attribut ServiceContract verwendest. Du musst der WCF explizit angeben, welche Methoden als Teil des WCF-Vertrags veröffentlicht werden sollen, indem du das OperationContractAttribute verwendest:

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

Du kannst das OperationContract Attribut nur auf Methoden anwenden, nicht auf Eigenschaften, Indexer oder Ereignisse, die CLR-Konzepte sind. WCF versteht nur Operationen - logischeFunktionen - und das Attribut OperationContract macht eine Vertragsmethode als logische Operation sichtbar, die als Teil des Servicevertrags ausgeführt wird. Andere Methoden der Schnittstelle (oder Klasse), die nicht das Attribut OperationContract haben, sind nicht Teil des Vertrags. Dadurch werden explizite Dienstgrenzen durchgesetzt und ein explizites Opt-in-Modell für die Operationen selbst aufrechterhalten. Darüber hinaus kann eine Vertragsoperation keine Objektreferenzen als Parameter verwenden: Nur primitive Typen oder Datenverträge sind erlaubt.

Anwendung des Attributs ServiceContract

In WCF kannst du das Attribut ServiceContract auf eine Schnittstelle oder auf eine Klasse anwenden. Wenn du es auf eine Schnittstelle anwendest, muss eine Klasse diese Schnittstelle implementieren. In der Regel verwendest du einfaches C# oder VB, um die Schnittstelle zu implementieren, und nichts im Code der Dienstklasse hat damit zu tun, dass es sich um einen WCF-Dienst handelt:

[ServiceContract]
interface IMyContract
{
   [OperationContract]
   string MyMethod();
}
class MyService : IMyContract
{
   public string MyMethod()
   {
      return "Hello WCF";
   }
}

Du kannst eine implizite oder explizite Schnittstellenimplementierung verwenden:

class MyService : IMyContract
{
   string IMyContract.MyMethod()
   {
      return "Hello WCF";
   }
}
Hinweis

Da der Client die Serviceklasse nie direkt nutzen kann, sondern immer über einen Proxy gehen muss, ist die explizite Schnittstellenimplementierung in WCF weniger wichtig als in der normalen .NET-Programmierung.

Eine einzelne Klasse kann mehrere Verträge unterstützen, indem sie mehrere Schnittstellen ableitet und implementiert, die mit dem Attribut ServiceContract versehen sind:

[ServiceContract]
interface IMyContract
{
   [OperationContract]
   string MyMethod();
}
[ServiceContract]
interface IMyOtherContract
{
   [OperationContract]
   void MyOtherMethod();
}
class MyService : IMyContract,IMyOtherContract
{
   public string MyMethod()
   {...}
   public void MyOtherMethod()
   {...}
}

Es gibt jedoch einige Implementierungsbeschränkungen für die Dienstimplementierungsklasse. Du solltest parametrisierte Konstruktoren vermeiden, denn WCF wird nur den Standardkonstruktor verwenden. Außerdem kann die Klasse zwar interne Eigenschaften, Indexer und statische Mitglieder verwenden, aber kein WCF-Client kann auf sie zugreifen.

In WCF kannst du das Attribut ServiceContract auch direkt auf die Serviceklasse anwenden, ohne vorher einen separaten Vertrag zu definieren:

//Avoid
[ServiceContract]
class MyService
{
   [OperationContract]
   string MyMethod()
   {
      return "Hello WCF";
   }
}

Unter den deckt, wird WCF die Vertragsdefinition ableiten. Du kannst das Attribut OperationContract auf jede Methode der Klasse anwenden, egal ob sie privat oder öffentlich ist.

Warnung

Vermeide es, das Attribut ServiceContract direkt für die Serviceklasse zu verwenden. Definiere immer einen separaten Vertrag, damit du ihn sowohl unabhängig von der Klasse konsumieren als auch von anderen Klassen implementieren lassen kannst.

Namen und Namensräume

Du kannst und solltest einen Namensraum für deinen Vertrag definieren. Der Vertrags-Namensraum dient in WCF demselben Zweck wie in der .NET-Programmierung: Er dient dazu, einen Vertragstyp in den Geltungsbereich einzugrenzen und die Wahrscheinlichkeit von Kollisionen zu verringern. Du kannst die Eigenschaft Namespace des Attributs ServiceContract verwenden, um einen Namensraum anzugeben:

[ServiceContract(Namespace = "MyNamespace")]
interface IMyContract
{...}

Wenn du nichts angibst, lautet der Standard-Namensraum für den Vertrag http://tempuri.org. Für nach außen gerichtete Dienste verwendest du in der Regel die URL deines Unternehmens, und für Intranet-Dienste kannst du jeden aussagekräftigen, eindeutigen Namen verwenden, wie z. B. MyApplication.

In der Standardeinstellung ist der Name des Vertrags der Name der Schnittstelle. Du kannst jedoch einen Alias für einen Vertrag verwenden, um den Kunden in den Metadaten einen anderen Namen zu geben, indem du die Eigenschaft Name des Attributs ServiceContract verwendest:

[ServiceContract(Name = "IMyContract")]
interface IMyOtherContract
{...}

Ebenso ist der Name der öffentlich zugänglichen Operation standardmäßig der Name der Methode, aber du kannst die Eigenschaft Name des Attributs OperationContract verwenden, um einen anderen öffentlich zugänglichen Namen zu verwenden:

[ServiceContract]
interface IMyContract
{
   [OperationContract(Name = "SomeOperation")]
   void MyMethod(string text);
}

Du wirst in Kapitel 2 sehen, wie diese Eigenschaften verwendet werden.

Hosting

Die WCF-Serviceklasse kann nicht in einer Leere existieren. Jeder WCF-Dienst muss in einem Windows-Prozess, dem sogenannten Host-Prozess, gehostet werden. Ein einziger Host Prozess kann mehrere Dienste beherbergen, und derselbe Diensttyp kann in mehreren Host-Prozessen beherbergt werden. WCF hat keine Einschränkungen, ob der Host-Prozess auch der Client-Prozess ist oder nicht, obwohl ein separater Prozess die Fehler- und Sicherheitsisolierung fördert. Es ist auch unerheblich, wer den Prozess bereitstellt und um welche Art von Prozess es sich handelt. Der Host kann von den Internet-Informationsdiensten (IIS), vom Windows-Aktivierungsdienst (WAS) bei Windows-Versionen vor Windows Server 2008 R2 und Windows 7 oder vom Entwickler als Teil der Anwendung bereitgestellt werden.

Hinweis

Ein Sonderfall ist das prozessinterne (oder prozessinterne) Hosting, bei dem sich der Dienst im selben Prozess befindet wie , dem Client. Per Definition stellt der Entwickler den Host für den In-Process-Fall bereit.

IIS-Hosting

Der Hauptvorteil des Hostings eines Dienstes auf dem Microsoft IIS-Webserver ist, dass der Host-Prozess bei der ersten Client-Anfrage automatisch gestartet wird und IIS den Lebenszyklus des Host-Prozesses verwaltet. Der größte Nachteil des IIS-Hostings ist, dass du nur HTTP verwenden kannst.

Das Hosting im IIS ist dem Hosting eines Webdienstes oder ASP.NET Web API-Dienstes sehr ähnlich. Du musst ein virtuelles Verzeichnis unter IIS bereitstellen und eine .svc-Datei bereitstellen. IIS verwendet die .svc-Datei, um den Servicecode hinter der Datei und der Klasse zu identifizieren. Beispiel 1-2 zeigt die Syntax für die .svc-Datei.

Beispiel 1-2. Eine .svc-Datei
<%@ ServiceHost
   Language   = "C#"
   Debug      = "true"
   CodeBehind = "~/App_Code/MyService.cs"
   Service    = "MyService"
%>
Hinweis

Du kannst den Servicecode auch inline in die .svc-Datei einfügen, aber das ist nicht ratsam.

Wenn du das IIS-Hosting verwendest, muss die Basisadresse, die für den Dienst verwendet wird, immer mit der Adresse der .svc-Datei übereinstimmen.

Visual Studio verwenden

Du kannst Visual Studio verwenden, um einen von IIS gehosteten Dienst zu erstellen. Wählen Sie im Menü Datei die Option Neue Website und dann im Dialogfeld Neue Website die Option WCF-Dienst. Visual Studio erstellt eine neue Website, einen Dienstcode und eine passende .svc-Datei. Du kannst auch das Dialogfeld "Neues Element hinzufügen" verwenden, um später einen weiteren Dienst hinzuzufügen.

Die Web.Config Datei

Die Website-Konfigurationsdatei(web.config) listet normalerweise die Typen auf, die du als Dienste bereitstellen willst. Du musst vollständig qualifizierte Typnamen verwenden, einschließlich des Assembly-Namens, wenn der Diensttyp aus einer nicht referenzierten Assembly stammt:

<system.serviceModel>
   <services>
      <service name = "MyNamespace.MyService">
         ...
      </service>
   </services>
</system.serviceModel>

Anstatt eine .svc-Datei zu definieren, kannst du den Diensttyp und seine Adressinformationen direkt in der Datei web.config der Anwendung im Abschnitt serviceHostingEnvironment angeben. Du kannst dort so viele Dienste auflisten, wie du willst:

<system.serviceModel>
   <serviceHostingEnvironment>
      <serviceActivations>
         <add relativeAddress = "MyService.svc" service = 
         "MyNamespace.MyService"/>
         <add relativeAddress = "MyOtherService.svc" service = 
         "MyOtherService"/>
      </serviceActivations>
   </serviceHostingEnvironment>
   <services>
      <service name = "MyNamespace.MyService">
         ...
      </service>
      <service name = "MyOtherService">
         ...
      </service>
   </services>
</system.serviceModel>

Selbstgehostet

Self-Hosting ist eine Technik, bei der der Entwickler für die Bereitstellung und Verwaltung des Lebenszyklus des Host-Prozesses verantwortlich ist. Verwende Self-Hosting, wenn du eine Prozess- (oder Maschinen-) Grenze zwischen dem Client und dem Dienst haben willst und wenn du den Dienst in-proc verwendest, d.h. im selben Prozess wie der Client. Du kannst einen beliebigen Windows-Prozess bereitstellen, z. B. eine Windows Forms-Anwendung, eine WPF-Anwendung, eine Konsolenanwendung oder einen Windows-Dienst. Beachte, dass der Prozess laufen muss, bevor der Client den Dienst aufruft, was normalerweise bedeutet, dass du ihn vorher starten musst. Bei Windows-Diensten oder In-Proc-Hosting ist dies kein Problem. Du kannst einen Host mit nur wenigen Codezeilen bereitstellen. Im Gegensatz zum IIS kann ein selbst gehosteter Dienst jedes WCF-Transportprotokoll verwenden, und du kannst alle WCF-Funktionen nutzen, z. B. den Service Bus, die Erkennung und die Verwendung eines Singleton-Dienstes.

Wie beim IIS-Hosting listet die Konfigurationsdatei der Hosting-Anwendung(app.config) normalerweise die Arten der Dienste auf, die du hosten und der Welt zugänglich machen möchtest:

<system.serviceModel>
   <services>
      <service name = "MyNamespace.MyService">
         ...
      </service>
   </services>
</system.serviceModel>

Außerdem muss der Host-Prozess die Diensttypen zur Laufzeit explizit registrieren und den Host für Client-Aufrufe öffnen. Deshalb muss der Host-Prozess laufen, bevor die Client-Aufrufe eintreffen. Die Erstellung des Hosts erfolgt in der Regel in der Methode Main() unter Verwendung der Klasse ServiceHost, die in Beispiel 1-3 definiert ist.

Beispiel 1-3. Die Klasse ServiceHost
public interface ICommunicationObject
{
   void Open();
   void Close();
   //More members
}
public abstract class CommunicationObject : ICommunicationObject
{...}
public abstract class ServiceHostBase : CommunicationObject,IDisposable,...
{...}
public class ServiceHost : ServiceHostBase
{
   public ServiceHost(Type serviceType,params Uri[] baseAddresses);
   //More members
}

Du musst den Konstruktor von ServiceHost mit dem Diensttyp und optional mit Standard-Basisadressen versehen. Die Menge der Basisadressen kann leer sein, und selbst wenn du Basisadressen angibst, kannst du den Dienst so konfigurieren, dass er verschiedene Basisadressen verwendet. Mit einer Reihe von Basisadressen kann der Dienst Anrufe über mehrere Adressen und Protokolle annehmen und nur einen relativen URI verwenden.

Beachte, dass jede ServiceHost Instanz mit einem bestimmten Diensttyp verbunden ist. Wenn der Host-Prozess mehrere Diensttypen bereitstellen muss, benötigst du eine entsprechende Anzahl von ServiceHost Instanzen. Wenn du die Methode Open() auf dem Host aufrufst, lässt du Anrufe zu, und wenn du die Methode Close() aufrufst, beendest du die Host-Instanz ordnungsgemäß, so dass laufende Anrufe abgeschlossen werden können, während zukünftige Client-Anrufe abgewiesen werden, selbst wenn der Host-Prozess noch läuft. Das Schließen des Service-Hosts erfolgt in der Regel, wenn der Host-Prozess heruntergefahren wird. Beispiel: Um diesen Dienst in einer Windows Forms-Anwendung zu hosten:

[ServiceContract]
interface IMyContract
{...}
class MyService : IMyContract
{...}

würdest du den folgenden Hosting-Code schreiben:

static void Main()
{
   ServiceHost host = new ServiceHost(typeof(MyService));

   host.Open();

   //Can do blocking calls:
   Application.Run(new MyForm());

   host.Close();
}

Das Öffnen eines Hosts lädt die WCF-Laufzeit und startet Worker-Threads, um eingehende Anfragen zu überwachen. Die Überwachungs-Threads leiten eingehende Anfragen an Worker-Threads aus dem I/O-Completion-Threadpool weiter (in dem standardmäßig bis zu 1.000 Threads vorhanden sind). Da Worker-Threads beteiligt sind, kannst du nach dem Öffnen des Hosts blockierende Operationen durchführen.

Da der Host ordnungsgemäß geschlossen wird, ist die Zeitspanne, die dafür benötigt wird, unbestimmt. Standardmäßig wartet der Host 10 Sekunden lang auf die Rückkehr von Close() und fährt mit dem Herunterfahren fort, wenn diese Zeitspanne verstrichen ist. Bevor du den Host öffnest, kannst du mit der Eigenschaft CloseTimeout von ServiceHostBase ein anderes Zeitlimit für das Schließen festlegen:

public abstract class ServiceHostBase : ...
{
   public TimeSpan CloseTimeout
   {get;set;}
   //More members
}

Du kannst zum Beispiel programmatische Aufrufe verwenden, um das Zeitlimit für das Schließen auf 20 Sekunden zu setzen:

ServiceHost host = new ServiceHost(...);
host.CloseTimeout = TimeSpan.FromSeconds(20);
host.Open();

Du kannst dasselbe in einer Konfigurationsdatei tun, indem du die Zeitüberschreitung beim Schließen in den Abschnitt host des Dienstes einträgst:

<system.serviceModel>
   <services>
      <service name = "MyNamespace.MyService">
         <host>
            <timeouts
               closeTimeout = "00:00:20"
            />
         </host>
         ...
      </service>
   </services>
</system.serviceModel>

Visual Studio verwenden

Mit Visual Studio kannst du jedem Anwendungsprojekt einen WCF-Dienst hinzufügen, indem du im Dialogfeld Neues Element hinzufügen die Option WCF-Dienst auswählst. Ein auf diese Weise hinzugefügter Dienst ist natürlich in-proc zum Host-Prozess, aber auch out-of-proc-Clients können auf ihn zugreifen.

Selbst-Hosting und Basisadressen

Du kannst einen Service-Host starten, ohne eine Basisadresse anzugeben, indem du die Basisadressen ganz weglässt:

ServiceHost host = new ServiceHost(typeof(MyService));
Warnung

Gib keine null anstelle einer leeren Liste an, denn das führt zu einer Ausnahme:

ServiceHost host;
host = new ServiceHost(typeof(MyService),null);

Du kannst auch mehrere durch Kommas getrennte Basisadressen registrieren, wie im folgenden Ausschnitt, solange die Adressen nicht dasselbe Transportschema verwenden (beachte die Verwendung des params Qualifiers in Beispiel 1-3):

Uri tcpBaseAddress  = new Uri("net.tcp://localhost:8001/");
Uri httpBaseAddress = new Uri("http://localhost:8002/");

ServiceHost host = new ServiceHost(typeof(MyService),
                                   tcpBaseAddress,httpBaseAddress);

Mit WCF kannst du auch die Basisadressen in der Host-Konfigurationsdatei auflisten:

<system.serviceModel>
   <services>
      <service name = "MyNamespace.MyService">
         <host>
            <baseAddresses>
               <add baseAddress = "net.tcp://localhost:8001/"/>
               <add baseAddress = "http://localhost:8002/"/>
            </baseAddresses>
         </host>
         ...
      </service>
   </services>
</system.serviceModel>

Wenn du den Host erstellst, verwendet er die Basisadressen, die er in der Konfigurationsdatei findet, sowie alle Basisadressen, die du programmatisch angibst. Achte besonders darauf, dass sich die konfigurierten Basisadressen und die programmatischen Adressen im Schema nicht überschneiden.

Hinweis

Auf jedem Rechner mit Windows XP oder höher musst du für andere HTTP-Adressen als Port 80 den Host-Prozess (oder Visual Studio beim Testen oder Debuggen) als Administrator starten. Anstatt dies jedes Mal zu tun, kannst du Windows anweisen, den Port-Namensraum für den Benutzer zu reservieren, der den Host ausführt. Verwende dazu das Befehlszeilenprogramm netsh.exe. Um zum Beispiel den HTTP-Port 8002 auf dem lokalen Rechner zu reservieren, musst du diesen Befehl in einer Eingabeaufforderung als Administrator ausführen:

netsh http add urlacl url=http://+:8002/user=
                          "MachineOrDomain\UserName"

Du kannst sogar mehrere Hosts für denselben Typ registrieren, solange die Hosts unterschiedliche Basisadressen verwenden:

Uri baseAddress1  = new Uri("net.tcp://localhost:8001/");
ServiceHost host1 = new ServiceHost(typeof(MyService),baseAddress1);
host1.Open();

Uri baseAddress2  = new Uri("net.tcp://localhost:8002/");
ServiceHost host2 = new ServiceHost(typeof(MyService),baseAddress2);
host2.Open();

Abgesehen von einigen Threading-Problemen, die in Kapitel 8 behandelt werden, bietet das Öffnen mehrerer Hosts auf diese Weise jedoch keinen wirklichen Vorteil. Außerdem funktioniert das Öffnen mehrerer Hosts für denselben Typ nicht mit Basisadressen, die in der Konfigurationsdatei angegeben sind, und erfordert die Verwendung des ServiceHost Konstruktors.

Erweiterte Hosting-Funktionen

Die Schnittstelle ICommunicationObject, die ServiceHost unterstützt, bietet einige erweiterte Funktionen, die in Beispiel 1-4 aufgeführt sind.

Beispiel 1-4. Die Schnittstelle ICommunicationObject
public interface ICommunicationObject
{
   void Open();
   void Close();
   void Abort();

   event EventHandler Closed;
   event EventHandler Closing;
   event EventHandler Faulted;
   event EventHandler Opened;
   event EventHandler Opening;
   IAsyncResult BeginClose(AsyncCallback callback,object state);
   IAsyncResult BeginOpen(AsyncCallback callback,object state);
   void EndClose(IAsyncResult result);
   void EndOpen(IAsyncResult result);

   CommunicationState State
   {get;}
  //More members
}
public enum CommunicationState
{
   Created,
   Opening,
   Opened,
   Closing,
   Closed,
   Faulted
}

Wenn das Öffnen oder Schließen des Hosts ein langwieriger Vorgang ist, kannst du dies mit den Methoden BeginOpen() und BeginClose() asynchron erledigen.

Du kannst Hosting-Ereignisse wie Zustandsänderungen oder Fehler abonnieren und die Eigenschaft State verwenden, um den Host-Status abzufragen. Schließlich bietet die Klasse ServiceHost auch die Methode Abort(). Abort() ist ein ungraceful exit - wenn sie aufgerufen wird, bricht sie sofort alle laufenden Serviceaufrufe ab und fährt den Host herunter. Aktive Clients erhalten jeweils eine Ausnahme.

Die Klasse ServiceHost<T>

Du kannst die von WCF zur Verfügung gestellte Klasse ServiceHost verbessern, indem du die Klasse ServiceHost<T> definierst, wie in Beispiel 1-5 gezeigt.

Beispiel 1-5. Die Klasse ServiceHost<T>
public class ServiceHost<T> : ServiceHost
{
   public ServiceHost() : base(typeof(T))
   {}

   public ServiceHost(params string[] baseAddresses) : base(typeof(T),
                      baseAddresses.Select(address=>new Uri(address)).ToArray())
   {}
   public ServiceHost(params Uri[] baseAddresses) : base(typeof(T),baseAddresses)
   {}
}

ServiceHost<T> bietet einfache Konstruktoren, die den Servicetyp nicht als Konstruktionsparameter benötigen und die mit rohen Strings arbeiten können, anstatt mit dem umständlichen Uri. Im Laufe dieses Buches werde ich einige Erweiterungen, Funktionen und Möglichkeiten von ServiceHost<T> hinzufügen.

WAS Hosting

Das Problem beim Hosting im IIS ist, dass es sich um einen Webserver und nicht um eine Hosting-Engine handelt. musst du daher deinen Dienst als Website tarnen. ASP.NET kapselt diesen Schritt zwar für dich, aber die interne Komplexität steigt dadurch erheblich, da die HTTP-Module und die ASP.NET-Pipeline involviert sind. Das Problem ist: Je mehr bewegliche Teile beteiligt sind, desto höher ist die Wahrscheinlichkeit, dass etwas schief geht. Außerdem ist der IIS durch die Beschränkung des Dienstes auf HTTP nicht für Intranet-Anwendungen geeignet.

Mit der nächsten Welle von Windows hat Microsoft dieses Problem behoben, indem es eine universelle Hosting-Engine namens Windows Activation Service (WAS) zur Verfügung stellte. Der WAS ist ein Systemdienst, der mit den Windows-Versionen ab Windows Server 2008 verfügbar ist. Der WAS ist eine echte Allzweck-Hosting-Engine. Er kann Websites hosten (ab IIS 7 werden die Websites standardmäßig im WAS gehostet), aber er kann genauso gut deine Dienste hosten, wobei du jeden Transportweg wie TCP, IPC oder MSMQ verwenden kannst. Du kannst den WAS separat von IIS installieren und konfigurieren. Das Hosten eines WCF-Dienstes im WAS sieht genauso aus wie das Hosten im IIS. Du musst entweder eine .svc-Datei bereitstellen, genau wie beim IIS, oder die entsprechenden Informationen in der Konfigurationsdatei angeben. Alle anderen Entwicklungsaspekte, wie die Unterstützung in Visual Studio, bleiben genau gleich. Da der WAS ein Systemdienst ist, musst du deinen Service-Host-Prozess nicht vorab starten. Wenn der erste Client-Aufruf eintrifft, fängt der WAS ihn ab, startet einen Worker-Prozess, der deinen Dienst hostet, und leitet den Aufruf weiter.

WAS bietet viele Funktionen, die über das Self-Hosting hinausgehen, z. B. Anwendungspooling, Recycling, Leerlaufzeitverwaltung, Identitätsmanagement und Isolierung. Um den WAS zu nutzen, musst du eine Plattform verwenden, die ihn unterstützt, z. B. einen Windows Server 2008 (oder höher) für Skalierbarkeit oder einen Windows 7 (oder höher) Client-Rechner für eine Handvoll von Clients.

Dennoch bieten selbst gehostete Prozesse einzigartige Vorteile, wie z.B. das In-Proc-Hosting, den Umgang mit unbekannten Kundenumgebungen und den einfachen programmatischen Zugriff auf die zuvor beschriebenen erweiterten Hosting-Funktionen.

Benutzerdefiniertes Hosting in IIS/WAS

Oft musst du mit der Host-Instanz interagieren. Während dies bei einer selbst gehosteten Lösung unabdingbar ist, hast du bei der Verwendung von IIS oder WAS keinen direkten Zugriff auf den Host. Um diese Hürde zu überwinden, bietet WCF einen Hook namens Host Factory. Mit dem Tag Factory in der .svc-Datei kannst du eine von dir bereitgestellte Klasse angeben, die die Host-Instanz erstellt:

<%@ ServiceHost
   Language   = "C#"
   Debug      = "true"
   CodeBehind = "~/App_Code/MyService.cs"
   Service    = "MyService"
   Factory    = "MyServiceFactory"
%>

Du kannst die Host-Factory auch in der Konfigurationsdatei angeben, wenn du nicht explizit eine .svc-Datei verwendest:

<serviceActivations>
   <add relativeAddress = "MyService.svc"
        service = "MyService"
        factory = "MyServiceFactory"
   />
</serviceActivations>

Die Host-Factory-Klasse muss von der Klasse ServiceHostFactory abgeleitet sein und die virtuelle Methode CreateServiceHost() überschreiben:

public class ServiceHostFactory : ...
{
   protected virtual ServiceHost CreateServiceHost(Type serviceType,
                                                   Uri[] baseAddresses);
   //More members
}

Zum Beispiel:

class MyServiceFactory : ServiceHostFactory
{
   protected override ServiceHost CreateServiceHost(Type serviceType,
                                                    Uri[] baseAddresses)
   {
      ServiceHost host = new ServiceHost(serviceType,baseAddresses);

      //Custom steps here

      return host;
   }
}
Hinweis

Die CreateServiceHost() Methode ist logischerweise die Main() Methode eines IIS oder WAS gehosteten Dienstes, und du kannst sie nur für diesen Zweck verwenden.

Auswahl eines Gastgebers

Obwohl WCF eine Vielzahl von Optionen bietet, vom IIS über den WAS bis hin zum Self-Hosting, ist es einfach, den richtigen Host zu wählen, wie in Abbildung 1-4 dargestellt. Für eine Internetanwendung (d.h. eine Anwendung, die Anrufe von Clients über das Internet empfängt) bieten IIS oder WAS die besten Möglichkeiten, um deine Dienste gegen die Sicherheitsbedenken beim Zugriff über das Internet zu schützen. Andernfalls solltest du deine Dienste selbst hosten. Self-Hosting bietet deiner Organisation ein Hosting-Modell, das die Verwaltung und Bereitstellung von Intranetdiensten erheblich vereinfacht. WCF-Dienste benötigen nicht die vielen zusätzlichen Lifecycle-Management-Funktionen, die der WAS bietet.

Choosing a host for any service
Abbildung 1-4. Auswahl eines Hosts für einen beliebigen Dienst
Warnung

Microsoft wird Windows Server AppFabric im April 2016 aus dem Verkehr ziehen. Du solltest keine der Funktionen, insbesondere die Autostart-Funktion, für eines deiner Systeme nutzen.

Bindungen

gibt es mehrere Aspekte der Kommunikation mit einem bestimmten Dienst, und es gibt viele mögliche Kommunikationsmuster. Nachrichten können einem synchronen Anfrage-Antwort- oder einem asynchronen Fire-and-Forget-Muster folgen, Nachrichten können bidirektional sein, Nachrichten können sofort zugestellt oder in eine Warteschlange gestellt werden, und die Warteschlangen können dauerhaft oder flüchtig sein. Wie bereits erwähnt, gibt es viele mögliche Transportprotokolle für die Nachrichten, z. B. HTTP (oder HTTPS), TCP, IPC, MSMQ. Außerdem gibt es einige Optionen für die Nachrichtenkodierung. Du kannst Klartext wählen, um Interoperabilität zu ermöglichen, binäre Kodierung, um die Leistung zu optimieren, oder den Message Transport Optimization Mechanism (MTOM) für große Nutzdaten. Schließlich gibt es mehrere Optionen zur Sicherung von Nachrichten. Du kannst wählen, ob du sie überhaupt nicht sichern willst, ob du nur die Sicherheit auf Transportebene gewährleisten willst oder ob du Datenschutz und Sicherheit auf Nachrichtenseite gewährleisten willst, und natürlich gibt es zahlreiche Möglichkeiten, die Clients zu authentifizieren und zu autorisieren. Die Nachrichtenzustellung kann unzuverlässig oder zuverlässig sein, über Zwischenstationen und unterbrochene Verbindungen hinweg, und die Nachrichten können in der Reihenfolge verarbeitet werden, in der sie gesendet oder empfangen wurden. Dein Dienst muss möglicherweise mit anderen Diensten oder Clients interagieren, die nur das grundlegende Webservice-Protokoll kennen, oder mit Clients und Diensten, die die WS-*-Protokolle wie WS-Security und WS-Atomic Transactions nutzen können. Es kann sein, dass dein Dienst mit jedem Client zusammenarbeiten muss oder dass du deinen Dienst nur auf die Zusammenarbeit mit anderen WCF-Diensten oder -Clients beschränken möchtest.

Wenn du alle möglichen Kommunikations- und Interaktionsoptionen zählen würdest, würdest du wahrscheinlich feststellen, dass die Zahl der Möglichkeiten in die Zehntausende geht. Einige dieser Möglichkeiten können sich gegenseitig ausschließen und andere können andere Möglichkeiten vorschreiben. Es liegt auf der Hand, dass sowohl der Kunde als auch der Dienst all diese Optionen kennen müssen, um richtig kommunizieren zu können. Der Umgang mit dieser Komplexität bringt den meisten Anwendungen keinen Mehrwert, aber die Auswirkungen falscher Entscheidungen auf Produktivität und Qualität sind gravierend.

Um diese Entscheidungen zu vereinfachen und handhabbar zu machen, fasst die WCF verschiedene Kommunikationsaspekte in Bindungen zusammen. Eine Bindung ist lediglich ein konsistenter, festgelegter Satz von Entscheidungen in Bezug auf das Transportprotokoll, die Nachrichtenkodierung, das Kommunikationsmuster, die Zuverlässigkeit, die Sicherheit, die Transaktionsweiterleitung und die Interoperabilität. Du musst nur das Zielszenario für deinen Dienst festlegen, und WCF trifft für dich eine korrekte mehrdimensionale Entscheidung über alle Aspekte der Kommunikation. Im Idealfall kannst du all diese "Klempner"-Aspekte aus dem Code deines Dienstes herausnehmen und dem Dienst erlauben, sich ausschließlich auf die Implementierung der Geschäftslogik zu konzentrieren. Bindungen ermöglichen es dir, dieselbe Servicelogik über drastisch unterschiedliche Leitungen zu verwenden.

Du kannst die von WCF bereitgestellten Bindungen verwenden, ihre Eigenschaften verändern oder deine eigenen Bindungen von Grund auf neu schreiben. Der Dienst veröffentlicht die von ihm gewählte Bindung in seinen Metadaten, damit die Clients den Typ und die spezifischen Eigenschaften der Bindung abfragen können. Das ist wichtig, weil der Kunde genau dieselben Bindungswerte verwenden muss wie der Dienst. Ein einzelner Dienst kann mehrere Bindungen an verschiedenen Adressen unterstützen.

Die gemeinsamen Bindungen

WCF definiert fünf häufig verwendete Bindungen:

Grundlegende Bindung
Das Basic Binding wird von der Klasse BasicHttpBinding angeboten ( ) und dient dazu, einen WCF-Dienst als ASMX-Webservice darzustellen, damit alte Clients mit neuen Diensten arbeiten können. Die Basisbindung lässt deinen Dienst auf dem Kabel wie einen Legacy-Webdienst aussehen, der über das Basis-Webdienstprofil kommuniziert. Wenn diese Bindung von Kunden verwendet wird, können neue WCF-Kunden mit alten ASMX-Diensten arbeiten.
TCP-Bindung
Die von der Klasse NetTcpBinding angebotene TCP-Bindung verwendet TCP für die maschinenübergreifende Kommunikation im Intranet. Sie unterstützt eine Reihe von Funktionen, darunter Zuverlässigkeit, Transaktionen und Sicherheit, und ist für die Kommunikation zwischen WCF und WCF optimiert. Daher müssen sowohl der Client als auch der Dienst WCF verwenden.
IPC-Bindung
Die IPC-Bindung, die von der Klasse NetNamedPipeBinding angeboten wird ( ), verwendet Named Pipes als Transportmittel für die Kommunikation mit demselben Rechner. Sie ist die sicherste Bindung, da sie keine Anrufe von außerhalb der Maschine annehmen kann. Die IPC-Bindung unterstützt eine Reihe von Funktionen, die denen der TCP-Bindung ähneln. Sie ist auch die leistungsfähigste Bindung, da IPC ein leichteres Protokoll als TCP ist.
Hinweis

Die Klasse NetNamedPipeBinding ist inkonsistent benannt, da sich die Konvention zur Benennung der Bindung auf das Protokoll und nicht auf den Kommunikationsmechanismus bezieht (daher haben wir NetTcpBinding und nicht NetSocketBinding). Der korrekte Name für diese Bindung hätte NetIpcBinding lauten müssen. In diesem Buch werde ich NetNamedPipeBinding als IPC-Bindung bezeichnen.

Web Service (WS) Bindung
Die von der Klasse WSHttpBinding angebotene WS-Bindung verwendet HTTP oder HTTPS für den Transport und bietet eine Vielzahl von Funktionen (wie Zuverlässigkeit, Transaktionen und Sicherheit) über das Internet, die alle die WS-*-Standards verwenden. Diese Bindung ist so konzipiert, dass sie mit jeder Partei zusammenarbeitet, die die WS-*-Standards unterstützt.
MSMQ-Anbindung
Die MSMQ-Bindung, die von der Klasse NetMsmqBinding angeboten wird ( ), verwendet MSMQ für den Transport und bietet Unterstützung für Anrufe in Warteschlangen ohne Verbindung. Die Verwendung dieser Bindung ist Gegenstand von Kapitel 9.

Format und Kodierung

Jede der häufig verwendeten Bindungen verwendet ein anderes Transportschema und eine andere Kodierung, wie in Tabelle 1-1 aufgeführt. Wo mehrere Kodierungen möglich sind, sind die Standardwerte fett gedruckt.

Tabelle 1-1. Transport und Kodierung für gängige Bindungen
Name Transport Kodierung Interoperabel
BasicHttpBinding HTTP/HTTPS Text, MTOM Ja
NetTcpBinding TCP Binär Nein
NetNamedPipeBinding IPC Binär Nein
WSHttpBinding HTTP/HTTPS Text, MTOM Ja
NetMsmqBinding MSMQ Binär Nein

Mit der textbasierten Kodierung kann ein WCF-Dienst (oder Client) in der Regel über HTTP mit jedem anderen Dienst (oder Client) kommunizieren, unabhängig von dessen Technologie und über Firewalls hinweg. Die binäre Kodierung über TCP, IPC oder MSMQ bietet die beste Leistung, geht aber auf Kosten der Interoperabilität, da sie eine Kommunikation von WCF zu WCF voraussetzt. Bei den TCP-, IPC- und MSMQ-Bindungen ist Interoperabilität jedoch oft nicht erforderlich. Im Fall von IPC kann der Client sicher sein, dass der Zielcomputer unter Windows läuft und WCF installiert ist, da der Aufruf den Clientcomputer nie verlassen kann. Im Falle der TCP-Bindung muss deine Anwendung zwar mit anderen Anwendungen, die in anderen Technologien geschrieben wurden, zusammenarbeiten, aber die Anwendungen selbst sind in der Regel intern homogen. Solange sich deine Anwendung nur auf das lokale Intranet erstreckt, kannst du in der Regel von einer homogenen Windows-Umgebung ohne interne Firewalls zwischen den Rechnern ausgehen. Schließlich erfordert die MSMQ-Bindung die Verwendung eines MSMQ-Servers, der natürlich Windows-spezifisch ist.

Warnung

Der binäre Kodierer, den die TCP-, IPC- und MSMQ-Bindungen verwenden, ist WCF-eigen. Versuche nicht, einen eigenen Parser dafür auf anderen Plattformen zu schreiben. Microsoft behält sich das Recht vor, das Format im Laufe der Zeit zu ändern, um es zu optimieren und weiterzuentwickeln.

Auswahl einer Bindung

Wenn du eine Bindung für deinen Dienst auswählst, solltest du dem in Abbildung 1-5 dargestellten Entscheidungsdiagramm folgen.

Choosing a binding
Abbildung 1-5. Auswahl einer Bindung

Die erste Frage, die du dir stellen solltest, ist, ob dein Dienst mit Nicht-WCF-Clients interagieren muss. Wenn dies der Fall ist und diese Kunden das grundlegende Webservice-Protokoll (ASMX-Webservices) erwarten, wähle BasicHttpBinding, um deinen WCF-Dienst nach außen hin so darzustellen, als wäre er ein ASMX-Webservice (d.h. ein WSI-Basic-Profil). Der Nachteil dieser Wahl ist, dass du die meisten der standardmäßigen WS-*-Protokolle nicht nutzen kannst. Wenn der Nicht-WCF-Client diese Standards jedoch verstehen kann, kannst du stattdessen die WS-Bindung wählen. Wenn du davon ausgehen kannst, dass der Client ein WCF-Client ist und eine Offline- oder Disconnected-Interaktion benötigt, wählst du die NetMsmqBinding, die MSMQ für den Transport der Nachrichten verwendet. Wenn der Client eine verbundene Kommunikation benötigt, aber über Rechnergrenzen hinweg anrufen könnte, wähle die NetTcpBinding, die über TCP kommuniziert. Befindet sich der Client auf demselben Rechner wie der Dienst, wählst du den NetNamedPipeBinding, der IPC verwendet, um die Leistung zu maximieren.

Hinweis

Die meisten Bindungen funktionieren auch außerhalb ihrer Zielszenarien gut. So kannst du zum Beispiel die TCP-Bindung für die same-machine- oder sogar in-proc-Kommunikation verwenden und die Basisbindung für die Intranet-WCF-zu-WCF-Kommunikation. Versuche jedoch, eine Bindung gemäß Abbildung 1-5 zu wählen.

Zusätzliche Bindungen

Zusätzlich zu den fünf bisher beschriebenen, häufig verwendeten Bindungen bietet die WCF drei Spezialisierungen dieser Bindungen: die BasicHttpContextBinding, die WSHttpContextBinding und die NetTcpContextBinding. Die Kontextbindungen (beschrieben in Anhang B) leiten sich alle von ihren jeweiligen regulären Bindungen ab und unterstützen zusätzlich ein Kontextprotokoll. Das Kontextprotokoll ermöglicht es dir, Out-of-Band-Parameter an den Dienst zu übergeben. Du kannst die Kontextbindungen auch für die Unterstützung dauerhafter Dienste verwenden, wie in Kapitel 4 beschrieben.

WCF definiert acht Bindungen, die du unserer Meinung nach nicht häufig verwenden solltest. Diese Bindungen (im Folgenden aufgeführt) sind jeweils für ein bestimmtes Zielszenario konzipiert und können außerhalb dieses Szenarios nicht ohne Weiteres verwendet werden. In diesem Buch werden diese Bindungen wenig oder gar nicht verwendet, da sie etwas esoterisch sind und es bessere Alternativen gibt.

WS-Doppelbindung
wird von der Klasse WSDualHttpBinding angeboten und ähnelt der WS-Bindung, mit dem Unterschied, dass sie auch die bidirektionale Duplex-Kommunikation zwischen dem Dienst und dem Client unterstützt, wie in Kapitel 5 beschrieben. Diese Bindung nutzt zwar Industriestandards (sie ist nichts anderes als zwei WSHttpBinding Bindungen, die miteinander verdrahtet sind, um Rückrufe zu unterstützen), aber es gibt keinen Industriestandard für die Einrichtung des Rückrufs, und deshalb ist WSDualHttpBinding nicht interoperabel. Diese Bindung ist ein Erbe der ersten Version von WCF. Mit der Verfügbarkeit der WebSocket-Bindung NetHttpBinding ist die WSDualHttpBinding für Callbacks über das Internet veraltet.
WebSocket-Bindung
Diese Bindung wird von der Klasse NetHttpBinding angeboten und ist de facto ein .NET 4.5 Ersatz für die .NET 3.0 WSDualHttpBinding, da sie ein besseres Modell für Rückrufe über das Internet bietet. Wie die WSDualHttpBinding bietet auch die NetHttpBinding eine Reihe zusätzlicher Funktionen wie Zuverlässigkeit und Sicherheit. Wenn du nicht unbedingt Rückrufe über das Internet verwenden musst, solltest du stattdessen eine der gängigen Bindungen wählen, wie in "Auswahl einer Bindung" beschrieben .
Warnung

Das WebSocket-Protokoll und die dazugehörige WebSocket-API sind beides Industriestandards, aber die NetHttpBinding stellt eine proprietäre WCF-zu-WCF-Implementierung dar. Daher ist NetHttpBinding nicht mit der interoperablen WebSocket-API kompatibel. Wir sind der Meinung, dass diese fehlende Standardkonformität die Reichweite und Anwendbarkeit der WebSocket-Bindung stark einschränkt. Daher betrachten wir die WebSocket-Bindung nicht als eine der empfohlenen Bindungen und haben die Rolle von NetHttpBinding auf fortgeschrittene Szenarien beschränkt, in denen Rückrufe über das Internet sinnvoll sind.

Föderierte WS-Bindung
Die von der Klasse WSFederationHttpBinding angebotene ist eine Spezialisierung der WS-Bindung, die Unterstützung für föderierte Sicherheit bietet. Föderierte Sicherheit würde den Rahmen dieses Buches sprengen, da es der Industrie derzeit an guter Unterstützung (sowohl bei der Technologie als auch bei den Geschäftsmodellen) für echte föderierte Szenarien mangelt. Ich gehe davon aus, dass Föderation im Laufe der Zeit zum Mainstream wird.
Föderierte WS 2007 verbindlich
wird von der Klasse WS2007FederationHttpBinding angeboten und ist eine Aktualisierung von WSFederationHttpBinding.
MSMQ-Integrationsbindung
Sie wird von der Klasse MsmqIntegrationBinding angeboten ( ) und ist die analoge Queued-World-Bindung zur Basisbindung. Die Integrationsbindung konvertiert WCF-Nachrichten in und aus MSMQ-Nachrichten und ist so konzipiert, dass sie mit älteren MSMQ-Clients zusammenarbeitet, was zu der Zeit, als WCF auf den Markt kam, ein Randszenario war (heute ist es noch weniger anwendbar).
WS 2007 verbindlich
Dieses Binding wird von der Klasse WS2007HttpBinding angeboten und leitet sich von der Klasse WSHttpBinding ab. Es bietet Unterstützung für den Koordinationsstandard und Aktualisierungen für die Transaktions-, Sicherheits- und Zuverlässigkeitsstandards.
UDP-Bindung
von der Klasse UdpBinding angeboten wird, bietet diese Bindung Unterstützung für das User Datagram Protocol (UDP). UDP bietet keine Garantie für die Zustellung von Nachrichten, die Reihenfolge oder die Erkennung von doppelten Nachrichten. Aufgrund dieser Unzulänglichkeiten ist der Nutzen von UdpBindingauf die wenigen Szenarien beschränkt, in denen Nachrichtenverluste, fehlende Reihenfolge oder Duplikate toleriert werden können. In den meisten Geschäftsszenarien kommt die Verwendung von UDP daher nicht in Frage. Du könntest zwar UDP für die Übertragung von Ereignissen verwenden, aber eine weitaus bessere und zuverlässigere Lösung ist die Verwendung eines Pub/Sub-Dienstes, wie in Anhang D gezeigt wird.
Webbindung
Diese Bindung wird von der Klasse WebHttpBinding auf angeboten und ermöglicht es deinem Dienst, einfache Aufrufe über Webprotokolle wie HTTP-GET unter Verwendung der REST/POX/JSON-Muster zu akzeptieren. WebHttpBinding wurde durch das neue ASP.NET Web API-Framework abgelöst, das mit .NET 4.0 eingeführt wurde und ab IIS 7 unterstützt wird. Die ASP.NET Web API bietet ein viel klareres Modell für die Implementierung von HTTP-basierten Diensten und REST-ähnlichen Interaktionen.
Warnung

Ab .NET 4.5 wurde NetPeerTcpBinding als Obsolete markiert und sollte nicht mehr verwendet werden.

Eine Bindung verwenden

Jede Bindung bietet buchstäblich Dutzende von konfigurierbaren Eigenschaften. Es gibt drei Möglichkeiten, mit Bindungen zu arbeiten: Du kannst die eingebauten Bindungen so verwenden, wie sie sind, wenn sie deinen Anforderungen entsprechen; du kannst einige ihrer Eigenschaften wie Transaktionsweiterleitung, Zuverlässigkeit und Sicherheit optimieren und konfigurieren; oder du kannst deine eigenen Bindungen schreiben. Das häufigste Szenario besteht darin, eine bestehende Bindung weitgehend unverändert zu verwenden und lediglich zwei oder drei ihrer Aspekte zu konfigurieren. Anwendungsentwickler/innen werden kaum jemals eine eigene Bindung schreiben müssen, aber Framework-Entwickler/innen müssen es vielleicht.

Endpunkte

Jeder Dienst ist mit einer Adresse verbunden, die definiert, wo sich der Dienst befindet, einer Bindung, die festlegt, wie mit dem Dienst kommuniziert werden kann, und einem Vertrag, der definiert, was der Dienst tut. Dieses Dreigestirn, das den Dienst steuert, ist leicht als das ABC des Dienstes zu erinnern. WCF formalisiert diese Beziehung in Form eines Endpunkts. Der Endpunkt ist die Verschmelzung von Adresse, Vertrag und Bindung (siehe Abbildung 1-6).

The endpoint
Abbildung 1-6. Der Endpunkt

Jeder Endpunkt muss über alle drei Elemente verfügen, und der Host stellt den Endpunkt zur Verfügung. Logischerweise ist der Endpunkt die Schnittstelle des Dienstes und entspricht einer CLR- oder COM-Schnittstelle. Beachte die Verwendung der traditionellen "Lollipop"-Notation zur Bezeichnung eines Endpunkts in Abbildung 1-6.

Hinweis

Auch in C# oder VB gibt es Endpunkte: Die Adresse ist die Speicheradresse der virtuellen Tabelle des Typs, die Bindung ist die CLR und der Vertrag ist die Schnittstelle selbst. Weil du dich in der klassischen .NET-Programmierung nie mit Adressen oder Bindungen beschäftigst, nimmst du sie als selbstverständlich hin und hast dich wahrscheinlich daran gewöhnt, vor deinem geistigen Auge die Schnittstelle (die lediglich ein Programmierkonstrukt ist) mit allem gleichzusetzen, was nötig ist, um eine Schnittstelle zu einem Objekt herzustellen. Der WCF-Endpunkt ist eine echte Schnittstelle, weil er alle Informationen enthält, die für die Verbindung mit dem Objekt erforderlich sind. In WCF sind die Adresse und die Bindung nicht voreingestellt und müssen von dir angegeben werden.

Jeder Dienst muss mindestens einen geschäftlichen Endpunkt bereitstellen, und jeder Endpunkt hat genau einen Vertrag. Alle Endpunkte eines Dienstes haben eindeutige Adressen, und ein einzelner Dienst kann mehrere Endpunkte bereitstellen. Diese Endpunkte können dieselben oder unterschiedliche Bindungen verwenden und dieselben oder unterschiedliche Verträge anbieten. Zwischen den verschiedenen Endpunkten, die ein Dienst anbietet, gibt es keinerlei Beziehung.

Es ist wichtig, darauf hinzuweisen, dass der Dienstcode nichts mit seinen Endpunkten zu tun hat und diese immer außerhalb des Dienstcodes liegen. Du kannst Endpunkte entweder administrativ (mit einer Konfigurationsdatei) oder programmatisch konfigurieren.

Administrative Endpunktkonfiguration

Um einen Endpunkt administrativ zu konfigurieren, müssen die Endpunktdetails in die Konfigurationsdatei des Hosting-Prozesses eingetragen werden. Nehmen wir zum Beispiel diese Dienstdefinition:

namespace MyNamespace
{
   [ServiceContract]
   interface IMyContract
   {...}
   class MyService : IMyContract
   {...}
}

Beispiel 1-6 zeigt die erforderlichen Einträge in der Konfigurationsdatei. Unter jedem Diensttyp listest du die Endpunkte auf.

Beispiel 1-6. Administrative Endpunktkonfiguration
<system.serviceModel>
   <services>
      <service name = "MyNamespace.MyService">
         <endpoint
            address  = "http://localhost:8000/MyService"
            binding  = "wsHttpBinding"
            contract = "MyNamespace.IMyContract"
         />
      </service>
   </services>
</system.serviceModel>

Wenn du den Dienst und den Vertragstyp angibst, musst du voll qualifizierte Typnamen verwenden. Ich werde den Namensraum in den Beispielen im weiteren Verlauf dieses Buches weglassen, aber du solltest einen Namensraum verwenden, wenn es möglich ist. Wenn der Endpunkt eine Basisadresse angibt, muss dieses Adressschema mit der Bindung übereinstimmen, z. B. HTTP mit WSHttpBinding. Eine Nichtübereinstimmung führt zu einer Ausnahme beim Laden des Dienstes.

Beispiel 1-7 zeigt eine Konfigurationsdatei, die einen einzelnen Dienst definiert, der mehrere Endpunkte zur Verfügung stellt. Du kannst mehrere Endpunkte mit der gleichen Basisadresse konfigurieren, solange die URI unterschiedlich ist.

Beispiel 1-7. Mehrere Endpunkte auf demselben Dienst
<service name = "MyService">
   <endpoint
      address  = "http://localhost:8000/MyService"
      binding  = "wsHttpBinding"
      contract = "IMyContract"
   />
   <endpoint
      address  = "net.tcp://localhost:8001/MyService"
      binding  = "netTcpBinding"
      contract = "IMyContract"
   />
   <endpoint
      address  = "net.tcp://localhost:8002/MyService"
      binding  = "netTcpBinding"
      contract = "IMyOtherContract"
   />
</service>

Basisadressen verwenden

In Beispiel 1-7 hat jeder Endpunkt seine eigene Basisadresse angegeben. Wenn du eine explizite Basisadresse angibst, überschreibt sie alle Basisadressen, die der Host möglicherweise angegeben hat.

Du kannst auch mehrere Endpunkte die gleiche Basisadresse verwenden lassen, solange sich die Endpunktadressen in ihren URIs unterscheiden:

<service name = "MyService">
   <endpoint
      address  = "net.tcp://localhost:8001/MyService"
      binding  = "netTcpBinding"
      contract = "IMyContract"
   />
   <endpoint
      address  = "net.tcp://localhost:8001/MyOtherService"
      binding  = "netTcpBinding"
      contract = "IMyContract"
   />
</service>

Wenn der Host eine Basisadresse mit einem passenden Transportschema angibt, kannst du die Adresse auch weglassen. In diesem Fall ist die Endpunktadresse dieselbe wie die Basisadresse des passenden Transports:

<endpoint
   binding  = "wsHttpBinding"
   contract = "IMyContract"
/>

Wenn der Host keine passende Basisadresse angibt, schlägt das Laden des Service-Hosts mit einer Ausnahme fehl.

Wenn du die Endpunktadresse konfigurierst, kannst du nur die relative URI unter der Basisadresse hinzufügen:

<endpoint
   address  = "SubAddress"
   binding  = "wsHttpBinding"
   contract = "IMyContract"
/>

Die Endpunktadresse ist in diesem Fall die passende Basisadresse plus die URI, und auch hier muss der Host eine passende Basisadresse angeben.

Konfiguration der Bindung

Du kannst die Konfigurationsdatei verwenden, um die vom Endpunkt verwendete Bindung anzupassen. Dazu fügst du das Tag bindingConfiguration zum Abschnitt endpoint hinzu und benennst einen angepassten Abschnitt im Abschnitt bindings der Konfigurationsdatei. Beispiel 1-8 zeigt, wie diese Technik verwendet wird, um die Transaktionsweiterleitung zu aktivieren. In Kapitel 7 wird die Funktion des transactionFlow Tags erklärt.

Beispiel 1-8. Konfiguration der diensteseitigen Bindung
<system.serviceModel>
   <services>
      <service name = "MyService">
         <endpoint
            address  = "net.tcp://localhost:8000/MyService"
            bindingConfiguration = "TransactionalTCP"
            binding  = "netTcpBinding"
            contract = "IMyContract"
         />
         <endpoint
            address  = "net.tcp://localhost:8001/MyService"
            bindingConfiguration = "TransactionalTCP"
            binding  = "netTcpBinding"
            contract = "IMyOtherContract"
         />
      </service>
   </services>
   <bindings>
      <netTcpBinding>
         <binding name = "TransactionalTCP"
            transactionFlow = "true"
         />
      </netTcpBinding>
   </bindings>
</system.serviceModel>

Wie in Beispiel 1-8 gezeigt, kannst du die benannte Bindungskonfiguration in mehreren Endpunkten wiederverwenden, indem du einfach auf sie verweist.

Standardbindung

WCF ermöglicht es dir, eine Standardbindung zu verwenden, die alle Endpunkte aller Dienste der Anwendung betrifft, die die Konfigurationsdatei verwendet. Eine Standardbindung ist einfach ein namenloser Bindungsabschnitt. Zum Beispiel im Fall von TCP:

<netTcpBinding>
   <binding
      transactionFlow = "true"
   />
</netTcpBinding>

Die Standardbindung konfiguriert implizit alle Endpunkte, die nicht explizit auf eine Bindungskonfiguration verweisen.

Wenn du zum Beispiel eine Standardbindung verwendest, reduziert sich Beispiel 1-8 auf:

<system.serviceModel>
   <services>
      <service name = "MyService">
         <endpoint
            address  = "net.tcp://localhost:8000/MyService"
            binding  = "netTcpBinding"
            contract = "IMyContract"
         />
         <endpoint
            address  = "net.tcp://localhost:8001/MyService"
            binding  = "netTcpBinding"
            contract = "IMyOtherContract"
         />
      </service>
   </services>
   <bindings>
      <netTcpBinding>
         <binding
            transactionFlow = "true"
         />
      </netTcpBinding>
   </bindings>
</system.serviceModel>

Du kannst nur eine Standardkonfiguration pro Bindungstyp haben.

Das Problem mit der Standardbindung ist, dass die Konfigurationsdatei für Menschen schwer zu analysieren und zu verstehen sein kann, wenn du Standardbindungen mit benannten Bindungskonfigurationen kombinierst, wie in Abbildung 1-7 gezeigt.

Named and default binding configuration
Abbildung 1-7. Benannte und standardmäßige Bindungskonfiguration

Obwohl Abbildung 1-8 eine absolut gültige Konfiguration ist, empfehle ich nicht, benannte und Standardbindungen zu mischen. Entweder du benennst alle deine Bindungskonfigurationen oder du verwendest nur die Standardkonfiguration. Ein weiterer Vorteil einer benannten Konfiguration ist, dass du über den Namen der Bindungskonfiguration ein wenig dokumentieren kannst, was diese Konfiguration erreichen soll. Die meisten, wenn nicht sogar alle Bindungskonfigurationen in diesem Buch sind aus genau diesem Grund benannt.

Programmatische Endpunkt-Konfiguration

Die programmatische Endpunktkonfiguration entspricht der administrativen Konfiguration, aber anstatt auf eine Konfigurationsdatei zurückzugreifen, verlässt du dich auf programmatische Aufrufe, um Endpunkte zur ServiceHost Instanz hinzuzufügen. Auch diese Aufrufe liegen immer außerhalb des Dienstcodes. ServiceHost bietet überladene Versionen der Methode AddServiceEndpoint():

public class ServiceHost : ServiceHostBase
{
   public ServiceEndpoint AddServiceEndpoint(Type implementedContract,
                                             Binding binding,
                                             string address);
   //Additional members
}

Du kannst AddServiceEndpoint() Methoden entweder mit relativen oder absoluten Adressen versehen, genau wie bei einer Konfigurationsdatei. Beispiel 1-9 zeigt die programmatische Konfiguration der gleichen Endpunkte wie in Beispiel 1-7.

Beispiel 1-9. Service-seitige programmatische Endpunktkonfiguration
ServiceHost host = new ServiceHost(typeof(MyService));

Binding wsBinding  = new WSHttpBinding();
Binding tcpBinding = new NetTcpBinding();

host.AddServiceEndpoint(typeof(IMyContract),wsBinding,
                        "http://localhost:8000/MyService");
host.AddServiceEndpoint(typeof(IMyContract),tcpBinding,
                        "net.tcp://localhost:8001/MyService");
host.AddServiceEndpoint(typeof(IMyOtherContract),tcpBinding,
                        "net.tcp://localhost:8002/MyService");

host.Open();

Wenn du einen Endpunkt programmatisch hinzufügst, wird die Adresse als String, der Vertrag als Type und die Bindung als eine der Unterklassen der abstrakten Klasse Binding angegeben, wie in:

public class NetTcpBinding : Binding,...
{...}

Wenn du dich auf die Basisadresse des Hosts verlassen willst, gib einen leeren String an, wenn du nur die Basisadresse verwenden willst, oder nur die URI, um die Basisadresse plus diese URI zu verwenden:

Uri tcpBaseAddress = new Uri("net.tcp://localhost:8000/");

ServiceHost host = new ServiceHost(typeof(MyService),tcpBaseAddress);

Binding tcpBinding = new NetTcpBinding();

//Use base address as address
host.AddServiceEndpoint(typeof(IMyContract),tcpBinding,"");
//Add relative address
host.AddServiceEndpoint(typeof(IMyContract),tcpBinding,"MyService");
//Ignore base address
host.AddServiceEndpoint(typeof(IMyContract),tcpBinding,
                        "net.tcp://localhost:8001/MyService");
host.Open();

Wie bei der administrativen Konfiguration über eine Konfigurationsdatei muss der Host eine passende Basisadresse angeben, sonst kommt es zu einer Ausnahme. Was die Möglichkeiten angeht, gibt es eigentlich keinen Unterschied zwischen programmatischer und administrativer Konfiguration. Wenn du eine Konfigurationsdatei verwendest, analysiert die WCF lediglich die Datei und führt die entsprechenden programmatischen Aufrufe an ihrer Stelle aus.

Konfiguration der Bindung

Du kannst die Eigenschaften der verwendeten Bindung programmatisch einstellen. Im Folgenden findest du zum Beispiel den Code, der erforderlich ist, um die Transaktionsweitergabe zu aktivieren (ähnlich wie in Beispiel 1-8):

ServiceHost host = new ServiceHost(typeof(MyService));
NetTcpBinding tcpBinding = new NetTcpBinding();

tcpBinding.TransactionFlow = true;

host.AddServiceEndpoint(typeof(IMyContract),tcpBinding,
                        "net.tcp://localhost:8000/MyService");
host.Open();

Wenn du mit bestimmten Bindungseigenschaften zu tun hast, interagierst du normalerweise mit einer konkreten Bindungsunterklasse wie NetTcpBinding und nicht mit ihrer abstrakten Basisklasse Binding (wie in Beispiel 1-9).

Alle Bindungsklassen bieten auch einen Konstruktor, der z.B. eine Zeichenkette annimmt:

public class NetTcpBinding : Binding,...
{
   public NetTcpBinding(string configurationName);
   //More members
}

Du kannst diesen Konstruktor verwenden, um ein Bindungsobjekt programmatisch zu initialisieren, das auf den Einstellungen in einem bestimmten Bindungsabschnitt in der Konfigurationsdatei basiert. Du kannst auch einen leeren String übergeben, um WCF anzuweisen, die (namenlose) Standardkonfiguration der Bindung zu verwenden. Wenn die Config-Datei keine Standard-Bindungsdefinition enthält, wird eine KeyNotFoundException angezeigt.

Standard-Endpunkte

Wenn der Diensthost keine Endpunkte definiert (weder in der Konfiguration noch programmatisch), aber mindestens eine Basisadresse angibt, fügt WCF dem Dienst standardmäßig Endpunkte hinzu. Diese werden als Standardendpunkte bezeichnet. WCF fügt für jeden Vertrag einen Endpunkt pro Basisadresse hinzu und verwendet die Basisadresse als Adresse des Endpunkts. WCF leitet die Bindung aus dem Schema der Basisadresse ab. Für HTTP verwendet die WCF die Standardbindung. Beachte, dass die Standardbindungen die Standardendpunkte betreffen. WCF benennt den Endpunkt auch durch Verkettung des Bindungsnamens und des Vertragsnamens.

Nehmen wir zum Beispiel die folgende Definition eines Dienstes:

[ServiceContract]
interface IMyContract
{...}

[ServiceContract]
interface IMyOtherContract
{...}

class MyService : IMyContract,IMyOtherContract
{...}

für diesen Hosting-Code:

Uri httpBaseAddress = new Uri("http://localhost:8000/");
Uri tcpBaseAddress  = new Uri("net.tcp://localhost:9000/");
Uri ipcBaseAddress  = new Uri("net.pipe://localhost/");

ServiceHost host = new ServiceHost(typeof(MyService),httpBaseAddress,
                   tcpBaseAddress,ipcBaseAddress);
host.Open();

Unter der Annahme, dass keine Konfigurationsdatei verwendet wird, um zusätzliche Endpunkte zu definieren, fügt WCF diese Endpunkte hinzu, als ob sie in der Konfiguration definiert wären:

<service name = "MyService">
   <endpoint name = "BasicHttpBinding_IMyContract"
      address  = "http://localhost:8000/"
      binding  = "basicHttpBinding"
      contract = "IMyContract"
   />
   <endpoint name = "NetTcpBinding_IMyContract"
      address  = "net.tcp://localhost:9000"
      binding  = "netTcpBinding"
      contract = "IMyContract"
   />
   <endpoint name = "NetNamedPipeBinding_IMyContract"
      address  = "net.pipe://localhost/"
      binding  = "netNamedPipeBinding"
      contract = "IMyContract"
   />

   <endpoint name = "BasicHttpBinding_IMyOtherContract"
      address  = "http://localhost:8000/"
      binding  = "basicHttpBinding"
      contract = "IMyOtherContract"
   />
   <endpoint name = "NetTcpBinding_IMyOtherContract"
      address  = "net.tcp://localhost:9000"
      binding  = "netTcpBinding"
      contract = "IMyOtherContract"
   />
   <endpoint name = "NetNamedPipeBinding_IMyOtherContract"
      address  = "net.pipe://localhost/"
      binding  = "netNamedPipeBinding"
      contract = "IMyOtherContract"
   />
</service>

Beachte, dass WCF die gleiche Adresse mehrmals an verschiedene Endpunkte weiterleitet. Während dies für den Aufruf funktioniert (weil der Host die eingehenden Ports oder Pipes nur einmal überwacht und die Nachricht einfach intern an den richtigen Endpunkt weiterleitet), schlägt diese Konfiguration aufgrund einer internen Einschränkung von WCF bei der Veröffentlichung von Metadaten fehl.

Du kannst die Standardendpunkte auch explizit mit der Methode AddDefaultEndpoints() von ServiceHost hinzufügen:

public class ServiceHost : ...
{
   public void AddDefaultEndpoints();
   //More members
}

Du kannst die Standard-Endpunkte auch dann hinzufügen, wenn du andere Endpunkte konventionell über eine Konfigurationsdatei oder programmatisch hinzugefügt hast. Das Einzige, worauf du achten musst, sind Konflikte mit anderen Endpunkten, die die Basisadresse als ihre Adresse verwenden.

Protokoll-Zuordnung

Bei den Standard-Endpunkten leitet WCF die zu verwendende Bindung aus dem Schema der Basisadresse ab. Diese Herleitung wird als Protokollzuordnung bezeichnet. Im Falle von TCP, IPC und MSMQ gibt es nur eine einzige Zuordnungsoption. Im Falle von HTTP (oder HTTPS) verwendet WCF jedoch standardmäßig die Basisbindung für die Zuordnung. Wenn du dich stattdessen auf die WS-Bindung verlassen möchtest (was du in den meisten Fällen tun solltest), musst du die Standard-Protokollzuordnung im Abschnitt protocolMapping in der Konfigurationsdatei außer Kraft setzen:

<system.serviceModel>
   <protocolMapping>
      <add
         scheme  = "http"
         binding = "wsHttpBinding"
      />
   </protocolMapping>
</system.serviceModel>

Du kannst auch eine bestimmte Bindungskonfiguration angeben, die verwendet werden soll:

<protocolMapping>
   <add
      scheme  = "http"
      binding = "wsHttpBinding"
      bindingConfiguration = "..."
   />
</protocolMapping>

Du musst die Protokollzuordnung administrativ in der Konfigurationsdatei vornehmen. Es gibt keinen entsprechenden programmatischen Weg.

Hinweis

Die Protokollzuordnung ist die einzige Konfigurationsoption, die WCF im Bereich des Dienstmodells anbietet und die keine programmatische Entsprechung hat.

Die Methode Configure()

Vor .NET 4.5 war dein Hosting-Code bei der programmatischen Konfiguration oft an den Hosting-Prozess (oder Windows-Dienst) gekoppelt, mit dem er bereitgestellt wurde. Bei der Verwendung von Self-Hosting musstest du die programmatische Konfiguration durch direkte Interaktion mit einer Service-Host-Instanz vornehmen. Beim Hosting in WAS musstest du eine Service-Host-Factory bereitstellen. Und wenn du deine Konfigurationsstrategie weiterentwickelt hast, um einen zentralen Konfigurationsspeicher zu nutzen, musstest du die gespeicherte Konfiguration deines Hosts mit seiner dateibasierten Konfiguration zusammenführen.

Als Teil der WCF-Vereinfachungsfunktionen, die in .NET 4.5 eingeführt wurden, bietet dir die Methode Configure() einen vom Hosting-Prozess unabhängigen Ansatz zur programmatischen Konfiguration deiner Dienste. Unabhängig von der Hosting-Umgebung kannst du mit der Methode Configure() die Konfiguration für die Endpunkte deines Dienstes direkt in seinem Code festlegen.

Du aktivierst diese Funktion für deinen Dienst, indem du eine öffentliche statische Methode namens Configure() mit der folgenden Signatur in die Implementierung deines Dienstes einfügst:

class MyService : IMyContract
{
   public static void Configure(ServiceConfiguration config)
   {...}

   // More members
}
Warnung

Die Configure() Methodenkonvention ist sehr spezifisch. Wenn du deine Methode Configure() nicht sowohl als public als auch als static markierst, wird sie von WCF nicht erkannt.

Das Argument ServiceConfiguration, das an die Methode Configure() übergeben wird, bietet viele der gleichen Methoden für die programmatische Dienstkonfiguration, die auch die Klasse ServiceHost bietet:

public class ServiceConfiguration
{
   public void AddServiceEndpoint(ServiceEndpoint endpoint);
   public ServiceEndpoint AddServiceEndpoint(Type contractType,
                                             Binding binding,
                                             Uri address);

   public Collection<ServiceEndpoint> EnableProtocol(Binding protocol);

   public void LoadFromConfiguration();
   public void LoadFromConfiguration(Configuration configuration);

   //More members
}

So kannst du die Klasse ServiceConfiguration verwenden, um deinen Dienst mit denselben Techniken zu konfigurieren, die bisher beschrieben wurden. Beispiel 1-10 zeigt die gleiche programmatische Endpunktkonfiguration wie Beispiel 1-9, verwendet aber stattdessen den Parameter ServiceConfiguration der Methode Configure().

Beispiel 1-10. Configure()-Methode zur programmatischen Endpunktkonfiguration
public static void Configure(ServiceConfiguration config)
{
   Binding wsBinding  = new WSHttpBinding();
   Binding tcpBinding = new NetTcpBinding();

   config.AddServiceEndpoint(typeof(IMyContract),wsBinding,
                             "http://localhost:8000/MyService");
   config.AddServiceEndpoint(typeof(IMyContract),tcpBinding,
                             "net.tcp://localhost:8001/MyService");
   config.AddServiceEndpoint(typeof(IMyOtherContract),tcpBinding,
                             "net.tcp://localhost:8002/MyService");
}

Wenn vorhanden, ruft WCF die Methode Configure() deines Dienstes während der Instanziierung des Service-Hosts auf, bevor der Service-Host geöffnet wird. Dies gibt dir die Möglichkeit, die Endpunkte deines Dienstes programmatisch zu konfigurieren oder den Konfigurationsabschnitt deines Dienstes explizit zu lesen, indem du eine der LoadFromConfiguration() Methoden aufrufst, die von der Klasse ServiceConfiguration angeboten werden.

Warnung

Wenn du eine Configure() Methode verwendest, ignoriert WCF den Dienstkonfigurationsabschnitt für deinen Dienst in der App- oder Webkonfigurationsdatei.

Du kannst auch Standardendpunkte zu den verfügbaren Basisadressen hinzufügen, indem du die Methode EnableProtocol() mit einer bestimmten Bindungsinstanz aufrufst. EnableProtocol() fügt dann Standardendpunkte hinzu und verwendet dabei die gleichen Konventionen wie unter "Standardendpunkte" beschrieben . Der folgende EnableProtocol() Aufruf erzeugt zum Beispiel die gleichen Endpunktkonfigurationen für BasicHttpBinding wie die Standardendpunkte, die von WCF für jede HTTP-kompatible Basisadresse hinzugefügt werden:

config.EnableProtocol(new BasicHttpBinding());

Da du die Bindung bereitstellst, kannst du natürlich EnableProtocol() verwenden, um die Standard-Endpunktkonfigurationen, die WCF erzeugt, besser zu kontrollieren. WCF fügt weiterhin Standardendpunkte für jede Basisadresse hinzu, für die du nicht ausdrücklich einen Endpunkt angegeben oder ein Protokoll aktiviert hast. Wenn du den Namen der Bindung, die du bei der Aktivierung eines Protokolls angibst, änderst, erstellst du einen Endpunkt, der nicht mehr den Standardkonventionen von WCF für die Benennung von Endpunkten entspricht.

Warnung

Wenn du ein Protokoll aktivierst, ignoriert WCF die Standardbindung, das Standardendpunktverhalten und alle Protokollzuordnungen, die mit dem von dir angegebenen Bindungstyp zusammenhängen.

Die Methode Configure() bietet dir zwar einen hostunabhängigen Ansatz für die programmatische Konfiguration deiner Dienste, aber sie koppelt die Konfiguration deines Dienstes auch direkt an seine Implementierung und damit an seine Bereitstellung. In einfachen Szenarien mag dieser Ansatz wünschenswert und sogar arbeitssparend sein. Wenn dein serviceorientiertes System jedoch wächst, wirst du feststellen, dass du eine klarere Trennung der Bereiche vornehmen musst.

Die Methode Configure() hat ihren Preis, wenn du mehrere Dienste hast, die sich alle sehr ähnlich, wenn nicht sogar identisch verhalten. Es ist natürlich nicht ratsam, dieselbe programmatische Konfiguration für alle Dienste zu wiederholen. Gleichzeitig ist der Rückgriff auf eine dateibasierte Konfiguration oft nicht wünschenswert und bringt die gleiche Duplizierung und zusätzliche langfristige Wartungskosten in verschiedenen Hosting-Umgebungen mit sich.

Obwohl die Konvention für die Implementierung einer Configure() Methode öffentlich und statisch ist, kannst du die Duplizierung von Konfigurationscode verringern, indem du die Configure() Methode in eine Basisklasse verlegst. So kannst du einen einzigen Konfigurationsausdruck für eine Reihe zusammenhängender Dienste bereitstellen:

class MyServiceBase
{
   public static void Configure(ServiceConfiguration config)
   {...}
}

class MyService : MyServiceBase,IMyContract
{...}

class MyOtherService : MyServiceBase,IMyOtherContract
{...}

Da deine Configure() Methode statisch sein muss, kannst du sie in deiner Basisklasse nicht als virtuell kennzeichnen. Wenn du eine Configure() Methode in einer untergeordneten Klasse implementierst, wird die Implementierung in deiner Basisklasse versteckt. In diesem Fall solltest du alle Anpassungen, die du deinen Unterklassen hinzufügst, als neu markieren und die Implementierung deiner Basisklasse aufrufen:

class MyService : MyServiceBase,IMyContract
{
   public new static void Configure(ServiceConfiguration config)
   {
      MyServiceBase.Configure(config);
      //Optional additional processing
   }
}

class MyOtherService : MyServiceBase,IMyOtherContract
{
   public new static void Configure(ServiceConfiguration config)
   {
      MyServiceBase.Configure(config);
      //Optional additional processing
   }
}

Die Vererbung verringert zwar die Redundanz im Code, bietet aber immer noch nicht die sauberste Trennung zwischen der Implementierung deines Dienstes und seiner Konfiguration.

Du solltest dich immer bemühen, die vielen Facetten deiner Dienste voneinander zu kapseln, um Wiederverwendung, Flexibilität, Testbarkeit, Wartbarkeit und vor allem Agilität zu fördern. Von einfachen Hilfsklassen über ein zentrales Konfigurations-Repository bis hin zu IoC-Ansätzen (Inversion of Control) kann alles dabei helfen, den Code deiner Konfigurationsinfrastruktur von der Implementierung deines Dienstes zu kapseln.

In Verbindung mit diesen Optionen kannst du, wenn sich deine Anforderungen weiterentwickeln, deine Kontrolle über die programmatische Konfiguration sowie viele andere Servicefunktionen weiter ausbauen, indem du ein custom ServiceHost erstellst.

Metadaten-Austausch

Unter werden die Metadaten des Dienstes standardmäßig nicht veröffentlicht. Das schließt jedoch nicht aus, dass Clients, die die Metadaten über einen anderen Mechanismus erhalten haben (z. B. über eine Projektreferenz auf eine Klassenbibliothek, die die Verträge enthält), Operationen auf dem Dienst aufrufen.

Die Veröffentlichung der Metadaten deines Dienstes ist mit erheblichem Aufwand verbunden, da du die CLR-Typen und Bindungsinformationen in WSDL oder eine andere Low-Level-Darstellung umwandeln musst, und all dieser Aufwand bringt keinen geschäftlichen Mehrwert. Glücklicherweise weiß der Host bereits alles, was es über deinen Dienst und seine Endpunkte zu wissen gibt, so dass er die Metadaten für dich veröffentlichen kann, wenn du ihn ausdrücklich dazu aufforderst.

WCF bietet zwei Möglichkeiten, die Metadaten eines Dienstes zu veröffentlichen: Du kannst die Metadaten über HTTP-GET bereitstellen, ein einfaches textbasiertes Protokoll, das von den meisten Plattformen unterstützt wird, oder du kannst einen speziellen Endpunkt verwenden.

Metadaten über HTTP-GET

WCF kann die Metadaten für deinen Dienst automatisch über HTTP-GET bereitstellen; du musst sie nur aktivieren, indem du ein explizites Dienstverhalten hinzufügst. Verhaltensweisen werden in den folgenden Kapiteln ausführlich beschrieben. Für den Moment musst du nur wissen, dass ein Verhalten ein lokaler Aspekt des Dienstes ist, z. B. ob der Host seine Metadaten über HTTP-GET veröffentlichen soll oder nicht. Du kannst dieses Verhalten administrativ oder programmatisch hinzufügen.

Den Metadatenaustausch administrativ ermöglichen

Beispiel 1-11 zeigt eine Host-Anwendungskonfigurationsdatei, in der beide gehosteten Dienste auf einen benutzerdefinierten behavior Abschnitt verweisen, der die Veröffentlichung von Metadaten über HTTP-GET ermöglicht.

Beispiel 1-11. Aktivieren des Metadatenaustauschverhaltens mithilfe einer Konfigurationsdatei
<system.serviceModel>
   <services>
      <service name = "MyService" behaviorConfiguration = "MEXGET">
         <host>
            <baseAddresses>
               <add baseAddress = "http://localhost:8000/"/>
            </baseAddresses>
         </host>
         ...
      </service>
      <service name = "MyOtherService" behaviorConfiguration = "MEXGET">
         <host>
            <baseAddresses>
               <add baseAddress = "http://localhost:8001/"/>
            </baseAddresses>
         </host>
         ...
      </service>
   </services>
   <behaviors>
      <serviceBehaviors>
         <behavior name = "MEXGET">
            <serviceMetadata httpGetEnabled = "true"/>
         </behavior>
      </serviceBehaviors>
   </behaviors>
</system.serviceModel>

Die Adresse, die die Clients für HTTP-GET verwenden müssen, ist standardmäßig die registrierte HTTP-Basisadresse des Dienstes. Wenn der Host nicht mit einer HTTP-Basisadresse konfiguriert ist, wird beim Laden des Dienstes eine Ausnahme ausgelöst. Du kannst auch eine andere Adresse (oder nur eine URI, die an die HTTP-Basisadresse angehängt wird) angeben, unter der die Metadaten veröffentlicht werden sollen, indem du die Eigenschaft httpGetUrl des serviceMetadata -Tags setzt:

<behavior name = "MEXGET">
   <serviceMetadata httpGetEnabled = "true" httpGetUrl = "MyMEXAddress"/>
</behavior>

Sobald du den Metadatenaustausch über HTTP-GET aktiviert hast, kannst du mit einem Browser zu der von dir konfigurierten Adresse (standardmäßig die HTTP-Basisadresse oder eine explizite Adresse) navigieren. Wenn alles in Ordnung ist, erhältst du eine Bestätigungsseite wie in Abbildung 1-8, die dir mitteilt, dass du einen Dienst erfolgreich gehostet hast. Die Bestätigungsseite hat nichts mit dem IIS-Hosting zu tun und du kannst einen Browser verwenden, um zur Adresse des Dienstes zu navigieren, auch wenn du selbst hostest.

A service confirmation page
Abbildung 1-8. Eine Seite zur Dienstbestätigung

Den Austausch von Metadaten programmatisch ermöglichen

Um den Austausch von Metadaten über HTTP-GET programmatisch zu ermöglichen, musst du zunächst das Verhalten zu der Sammlung von Verhaltensweisen hinzufügen, die der Host für den Diensttyp unterhält. Die Klasse ServiceHostBase bietet die Eigenschaft Description des Typs ServiceDescription:

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

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

public class KeyedByTypeCollection<T> : KeyedCollection<Type,T>
{
   public U Find<U>();
   public U Remove<U>();
   //More members
}
public class ServiceDescription
{
   public KeyedByTypeCollection<IServiceBehavior> Behaviors
   {get;}

   //More members
}

IServiceBehavior ist die Schnittstelle, die alle Verhaltensklassen und Attribute implementieren. KeyedByTypeCollection<T> bietet die generische Methode Find<U>(), die das angeforderte Verhalten zurückgibt, wenn es in der Sammlung enthalten ist, und andernfalls null. Ein bestimmter Verhaltenstyp kann höchstens einmal in der Sammlung gefunden werden.

Beispiel 1-12 zeigt, wie du das Verhalten beim Austausch von Metadaten programmatisch aktivieren kannst.

Beispiel 1-12. Das Verhalten des Metadatenaustauschs programmatisch aktivieren
ServiceHost host = new ServiceHost(typeof(MyService));

ServiceMetadataBehavior metadataBehavior;
metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
if(metadataBehavior == null)
{
   Debug.Assert(host.BaseAddresses.Any(baseAddress=>baseAddress.Uri.Scheme ==
                                                                         "http"));

   metadataBehavior = new ServiceMetadataBehavior();
   metadataBehavior.HttpGetEnabled = true;
   host.Description.Behaviors.Add(metadataBehavior);
}

host.Open();

Beachte die defensive Art und Weise, mit der der Hosting-Code zunächst überprüft, dass kein Metadatenverhalten in der Konfigurationsdatei angegeben wurde, indem er die Methode Find<T>() von KeyedByTypeCollection<I> aufruft und ServiceMetadataBehavior als Typparameter verwendet. ServiceMetadataBehavior ist im Namensraum System.ServiceModel.Description definiert:

public class ServiceMetadataBehavior : IServiceBehavior
{
   public bool HttpGetEnabled
   {get;set;}

   public Uri HttpGetUrl
   {get;set;}
   //More members
}

Wenn das zurückgegebene Verhalten null ist, bedeutet das, dass die Konfigurationsdatei kein Metadatenverhalten enthält. In diesem Fall erstellt der Hosting-Code eine neue ServiceMetadataBehavior Instanz, setzt HttpGetEnabled auf true und fügt es zu den Verhaltensweisen in der Servicebeschreibung hinzu. Indem er zunächst defensiv das Vorhandensein des Verhaltens prüft, vermeidet der Hosting-Code, dass die Konfigurationsdatei überschrieben wird und der Administrator immer die Möglichkeit hat, das Verhalten zu ändern oder es ein- oder auszuschalten. Beachte auch, dass der Code das Vorhandensein einer HTTP-Basisadresse bestätigt.

Der Endpunkt für den Metadatenaustausch

Die Veröffentlichung von Metadaten über HTTP-GET ist lediglich eine WCF-Funktion; es gibt keine Garantie dafür, dass andere Plattformen, mit denen du interagierst, dies unterstützen. Es gibt jedoch eine Standardmethode zur Veröffentlichung von Metadaten über einen speziellen Endpunkt, den Metadatenaustausch-Endpunkt (manchmal auch MEX-Endpunkt genannt). Abbildung 1-9 zeigt einen Dienst mit Geschäftsendpunkten und einem Endpunkt für den Austausch von Metadaten. In der Regel wird der Metadatenaustausch-Endpunkt jedoch nicht in den Entwurfsdiagrammen dargestellt.

The metadata exchange endpoint
Abbildung 1-9. Der Endpunkt für den Metadatenaustausch

Der MEX-Endpunkt unterstützt einen Industriestandard für den Austausch von Metadaten, der in der WCF durch die Schnittstelle IMetadataExchange repräsentiert wird:

[ServiceContract(...)]
public interface IMetadataExchange
{
   [OperationContract(...)]
   Message Get(Message request);
   //More members
}

Die Details dieser Schnittstelle sind unerheblich. Wie die meisten dieser Industriestandards ist sie schwer zu implementieren, aber glücklicherweise kann WCF den Diensthost automatisch die Implementierung von IMetadataExchange bereitstellen und den Endpunkt für den Austausch von Metadaten offenlegen. Alles, was du tun musst, ist, die Adresse und die zu verwendende Bindung festzulegen und das Verhalten der Dienstmetadaten hinzuzufügen. Für die Bindungen bietet WCF dedizierte Bindungs-Transportelemente für die Protokolle HTTP, HTTPS, TCP und IPC. Für die Adresse kannst du eine vollständige Adresse angeben oder eine der registrierten Basisadressen verwenden. Es ist nicht notwendig, die Option HTTP-GET zu aktivieren, aber es kann nicht schaden, dies zu tun. Beispiel 1-13 zeigt einen Dienst, der drei MEX-Endpunkte über HTTP, TCP und IPC zur Verfügung stellt. Zu Demonstrationszwecken verwenden die TCP- und IPC-MEX-Endpunkte relative Adressen und der HTTP-Endpunkt eine absolute Adresse.

Beispiel 1-13. Hinzufügen von MEX-Endpunkten
<services>
   <service name = "MyService" behaviorConfiguration = "MEX">
      <host>
         <baseAddresses>
            <add baseAddress = "net.tcp://localhost:8001/"/>
            <add baseAddress = "net.pipe://localhost/"/>
         </baseAddresses>
      </host>
      <endpoint
         address  = "MEX"
         binding  = "mexTcpBinding"
         contract = "IMetadataExchange"
      />
      <endpoint
         address  = "MEX"
         binding  = "mexNamedPipeBinding"
         contract = "IMetadataExchange"
      />
      <endpoint
         address  = "http://localhost:8000/MEX"
         binding  = "mexHttpBinding"
         contract = "IMetadataExchange"
      />
      ...
   </service>
</services>
<behaviors>
   <serviceBehaviors>
      <behavior name = "MEX">
         <serviceMetadata/>
      </behavior>
   </serviceBehaviors>
</behaviors>
Hinweis

In Beispiel 1-13 musst du nur das Tag serviceMetadata in das Verhalten einfügen, damit der Host den MEX-Endpunkt für deinen Dienst implementiert. Wenn du das Verhalten nicht referenzierst, erwartet der Host, dass dein Service IMetadataExchange implementiert. Obwohl dies normalerweise keinen zusätzlichen Nutzen bringt, ist es die einzige Möglichkeit, die Implementierung von IMetadataExchange für erweiterte Interoperabilitätsanforderungen vorzusehen.

Standard-Endpunkte

In den allermeisten Fällen besteht ein MEX-Endpunkt immer aus denselben drei Elementen: Der Vertrag ist immer IMetadataExchange, die Bindung ist immer das reservierte Bindungselement und die einzige Variable ist die Adresse (und selbst das ist normalerweise nur die Basisadresse). Es ist übertrieben, dass die Entwickler diese Endpunktelemente immer wieder neu festlegen müssen. Um diese und ähnliche Infrastruktur-Endpunkte zu vereinfachen, bietet WCF vorgefertigte Definitionen für verschiedene Endpunkttypen, die so genannten Standard-Endpunkte. WCF bietet Standardendpunkte für Metadatenaustausch, Discovery, Ankündigungen, Workflow und Web. Du kannst die Standardendpunkte sowohl in der Konfiguration als auch programmatisch verwenden.

Du kannst den gewünschten Standardendpunkt mit dem kind Tag referenzieren:

<endpoint
   kind = "..."
/>

Was auch immer nicht angegeben wird (normalerweise die Adresse oder die Bindung), wird immer auf einen vordefinierten Wert gesetzt, der von den anderen Feldern des Endpunkts abhängt. In Anhang C werden die Standard-Discovery- und Ankündigungsendpunkte verwendet. Im Rahmen dieses Abschnitts kannst du den kind-Wert von mexEndpoint verwenden, um den MEX-Endpunkt zu definieren.

Nehmen wir zum Beispiel an, du gibst keine Adresse und keine Bindung an, so wie hier:

<service ...
   <host>
      <baseAddresses>
         <add baseAddress = "http://..."/>
         <add baseAddress = "net.tcp://..."/>
      </baseAddresses>
   </host>

   <endpoint
      kind = "mexEndpoint"
   />
   ...
</service>

WCF fügt einen MEX-Endpunkt hinzu, dessen Adresse die HTTP-Basisadresse ist. Dies setzt voraus, dass eine HTTP-Basisadresse vorhanden ist und dass kein anderer Endpunkt die Basisadresse für seine Adresse verwendet.

Du kannst auch eine URI an die Basisadresse anhängen:

<endpoint
   kind = "mexEndpoint"
   address = "MEX"
/>

Wenn du die Bindung angibst, leitet WCF die richtige Basisadresse ab, die zum Beispiel aus dem Bindungstyp abgeleitet wird:

<service ...
   <host>
      <baseAddresses>
         <add baseAddress = "http://..."/>
         <add baseAddress = "net.tcp://..."/>
      </baseAddresses>
   </host>

   <endpoint
      kind = "mexEndpoint"
      binding = "mexTcpBinding"
   />
   <endpoint
      kind = "mexEndpoint"
      address = "MEX"
      binding = "mexTcpBinding"
   />
   ...
</service>

Unabhängig von der Basisadresse kannst du auch eine voll qualifizierte Adresse angeben.

Beachte, dass WCF nicht intelligent genug ist, um die zu verwendende Bindung aus dem Adressschema abzuleiten, was bedeutet, dass die folgende Konfiguration ungültig ist:

<!-- Invalid configuration -->
<endpoint
   kind = "mexEndpoint"
   address = "net.tcp://..."
/>

MEX-Endpunkte programmatisch hinzufügen

Wie jeder andere Endpunkt kannst du einen Metadatenaustausch-Endpunkt nur programmatisch hinzufügen, bevor du den Host öffnest. WCF bietet keinen eigenen Bindungstyp für den Metadatenaustausch-Endpunkt. Stattdessen musst du eine benutzerdefinierte Bindung erstellen, die das passende Transportbindungselement verwendet, und dieses Bindungselement als Konstruktionsparameter für eine Instanz einer benutzerdefinierten Bindung bereitstellen. Um diesen Prozess zu vereinfachen, kannst du die statische Hilfsklasse MetadataExchangeBindings verwenden, die wie folgt definiert ist:

public static class MetadataExchangeBindings
{
   public static Binding CreateMexHttpBinding();
   public static Binding CreateMexNamedPipeBinding();
   public static Binding CreateMexTcpBinding();

   //More members
}

Zum Schluss rufst du die Methode AddServiceEndpoint() des Hosts auf und übermittelst ihm die Adresse, die MEX-Bindung und den Vertragstyp IMetadataExchange. Beispiel 1-14 zeigt den Code, der erforderlich ist, um einen MEX-Endpunkt über TCP hinzuzufügen. Beachte, dass du vor dem Hinzufügen des Endpunkts das Vorhandensein des Metadatenverhaltens überprüfen musst.

Beispiel 1-14. Programmgesteuertes Hinzufügen eines TCP MEX-Endpunkts
Uri tcpBaseAddress = new Uri("net.tcp://localhost:9000/");
ServiceHost host = new ServiceHost(typeof(MyService),tcpBaseAddress);

ServiceMetadataBehavior metadataBehavior;
metadataBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
if(metadataBehavior == null)
{
   metadataBehavior = new ServiceMetadataBehavior();
   host.Description.Behaviors.Add(metadataBehavior);
}
Binding binding = MetadataExchangeBindings.CreateMexTcpBinding();
host.AddServiceEndpoint(typeof(IMetadataExchange),binding,"MEX");
host.Open();

Du kannst auch einen MEX-Endpunkt hinzufügen, indem du den Standard-MEX-Endpunkt verwendest. Verwende dazu den Typ ServiceMetadataEndpoint, definiert als:

public class ServiceMetadataEndpoint : ServiceEndpoint
{
   public ServiceMetadataEndpoint();
   public ServiceMetadataEndpoint(EndpointAddress address);
   public ServiceMetadataEndpoint(Binding binding,EndpointAddress address);
}

Der Standardkonstruktor von ServiceMetadataEndpoint verwendet standardmäßig die HTTP-Basisadresse und die Bindung. Der Konstruktor, der eine Endpunktadresse annimmt, muss eine voll qualifizierte HTTP- oder HTTPS-Adresse erhalten:

ServiceHost host = new ServiceHost(typeof(MyService));
host.Description.Behaviors.Add(new ServiceMetadataBehavior());

EndpointAddress address = new EndpointAddress("http://localhost:8000/MEX");

ServiceEndpoint endpoint = new ServiceMetadataEndpoint(address);
host.AddServiceEndpoint(endpoint);
...
host.Open();

Außerdem wird ServiceMetadataEndpoint niemals die Basisadressen der Hosts verwenden.

Rationalisierung mit ServiceHost<T>

Du kannst ServiceHost<T> erweitern, um den Code in den Beispielen 1-12 und 1-14 zu automatisieren. ServiceHost<T> bietet die Methode EnableMetadataExchange(), die du aufrufen kannst, um sowohl Metadaten über HTTP-GET zu veröffentlichen als auch die MEX-Endpunkte hinzuzufügen:

public class ServiceHost<T> : ServiceHost
{
   public void EnableMetadataExchange(bool enableHttpGet = true);

   public bool HasMexEndpoint
   {get;}
   public void AddAllMexEndPoints();
   //More members
}

Die Standardeinstellung EnableMetadataExchange() veröffentlicht Metadaten über HTTP-GET. Wenn kein MEX-Endpunkt verfügbar ist, fügt EnableMetadataExchange() einen MEX-Endpunkt für jedes registrierte Basisadressschema hinzu. Mit ServiceHost<T> werden 1-12 und 1-14 auf reduziert:

ServiceHost<MyService> host = new ServiceHost<MyService>();
host.EnableMetadataExchange();
host.Open();

EnableMetadataExchange() setzt das Verhalten in der Konfigurationsdatei nicht außer Kraft, wenn eine solche vorhanden ist.

ServiceHost<T> bietet die Eigenschaft HasMexEndpoint Boolean , die true zurückgibt, wenn der Dienst einen MEX-Endpunkt hat (unabhängig vom Transportprotokoll), und die Methode AddAllMexEndPoints(), die einen MEX-Endpunkt für jede registrierte Basisadresse des Schematyps HTTP, TCP oder IPC hinzufügt. Beispiel 1-15 zeigt die Implementierung dieser Methoden.

Beispiel 1-15. EnableMetadataExchange und seine unterstützenden Methoden implementieren
public class ServiceHost<T> : ServiceHost
{
   public void EnableMetadataExchange(bool enableHttpGet = true)
   {
      if(State == CommunicationState.Opened)
      {
         throw new InvalidOperationException("Host is already opened");
      }
      ServiceMetadataBehavior metadataBehavior
                          = Description.Behaviors.Find<ServiceMetadataBehavior>();

      if(metadataBehavior == null)
      {
         metadataBehavior = new ServiceMetadataBehavior();
         Description.Behaviors.Add(metadataBehavior);

         if(BaseAddresses.Any(uri=>uri.Scheme == "http"))
         {
            metadataBehavior.HttpGetEnabled = enableHttpGet;
         }
      }
      AddAllMexEndPoints();
   }
   public bool HasMexEndpoint
   {
      get
      {
         return Description.Endpoints.Any(
                                       endpoint=>endpoint.Contract.ContractType ==
                                       typeof(IMetadataExchange));
      }
   }
   public void AddAllMexEndPoints()
   {
      Debug.Assert(HasMexEndpoint == false);

      foreach(Uri baseAddress in BaseAddresses)
      {
         Binding binding = null;

         switch(baseAddress.Scheme)
         {
            case "net.tcp":
            {
               binding = MetadataExchangeBindings.CreateMexTcpBinding();
               break;
            }
            case "net.pipe":
            {...}
            case "http":
            {...}
            case "https":
            {...}
         }
         if(binding != null)
         {
            AddServiceEndpoint(typeof(IMetadataExchange),binding,"MEX");
         }
      }
   }
}

EnableMetadataExchange() überprüft mit Hilfe der Eigenschaft State der Basisklasse CommunicationObject, ob der Host noch nicht geöffnet wurde. Die Eigenschaft HasMexEndpoint prüft, ob der Vertrag eines bestimmten Endpunkts tatsächlich IMetadataExchange ist. Any() ruft den Ausdruck auf den Endpunkten in der Sammlung auf und gibt true zurück, wenn einer der Endpunkte in der Sammlung das Prädikat erfüllt (d.h. wenn der Aufruf der Lambda-Ausdruck-Methode true zurückgegeben hat) und false andernfalls. Die Methode AddAllMexEndPoints() iteriert über die Sammlung BaseAddresses. Für jede gefundene Basisadresse erstellt sie eine passende MEX-Bindung und fügt den MEX-Endpunkt mit einer MEX-URI unter der Basisadresse hinzu.

Der Metadaten-Explorer

Der Endpunkt für den Metadatenaustausch liefert Metadaten, die nicht nur Verträge und Operationen beschreiben, sondern auch Informationen über Datenverträge, Sicherheit, Transaktionen, Zuverlässigkeit und Fehler. Um die Metadaten eines laufenden Dienstes zu visualisieren, habe ich das Tool Metadata Explorer entwickelt, das zusammen mit dem restlichen Quellcode dieses Buches verfügbar ist. Abbildung 1-10 zeigt den Metadata Explorer mit den Endpunkten aus Beispiel 1-7. Um den Metadata Explorer zu verwenden, gibst du ihm einfach die HTTP-GET-Adresse oder den Endpunkt für den Austausch von Metadaten des laufenden Dienstes, und er zeigt die zurückgegebenen Metadaten an.

The Metadata Explorer
Abbildung 1-10. Der Metadaten-Explorer

Mehr zur Verhaltenskonfiguration

Das im vorigen Abschnitt gezeigte Verhalten für die Dienstmetadaten ist nur eines von vielen solchen gebrauchsfertigen Verhaltensweisen, und du wirst in den folgenden Kapiteln viele Beispiele sehen. Du kannst Verhaltensweisen auf der Dienstebene (wie das Metadatenverhalten) oder auf der Endpunktebene konfigurieren:

<services>
   <service name = "MyService" behaviorConfiguration = "MyServiceBehavior">
      <endpoint behaviorConfiguration = "MyEndpointBehavior"
         ...
      />
   </service>
</services>
<behaviors>
   <endpointBehaviors>
      <behavior name = "MyEndpointBehavior">
         ...
      </behavior>
   </endpointBehaviors>
   <serviceBehaviors>
      <behavior name = "MyServiceBehavior">
         ...
      </behavior>
   </serviceBehaviors>
</behaviors>

Ähnlich wie bei den standardmäßigen Bindungen gibt es in der WCF das Konzept des Standardverhaltens. Ein Standardverhalten ist ein namenloses Verhalten (entweder auf Dienst- oder Endpunktebene), das implizit alle Dienste oder Endpunkte betrifft, die nicht explizit auf eine Verhaltenskonfiguration verweisen. Betrachten wir zum Beispiel die Dienste MyService1, MyService2 und MyService3. Um den Austausch von Dienstmetadaten für alle Dienste in der Anwendung hinzuzufügen, kannst du diese Konfigurationsdatei verwenden:

<services>
   <service name = "MyService1">
      ...
   </service>
   <service name = "MyService2">
      ...
   </service>
</services>
<behaviors>
   <serviceBehaviors>
      <behavior>
         <serviceMetadata/>
      </behavior>
   </serviceBehaviors>
</behaviors>

Zusammen mit diesem Hosting-Code:

ServiceHost host1 = new ServiceHost(typeof(MyService1));
ServiceHost host2 = new ServiceHost(typeof(MyService2));
ServiceHost host3 = new ServiceHost(typeof(MyService3));
host3.AddServiceEndpoint(...);

host1.Open();
host2.Open();
host3.Open();

Beachte, dass sich das Standardverhalten auf alle Dienste in der Anwendung auswirkt, die nicht auf ein Verhalten verweisen, auch auf solche (wie MyService3), die überhaupt nicht auf die Konfigurationsdatei angewiesen sind.

Du kannst höchstens ein Standard-Serviceverhalten und ein Standard-Endpunktverhalten haben.

Wie bei den Standard-Bindings besteht auch bei den Standard-Verhaltensweisen das Problem, dass eine Konfigurationsdatei für Menschen schwer zu analysieren und zu verstehen ist, wenn du Standard-Verhaltensweisen mit benannten Verhaltensweisen kombinierst, wie in Abbildung 1-11 gezeigt.

Named and default behavior configuration
Abbildung 1-11. Benannte und standardmäßige Verhaltenskonfiguration

Aufgrund dieser Schwierigkeit und des Nebeneffekts, dass auch Dienste betroffen sind, die gar nicht auf die Konfigurationsdatei angewiesen sind, empfehle ich, bei der Verwendung eines Standardverhaltens Vorsicht walten zu lassen. Verwende es nur, wenn du alle Dienste in der Anwendung beeinflussen willst. Kombiniere sie nie mit benannten Verhaltensweisen, denn jeder Dienst, der eine benannte Verhaltensweise verwendet, ist von der Standardverhaltensweise ausgenommen. Im Interesse der Lesbarkeit sind die meisten, wenn nicht sogar alle Verhaltensweisen in diesem Buch explizit benannt, außer in den seltenen Fällen, in denen ein Standardverhalten erforderlich ist.

Client-seitige Programmierung

Um einen Dienst aufzurufen, muss ein Client zunächst den Dienstvertrag in die native Darstellung des Clients importieren. Wenn der Kunde WCF verwendet, ist die übliche Methode zum Aufrufen von Operationen die Verwendung eines Proxys. Der Proxy ist eine CLR-Klasse, die eine einzige CLR-Schnittstelle zur Verfügung stellt, die den Dienstvertrag repräsentiert. Wenn der Dienst mehrere Verträge (über mindestens ebenso viele Endpunkte) unterstützt, benötigt der Kunde einen Proxy pro Vertragstyp. Der Proxy bietet die gleichen Operationen wie der Vertrag des Dienstes, verfügt aber zusätzlich über Methoden zur Verwaltung des Lebenszyklus des Proxys und der Verbindung zum Dienst. Der Proxy kapselt jeden Aspekt des Dienstes vollständig ein: seinen Standort, seine Implementierungstechnologie und Laufzeitplattform sowie den Kommunikationstransport.

Generierung der Vollmacht

Du kannst Visual Studio verwenden, um die Metadaten des Dienstes zu importieren und einen Proxy zu erstellen. Wenn der Dienst außerhalb der Projektmappe gehostet wird, starte ihn zunächst und wähle dann die Option Dienstreferenz hinzufügen aus dem Kontextmenü des Client-Projekts. Wenn der Dienst in derselben Projektmappe selbst gehostet wird, starte ihn zunächst ohne den Debugger und wähle dann die Option Dienstreferenz hinzufügen aus dem Kontextmenü.

Wenn der Dienst im IIS oder WAS gehostet wird, ist es nicht notwendig, den Dienst vorab zu starten. Wähle einfach Dienstreferenz hinzufügen aus dem Kontextmenü des Client-Projekts und Visual Studio öffnet den Dialog Dienstreferenz hinzufügen, wie in Abbildung 1-12 dargestellt.

Hinweis

Damit die Option Dienstreferenz hinzufügen im Kontextmenü eines Projekts erscheint, muss das Projekt für .NET Framework 3.0 oder höher konfiguriert sein.

Im Dialogfeld Dienstreferenz hinzufügen gibst du die Adresse der Dienstmetadaten an (nicht die Dienst-URL, wie im Dialogfeld angegeben) und klickst auf Los, um die verfügbaren Dienstendpunkte anzuzeigen (nicht Dienste, wie angegeben). Gib einen Namensraum an (z. B. MyService), der den generierten Proxy enthalten soll, und klicke dann auf OK, um den Proxy zu generieren und die Konfigurationsdatei zu aktualisieren. Benutze die Schaltfläche Entdecken, um WCF-Dienste in deiner eigenen Lösung zu entdecken, sofern sie entweder in einem Website-Projekt oder in einem der Projekttypen der WCF-Dienstbibliothek gehostet werden. Im Falle eines Website-Projekts ruft Visual Studio entweder die Metadaten vom IIS ab oder startet IIS Express. Im Falle einer WCF-Servicebibliothek startet WCF automatisch seinen Host (WcfSvcHost, beschrieben in "Der WCF-Provided Test Host"), um die Metadaten zu erhalten.

Generating a proxy using Visual Studio
Abbildung 1-12. Erzeugen eines Proxys mit Visual Studio

Klicke auf die Schaltfläche Erweitert, um das Dialogfeld Service-Referenzeinstellungen aufzurufen, in dem du die Optionen für die Proxy-Generierung anpassen kannst (siehe Abbildung 1-13).

Advanced options for the service reference
Abbildung 1-13. Erweiterte Optionen für die Dienstreferenz

Mit den intuitiveren Optionen kannst du die Sichtbarkeit des generierten Proxys und der Verträge konfigurieren (öffentlich oder intern) und du kannst Nachrichtenverträge für deine Datentypen für erweiterte Interoperabilitätsszenarien generieren, bei denen du ein bestehendes (typischerweise benutzerdefiniertes) Nachrichtenformat einhalten musst. Du kannst auch auf die Schaltfläche Webreferenz hinzufügen klicken, um die Referenz in eine alte ASMX-Webdienstreferenz umzuwandeln, solange der Dienst die Basisbindung verwendet.

Sobald du eine Referenz hinzugefügt hast, enthält dein Projekt einen neuen Ordner mit dem Namen Dienstreferenzen. Darin findest du für jeden referenzierten Dienst ein Dienstreferenz-Element (siehe Abbildung 1-14).

Du kannst jederzeit mit der rechten Maustaste auf eine Referenz klicken und Dienstreferenz aktualisieren wählen, um den Proxy neu zu generieren. Das ist möglich, weil jede Dienstreferenz auch eine Datei enthält, in der die ursprünglich verwendete Metadatenadresse gespeichert ist. Du kannst auch Service-Referenz konfigurieren wählen, um ein Dialogfeld aufzurufen, das dem Dialogfeld für erweiterte Einstellungen ähnelt, das beim Hinzufügen einer Referenz verwendet wird. Im Dialogfeld "Dienstreferenz konfigurieren" kannst du die Metadatenadresse des Dienstes sowie die übrigen erweiterten Proxy-Einstellungen ändern.

The Service References folder
Abbildung 1-14. Der Ordner Service-Referenzen

Erzeugen des Proxys mit SvcUtil

Als Alternative zu Visual Studio kannst du das Befehlszeilenprogramm SvcUtil.exe verwenden, um die Metadaten des Dienstes zu importieren und einen Proxy zu erstellen. Du musst SvcUtil die Adresse für den Austausch der Metadaten und optional den Dateinamen des Proxys mitteilen. Der Standarddateiname des Proxys ist output.cs, aber du kannst mit dem Schalter /out auch einen anderen Namen angeben.

Wenn du zum Beispiel den Dienst MyService im WAS hostest und die Veröffentlichung von Metadaten über HTTP-GET aktiviert hast, kannst du einfach diese Befehlszeile ausführen:

SvcUtil http://localhost/MyService/MyService.svc /out:Proxy.cs

Nehmen wir an, der selbst gehostete Dienst hat die Veröffentlichung von Metadaten über HTTP-GET unter der Adresse

http://localhost:8000/

und hat MEX-Endpunkte mit diesen Adressen veröffentlicht:

http://localhost:8000/MEX
http://localhost:8001/MEX
net.tcp://localhost:8002/MEX
net.pipe://localhost/MyPipe/MEX

Nachdem du den Host gestartet hast, kannst du die folgenden Befehle verwenden, um den Proxy zu erstellen:

SvcUtil http://localhost:8000            /out:Proxy.cs
SvcUtil http://localhost:8000/MEX        /out:Proxy.cs
SvcUtil http://localhost:8001/MEX        /out:Proxy.cs
SvcUtil net.tcp://localhost:8002/MEX     /out:Proxy.cs
SvcUtil net.pipe://localhost/MyPipe/MEX  /out:Proxy.cs
Hinweis

Der Hauptvorteil der Verwendung von SvcUtil gegenüber Visual Studio ist, dass du die Befehlszeile für die Erstellung des Proxys als Pre-Build-Ereignis einfügen kannst.

SvcUtil bietet zahlreiche Befehlszeilenschalter, die den Optionen im Dialogfeld "Erweiterte Einstellungen" von Visual Studio entsprechen, das in Abbildung 1-14 dargestellt ist.

Unabhängig davon, ob du Visual Studio oder SvcUtil zum Generieren des Proxys verwendest, zeigt Beispiel 1-16 den importierten Vertrag und das generierte Proxy für diese Dienstdefinition:

[ServiceContract(Namespace = "MyNamespace")]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}
class MyService : IMyContract
{
   public void MyMethod()
   {...}
}

Du kannst viele der von den Werkzeugen erzeugten Gunk-Attribute, die lediglich die Standardeinstellungen wiedergeben, sicher entfernen, so dass du am Ende den in Beispiel 1-16 gezeigten bereinigten Proxy erhältst.

Beispiel 1-16. Client-Proxy-Datei
[ServiceContract(Namespace = "MyNamespace")]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}

class MyContractClient : ClientBase<IMyContract>,IMyContract
{
   public MyContractClient()
   {}
   public MyContractClient(string endpointName) : base(endpointName)
   {}
   public MyContractClient(Binding binding,EndpointAddress remoteAddress) :
                                                       base(binding,remoteAddress)
   {}

   /* Additional constructors */

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

Du kannst SvcUtil auch anweisen, auf die Erstellung des Proxys zu verzichten und nur die Dienst- und Datenverträge für deinen Dienst zu erstellen, indem du den Schalter /servicecontract angibst.

Der auffälligste Aspekt der Proxy-Klasse ist, dass sie keinen Verweis auf die Klasse hat, die den Dienst implementiert, sondern nur auf den Vertrag, den der Dienst offenlegt. Du kannst den Proxy in Verbindung mit einer clientseitigen Konfigurationsdatei verwenden, die die Adresse und die Bindung enthält, oder du kannst ihn ohne Konfigurationsdatei verwenden. Beachte, dass jede Proxy-Instanz auf genau einen Endpunkt zeigt. Der Endpunkt, mit dem interagiert werden soll, wird dem Proxy zur Erstellungszeit mitgeteilt. Wenn der service-seitige Vertrag keinen Namespace angibt, wird standardmäßig an den http://tempuri.org Namespace übergeben.

Administrative Client-Konfiguration

Der Client muss wissen, wo sich der Dienst befindet, er muss die gleiche Bindung wie der Dienst verwenden und natürlich die Definition des Dienstvertrags importieren. Im Grunde sind das genau dieselben Informationen, die im Endpunkt des Dienstes erfasst sind. Deshalb kann die Konfigurationsdatei des Clients Informationen über die Zielendpunkte enthalten und sogar das gleiche Endpunkt-Konfigurationsschema wie der Host verwenden.

Beispiel 1-17 zeigt die Client-Konfigurationsdatei, die benötigt wird, um mit einem Dienst zu interagieren, dessen Host wie in Beispiel 1-6 konfiguriert ist.

Beispiel 1-17. Client-Konfigurationsdatei
<system.serviceModel>
   <client>
      <endpoint name = "MyEndpoint"
         address  = "http://localhost:8000/MyService"
         binding  = "wsHttpBinding"
         contract = "IMyContract"
      />
   </client>
</system.serviceModel>

Die Client-Konfigurationsdatei kann so viele Endpunkte auflisten, wie die Dienste, mit denen sie zu tun hat, unterstützen, und der Client kann jeden von ihnen verwenden. Beispiel 1-18 zeigt die Client-Konfigurationsdatei, die mit der Host-Konfigurationsdatei aus Beispiel 1-7 übereinstimmt. Es gibt keine Beziehung zwischen den verschiedenen Endpunkten in der Konfigurationsdatei des Clients: Sie können alle auf denselben Endpunkt des Dienstes, auf verschiedene Endpunkte des Dienstes, auf verschiedene Endpunkte verschiedener Dienste oder auf eine beliebige Kombination dazwischen verweisen. Beachte, dass du auf der Client-Seite die Endpunkte normalerweise mit eindeutigen Namen benennst (du wirst gleich sehen, warum). Die Benennung der Endpunkte auf der Client-Seite ist optional, genauso wie auf der Service-Seite, doch auf der Service-Seite benennst du die Endpunkte normalerweise nicht, während du sie auf der Client-Seite normalerweise benennst.

Beispiel 1-18. Client-Konfigurationsdatei mit mehreren Ziel-Endpunkten
<system.serviceModel>
   <client>
      <endpoint name = "FirstEndpoint"
         address  = "http://localhost:8000/MyService"
         binding  = "wsHttpBinding"
         contract = "IMyContract"
      />
      <endpoint name = "SecondEndpoint"
         address  = "net.tcp://localhost:8001/MyService"
         binding  = "netTcpBinding"
         contract = "IMyContract"
      />
      <endpoint name = "ThirdEndpoint"
         address  = "net.tcp://localhost:8002/MyService"
         binding  = "netTcpBinding"
         contract = "IMyOtherContract"
      />
   </client>
</system.serviceModel>

Konfiguration der Bindung

Du kannst die clientseitigen Bindungen so anpassen, dass sie mit der Dienstbindung identisch sind, wie in Beispiel 1-19 gezeigt.

Beispiel 1-19. Client-seitige Bindungskonfiguration
<system.serviceModel>
   <client>
      <endpoint name = "MyEndpoint"
         address  = "net.tcp://localhost:8000/MyService"
         bindingConfiguration = "TransactionalTCP"
         binding  = "netTcpBinding"
         contract = "IMyContract"
      />
   </client>
   <bindings>
      <netTcpBinding>
         <binding name = "TransactionalTCP"
            transactionFlow = "true"
         />
      </netTcpBinding>
   </bindings>
</system.serviceModel>

Der Client kann sich auf eine Standardkonfiguration für die Bindung verlassen, genau wie der Dienst. Wenn die Konfigurationsdatei sowohl Client- als auch Service-Abschnitte enthält, wirkt sich die Standardbindung auf beide aus.

Generierung der Client-Konfigurationsdatei

Wenn du eine Dienstreferenz in Visual Studio hinzufügst, versucht es auch, die Konfigurationsdatei des Clients automatisch zu bearbeiten und den erforderlichen Abschnitt client einzufügen, der die Endpunkte des Dienstes beschreibt. In den Versionen von Visual Studio vor 2012 war Visual Studio nicht intelligent genug, um die saubersten Bindungswerte zu ermitteln, so dass es die Konfigurationsdatei mit allen Standardwerten für die Bindungen versaut hat, was die Datei praktisch unlesbar gemacht hat. Visual Studio 2012 und höher löst dieses Problem, indem es der Konfigurationsdatei nur Werte für Bindungseigenschaften hinzufügt, die nicht auf Standardwerte gesetzt sind.

Wie Visual Studio generiert auch SvcUtil automatisch eine clientseitige Konfigurationsdatei namens output.config. Du kannst mit dem Schalter /config einen anderen Dateinamen für die Konfiguration angeben:

SvcUtil http://localhost:8002/MyService/out:Proxy.cs /config:App.Config

Wie Visual Studio ist auch SvcUtil mit .NET 4.5 intelligent genug, um nur Konfigurationswerte für Bindungseigenschaften zu erzeugen, die auf Nicht-Standardwerte gesetzt sind. Anders als bei Visual Studio kannst du bei SvcUtil jedoch die Erzeugung der Konfigurationsdatei mit dem Schalter /noconfig unterdrücken:

SvcUtil http://localhost:8002/MyService/out:Proxy.cs /noconfig

Obwohl diese Tools die Konfigurationsdatei nicht mehr mit überflüssigen Standardwerten überladen, empfehlen wir trotzdem, SvcUtil oder Visual Studio niemals die Kontrolle über die Konfigurationsdatei zu überlassen.

In-proc-Konfiguration

Beim In-Proc-Hosting ist die Client-Konfigurationsdatei auch die Konfigurationsdatei des Service-Hosts, und dieselbe Datei enthält sowohl Service- als auch Client-Einträge, wie in Beispiel 1-20 gezeigt.

Beispiel 1-20. In-proc-Hosting-Konfigurationsdatei
<system.serviceModel>
   <services>
      <service name = "MyService">
         <endpoint
            address  = "net.pipe://localhost/MyPipe"
            binding  = "netNamedPipeBinding"
            contract = "IMyContract"
         />
      </service>
   </services>
   <client>
      <endpoint name = "MyEndpoint"
         address  = "net.pipe://localhost/MyPipe"
         binding  = "netNamedPipeBinding"
         contract = "IMyContract"
      />
   </client>
</system.serviceModel>

Beachte die Verwendung der benannten Pipe-Bindung für das In-Proc-Hosting.

Der SvcConfigEditor

WCF bietet einen Konfigurationsdatei-Editor namens SvcConfigEditor.exe, mit dem sowohl Host- als auch Client-Konfigurationsdateien bearbeitet werden können (siehe Abbildung 1-15). Du kannst den Editor von Visual Studio aus starten, indem du mit der rechten Maustaste auf die Konfigurationsdatei (entweder für den Client oder den Host) klickst und WCF-Konfiguration bearbeiten wählst.

Hinweis

Wenn du eine frühere Version von Visual Studio als 2012 verwendest, musst du den Editor zunächst über das Menü Extras starten.

SvcConfigEditor is used to edit both host and client config files
Abbildung 1-15. SvcConfigEditor wird verwendet, um sowohl Host- als auch Client-Konfigurationsdateien zu bearbeiten

Ich habe gemischte Gefühle gegenüber SvcConfigEditor. Einerseits lassen sich die Konfigurationsdateien gut bearbeiten und Entwickler müssen sich nicht mit dem Konfigurationsschema vertraut machen. Andererseits erspart er den Entwicklern nicht, die WCF-Konfiguration gründlich zu verstehen, und es ist oft schneller, die leichten Änderungen, die normalerweise in einer Konfigurationsdatei erforderlich sind, von Hand vorzunehmen als mit Visual Studio.

Arbeiten mit dem Proxy

Die Proxy-Klasse leitet sich von der Klasse ClientBase<T> ab, die wie folgt definiert ist:

public abstract class ClientBase<T> : ICommunicationObject,IDisposable
{
   protected ClientBase(string endpointName);
   protected ClientBase(Binding binding,EndpointAddress remoteAddress);

   public void Open();
   public void Close();

   protected T Channel
   {get;}

   //Additional members
}

ClientBase<T> akzeptiert einen einzelnen generischen Typ-Parameter, der den Servicevertrag identifiziert, den dieser Proxy kapselt. Die Eigenschaft Channel von ClientBase<T> gehört zu diesem Typ-Parameter. Wie in Beispiel 1-16 gezeigt, delegiert die generierte Unterklasse von ClientBase<T> den Methodenaufruf einfach an Channel. Der Aufruf der Methode über die Eigenschaft Channel sendet die entsprechende WCF-Nachricht an den Dienst.

Um den Proxy zu nutzen, muss der Kunde zunächst ein Proxy-Objekt instanziieren und dem Konstruktor die Endpunktinformationen übergeben: entweder den Namen des Endpunktabschnitts aus der Konfigurationsdatei oder die Endpunktadresse und die Bindungsobjekte, wenn du keine Konfigurationsdatei verwendest. Der Client kann dann die Proxy-Methoden verwenden, um den Dienst aufzurufen, und wenn er fertig ist, muss er die Proxy-Instanz schließen. Mit den gleichen Definitionen wie in den Beispielen 1-16 und 1-17 baut der Client den Proxy auf, identifiziert den zu verwendenden Endpunkt aus der Konfigurationsdatei, ruft die Methode auf und schließt den Proxy:

MyContractClient proxy = new MyContractClient("MyEndpoint");
proxy.MyMethod();
proxy.Close();

Wenn der Proxy den Namen des Endpunkts angibt, prüft sein Konstruktor auch, ob der für diesen Endpunkt konfigurierte Vertrag mit dem Typ-Parameter des Proxys übereinstimmt. Wenn in der Konfigurationsdatei des Clients genau ein Endpunkt für den Vertragstyp definiert ist, den der Proxy verwendet, kann der Client den Endpunktnamen im Konstruktor des Proxys weglassen, weil diese Überprüfung möglich ist:

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

Der Proxy sucht dann einfach nach diesem Endpunkt (ob er in der Konfigurationsdatei benannt ist oder nicht) und verwendet ihn. Wenn du diese Technik jedoch verwendest, wenn mehrere (oder gar keine) Endpunkte für denselben Vertragstyp verfügbar sind, löst der Proxy eine Ausnahme aus.

Schließen der Vollmacht

Es ist eine bewährte Methode, den Proxy immer zu schließen, wenn der Kunde ihn nicht mehr benutzt. Das Schließen des Proxys gibt die Verbindung zum Dienst frei, was besonders wichtig ist, wenn eine Transportsitzung besteht (wie später in diesem Kapitel beschrieben). Außerdem wird so sichergestellt, dass der Schwellenwert für die maximale Anzahl von Verbindungen auf dem Rechner des Kunden nicht erreicht wird. Wie du in Kapitel 4 sehen wirst, wird durch das Schließen des Proxys die Sitzung mit der Dienstinstanz beendet.

Anstatt den Proxy zu schließen, kannst du seine Dispose() Methode verwenden. Intern ruft Dispose() einfach Close() auf. Der Vorteil der Methode Dispose() ist, dass du sie mit der Anweisung using auch bei Ausnahmen aufrufen kannst:

using(MyContractClient proxy = new MyContractClient())
{
   //Any exception here automatically closes the proxy;
}

Wenn der Kunde den Vertrag direkt deklariert und nicht die konkrete Proxy-Klasse, kann der Kunde das Vorhandensein von IDisposable abfragen:

IMyContract proxy = new MyContractClient();
proxy.MyMethod();
IDisposable disposable = proxy as IDisposable;
if(disposable != null)
{
   disposable.Dispose();
}

Alternativ kann der Kunde die Abfrage innerhalb der Anweisung using zusammenfassen:

IMyContract proxy = new MyContractClient();
using(proxy as IDisposable)
{
   proxy.MyMethod();
}
Hinweis

Obwohl die Ergebnisse der Aufrufe von Dispose() und Close() identisch sind, wirst du in Kapitel 6 sehen, dass es immer besser ist, Close() aufzurufen als die Anweisung using zu verwenden.

Anrufzeitüberschreitung

Jeder Aufruf eines WCF-Clients muss innerhalb eines konfigurierbaren Timeouts abgeschlossen werden. Wenn die Dauer des Aufrufs aus irgendeinem Grund die Zeitüberschreitung überschreitet, wird der Aufruf abgebrochen und der Client erhält eine TimeoutException. Dieses Verhalten ist sehr praktisch, da es eine elegante Möglichkeit bietet, mit Deadlocks auf der Seite des Dienstes oder einfach mit schlechter Verfügbarkeit umzugehen. In herkömmlichem .NET muss der Client einen Worker-Thread starten, der die Klasse aufruft (und sich dabei möglicherweise aufhängt), und der Client überwacht dann ein Ereignis mit Zeitüberschreitung, das der Worker-Thread melden muss, wenn er fertig ist. Das ist natürlich ein kompliziertes Programmiermodell. Der Vorteil der Verwendung eines Proxys für jeden Aufruf ist, dass der Proxy all dies für dich erledigen kann. Der genaue Wert der Zeitüberschreitung ist eine Eigenschaft der Bindung, und die Standardzeitüberschreitung beträgt eine Minute. Um eine andere Zeitüberschreitung festzulegen, stellst du die Eigenschaft SendTimeout der abstrakten Basisklasse Binding ein:

public abstract class Binding : ...
{
   public TimeSpan SendTimeout
   {get;set;}
   //More members
}

So konfigurierst du zum Beispiel die WSHttpBinding mit einer fünfminütigen Anrufzeitüberschreitung:

<client>
   <endpoint
      ...
      binding = "wsHttpBinding"
      bindingConfiguration = "LongTimeout"
      ...
   />
</client>
<bindings>
   <wsHttpBinding>
      <binding name = "LongTimeout" sendTimeout = "00:05:00"/>
   </wsHttpBinding>
</bindings>

Programmatische Client-Konfiguration

Anstatt sich auf eine Konfigurationsdatei zu verlassen, kann der Kunde programmatisch Adress- und Bindungsobjekte konstruieren, die dem Service-Endpunkt entsprechen, und sie dem Proxy-Konstruktor übergeben. Der Vertrag muss nicht angegeben werden, da er in Form des generischen Typparameters des Proxys bereitgestellt wurde. Um die Adresse zu repräsentieren, muss der Kunde eine EndpointAddress Klasse instanziieren, die wie folgt definiert ist:

public class EndpointAddress
{
   public EndpointAddress(string uri);
   //More members
}

Beispiel 1-21 demonstriert diese Technik und zeigt das Code-Äquivalent von Beispiel 1-17, das auf den Service in Beispiel 1-9 abzielt.

Beispiel 1-21. Programmatische Client-Konfiguration
Binding wsBinding = new WSHttpBinding();
EndpointAddress endpointAddress = new EndpointAddress(
                                               "http://localhost:8000/MyService");

MyContractClient proxy = new MyContractClient(wsBinding,endpointAddress);
proxy.MyMethod();
proxy.Close();

Ähnlich wie bei der Verwendung eines binding Abschnitts in einer Konfigurationsdatei, kann der Kunde die Bindungseigenschaften programmatisch konfigurieren:

WSHttpBinding wsBinding = new WSHttpBinding();
wsBinding.SendTimeout = TimeSpan.FromMinutes(5);
wsBinding.TransactionFlow = true;

EndpointAddress endpointAddress = new EndpointAddress
                                  ("http://localhost:8000/MyService");

MyContractClient proxy = new MyContractClient(wsBinding,endpointAddress);
proxy.MyMethod();
proxy.Close();

Beachte auch hier die Verwendung der konkreten Unterklasse von Binding, um auf bindungsspezifische Eigenschaften wie den Transaktionsablauf zugreifen zu können.

Der WCF-gestützte Test-Client

Visual Studio wird mit einem einfachen Allzweck-Testclient für rudimentäre Tests ausgeliefert, mit dem du die meisten Dienste aufrufen kannst. Der Testclient heißt WcfTestClient.exe und befindet sich nach einer normalen Installation in C:\Programme\Microsoft Visual Studio <Version>\Common7\IDE. Du kannst WcfTestClient ein einzelnes Befehlszeilenargument mit der Metadatenadresse des zu testenden Dienstes übergeben:

WcfTestClient.exe http://localhost:9000/

Du kannst jede beliebige Metadatenadresse angeben (sei es eine HTTP-GET-Adresse oder ein Endpunkt für den Metadatenaustausch über HTTP, TCP oder IPC). Du kannst auch mehrere Metadatenadressen angeben:

WcfTestClient.exe http://localhost:8000/ net.tcp://localhost:9000/MEX

Du kannst den Testclient auch ohne Befehlszeilenparameter starten. Sobald er läuft, kannst du einen neuen Dienst hinzufügen, indem du im Menü Datei die Option Dienst hinzufügen wählst und im Dialogfeld Dienst hinzufügen die Adresse der Metadaten angibst. Du kannst einen Dienst auch entfernen, indem du mit der rechten Maustaste auf ihn in der Dienste-Struktur klickst.

WcfTestClient ist eine Windows Forms-Anwendung. Die Baumstruktur im linken Bereich enthält die getesteten Dienste und ihre Endpunkte. Du kannst den Vertrag eines Endpunkts aufrufen und eine Operation auswählen, um eine eigene Registerkarte für diesen Aufruf im rechten Bereich anzuzeigen. Zum Beispiel für diesen einfachen Vertrag und die Implementierung:

[ServiceContract]
interface IMyContract
{
   [OperationContract]
   string MyMethod(int someNumber,string someText);
}
class MyService : IMyContract
{
   public string MyMethod(int someNumber,string someText)
   {
      return "Hello";
   }
}

Auf der Registerkarte "Methode" kannst du im Abschnitt "Anfrage" eine ganze Zahl und eine Zeichenkette als Operationsparameter angeben, wie in Abbildung 1-16 dargestellt.

Using WcfTestClient
Abbildung 1-16. WcfTestClient verwenden

Wenn du auf die Schaltfläche Aufrufen klickst, sendet der WcfTestClient den Aufruf an den Dienst und zeigt den zurückgegebenen Wert oder die Ausgangsparameter im Bereich Antwort an. Im Falle einer Ausnahme zeigt WcfTestClient die Ausnahmeinformationen in einem Meldungsfeld an und ermöglicht es dir, weitere Aufrufe zu tätigen. Alle Aufrufe werden auf neuen Proxy-Instanzen ausgeführt. Außerdem erfolgen alle Aufrufe asynchron, damit die Benutzeroberfläche reaktionsfähig bleibt. Auch wenn die Aufrufe asynchron sind, lässt WcfTestClient dich immer nur einen Aufruf zur gleichen Zeit ausführen.

Der WcfTestClient erstellt im Hintergrund eine Baugruppe aus einer Proxydatei mit einer Konfigurationsdatei und lädt sie dann zur Verwendung von einem temporären Speicherort. Wenn du im Baum auf der linken Seite auf das Element Config File klickst, kannst du diese Konfigurationsdatei (dieselbe Konfigurationsdatei, die beim Hinzufügen einer Dienstreferenz erstellt wird) abrufen und in einer eigenen Registerkarte anzeigen. Du kannst die Konfigurationsdatei sogar mit dem SvcConfigEditor bearbeiten.

Mit WcfTestClient kannst du Operationen mit Aufzählungen, zusammengesetzten Parametern wie Klassen oder Strukturen (die jeweils aus anderen Klassen oder Strukturen zusammengesetzt sind) und sogar Sammlungen und Arrays von Parametern aufrufen. Erweitere einfach die Elemente im Abschnitt Anfrage, lege ihre Werte in den Dropdown-Listen fest (z. B. Aufzählungswerte) und rufe den Aufruf auf. Wenn der Vorgang eine Sammlung oder ein Array akzeptiert, musst du auch die Länge festlegen. Auch hier enthält der Antwortbereich alle zusammengesetzten Rückgabewerte oder Ausgangsparameter.

Wie bei WcfSvcHost (siehe Seitenleiste "Der WCF-Provided Test Host") kannst du auch WcfTestClient direkt in deine Visual Studio-Lösung integrieren. Füge der Lösung zunächst ein Klassenbibliotheksprojekt hinzu und lösche daraus alle Verweise, Ordner und Quelldateien, da du diese nicht benötigst. Als Nächstes legst du WcfTestClient.exe als externes Startprogramm fest und gibst die Metadatenadresse (oder -adressen) des getesteten Dienstes (oder der getesteten Dienste) an. Dabei kann es sich um die .svc-Adresse eines IIS- oder WAS-gehosteten Projekts handeln oder um eine beliebige andere Metadatenadresse eines Host-Projekts, egal ob innerhalb oder außerhalb deiner Lösung.

Natürlich kannst du die Verwendung von WcfTestClient und WcfSvcHost in einem einzigen Schritt kombinieren, um einen Dienst automatisch in einer Dienstbibliothek zu hosten und zu testen:

WcfSvcHost.exe /service:MyService.dll    /config:App.config
               /client:WcfTestClient.exe /clientArg:http://localhost:9000/

Bei WcfSvcHost ist die Angabe der Metadatenargumente jedoch optional. Standardmäßig leitet WcfSvcHost alle Metadatenadressen, die er in der Konfigurationsdatei des Dienstes findet, an die angegebene Client-Anwendung weiter. Du solltest die Metadatenadresse eines Dienstes nur dann explizit angeben, wenn der Dienst (oder die Dienste) keine eigenen Metadaten bereitstellt oder wenn du möchtest, dass der Testclient andere Adressen verwendet. Wenn die Dienstkonfigurationsdatei mehrere Metadaten-Endpunkte für einen bestimmten Dienst enthält, werden sie in dieser Reihenfolge angegeben: HTTP, TCP, IPC, HTTP-GET.

Du kannst diese Schritte in Visual Studio einbauen, um ein nahtloses Hosting und Testen zu ermöglichen. Dazu gibst du WcfSvcHost.exe zusammen mit der Konfigurationsdatei als Startprogramm und WcfTestClient.exe als Client an. Wenn du WcfTestClient mit /client aufrufst, wird beim Schließen des Testclients auch der Host beendet.

Programmatische versus administrative Konfiguration

Die beiden bisher vorgestellten Techniken zur Konfiguration von Client und Dienst ergänzen sich gegenseitig. Die administrative Konfiguration gibt dir die Möglichkeit, wichtige Aspekte des Dienstes und des Clients nach der Bereitstellung zu ändern, ohne dass du sie neu erstellen oder erneut bereitstellen musst. Der größte Nachteil der administrativen Konfiguration ist, dass sie nicht typsicher ist; du wirst bestimmte Konfigurationsfehler erst zur Laufzeit entdecken und WCF-Konfigurationsdateien werden oft unhandlich, wenn ein System reift.

Hinweis

Im Gegensatz zu früheren Versionen von Visual Studio überprüft Visual Studio 2012 und höher nun die meisten Werte im Abschnitt <serviceModel> einer Konfigurationsdatei. Visual Studio prüft jedoch immer noch keine Konfigurationswerte, die externe Verweise enthalten, wie z. B. die Adresseigenschaft.

Die programmatische Konfiguration ist nützlich, wenn die Konfigurationsentscheidung entweder völlig dynamisch ist (d.h. wenn sie zur Laufzeit auf der Grundlage der aktuellen Eingaben oder Bedingungen getroffen wird) oder statisch ist und sich nie ändert (in diesem Fall kannst du sie genauso gut fest codieren). Wenn du zum Beispiel nur In-Proc-Aufrufe hosten willst, kannst du die Verwendung von NetNamedPipeBinding und seine Konfiguration fest programmieren.

Konfigurationspolitik

Im Allgemeinen ist es am besten, für jede Anwendung einen einzigen Konfigurationsansatz zu wählen und bei diesem zu bleiben. Wenn du verschiedene Konfigurationsansätze mischen musst, solltest du dies konsequent tun, sonst kann es verwirrend werden. Wenn du eine Konfigurationsrichtlinie für deine Anwendung festlegst und weitergibst, ist das ein guter Weg, um einen einheitlichen Konfigurationsansatz für deine gesamte Entwicklung zu schaffen. In deinen Konfigurationsrichtlinien sollte klar festgelegt sein, ob Entwickler dateibasierte Konfigurationen verwenden, sich auf Standardendpunkte verlassen, programmatische Vorgaben fest codieren oder die Konfiguration dynamisch auf der Grundlage von Laufzeitbedingungen anwenden dürfen.

Deine Konfigurationsrichtlinie sollte auch festlegen, wann Entwickler verschiedene Konfigurationsmechanismen für Client und Service nutzen sollten. So sollte in deiner Konfigurationsrichtlinie für den Dienst klar festgelegt sein, wann eine Configure() Methode, eine programmatische ServiceHost Verwendung oder sogar eine benutzerdefinierte ServiceHost Implementierung angemessen ist. Und deine Konfigurationsrichtlinie für den Client sollte vorschlagen, wann eine direkte Kanalnutzung, eine proxy-basierte Hierarchie oder sogar eine benutzerdefinierte ChannelFactory notwendig ist.

Da es viele Möglichkeiten, aber nur wenige richtige Entscheidungen gibt, solltest du in Erwägung ziehen, deine Konfigurationsrichtlinien in den WCF-Teil deiner Infrastruktur zu integrieren. Auf diese Weise kannst du die Einstiegshürde für deine Entwicklergemeinschaft senken, bewährte Methoden einbinden, Richtlinien durchsetzen und Erweiterbarkeit ermöglichen.

WCF-Architektur

Bis jetzt habe ich in diesem Kapitel alles behandelt, was nötig ist, um einfache WCF-Dienste einzurichten und zu nutzen. Wie du jedoch im weiteren Verlauf des Buches sehen wirst, bietet WCF eine äußerst wertvolle Unterstützung für Zuverlässigkeit, Transaktionen, Gleichzeitigkeitsmanagement, Sicherheit und Instanzaktivierung, die alle auf der abfangenden Architektur von WCF basieren. Wenn der Client mit einem Proxy interagiert, bedeutet das, dass WCF immer zwischen dem Dienst und dem Client steht, den Aufruf abfängt und die Verarbeitung vor und nach dem Aufruf durchführt. Das Abfangen beginnt, wenn der Proxy den Aufruf-Stack-Frame in eine Nachricht serialisiert und die Nachricht über eine Kette von Kanälen weiterleitet. Der Kanal ist lediglich ein Abfangjäger, dessen Aufgabe es ist, eine bestimmte Aufgabe zu erfüllen. Jeder clientseitige Kanal verarbeitet die Nachricht vor dem Aufruf. Die genaue Struktur und Zusammensetzung der Kette hängt hauptsächlich von der Bindung ab. Einer der Kanäle kann zum Beispiel für die Kodierung der Nachricht (binär, Text oder MTOM) zuständig sein, ein anderer für die Übergabe des Sicherheitsaufrufkontextes, ein weiterer für die Weitergabe der Client-Transaktion, ein weiterer für die Verwaltung der zuverlässigen Sitzung, ein weiterer für die Verschlüsselung des Nachrichtentextes (falls so konfiguriert) und so weiter. Der letzte Kanal auf der Client-Seite ist der Transportkanal, der die Nachricht über den konfigurierten Transport an den Host sendet.

Auf der Host-Seite durchläuft die Nachricht eine weitere Kette von Kanälen, die eine hostseitige Vorverarbeitung der Nachricht durchführen. Der erste Kanal auf der Hostseite ist der Transportkanal, der die Nachricht vom Transportdienst empfängt. Die nachfolgenden Kanäle führen verschiedene Aufgaben aus, z. B. die Entschlüsselung des Nachrichtentextes, die Dekodierung der Nachricht, den Beitritt zur propagierten Transaktion, die Einstellung des Sicherheitsprinzips, die Verwaltung der Sitzung und die Aktivierung der Serviceinstanz. Der letzte Kanal auf der Host-Seite leitet die Nachricht an den Dispatcher weiter. Der Dispatcher wandelt die Nachricht in einen Stackframe um und ruft die Serviceinstanz auf. Dieser Ablauf ist in Abbildung 1-17 dargestellt.

The WCF architecture
Abbildung 1-17. Die WCF-Architektur

Der Dienst hat keine Möglichkeit zu wissen, dass er nicht von einem lokalen Kunden aufgerufen wurde. Vielmehr wurde er von einem lokalen Client, dem Dispatcher, aufgerufen. Das Abfangen sowohl auf der Client- als auch auf der Service-Seite stellt sicher, dass der Client und der Service die Laufzeitumgebung erhalten, die sie für einen ordnungsgemäßen Betrieb benötigen.

Die Service-Instanz führt den Aufruf aus und gibt die Kontrolle an den Dispatcher zurück, der dann die zurückgegebenen Werte und Fehlerinformationen (falls vorhanden) in eine Rückmeldung umwandelt. Dann wird der Prozess umgekehrt: Der Dispatcher leitet die Nachricht durch die hostseitigen Kanäle, um die Verarbeitung nach dem Aufruf durchzuführen, z. B. die Transaktion zu verwalten, die Instanz zu deaktivieren, die Antwort zu kodieren, zu verschlüsseln und so weiter. Die zurückgesendete Nachricht geht dann an den Transportkanal, der sie an die clientseitigen Kanäle zur clientseitigen Nachbearbeitung weiterleitet. Dieser Prozess besteht wiederum aus Aufgaben wie Entschlüsselung, Dekodierung, Bestätigung oder Abbruch der Transaktion und so weiter. Der letzte Kanal gibt die Nachricht an den Proxy weiter, der die zurückgegebene Nachricht in einen Stack-Frame umwandelt und die Kontrolle an den Client zurückgibt.

Am bemerkenswertesten ist, dass fast alle Punkte der Architektur Haken für die Erweiterbarkeit bieten - du kannst benutzerdefinierte Kanäle für eigene Interaktionen, benutzerdefinierte Verhaltensweisen für die Instanzverwaltung, benutzerdefiniertes Sicherheitsverhalten und so weiter bereitstellen. Die Standardfunktionen, die WCF bietet, werden alle nach demselben Erweiterungsmodell implementiert. Du wirst in diesem Buch viele Beispiele und Anwendungen für Erweiterbarkeit sehen.

Host Architektur

Es ist auch wichtig zu untersuchen, wie der Übergang von einer technologieneutralen, serviceorientierten Interaktion zu CLR-Schnittstellen und -Klassen erfolgt. Der Host führt die Überbrückung durch. Jeder .NET-Host-Prozess kann viele App-Domänen haben, und jede App-Domäne kann null oder mehr Service-Host-Instanzen haben. Jede Service-Host-Instanz ist für einen bestimmten Diensttyp bestimmt. Wenn du also eine Host-Instanz erstellst, registrierst du diese Service-Host-Instanz bei allen Endpunkten dieses Typs auf dem Host-Rechner, die seinen Basisadressen entsprechen. Jede Service-Host-Instanz hat null oder mehr Kontexte. Der Kontext ist der innerste Ausführungsbereich der Serviceinstanz. Ein Kontext ist mit null oder einer Serviceinstanz verbunden, d. h. er kann auch leer sein (d. h. mit keiner Serviceinstanz verbunden sein). Diese Architektur ist in Abbildung 1-18 dargestellt.

The WCF host architecture
Abbildung 1-18. Die Architektur des WCF-Hosts
Hinweis

Der WCF-Kontext ist konzeptionell ähnlich wie der Enterprise Services-Kontext oder der kontextgebundene Objektkontext von .NET.

Es ist die gemeinsame Arbeit des Service-Hosts und des Kontexts, der einen nativen CLR-Typ als Dienst zur Verfügung stellt. Nachdem die Nachricht durch die Kanäle geleitet wurde, ordnet der Host diese Nachricht einem neuen oder bestehenden Kontext (und der darin enthaltenen Objektinstanz) zu und lässt ihn den Aufruf verarbeiten.

Arbeiten mit Kanälen

Du kannst Kanäle direkt verwenden, um Operationen auf einem Dienst aufzurufen, ohne dass du eine Proxy Klasse verwenden musst. Die Klasse ChannelFactory<T> (und ihre unterstützenden Typen), die in Beispiel 1-22 gezeigt wird, ermöglicht es dir, einen Proxy zu erstellen.

Beispiel 1-22. Die Klasse ChannelFactory<T>
public class ContractDescription
{
   public Type ContractType
   {get;set;}
   //More members
}

public class ServiceEndpoint
{
   public ServiceEndpoint(ContractDescription contract,Binding binding,
                          EndpointAddress address);
   public EndpointAddress Address
   {get;set;}
   public Binding Binding
   {get;set;}
   public ContractDescription Contract
   {get;}
   //More members
}

public abstract class ChannelFactory : ...
{
   public ServiceEndpoint Endpoint
   {get;}
   //More members
}
public class ChannelFactory<T> : ChannelFactory,...
{
   public ChannelFactory(ServiceEndpoint endpoint);
   public ChannelFactory(string configurationName);
   public ChannelFactory(Binding binding,EndpointAddress endpointAddress);

   public static T CreateChannel(Binding binding,EndpointAddress endpointAddress);
   public T CreateChannel();

   //More members
}

Du brauchst , um dem Konstruktor von ChannelFactory<T> den Endpunkt mitzuteilen. Dies kann der Endpunktname aus der Client-Konfigurationsdatei, die Bindungs- und Adressobjekte oder ein ServiceEndpoint Objekt sein. Als Nächstes verwendest du die Methode CreateChannel(), um eine Referenz auf den Proxy zu erhalten und seine Methoden zu verwenden. Zum Schluss schließt du den Proxy, indem du ihn entweder in IDisposable umwandelst und die Methode Dispose() aufrufst oder ihn in ICommunicationObject umwandelst und die Methode Close() aufrufst:

ChannelFactory<IMyContract> factory = new ChannelFactory<IMyContract>();

IMyContract proxy1 = factory.CreateChannel();
using(proxy1 as IDisposable)
{
   proxy1.MyMethod();
}

IMyContract proxy2 = factory.CreateChannel();
proxy2.MyMethod();
ICommunicationObject channel = proxy2 as ICommunicationObject;
Debug.Assert(channel != null);
channel.Close();

Du kannst auch die statische CreateChannel() Methode verwenden, um einen Proxy mit einer Bindung und einer Adresse zu erstellen, ohne direkt eine Instanz von ChannelFactory<T> zu erzeugen:

Binding binding = new NetTcpBinding();
EndpointAddress address = new EndpointAddress("net.tcp://localhost:8000");

IMyContract proxy = ChannelFactory<IMyContract>.CreateChannel(binding,address);
using(proxy as IDisposable)
{
   proxy.MyMethod();
}

Die Klasse InProcFactory

Um die Leistungsfähigkeit von ChannelFactory<T> zu demonstrieren, betrachte ich meine statische Hilfsklasse InProcFactory, die wie folgt definiert ist:

public static class InProcFactory
{
   public static I CreateInstance<S,I>() where I : class
                                         where S : I;
   public static void CloseProxy<I>(I instance) where I : class;
   //More members
}

InProcFactory wurde entwickelt, um das In-Proc-Hosting zu rationalisieren und zu automatisieren. Die Methode CreateInstance() benötigt zwei generische Typ-Parameter: den Typ des Dienstes S und den Typ des unterstützten Vertrags I. CreateInstance() zwingt S dazu, von I abzuleiten. Die Verwendung von InProcFactory ist einfach:

IMyContract proxy = InProcFactory.CreateInstance<MyService,IMyContract>();

proxy.MyMethod();

InProcFactory.CloseProxy(proxy);

Er nimmt buchstäblich eine Dienstklasse und erhebt sie zu einem WCF-Dienst. Dies ist dem C#-Operator new sehr ähnlich, da diese beiden Zeilen in ihrer Kopplung an den Diensttyp gleichwertig sind:

IMyContract proxy = InProcFactory.CreateInstance<MyService,IMyContract>();
IMyContract obj = new MyService();

Im Fall von C# überprüft der Compiler, ob der Typ die angeforderte Schnittstelle unterstützt, und überträgt die Schnittstelle dann in die Variable. Wenn der Compiler keine Unterstützung bietet, benötigt InProcFactory den Schnittstellentyp, um zu wissen, welcher Schnittstellentyp zurückgegeben werden soll.

Implementierung von InProcFactory<T>

Alle In-Proc-Aufrufe sollten benannte Pipes verwenden und alle Transaktionen sollten fließen. Du kannst die programmatische Konfiguration verwenden, um die Konfigurationen des Clients und des Dienstes zu automatisieren, und ChannelFactory<T> verwenden, um einen Proxy zu vermeiden. Beispiel 1-23 zeigt die Implementierung von InProcFactory, wobei ein Teil des Codes der Kürze halber entfernt wurde.

Beispiel 1-23. Die Klasse InProcFactory
public static class InProcFactory
{
   static readonly string BaseAddress = "net.pipe://localhost/" + Guid.NewGuid();
   static readonly Binding Binding;
   static Dictionary<Type,Tuple<ServiceHost,EndpointAddress>> m_Hosts =
                        new Dictionary<Type,Tuple<ServiceHost,EndpointAddress>>();
   static InProcFactory()
   {
      NetNamedPipeBinding binding = new NetNamedPipeBinding();
      binding.TransactionFlow = true;
      Binding = binding;
      AppDomain.CurrentDomain.ProcessExit += delegate
                                             {
                                        foreach(Tuple<ServiceHost,EndpointAddress>
                                                         record in m_Hosts.Values)
                                                   {
                                                      record.Item1.Close();
                                                   }
                                             };
   }   public static I CreateInstance<S,I>() where I : class
                                         where S : I
   {
      EndpointAddress address = GetAddress<S,I>();
      return ChannelFactory<I>.CreateChannel(Binding,address);
   }
   static EndpointAddress GetAddress<S,I>() where I : class
                                            where S : class,I
   {
      Tuple<ServiceHost,EndpointAddress> record;

      if(m_Hosts.ContainsKey(typeof(S)))
      {
         hostRecord = m_Hosts[typeof(S)];
      }
      else
      {
         ServiceHost host = new ServiceHost(typeof(S));
         string address = BaseAddress + Guid.NewGuid();
         record = new Tuple<ServiceHost,EndpointAddress>(
                                               host,new EndpointAddress(address));
         m_Hosts[typeof(S)] = record;
         host.AddServiceEndpoint(typeof(I),Binding,address);
         host.Open();
      }
      return hostRecord;
   }
   public static void CloseProxy<I>(I instance) where I : class
   {
      ICommunicationObject proxy = instance as ICommunicationObject;
      Debug.Assert(proxy != null);
      proxy.Close();
   }
}

InProcFactoryDer statische Konstruktor von wird einmal pro App-Domäne aufgerufen und weist in jedem Fall eine neue eindeutige Basisadresse mit einer GUID zu. Auf diese Weise kannst du InProcFactory mehrfach auf demselben Rechner, in verschiedenen App-Domänen und Prozessen verwenden.

Die größte Herausforderung für InProcFactory ist, dass CreateInstance() aufgerufen werden kann, um Dienste jedes Typs zu instanziieren. Für jeden Diensttyp sollte es einen einzigen passenden Host geben (eine Instanz von ServiceHost). Es ist keine gute Idee, für jeden Aufruf eine Host-Instanz zuzuweisen. Das Problem ist, was CreateInstance() tun soll, wenn es aufgefordert wird, ein zweites Objekt desselben Typs zu instanziieren, etwa so:

IMyContract proxy1 = InProcFactory.CreateInstance<MyService,IMyContract>();
IMyContract proxy2 = InProcFactory.CreateInstance<MyService,IMyContract>();

Die Lösung besteht darin, dass InProcFactory intern ein Wörterbuch verwaltet, das die Diensttypen mit Hilfe eines Tupels einer bestimmten Host-Instanz und der Endpunktadresse zuordnet. Wenn CreateInstance() aufgerufen wird, um eine Instanz eines bestimmten Typs zu erstellen, sucht es mit einer Hilfsmethode namens GetAddress() im Wörterbuch. Wenn das Wörterbuch den Diensttyp noch nicht enthält, erstellt diese Hilfsmethode eine Host-Instanz für ihn. Wenn ein Host erstellt werden muss, fügt GetAddress() programmatisch einen Endpunkt zu diesem Host hinzu und verwendet eine neue GUID als eindeutigen Pipe-Namen. GetAddress() speichert den neuen Host und seine Adresse im Wörterbuch. CreateInstance() verwendet dann ChannelFactory<T>, um den Proxy zu erstellen. In seinem statischen Konstruktor, der bei der ersten Verwendung der Klasse aufgerufen wird, abonniert InProcFactory mit einer anonymen Methode das Ereignis "Prozessende", um alle Hosts zu schließen, wenn der Prozess beendet wird. Um den Clients beim Schließen des Proxys zu helfen, stellt InProcFactory die Methode CloseProxy() bereit, die den Proxy an ICommunicationObject abfragt und ihn schließt.

Der WcfWrapper

Wenn du dich dem C#-Programmiermodell vollständig annähern möchtest, kannst du die In-Proc-Factory (und damit die gesamte WCF) mit meiner Hilfsbasisklasse WcfWrapper verpacken, wie in Beispiel 1-24 gezeigt.

Beispiel 1-24. Die Klasse WcfWrapper
public abstract class WcfWrapper<S,I> : IDisposable,ICommunicationObject
                                                         where I : class
                                                         where S : class,I
{
   protected I Proxy
   {get;private set;}

   protected WcfWrapper()
   {
      Proxy = InProcFactory.CreateInstance<S,I>();
   }

   public void Dispose()
   {
      Close();
   }

   public void Close()
   {
      InProcFactory.CloseProxy(Proxy);
   }

   void ICommunicationObject.Close()
   {
      (Proxy as ICommunicationObject).Close();
   }
   //Rest of ICommunicationObject
}

Die Verwendung von WcfWrapper<S,I> ist einfach - leite von ihr und dem Vertrag ab und implementiere die Operationen des Vertrags, indem du sie an die Eigenschaft Proxy delegierst. Zum Beispiel für diese Dienstdefinition:

[ServiceContract]
interface IMyContract{
   [OperationContract]
   string MyMethod();
}

class MyService : IMyContract
{
   public string MyMethod()
   {...}
}

Dies ist die passende Wrapper-Klasse:

class MyClass : WcfWrapper<MyService,IMyContract>,IMyContract
{
   public string MyMethod()
   {
      return Proxy.MyMethod();
   }
}

Die Verwendung der Wrapper-Klasse ist nun nicht mehr von normalem C#-Code zu unterscheiden und dennoch sind alle Aufrufe tatsächlich WCF-Aufrufe:

MyClass obj = new MyClass();
string text = obj.MyMethod();
obj.Close();

In Anhang A werden die tiefgreifenden Auswirkungen dieses Programmiermodells näher erläutert.

Sitzungen auf Transportebene

In der traditionellen Programmierung ist ein Objekt durch den Aufrufstapel indirekt mit einem Client verbunden. Das heißt, jedes Objekt ist an einen bestimmten Kunden gebunden. Aber in WCF ist eine solche Zuordnung nicht möglich, da der Kunde eine Nachricht an den Dienst sendet und die Instanz nie direkt aufruft. Das analoge Konzept in WCF ist die Transportsitzung, die sicherstellt, dass alle Nachrichten, die von einem bestimmten Client kommen, an denselben Transportkanal auf dem Host gesendet werden. Es ist, als ob der Client und der Kanal eine logische Sitzung auf der Transportebene unterhalten (daher der Name). Wie bei der traditionellen Programmierung werden auch bei einer Transportsitzung die Aufrufe (oder besser gesagt, die Nachrichten) strikt in der Reihenfolge verarbeitet, in der sie empfangen wurden. Die Transportsitzung hat nichts mit einer Sitzung auf Anwendungsebene zu tun, die der Client mit der Instanz selbst haben kann oder auch nicht. Die Verwendung einer Transportsitzung ist optional und hängt größtenteils von der Konfiguration der Bindung ab, d. h. der Client und der Dienst können eine Transportsitzung haben oder auch nicht. Die Transportsitzung ist eines der wichtigsten Grundkonzepte der WCF, das sich auf die Zuverlässigkeit, das Instanzmanagement, das Fehlermanagement, die Synchronisierung, die Transaktionen und die Sicherheit auswirkt.

Eine Transportsitzung beruht auf der Fähigkeit der WCF, den Kunden zu identifizieren und alle seine Nachrichten einem bestimmten Kanal zuzuordnen. Es muss also etwas im Transport oder in der Nachricht enthalten sein, das den Kunden identifiziert.

Transport Session und Bindung

Die TCP-, IPC- und WebSocket-Bindungen sind verbindungsvoll. Das heißt, alle Aufrufe des Clients kommen über dieselbe Verbindung oder Pipe, so dass die WCF den Client leicht identifizieren kann. HTTP ist jedoch per Definition ein verbindungsloses Protokoll, und jede Nachricht vom Client kommt über eine neue Verbindung. Daher gibt es bei der Basisverbindung nie eine Transportsitzung. Genauer gesagt, gibt es eine Transportsitzung, aber sie dauert nur für einen Aufruf und nachdem der Aufruf zurückkommt, wird der Kanal zusammen mit der Verbindung zerstört. Der nächste Aufruf erfolgt über eine neue Verbindung und wird an einen neuen Kanal weitergeleitet. Die WS-Bindung kann diese Situation verbessern, indem sie eine Transportsitzung emuliert. Wenn sie so konfiguriert ist, fügt sie in jede Nachricht eine eindeutige ID ein, die den Kunden identifiziert, und sendet diese ID bei jedem Anruf von diesem Kunden weiter. Mehr über diese ID erfährst du in Kapitel 4.

Transport Session Termination

Normalerweise wird die Transportsitzung beendet, sobald der Kunde den Proxy schließt. Für den Fall, dass der Kunde die Sitzung unfreiwillig beendet oder ein Kommunikationsproblem auftritt, verfügt jede Transportsitzung über eine Leerlaufzeit, die standardmäßig auf 10 Minuten eingestellt ist. Die Transportsitzung wird nach 10 Minuten Inaktivität des Clients automatisch beendet, auch wenn der Client den Proxy noch nutzen möchte. Wenn der Kunde versucht, seinen Proxy zu benutzen, nachdem die Transportsitzung aufgrund der Leerlaufzeit beendet wurde, erhält er die Meldung CommunicationObjectFaultedException. Du kannst unterschiedliche Timeouts für den Kunden und den Dienst konfigurieren, indem du in der Bindung unterschiedliche Werte einstellst. Die Bindungen, die eine Sitzung auf Transportebene unterstützen, bieten die Eigenschaft ReliableSession, die vom Typ ReliableSession oder OptionalReliableSession sein kann. Die Klasse ReliableSession bietet die Eigenschaft InactivityTimeout, mit der du eine neue Leerlaufzeitüberschreitung konfigurieren kannst:

public class ReliableSession
{
   public TimeSpan InactivityTimeout
   {get;set;}
   //More members
}
public class OptionalReliableSession : ReliableSession
{...}
public class NetTcpBinding : Binding,...
{
   public OptionalReliableSession ReliableSession
   {get;set}
   //More members
}
public abstract class WSHttpBindingBase : ...
{
   public OptionalReliableSession ReliableSession
   {get;set}
   //More members
}
public class WSHttpBinding : WSHttpBindingBase,...
{...}

Hier ist zum Beispiel der Code, der erforderlich ist, um eine Leerlaufzeit von 25 Minuten für die TCP-Verbindung zu programmieren:

NetTcpBinding tcpSessionBinding = new NetTcpBinding();
tcpSessionBinding.ReliableSession.InactivityTimeout = TimeSpan.FromMinutes(25);

Hier ist die äquivalente Konfigurationseinstellung mit Hilfe einer Konfigurationsdatei:

<netTcpBinding>
   <binding name = "TCPSession">
      <reliableSession inactivityTimeout = "00:25:00"/>
   </binding>
</netTcpBinding>

Wenn sowohl der Kunde als auch der Dienst eine Zeitüberschreitung konfigurieren, hat die kürzere Zeitüberschreitung Vorrang.

Hinweis

Es gibt eine weitere esoterische service-seitige Konfiguration für die Sitzungsbeendigung: Das Attribut ServiceBehavior bietet eine erweiterte Option für die Verwaltung der Sitzungsbeendigung über die Eigenschaft AutomaticSessionShutdown. Diese Eigenschaft ist für die Optimierung bestimmter Callback-Szenarien gedacht und kann in den meisten Fällen ignoriert werden. Kurz gesagt: AutomaticSessionShutdown ist standardmäßig auf true eingestellt, so dass die Sitzung beendet wird, wenn der Client den Proxy schließt. Wenn du es auf false setzt, wird die Sitzung fortgesetzt, bis der Dienst seinen Sendekanal explizit schließt. Wenn dieses Attribut auf false gesetzt ist, muss der Client einer Duplex-Sitzung (wird in Kapitel 5 besprochen) die Ausgabesitzung auf dem Duplex-Client-Kanal manuell schließen; andernfalls bleibt der Client hängen und wartet auf die Beendigung der Sitzung.

Verlässlichkeit

WCF und andere serviceorientierte Technologien unterscheiden zwischen Transportzuverlässigkeit und Nachrichtenzuverlässigkeit. Die Transportzuverlässigkeit (wie die von TCP/IP) bietet eine garantierte Punkt-zu-Punkt-Zustellung auf der Ebene der Netzwerkpakete und garantiert auch die ordnungsgemäße Zustellung der Pakete. Die Transportzuverlässigkeit ist nicht resistent gegen unterbrochene Netzwerkverbindungen oder eine Vielzahl anderer Kommunikationsprobleme.

Beider Nachrichtenzuverlässigkeit geht es, wie der Name schon sagt, um die Zuverlässigkeit auf der Ebene der Nachricht, unabhängig davon, wie viele Pakete für die Zustellung der Nachricht erforderlich sind. Die Nachrichtenzuverlässigkeit bietet eine Ende-zu-Ende-Garantie für die Zustellung und die Reihenfolge der Nachrichten, unabhängig davon, wie viele Vermittler beteiligt sind und wie viele Netzwerksprünge erforderlich sind, um die Nachricht vom Kunden zum Dienst zu liefern. Die Nachrichtenzuverlässigkeit basiert auf einem Industriestandard für zuverlässige nachrichtenbasierte Kommunikation, der eine Sitzung auf der Transportebene aufrechterhält und Wiederholungsversuche bei Transportfehlern, wie z. B. dem Abbruch einer Funkverbindung, unterstützt. Sie kümmert sich automatisch um Überlastung, Nachrichtenpufferung und Flusskontrolle und kann den Nachrichtenfluss entsprechend anpassen. Die Nachrichtenzuverlässigkeit kümmert sich auch um die Verbindungsverwaltung, indem sie Verbindungen prüft und auflöst, wenn sie nicht mehr benötigt werden.

Hinweis

Die Nachrichtenzuverlässigkeit garantiert nicht die Zustellung der Nachricht. Sie garantiert nur, dass der Absender davon erfährt, wenn die Nachricht ihr Ziel nicht erreicht.

Bindungen, Verlässlichkeit und geordnete Nachrichten

In WCF kontrollierst und konfigurierst du die Zuverlässigkeit in der Bindung. Eine bestimmte Bindung kann zuverlässiges Messaging unterstützen oder nicht unterstützen, und wenn es unterstützt wird, kannst du es aktivieren oder deaktivieren. Ob eine Bindung Zuverlässigkeit unterstützt, hängt vom Zielszenario für die jeweilige Bindung ab. Tabelle 1-2 fasst die Beziehung zwischen Bindung, Zuverlässigkeit und bestellter Zustellung für die fünf empfohlenen Bindungen zusammen und listet die jeweiligen Standardwerte auf.

Tabelle 1-2. Verlässlichkeit und bestellte Lieferung
Name der Bindung Unterstützt die Zuverlässigkeit Standard-Zuverlässigkeit Unterstützt die bestellte Lieferung Standardmäßig bestellte Lieferung
BasicHttpBinding Nein N/A Nein N/A
NetTcpBinding Ja Aus Ja Auf
NetNamedPipeBinding Nein N/A (Ein) Ja N/A (Ein)
WSHttpBinding Ja Aus Ja Auf
NetMsmqBinding Nein N/A Nein N/A

BasicHttpBinding und NetMsmqBinding unterstützen keine Zuverlässigkeit. Die BasicHttpBinding ist auf die alte ASMX-Webdienstwelt ausgerichtet, die keine Zuverlässigkeit unterstützt, während die NetMsmqBinding für nicht verbundene Anrufe gedacht ist und ihre eigene Vorstellung von Zuverlässigkeit hat (siehe Kapitel 9).

Die Zuverlässigkeit ist standardmäßig deaktiviert, aber du kannst sie in den Bindungen NetTcpBinding und WSHttpBinding aktivieren. Schließlich gilt NetNamedPipeBinding als inhärent zuverlässig, weil es immer genau einen Hop vom Client zum Service gibt.

Die Zuverlässigkeit der Nachricht bietet auch eine geordnete Zustellungsgarantie. Sie ermöglicht die Ausführung von Nachrichten in der Reihenfolge, in der sie gesendet wurden, und nicht in der Reihenfolge, in der sie zugestellt wurden. Außerdem garantiert sie, dass jede Nachricht genau einmal zugestellt wird.

In WCF kannst du Reliability aktivieren, nicht aber Ordered Delivery. In diesem Fall werden die Nachrichten in der Reihenfolge ausgeführt, in der sie empfangen wurden. Die Standardeinstellung für alle Bindungen, die Zuverlässigkeit unterstützen, ist, dass bei aktivierter Zuverlässigkeit auch die geordnete Zustellung aktiviert ist. Die geordnete Zustellung setzt Zuverlässigkeit voraus. Wenn also die geordnete Zustellung aktiviert, die Zuverlässigkeit aber deaktiviert ist, werden die Aufrufe nicht in der richtigen Reihenfolge zugestellt.

Verlässlichkeit konfigurieren

Du kannst die Zuverlässigkeit (und die bestellte Lieferung) sowohl programmatisch als auch administrativ konfigurieren. Wenn du die Zuverlässigkeit aktivierst, musst du dies sowohl auf der Client- als auch auf der Hostseite des Dienstes tun, sonst kann der Client nicht mit dem Dienst kommunizieren. Du kannst die Zuverlässigkeit nur für die Bindungen konfigurieren, die sie unterstützen. Beispiel 1-25 zeigt eine service-seitige Konfigurationsdatei, die einen binding Konfigurationsabschnitt verwendet, um die Zuverlässigkeit bei der Verwendung der TCP-Bindung zu aktivieren.

Beispiel 1-25. Aktivieren der Zuverlässigkeit mit der TCP-Bindung
<system.serviceModel>
   <services>
      <service name = "MyService">
         <endpoint
            address  = "net.tcp://localhost:8000/MyService"
            binding  = "netTcpBinding"
            bindingConfiguration = "ReliableTCP"
            contract = "IMyContract"
         />
      </service>
   </services>
   <bindings>
      <netTcpBinding>
         <binding name = "ReliableTCP">
            <reliableSession enabled = "true"/>
         </binding>
      </netTcpBinding>
   </bindings>
</system.serviceModel>

Was die programmatische Konfiguration angeht, bieten sowohl die TCP- als auch die WS-Bindungen einen Konstruktionsparameter und eine Eigenschaft zur Konfiguration der Zuverlässigkeit. Die Bindung NetTcpBinding akzeptiert zum Beispiel einen booleschen Konstruktionsparameter, um die Zuverlässigkeit zu aktivieren:

public class NetTcpBinding : Binding,...
{
   public NetTcpBinding(...,bool reliableSessionEnabled);
   //More members
}

Du kannst die Zuverlässigkeit auch nach dem Bau aktivieren, indem du die Eigenschaft ReliableSession aufrufst:

public class ReliableSession
{
   public bool Ordered
   {get;set;}
   //More members
}
public class OptionalReliableSession : ReliableSession
{
   public bool Enabled
   {get;set;}
   //More members
}
public class NetTcpBinding : Binding,...
{
   public OptionalReliableSession ReliableSession
   {get;}
   //More members
}

Bestellte Lieferung anfordern

Theoretisch sollten der Dienstcode und die Vertragsdefinition unabhängig von der verwendeten Bindung und ihren Eigenschaften sein. Der Dienst sollte sich nicht um die Bindung kümmern, und nichts im Dienstcode hat mit der verwendeten Bindung zu tun. Der Dienst sollte in der Lage sein, mit jedem Aspekt der konfigurierten Bindung zu arbeiten. In der Praxis kann es jedoch vorkommen, dass die Implementierung des Dienstes oder der Vertrag selbst von der geordneten Zustellung der Nachrichten abhängt. Damit der Vertrags- oder Dienstentwickler die zulässigen Bindungen einschränken kann, definiert die WCF die DeliveryRequirementsAttribute:

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Interface,
                AllowMultiple = true)]
public sealed class DeliveryRequirementsAttribute : Attribute,...
{
   public Type TargetContract
   {get;set;}
   public bool RequireOrderedDelivery
   {get;set;}

   //More members
}

Du kannst das Attribut DeliveryRequirements auf der Dienstebene anwenden und damit alle Endpunkte des Dienstes betreffen oder nur die Endpunkte, die einen bestimmten Vertrag ausstellen. Wenn du das Attribut auf Dienstebene anwendest, ist die Anforderung einer geordneten Lieferung eine Entscheidung der Implementierung. Wenn du z. B. möchtest, dass alle Endpunkte des Dienstes, unabhängig von den Verträgen, die geordnete Zustellung aktiviert haben, musst du das Attribut direkt auf die Dienstklasse anwenden:

[DeliveryRequirements(RequireOrderedDelivery = true)]
class MyService : IMyContract,IMyOtherContract
{...}

Indem du die Eigenschaft TargetContract setzt, kannst du verlangen, dass nur Endpunkte des Dienstes, die den angegebenen Vertrag unterstützen, eine verlässliche bestellte Lieferung erhalten:

[DeliveryRequirements(TargetContract = typeof(IMyContract),
                      RequireOrderedDelivery = true)]
class MyService : IMyContract,IMyOtherContract
{...}

Du kannst das Attribut auch auf Vertragsebene verwenden, was sich auf alle Dienste auswirkt, die diesen Vertrag unterstützen. Wenn du das Attribut auf Vertragsebene anwendest, ist die Anforderung einer bestellten Lieferung eine Designentscheidung. Die Durchsetzung der Bedingung erfolgt zum Zeitpunkt des Ladens des Dienstes. Wenn ein Endpunkt eine Bindung hat, die keine Verlässlichkeit unterstützt, die Verlässlichkeit unterstützt, aber deaktiviert ist, oder die Verlässlichkeit aktiviert, aber die bestellte Lieferung deaktiviert ist, schlägt das Laden des Dienstes mit einer InvalidOperationException fehl.

Indem du das Attribut DeliveryRequirements auf die Vertragsschnittstelle anwendest, wendest du die Einschränkung auf alle Dienste an, die sie unterstützen:

[ServiceContract]
[DeliveryRequirements(RequireOrderedDelivery = true)]
interface IMyContract
{...}

class MyService : IMyContract
{...}

class MyOtherService : IMyContract
{...}

Der Standardwert von RequireOrderedDelivery ist false, so dass die Anwendung des Attributs keine Auswirkungen hat. Diese Anweisungen sind zum Beispiel gleichwertig:

[ServiceContract]
interface IMyContract
{...}

[ServiceContract]
[DeliveryRequirements]
interface IMyContract
{...}

[ServiceContract]
[DeliveryRequirements(RequireOrderedDelivery = false)]
interface IMyContract
{...}
Hinweis

Die IPC-Bindung erfüllt die Bedingung der bestellten Lieferung.

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.