Hosting a WCF service in Windows Sharepoint Services v3.0

With ASP.NET AJAX Extensions being baked into the .NET Framework 3.5 and the improvements to WCF to support JSON, it seems tempting to write WCF services and host the in Windows Sharepoint Services 3.0

Unfortunately, if you create a WCF service and drop it under a WSS controlled vroot like _layouts or _vti_bin, your service will fail to activate with the following message in the event log:

WebHost failed to process a request.
Exception: System.ArgumentException: virtualPath
at System.ServiceModel.AsyncResult.End[TAsyncResult](IAsyncResult result)
at System.ServiceModel.Activation.HostedHttpRequestAsyncResult.End(IAsyncResult result)

If you attach a debugger to w3wp.exe and enable break on first CLR change exceptions, the stack trace at the time of the failure will look like:

virtualPath 

at Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.IsExcludedPath(Stri­ng virtualPath)
at Microsoft.SharePoint.ApplicationRuntime.SPVirtualPathProvider.FileExists(St­ring virtualPath)
at System.ServiceModel.ServiceHostingEnvironment.HostingManager.EnsureServiceA­vailable(String normalizedVirtualPath)
at System.ServiceModel.ServiceHostingEnvironment.EnsureServiceAvailableFast(St­ring relativeVirtualPath)
at System.ServiceModel.Activation.HostedHttpRequestAsyncResult.HandleRequest()
at System.ServiceModel.Activation.HostedHttpRequestAsyncResult.BeginRequest()
at System.ServiceModel.Activation.HttpHandler.ProcessRequest(HttpContext context)

After debugging the issue, it turns out¹ that WSS own VirtualPathProvider (SPVirtualPathProvider, briefly described by this MSDN page) refuses to handle requests for which the virtual path starts with "~". 

Now that we understand the issue, a workaround¹ is easy to implement. There are three steps:

  1. Write our own VirtualPathProvider. It will detect if the request is made to a WCF service and if so, it will remove the leading "~" and hand off the request to WSS virtual path provider,
  2. Arrange for our new virtual path provider to be inserted before WSS provider so we get a crack at all requests before WSS does. This will be done using an ASP.NET HttpModule,
  3. Perform a few changes to web.config to insert our new module in the ASP.NET pipeline.

The following paragraphs describe these steps.

A custom VirtualPathProvider

VirtualPathProviders were introduced in ASP.NET 2.0. They are commly used by web applications which want to serve ASP.NET pages saved in other locations that the file system (a SQL table is the most canonical example).

All VirtualPathProviders are daisy chained by ASP.NET. This means that any custom VirtualPathProvider has a "Previous" VirtualPathProvider it can erly on to perform standard operations. For instance, Sharepoint's SPVirtualPathProvider relies on the default VirtualPathProvider to serve ASP.NET pages stored on the file system¹.

For us, we will essentially use the "Previous" VirtualPathProvider (which happens to be of type SPVirtualProvider) to implement our provider. We will pass most requests untouched. We will only look at requests ending with ".svc" and starting with "~". Such a request will be treated as a WCF service call and we will "patch" the virtual path (by removing the leading "~"). The patched virtual path will then be handed off to the previous virtual patch provider for further processing.

NOTE: This causes *all* requests starting with "~" and ending with ".svc" to be patched. If you are going to use this code for a production, you should also restrict this patching process to virtual directories where services can be installed (that is everything under _layouts or _vti_bin). Failure to do so has security implications.

Here is the code for our custom VirtualPathProvider:

public class WCFVirtualPathProvider : VirtualPathProvider {

     public override string CombineVirtualPaths(string basePath, string relativePath) {
         return Previous.CombineVirtualPaths(basePath, relativePath);
}

     public override System.Runtime.Remoting.ObjRef CreateObjRef(Type requestedType) {
         return Previous.CreateObjRef(requestedType);
}

     public override bool DirectoryExists(string virtualDir) {
         return Previous.DirectoryExists(virtualDir);
}

