Um in .NET einen WCF-Dienst auszuführen, muss prinzipiell lediglich eine Instanz der Klasse ServiceHost erzeugt werden, die anschließend das Hosting für den jeweiligen Dienst übernimmt.
Nahezu jedes Buch zu WCF enthält dementsprechend ein Beispiel, wie man einen einfachen Host für WCF-Dienste auf Basis der Konsole entwickelt. Auf Grund gewisser Anforderungen wie Verfügbarkeit, Skalierbarkeit und ähnlichem genügt ein solcher Host in der Praxis jedoch in den seltensten Fällen.
Da .NET erst ab der Version 4.0 über einen Application Server verfügen wird, stellt derzeit der IIS die beste Alternative dar, da dieser neben Webseiten auch WCF-Dienste hosten kann. Allerdings benötigt der IIS hierfür in jedem Fall eine .svc-Datei, die auf den eigentlichen Dienst verweist:
<% @ServiceHost language="C#" Debug="true" Service="MyService" CodeBehind="~/App_Code/MyService.cs" %>
Sofern man Wert auf ein einfaches Deployment legt und den WCF-Dienst in einer einzigen DLL bereitstellen will, die sämtlichen Code enthält, stößt man nun auf ein Problem: Es ist scheinbar unter WCF nicht möglich, die .svc-Datei ebenfalls in die DLL auszulagern – es müssen also immer zwei Dateien angefasst werden.
Doch es gibt eine Lösung für dieses Problem: ASP.NET stellt im Namensraum System.Web.Hosting die Klasse VirtualPathProvider zur Verfügung, mit deren Hilfe ein virtuelles Dateisystem implementiert werden kann.
Um also einen WCF-Dienst ohne .svc-Datei im IIS zu hosten, muss ein eigener VirtualPathProvider implementiert werden, der auf Anfragen nach der Datei Service.svc mit einer per Code erzeugten Variante reagiert.
Damit ein VirtualPathProvider funktioniert, müssen die beiden Methoden FileExists und GetFile überschrieben werden, die zudem prüfen müssen, ob der aktuelle Provider überhaupt zuständig ist – falls nicht, wird die Ausführung an den nächsten in ASP.NET registrierten Provider übergeben:
using System.Web.Hosting;
using System.Web;
namespace silkveil.net.Wcf
{
/// <summary>
/// Provides activation for a WCF service without the need for a .svc file.
/// </summary>
public class ServiceActivationProvider : VirtualPathProvider
{
/// <summary>
/// Checks whether the specified path is a virtual one.
/// </summary>
/// <param name="virtualPath">The virtual path.</param>
/// <returns><c>true</c> if the path is virtual; otherwise <c>false</c>.</returns>
private static bool IsVirtualPath(string virtualPath)
{
// Check whether the path is virtual.
virtualPath = VirtualPathUtility.ToAppRelative(virtualPath);
return virtualPath == "~/Service.svc";
}
/// <summary>
/// Gets a value that indicates whether a file exists in the virtual file system.
/// </summary>
/// <returns>
/// <c>true</c> if the file exists in the virtual file system; otherwise, <c>false</c>.
/// </returns>
/// <param name="virtualPath">The path to the virtual file.</param>
public override bool FileExists(string virtualPath)
{
// If the path is not virtual, delegate to the previous provider in the provider chain.
if(!IsVirtualPath(virtualPath))
{
return this.Previous.FileExists(virtualPath);
}
// Otherwise, always return true.
return true;
}
/// <summary>
/// Gets a virtual file from the virtual file system.
/// </summary>
/// <returns>
/// A descendent of the <see cref="T:System.Web.Hosting.VirtualFile" /> class that
/// represents a file in the virtual file system.
/// </returns>
/// <param name="virtualPath">The path to the virtual file.</param>
public override VirtualFile GetFile(string virtualPath)
{
// If the file does not exist in this virtual path provider, delegate execution to
// the previous provider in the provider chain.
if (!IsVirtualPath(virtualPath))
{
return this.Previous.GetFile(virtualPath);
}
// Create a Service.svc file on the fly and return it to the caller.
return new ServiceActivationFile(virtualPath);
}
}
}
Außer diesem Provider muss zusätzlich auch noch eine Klasse von VirtualFile abgeleitet werden, welche die eigentliche .svc-Datei erzeugt:
using System.IO;
using System.Web.Hosting;
namespace silkveil.net.Wcf
{
/// <summary>
/// Represents a virtual .svc file.
/// </summary>
public class ServiceActivationFile : VirtualFile
{
/// <summary>
/// Initializes a new instance of the <see cref="ServiceActivationFile" /> type.
/// </summary>
/// <param name="virtualPath">The virtual path.</param>
public ServiceActivationFile(string virtualPath) : base(virtualPath)
{
}
/// <summary>
/// Returns a read-only stream to the virtual resource.
/// </summary>
/// <returns>A read-only stream to the virtual file.</returns>
public override Stream Open()
{
// Create a stream.
Stream stream = new MemoryStream();
StreamWriter writer = new StreamWriter(stream);
// Write to the stream.
writer.Write("<%@ ServiceHost Language=\"C#\" Debug=\"false\" Service=\"silkveil.net.Wcf.Service\" %>");
writer.Flush();
// Reset the stream's pointer to its beginning and return the stream to the caller.
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
}
}
Kompiliert man diese beiden Klassen mit in die DLL, die den WCF-Dienst erhält, genügt es für ein Deployment zukünftig, nur noch diese DLL zu verteilen. Auf diese Art ist es ein Leichtes, beispielsweise eine bestehende ASP.NET-Anwendung um einen WCF-Dienst zu erweitern – ohne mit einer .svc-Datei hantieren zu müssen.
Allerdings muss der VirtualPathProvider noch registriert werden, damit ASP.NET von seiner Existenz weiß: Dazu genügt es, diesen beim Start der Anwendung in der Datei Global.asax zu registrieren:
// Initialize the service activation provider.
HostingEnvironment.RegisterVirtualPathProvider(new ServiceActivationProvider());
Der entscheidende Punkt an dieser Vorgehensweise ist, dass Dateien, die von einem VirtualPathProvider in ASP.NET geladen werden, nicht – wie beispielsweise bei einem HttpHandler – direkt in den Ausgabestrom geschrieben werden, sondern dass sie zunächst noch von der ASP.NET-Engine verarbeitet werden.
Damit wäre es also auch möglich, an Stelle einer .svc-Datei eine beliebige andere Datei per Code bereitzustellen – .Webseiten, Usercontrols, … einzig Dateien, welche die gesamte Anwendung betreffen, bleiben außen vor, wie beispielsweise die Global.asax und die web.config.
Für alles andere gilt: Der Fantasie sind dabei keine Grenzen gesetzt …