Jaa


How to create a WF4 WorkflowServiceHostFactory for AppFabric Auto-Start

Sometimes when using Windows Workflow Foundation (WF4) you need to create a custom WorkflowServiceHostFactory.  There are a couple of main reasons why you would want to do this.

  1. To configure the WorklfowServiceHost in some way like adding an extension
  2. To do some other type of initialization work when auto starting a service

This morning I was working on the WF4 Batch Processing Example (now Workflow Services and Auto-Start sample) to update it with things I’ve learned in the past few months.  I decided to create a custom WorkflowServiceHostFactory in order to take advantage of the Windows Server AppFabric Auto-Start capability.

Step 1: Create a class that inherits from WorkflowServiceHostFactory

This sounds like a simple step right?  The question is… which WorkflowServiceHostFactory?  That’s right… beware because there is more than one in the .NET Framework

I use Resharper because it is an amazing time saving tool but in this case it led me down the path to disaster because it works so well.  When Resharper sees a class it doesn’t understand it quite helpfully gives you some options.

image

Because I was in a hurry I quickly selected the first option and then spent the next 2 hours struggling to understand why it didn’t work.  Had I looked carefully I would have noticed the problem right away.  System.WorkflowServices.WorkflowServiceHostFactory is from WF 3.5.  What I should be using is System.ServiceModel.Activities.Activation.WorkflowServiceHostFactory from WF4.

Typically when you get confused about things between WF3 and WF4 you will find that they simply won’t compile but in this case the WF3 WorkflowServiceHost factory will happily compile even in the very strange case where you “use” the namespace from WF4 and reference the assembly from WF3. 

Why does this work?  Because the two classes do the same thing.  They both inherit from ServiceHostBase and override the CreateServiceHost method.

However, if you use the wrong one you will get this exception

The service '/BatchWeb/BWS2.xamlx' cannot be activated due to an exception during compilation.  The exception message is: The file '/BatchWeb/BWS2.xamlx' does not exist..

Make sure you reference System.ServiceModel.Activation and use System.ServiceModel.Activities.Activation.WorkflowServiceHostFactory

Then you can implement your factory class.  In my case I needed to update the WF4 Batch Job example because it relied on code in the Application_Start method to read the config file and start a batch job.  At the time I thought this would be invoked when the system booted and automatically start a job. 

When I tested the app it appeared to work but only because there were already persisted jobs with durable timers pending.  At boot time the Workflow Management Service would activate the app by sending a message to activate the jobs because of the timer.  So it had the illusion of working but actually if there were no jobs with persisted timers the AppPool would never start the app.

When I dug deep into how AutoStart actually works I wrote WCF and AppFabric Auto-Start and said that the only way you can run code that is triggered by the AppFabric Service Auto-Start function is to use a custom service host factory.

In my code what I want to do is on the first start read the configuration file and send a request to submit a new batch job.  This is done every time a new instance of the AppPool starts up.  This means at boot time but also any time the AppPool recycles.  This ensures that there is always at least one of the workflows running and for short periods of time more than one during a recycle.

In my factory I need to send a message to the service that will be hosted.  Obviously I can’t send the message before returning the host so I’m using Task.Factory.StartNew to dispatch the work to a different thread.

    1: public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses)
    2: {
    3:     // Run the auto start check on another thread.
    4:     Task.Factory.StartNew(CheckAutoStartJobs);
    5:  
    6:     return base.CreateServiceHost(constructorString, baseAddresses);
    7: }

Then on the other thread, I wait a few seconds, read the config entries and then send a message to the service over a named pipe

    1: private static void CheckAutoStartJobs()
    2: {
    3:     if (autoStarted)
    4:     {
    5:         return;
    6:     }
    7:     else
    8:     {
    9:         autoStarted = true;
   10:     }
   11:  
   12:     // Wait a few seconds for the host to spin up
   13:     Thread.Sleep(TimeSpan.FromSeconds(5));
   14:  
   15:     // Check for autostart
   16:     var autoCount = ConfigurationManager.AppSettings["AutoCount"];
   17:  
   18:     if (string.IsNullOrWhiteSpace(autoCount))
   19:     {
   20:         return;
   21:     }
   22:  
   23:     var values = autoCount.Split('|');
   24:  
   25:     // Check for valid value
   26:     if (values.Length != 2)
   27:     {
   28:         return;
   29:     }
   30:  
   31:     int count;
   32:     if (!Int32.TryParse(values[0], out count))
   33:     {
   34:         return;
   35:     }
   36:  
   37:     int delay;
   38:     if (!Int32.TryParse(values[1], out delay))
   39:     {
   40:         return;
   41:     }
   42:  
   43:     var request = new BatchRequest
   44:         {
   45:             CountTo = count, 
   46:             Delay = TimeSpan.FromSeconds(delay), 
   47:             StartAt = DateTime.Now, 
   48:             AutoStart = true
   49:         };
   50:  
   51:     var proxy = new BatchWorkerClient(
   52:         new NetNamedPipeBinding(), 
   53:         new EndpointAddress("net.pipe://localhost/BatchWeb/BatchWorker.xamlx"));
   54:     try
   55:     {
   56:         var response = proxy.SubmitJob(request);
   57:         proxy.Close();
   58:     }
   59:     catch
   60:     {
   61:         proxy.Abort();
   62:         throw;
   63:     }
   64: }

Step 2: Add ServiceActivation in web.config

With a WCF Service you can add a ServiceHostFactory entry in the .SVC file.  Workflow Services don’t have an SVC file so you will need to edit your web.config to add support for a factory

    1: <system.serviceModel>
    2: <!-- Other stuff deleted -->
    3:     <serviceHostingEnvironment>
    4:       <serviceActivations>
    5:         <!-- Use a Custom WorkflowServiceHostFactory to automatically start the batch jobs-->
    6:         <add relativeAddress="BatchWorker.xamlx" service="BatchWorker.xamlx" factory="BatchWeb.BatchAutoStartFactory" />
    7:       </serviceActivations>
    8:     </serviceHostingEnvironment>
    9: </system.serviceModel>
   10:  

Step 3: Configure Auto-Start

  1. Open IIS Manager and right click on your web app.
  2. Select Manage WCF and WF Services / Configure
  3. Select the Auto-Start tab and choose Enabled

SNAGHTML25c5ce

Step 4: Test It

To test your service, I recommend you get my AppFabricDBReset script.

  1. Stop the AppPool that your web site is hosted in
  2. Run the AppFabricDBReset Script to eliminate any persisted instances
  3. Start the AppPool
  4. In a few seconds browse to the site and you will see that an instance of the batch job has been auto started

Step 5: Get It

Download the Windows Workflow Foundation (WF4) - Workflow Services and Auto-Start Sample

Ron Jacobs
https://blogs.msdn.com/rjacobs
Twitter: @ronljacobs https://twitter.com/ronljacobs

Comments

  • Anonymous
    April 27, 2011
    On a side note, you can use configuration to add WF extensions to the host with a custom WCF service behavior. I wrote one into my toolkit project late last year. The BehaviorExtensionElement (neovolve.codeplex.com/.../70545) provides the configuration support for the IServiceBehavior (neovolve.codeplex.com/.../70545) which then adds the WF extension to the host. This method allows you to chop and change extensions as required via config. It also means you don't need a custom WorkflowServiceHostFactory. :)

  • Anonymous
    April 27, 2011
    @Rory - cool - I had thought about building such a behavior myself previously - glad to know you did it!