VirtualPathProvider

In .Net 3.5, WF services were introduced making it easier to host workflows as services.  With the send and receive activities, a workflow can be used instead of coding a service by hand.  The common way to expose workflows as services is to place the .svc/.xoml or .xamlx files and supporting libraries in the host path.  This works well if you have the workflow definitions stored in files.  If you would like to store your definition in a database or generate the workflow from code, you have to do some of your own coding to make that happen.  By using a VirtualPathProvider, you can use the existing WF services constructs and reduce the amount of infrastructure code you have to write.

One of the reasons I like the VirtualPathProvider is that it provides me a simple way of testing many concurrent workflow definitions without having to code-generate a bunch of files, transfer them to different machines, etc.  When you're trying to memory test 1,000,000 workflow definitions of "average" size, it's best to avoid having a directory with one xamlx file before definition.  One nice part about a VirtualPathProvider is that it doesn't prevent you from creating and using xamlx files.

For this test code, I will walk through creating a VPP that hosts WF4 services.  The first step is to create a new project with the template "ASP.Net Empty Web Application".  I called mine Wf4VppTest. 

The class that I'll start out with is the VirtualPathProvider itself:

 using System.Text.RegularExpressions;
using System.Web.Hosting;

namespace Wf4VppTest
{
    public class Wf4VirtualPathProvider : VirtualPathProvider
    {
        internal static Regex rePath = new Regex(@".*(Workflow\d+)\.xamlx$", RegexOptions.Compiled);

        public override bool FileExists(string virtualPath)
        {
            return base.FileExists(virtualPath) || rePath.IsMatch(virtualPath);
        }

        public override VirtualFile GetFile(string virtualPath)
        {
            if (rePath.IsMatch(virtualPath))
            {
                return new Wf4VirtualFile(virtualPath);
            }
            return base.GetFile(virtualPath);
        }
    }
}

The code is pretty simple.  The virtualPath passed in could be indicating a file that exists in the virtual directory such as CalculatorService.svc, so I first check that condition.  If there is no file there, then I compare the virtualPath against a regular expression that looks for Workflow#.xamlx where # is any number of digits.  For the purposes of performance testing, I don't really care what number appears there as the workflow definition I return won't be affected by it.  But you could certainly do anything you want here such as look for a Guid to identify what workflow you want to run.  In the above code, I used Wf4VirtualFile so that is the next class to create:

 using System.IO;
using System.Web.Hosting;

namespace Wf4VppTest
{
    public class Wf4VirtualFile : VirtualFile
    {
        public Wf4VirtualFile(string virtualPath) : base(virtualPath) { }

        public override System.IO.Stream Open()
        {
            MemoryStream ms = new MemoryStream();
            StreamWriter sw = new StreamWriter(ms);
            sw.Write(@"
<p:Sequence DisplayName=""Sequential Service""
            xmlns=""https://schemas.microsoft.com/netfx/2009/xaml/servicemodel""
            xmlns:p=""https://schemas.microsoft.com/netfx/2009/xaml/activities""
            xmlns:contract=""https://tempuri.org/""
            xmlns:x=""https://schemas.microsoft.com/winfx/2006/xaml"">
  <p:Sequence.Variables>
    <p:Variable x:TypeArguments=""CorrelationHandle"" Name=""handle"" />
    <p:Variable x:TypeArguments=""x:Int32"" Name=""data"" />
  </p:Sequence.Variables>
  <Receive x:Name=""__ReferenceID0"" DisplayName=""ReceiveRequest"" OperationName=""GetData"" ServiceContractName=""contract:IService"" CanCreateInstance=""True"">
    <Receive.CorrelationInitializers>
      <RequestReplyCorrelationInitializer CorrelationHandle=""[handle]"" />
    </Receive.CorrelationInitializers>
    <ReceiveMessageContent>
      <p:OutArgument x:TypeArguments=""x:Int32"">[data]</p:OutArgument>
    </ReceiveMessageContent>
  </Receive>
  <SendReply Request=""{x:Reference Name=__ReferenceID0}"" DisplayName=""SendResponse"" >
    <SendMessageContent>
      <p:InArgument x:TypeArguments=""x:String"">[data.ToString()]</p:InArgument>
    </SendMessageContent>
  </SendReply>
</p:Sequence>
");
            sw.Flush();
            ms.Seek(0L, SeekOrigin.Begin);
            return ms;
        }
    }
}

The VirtualFile can return whatever content you would want a file to have.  In this case I'm returning actual xamlx code.  The HTTP modules that handle files with the .xamlx extension will run afterwards to create the WorkflowServiceHost to host the workflow definition.

There is one last step before we can try this out.  That is to register the VirtualPathProvider in the global.asax.  To do this, add a new item to the project using the "Global Application Class" template.  The code looks like this:

 using System;
using System.Web.Hosting;

namespace Wf4VppTest
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            Wf4VirtualPathProvider provider = new Wf4VirtualPathProvider();
            HostingEnvironment.RegisterVirtualPathProvider(provider);
        }
    }
}

As you can see, the code for registering the VirtualPathProvider is very simple.  At this point, you should be able to press F5.  No start page was selected so you will most likely see a directory browsing page in the browser.  Add "Workflow1.xamlx" to the end of the URL and you should see the WCF page describing how to use the service.  There is no metadata published right now, so let's add that in.  Add the <system.ServiceModel> node to the web.config as shown below:

 <?xml version="1.0"?>
<configuration>
    <system.web>
        <compilation debug="true" targetFramework="4.0" />
    </system.web>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

This sets the default service behavior to allow metadata publishing.  Now you should be able to visit Workflow1.xamlx?wsdl.  To test this out, we can try the WCF Test Client.  It's located at "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\WcfTestClient.exe".  This tool should be pretty self-explanatory if you haven't already seen it.  You should be able to add the service, invoke the GetData operation, and receive a response.  Also try adding Workflow2.xamlx or Workflow1337.xamlx.  Notice that anything that doesn't meet the regex like Workflow.xamlx or SomethingElse.xamlx will return a 404 unless you explicitly create those files in the directory.

Obviously a VirtualPathProvider is useful for many other things besides workflow.  In my case, I used it as a means to gather memory data for the WF4 performance whitepaper.