One (AppFabric) workflow with multiple persistence stores
For a new project, we decided to model and implement the business processes of our customers using Windows Workflow Foundation 4.
To do this we use a single workflow definition in the form of a workflow service host (.xamlx) in IIS, which depending on the type of process invokes different final actions. To persist our workflow instances and manage their lifecycle we use Windows Server AppFabric.
Initially, we started with the default configuration: a single persistence database separate from our customer databases, but this presented a problem: in rollback scenarios, only rolling back the database of one customer would cause the workflows and the customer’s other business data to get out of sync. One solution would be to also rollback the workflow database, however, in a scenario with multiple customers who all use the same persistence database this is no option either.
In the end, the best approach would be to have a customer’s business data and workflow persistence data in the same location, so both can be managed at the same time. However, AppFabric doesn’t have built-in support for the scenario in which we have a single WF service definition, which is exposed to all customers, which then use their own instance stores for persistence.
So we had to create support for this scenario ourselves and the schematic solution consisted of the following steps:
- Create a factory for the xamlx service, which enables us to set the persistence store for a new workflow service host in code based on the customer.
- Tell the factory which customer should be serviced for a request. The only way in which we can do this is by exposing the factory on a different url for every customer.
- To expose the factory dynamically based on the customers in the database, we used a virtual path provider.
Technically what we did was:
1 – In Global.asax of the web project register add the virtual path provider:
protected override void OnApplicationStarted()
{
HostingEnvironment.RegisterVirtualPathProvider(new MyPathProvider());
base.OnApplicationStarted();
}
2 – The virtual path provider uses a simple parser – MyServiceUrlParser (not shown) – to find out which customer is connecting (unique id in the url), and if it ends on “svc” it returns a virtual file (MyServiceFile) with a service host definition. The first two overrides are necessary for serving the file, the last two for preventing IIS from throwing exceptions for not finding the folder or file in cache:
public class MyPathProvider : VirtualPathProvider
{
public override bool FileExists(string virtualPath)
{
return !string.IsNullOrWhiteSpace(MyServiceUrlParser.Parse(virtualPath).Service) || base.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
var match = MyServiceUrlParser.Parse(virtualPath);
if (match.ServiceType.EndsWith("svc")) return new MyServiceFile(virtualPath);
if (match.ServiceType.EndsWith("xamlx")) return base.GetFile("~/Workflows/MyFlow.xamlx");
return base.GetFile(virtualPath);
}
public override System.Web.Caching.CacheDependency GetCacheDependency(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, DateTime utcStart)
{
return MyServiceUrlParser.IsValidServiceUrl(virtualPath) ? null : base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
public override bool DirectoryExists(string virtualDir)
{
return MyServiceUrlParser.IsValidServiceUrl(virtualDir) || base.DirectoryExists(virtualDir);
}
}
3 – The virtual file (svc) provided at this customer dependent path, this will trigger the IIS to load the MyFlow.xamlx from the customer specific url. Since our virtualpath provider is setup to return MyFlow.xamlx fom any xamlx request, it will service the one workflow for all customers:
public class MyServiceFile : VirtualFile
{
public MyServiceFile(string virtualPath) : base(virtualPath) { }
//return the same factory and service for the same url...only the factory will specify another persistance db based on the url
public override Stream Open()
{
var serviceDef = new MemoryStream();
var defWriter = new StreamWriter(serviceDef);
// Write host definition
defWriter.Write("");
defWriter.Flush();
serviceDef.Position = 0;
return serviceDef;
}
}
4 – finally the factory for setting the persistence store:
public class DynamicHostFactory : WorkflowServiceHostFactory
{
protected override WorkflowServiceHost CreateWorkflowServiceHost(WorkflowService service, Uri[] baseAddresses)
{
var host = new WorkflowServiceHost(service, baseAddresses);
host.DurableInstancingOptions.InstanceStore = SetupInstanceStore(baseAddresses);
return host;
}
private static SqlWorkflowInstanceStore SetupInstanceStore(Uri[] baseaddresses)
{
//do stuff based on the url
}
}
And there we have it, a single xamlx workflow definition with a dedicated persistence database per customer.