     public override bool FileExists(string virtualPath) {
        // Patches requests to WCF services: That is a virtual path ending with ".svc"     
        string patchedVirtualPath = virtualPath;
        if (virtualPath.StartsWith("~", StringComparison.Ordinal) &&
virtualPath.EndsWith(".svc", StringComparison.InvariantCultureIgnoreCase))
{
patchedVirtualPath = virtualPath.Remove(0, 1);
}
        return Previous.FileExists(patchedVirtualPath);
}

     public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath,
System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart) {
        return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}

     public override string GetCacheKey(string virtualPath) {
        return Previous.GetCacheKey(virtualPath);
}

     public override VirtualDirectory GetDirectory(string virtualDir) {
        return Previous.GetDirectory(virtualDir);
}

     public override VirtualFile GetFile(string virtualPath) {
        return Previous.GetFile(virtualPath);
}

     public override string GetFileHash(string virtualPath, System.Collections.IEnumerable virtualPathDependencies) {
               return Previous.GetFileHash(virtualPath, virtualPathDependencies);
}

     protected override void Initialize() {
        base.Initialize();
}
}

Most method delegate work to the "Previous" virtual path. Virtual Paths appearing to be WCF service calls are patched.

Registering our VirtualPathProvider 

According to MSDN, VirtualPathProviders must be registered using HostingEnvironment.RegisterVirtualPathProvider. Moreover, VirtualPathProviders must be registered with ASP.NET before the first line of code is compiled. An HttpModule is the perfect place for us to register our provider. The module is trivial:

public class WCFPatchupModule : IHttpModule {

   static bool virtualPathProviderInitialized = false;
   static object virtualPathProviderInitializedSyncLock = new object();

   public void Dispose() {
   }

   public void Init(HttpApplication context) {
      if (!virtualPathProviderInitialized) {
         lock (virtualPathProviderInitializedSyncLock) {
             if (!virtualPathProviderInitialized) {
WCFVirtualPathProvider vpathProvider = new WCFVirtualPathProvider();
HostingEnvironment.RegisterVirtualPathProvider(vpathProvider);

virtualPathProviderInitialized = true;
}
}
}
}
}

Now, we need to compile the two classes above into a strongly named assembly and isntall it into the GAC.

Configuration changes

Assuming you have compiled the two classes above and signed the assembly, you are now ready to insert our HttpModule in the ASP.NET pipeline. This is easily done by changing the <httpModules> section of the sharepoint site as follows:

<httpModules>
<clear />
<add name="SPRequest" type="Microsoft.SharePoint.ApplicationRuntime.SPRequestModule, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" />
<add name="OutputCache" type="System.Web.Caching.OutputCacheModule" />
<add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />
<add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />
<add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" />
<add name="RoleManager" type="System.Web.Security.RoleManagerModule" />

<add name="WcfVirtualPathProvider" type="WcfVirtualPathProvider.WCFPatchupModule,
WcfVirtualPathProvider, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=[... put your assembly public key token here ...]"/>

<!-- <add name="Session" type="System.Web.SessionState.SessionStateModule"/> -->
</httpModules>

Our HttpModule will run after Sharepoint's "SPRequest" module. Since SPVirtualProvider is registered by the "SPRequest" module at startup¹, we know our VirtualPathProvider will always come into  play *after* Sharepoint's SPVirtualPathProvider and therefore, we will get a chance to patch the request's virtual path before it reaches Sharepoint. Of course, you will need to substitute your assembly name, version and public key token. 

You are now ready to test: deposit an WCF service under _layouts and call it. You should now see your service getting activated and calls should now be sucessful.

Disclaimer - Read Me !  

¹As with all statements of alleged fact, this statement is an interpretation of events based on my observation and thought and does not establish a statement of the official position of the Windows Sharepoint Services team or Microsoft Corporation. That interpretation may ultimately prove incorrect. Techniques and strategies described in this post should be considred "for entertainment only". This post is offered "as is".