Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Learn Windows Workflow Foundation 4.5 through Unit Testing: Workflow Services

5.00/5 (4 votes)
18 Apr 2016CPOL10 min read 12.5K  
Workflow WCF service

Overview

And this article is focused on hosting workflows as services, particularly for long running / persistable workflows.

Other articles in this series are given below:

For really long running workflows, using WorflowServiceHost and Workflow Persistence is the only way to go. However, somehow Workflow 4 and 4.5 had provided "weaker" supports for persistence, also, because of the deprecation of AppFabric, Workflow Management Service is not longer a viable solution. And we application developers have to write our own custom solutions to manage the lifecycle of workflows.

To some developers with experiences in earlier versions of Workflow Foundation prior to 4.0, such "weak" support for managing lifecycle of workflows is bad news resulting in great burdens for extra maintenance works and respective mindset shifting, unless the developers are working solely on Biztalk and Sharepoint with sophisticated workflow hosting.

Remarks

I consider that the move to not having built-in solution of persisting workflow definitions in WF 4 is a good move, since I think that many workflow applications do not need to persist the workflow definitions in a persistence layer, while the respective functions have knowledge of which workflow definitions to use when resuming a workflow instance, thus have no need for serialization and deserialization of workflows. For scenarios that need persisting workflow definitions, it is not hard to write custom solutions for example a dictionary. And the most tricky part I regard is scanning workflows hibernated in the persistence layer and loading them timely if not using Sharepoint or Biztalk.

 

References:

Using the Code

The source code is available at https://github.com/zijianhuang/WorkflowDemo.

Prerequisites:

  1. Visual Studio 2015 Update 1 or Visual Studio 2013 Update 4
  2. xUnit (included)
  3. EssentialDiagnostics (included)
  4. FonlowTesting (included)
  5. Workflow Persistence SQL database, with default local database WF.

Examples in this article are from a test class:  WorkflowServiceHostTests, WorkflowServiceHostPersistenceTests, WFServiceTests, WCFWithWorkflowTests, NonServiceWorkflowTests.

 

Non-Service Workflows on ServiceHost

Conditions

You have some non-persistable workflows that need to be running as a service.

Basic Solutions

You may create either a WCF service or Web API or any remote API technologies to trigger the execution of such workflows. Then use WorkflowInvoker.Invoke() or WorkflowApplication to execute the workflows in service functions.

Case 1

If you want the workflow to finish before returning response, you may use WorkflowInvoker.Invoke() in the implementation of the service function. However, obviously the execution of the workflow should not take over 1 minute, since the default timeout of many Web request operations on both service side and client side is 59.999 seconds. In such case, the execution of the workflow at the service side is just like a common function call.

Case 2

If you want the service function call to return immediately after the workflow execution being started, you may use WorkflowApplication since the Run() method is non-blocking. If the client does care about the result, the service function should return the Instance ID of the workflow application instance, so the client later could query the result with the ID, persisted by the logic of the workflow. In my tests, the WorkflowApplication instance is still running well after the service function had returned the response, and the variable holding the instance is out of scope of the service function block. Apparently the workflow runtime is still holding the reference to the WorkflowApplication instance so it won't be collected by GC. However, please bear in mind that, the WCF service host has no knowledge of the long running WorkflowApplication instance. The default idle recycle time of IIS is 20 minutes, so if the execution takes over 20 minutes, the instance may risk of being terminated abnormally, since the IIS has no knowledge of the long running WorkflowApplication instance.

 

Non-Service Workflows with Bookmark on ServiceHost

Conditions

You have some persistable workflows with bookmark that need to be running as a service.

Solutions

You may create either a WCF service or Web API or any remote API technologies to trigger the execution of such workflows. Then use WorkflowApplication to execute the workflows in service functions, however, you need to write a lot functions to manage the lifecycle of the workflow when idle.

The code below is for demonstrating how clunky could be if you write your own code without using the instruments of WorkflowServiceHost.

Service codes

C#
[ServiceContract]
public interface IWakeup
{
    /// <summary>
    /// instantiate a workflow and return the ID right after WorkflowApplication.Run()
    /// </summary>
    [OperationContract]
    Guid Create(string bookmarkName, TimeSpan duration);

    /// <summary>
    /// Reload persisted instance and run and return immediately.
    /// </summary>

    [OperationContract]
    bool LoadAndRun(Guid id);

    /// <summary>
    /// Send a bookmark call to a workflow running. If the workflow instance is not yet loaed upon other events, this call will reload the instance.
    /// </summary>
    [OperationContract]
    string Wakeup(Guid id, string bookmarkName);
}

public class WakeupService : IWakeup
{
    public Guid Create(string bookmarkName, TimeSpan duration)
    {
        var a = new WaitForSignalOrDelayWorkflow()
        {
            Duration = TimeSpan.FromSeconds(10),
            BookmarkName = bookmarkName,
        };

        var app = new WorkflowApplication(a)
        {
            InstanceStore = WFDefinitionStore.Instance.Store,
            PersistableIdle = (eventArgs) =>
            {
                return PersistableIdleAction.Unload;
            },

            OnUnhandledException = (e) =>
            {

                return UnhandledExceptionAction.Abort;
            },

            Completed = delegate (WorkflowApplicationCompletedEventArgs e)
            {
            },

            Aborted = (eventArgs) =>
            {

            },

            Unloaded = (eventArgs) =>
            {
            }
        };

        var id = app.Id;
        app.Run();
        return id;
    }

    public bool LoadAndRun(Guid id)
    {
        var app2 = new WorkflowApplication(new WaitForSignalOrDelayWorkflow())
        {
            Completed = e =>
            {
                if (e.CompletionState == ActivityInstanceState.Closed)
                {
                    System.Runtime.Caching.MemoryCache.Default.Add(id.ToString(), e.Outputs, DateTimeOffset.MaxValue);//Save outputs at the end of the workflow.
                }
            },

            Unloaded = e =>
            {
                System.Diagnostics.Debug.WriteLine("Unloaded in LoadAndRun.");
            },

            InstanceStore = WFDefinitionStore.Instance.Store,
        };

        try
        {
            app2.Load(id);
            app2.Run();
        }
        catch (System.Runtime.DurableInstancing.InstanceNotReadyException ex)
        {
            System.Diagnostics.Trace.TraceWarning(ex.Message);
            return false;
        }

        System.Runtime.Caching.MemoryCache.Default.Add(id.ToString()+"Instance", app2, DateTimeOffset.MaxValue);//Keep the reference to a running instance
        return true;
    }

    public string Wakeup(Guid id, string bookmarkName)
    {
        var savedDic = (System.Runtime.Caching.MemoryCache.Default.Get(id.ToString())) as IDictionary<string, object>;
        if (savedDic!=null)//So the long running process is completed because other events, before the bookmark call comes.
        {
            return (string)savedDic["Result"];
        }

        WorkflowApplication app2;
        var runningInstance = (System.Runtime.Caching.MemoryCache.Default.Get(id.ToString() + "Instance")) as WorkflowApplication;
        if (runningInstance!=null)//the workflow instance is already reloaded
        {
            app2 = runningInstance;
        }
        else
        {
            app2 = new WorkflowApplication(new WaitForSignalOrDelayWorkflow());
        }

        IDictionary<string, object> outputs=null;
        AutoResetEvent syncEvent = new AutoResetEvent(false);
        app2.Completed = e =>
        {
            if (e.CompletionState == ActivityInstanceState.Closed)
            {
                outputs = e.Outputs;
            }
            syncEvent.Set();
        };

        app2.Unloaded = e =>
        {
            syncEvent.Set();
        };

        if (runningInstance == null)
        {
            try
            {
                app2.InstanceStore = WFDefinitionStore.Instance.Store;
                app2.Load(id);
            }
            catch (System.Runtime.DurableInstancing.InstanceNotReadyException ex)
            {
                System.Diagnostics.Trace.TraceWarning(ex.Message);
                throw;
            }
        }

        var br = app2.ResumeBookmark(bookmarkName, null);

        switch (br)
        {
            case BookmarkResumptionResult.Success:
                break;
            case BookmarkResumptionResult.NotFound:
                throw new InvalidOperationException($"Can not find the bookmark: {bookmarkName}");
            case BookmarkResumptionResult.NotReady:
                throw new InvalidOperationException($"Bookmark not ready: {bookmarkName}");
            default:
                throw new InvalidOperationException("hey what's up");
        }

        syncEvent.WaitOne();
        if (outputs == null)
            throw new InvalidOperationException("How can outputs be null?");

        return (string)outputs["Result"];
    }
}

Client codes

C#
readonly Uri baseUri = new Uri("net.tcp://localhost/");

ServiceHost CreateHost()
{
    var host = new ServiceHost(typeof(Fonlow.WorkflowDemo.Contracts.WakeupService), baseUri);
    host.AddServiceEndpoint("Fonlow.WorkflowDemo.Contracts.IWakeup", new NetTcpBinding(), "wakeup");

    ServiceDebugBehavior debug = host.Description.Behaviors.Find<ServiceDebugBehavior>();

    if (debug == null)
    {
        host.Description.Behaviors.Add(
             new ServiceDebugBehavior() { IncludeExceptionDetailInFaults = true });
    }
    else
    {
        // make sure setting is turned ON
        if (!debug.IncludeExceptionDetailInFaults)
        {
            debug.IncludeExceptionDetailInFaults = true;
        }
    }

    return host;
}

[Fact]
public void TestWaitForSignalOrDelay()
{
    Guid id;
    // Create service host.
    using (var host = CreateHost())
    {
        host.Open();
        Assert.Equal(CommunicationState.Opened, host.State);

        // Create a client that sends a message to create an instance of the workflow.
        var client = ChannelFactory<Fonlow.WorkflowDemo.Contracts.IWakeup>.CreateChannel(new NetTcpBinding(), new EndpointAddress(new Uri(baseUri, "wakeup")));
        id = client.Create("Service wakeup", TimeSpan.FromSeconds(100));
        Assert.NotEqual(Guid.Empty, id);

        Thread.Sleep(2000);//so the service may have time to persist.

        var ok = client.LoadAndRun(id);//Optional. Just simulate that the workflow instance may be running againt because of other events.
        Assert.True(ok);

        var r = client.Wakeup(id, "Service wakeup");
        Assert.Equal("Someone waked me up", r);

    }
}

[Fact]
public void TestWaitForSignalOrDelayAfterDelay()
{
    Guid id;
    // Create service host.
    using (var host = CreateHost())
    {
        host.Open();
        Assert.Equal(CommunicationState.Opened, host.State);

        // Create a client that sends a message to create an instance of the workflow.
        var client = ChannelFactory<Fonlow.WorkflowDemo.Contracts.IWakeup>.CreateChannel(new NetTcpBinding(), new EndpointAddress(new Uri(baseUri, "wakeup")));
        id = client.Create("Service wakeup", TimeSpan.FromSeconds(1));
        Assert.NotEqual(Guid.Empty, id);

        Thread.Sleep(2000);//so the service may have time to persist. Upon being reloaded, no bookmark calls needed.

        var ok = client.LoadAndRun(id);
        Assert.True(ok);
        Thread.Sleep(8000);//So the service may have saved the result.

        var r = client.Wakeup(id, "Service wakeup");
        Assert.Equal("I sleep for good duration", r);

    }
}

 

Remarks

The house keeping for the lifecycle of the workflow is not pretty and not thread safe here, and far from being comprehensive for typical scenarios of long running workflows. It will take a load of efforts to write your own house keeping functions good enough for production uses.

Non-Service Workflows with Bookmark on WorkflowServiceHost

Conditions

You have a non-service workflow with bookmark, however, you would run it as a service, but without all the troubles of writing house keeping functions like the solution above.

Solutions

Use workflow service host extensibility.

ResumeBookmarkEndpoint

The design here is inspired by WCF Workflow Sample in MSDN. However, that sample is not really working, and the bugs get away because of OneWay calls and the immediate termination of the console program in that sample, so errors could not come up before the console program is terminated.

C#
[ServiceContract]
public interface IWorkflowWithBookmark
{
    [OperationContract(Name = "Create")]
    Guid Create(IDictionary<string, object> inputs);

    [OperationContract(Name = "ResumeBookmark")]
    void ResumeBookmark(Guid instanceId, string bookmarkName, string message);

}

public class ResumeBookmarkEndpoint : WorkflowHostingEndpoint
{

    public ResumeBookmarkEndpoint(Binding binding, EndpointAddress address)
        : base(typeof(IWorkflowWithBookmark), binding, address)
    {
    }

    protected override Guid OnGetInstanceId(object[] inputs, OperationContext operationContext)
    {
        //Create called
        if (operationContext.IncomingMessageHeaders.Action.EndsWith("Create"))
        {
            return Guid.Empty;
        }
        //CreateWithInstanceId or ResumeBookmark called. InstanceId is specified by client
        else if (operationContext.IncomingMessageHeaders.Action.EndsWith("ResumeBookmark"))
        {
            return (Guid)inputs[0];
        }
        else
        {
            throw new InvalidOperationException("Invalid Action: " + operationContext.IncomingMessageHeaders.Action);
        }
    }

    protected override WorkflowCreationContext OnGetCreationContext(object[] inputs, OperationContext operationContext, Guid instanceId, WorkflowHostingResponseContext responseContext)
    {
        WorkflowCreationContext creationContext = new WorkflowCreationContext();
        if (operationContext.IncomingMessageHeaders.Action.EndsWith("Create"))
        {
            Dictionary<string, object> arguments = (Dictionary<string, object>)inputs[0];
            if (arguments != null && arguments.Count > 0)
            {
                foreach (KeyValuePair<string, object> pair in arguments)
                {
                    //arguments for the workflow
                    creationContext.WorkflowArguments.Add(pair.Key, pair.Value);
                }
            }
            //reply to client with the InstanceId
            responseContext.SendResponse(instanceId, null);
        }
        else
        {
            throw new InvalidOperationException("Invalid Action: " + operationContext.IncomingMessageHeaders.Action);
        }
        return creationContext;
    }
    protected override System.Activities.Bookmark OnResolveBookmark(object[] inputs, OperationContext operationContext, WorkflowHostingResponseContext responseContext, out object value)
    {
        Bookmark bookmark = null;
        value = null;
        if (operationContext.IncomingMessageHeaders.Action.EndsWith("ResumeBookmark"))
        {
            //bookmark name supplied by client as input to IWorkflowCreation.ResumeBookmark
            bookmark = new Bookmark((string)inputs[1]);
            //value supplied by client as argument to IWorkflowCreation.ResumeBookmark
            value = (string)inputs[2];

            responseContext.SendResponse(null, null);//Not OneWay anymore.
        }
        else
        {
            //  throw new NotImplementedException(operationContext.IncomingMessageHeaders.Action);
            responseContext.SendResponse(typeof(void), null);
        }
        return bookmark;
    }
}

Differences from what in the MSDN sample:

  1. Remove service function CreateWithInstanceId. I don't really understand what it is, and so far find no article explaining it. If you have some idea, please leave a comment.
  2. Change service function ResumeBookmark to a normal call rather than one-way call. So error during handing the bookmark could be returned to the client.

Helper Function to Create WorkflowServiceHost

C#
static WorkflowServiceHost CreateHost(Activity activity, System.ServiceModel.Channels.Binding binding, EndpointAddress endpointAddress)
{
    var host = new WorkflowServiceHost(activity);
    {
        SqlWorkflowInstanceStoreBehavior instanceStoreBehavior = new SqlWorkflowInstanceStoreBehavior("Server =localhost; Initial Catalog = WF; Integrated Security = SSPI")
        {
            HostLockRenewalPeriod = new TimeSpan(0, 0, 5),
            RunnableInstancesDetectionPeriod = new TimeSpan(0, 0, 2),
            InstanceCompletionAction = InstanceCompletionAction.DeleteAll,
            InstanceLockedExceptionAction = InstanceLockedExceptionAction.AggressiveRetry,
            InstanceEncodingOption = InstanceEncodingOption.GZip,
            MaxConnectionRetries = 3,
        };
        host.Description.Behaviors.Add(instanceStoreBehavior);

        //Make sure this is cleared defined, otherwise the bookmark is not really saved in DB though a new record is created. https://msdn.microsoft.com/en-us/library/ff729670%28v=vs.110%29.aspx
        WorkflowIdleBehavior idleBehavior = new WorkflowIdleBehavior()
        {
            TimeToPersist = TimeSpan.Zero,
            TimeToUnload = TimeSpan.Zero,
        };
        host.Description.Behaviors.Add(idleBehavior);

        WorkflowUnhandledExceptionBehavior unhandledExceptionBehavior = new WorkflowUnhandledExceptionBehavior()
        {
            Action = WorkflowUnhandledExceptionAction.Terminate,
        };
        host.Description.Behaviors.Add(unhandledExceptionBehavior);

        ResumeBookmarkEndpoint endpoint = new ResumeBookmarkEndpoint(binding, endpointAddress);
        host.AddServiceEndpoint(endpoint);

        host.Description.Behaviors.Add(new ServiceDebugBehavior() { IncludeExceptionDetailInFaults = true });
    }

    return host;
}

Hints:

  • The behaviors could be wired through config. Please check MSDN for details.

Test Cases

C#
[Fact]
public void TestWaitForSignalOrDelayWithBookmark()
{
    var endpointAddress = new EndpointAddress("net.tcp://localhost/nonservice/wakeup");
    var endpointBinding = new NetTcpBinding(SecurityMode.None);
    using (var host = CreateHost(new WaitForSignalOrDelayWorkflow(), endpointBinding, endpointAddress))
    {
        host.Open();
        Assert.Equal(CommunicationState.Opened, host.State);

        IWorkflowWithBookmark client = new ChannelFactory<IWorkflowWithBookmark>(endpointBinding, endpointAddress).CreateChannel();
        Guid id = client.Create(new Dictionary<string, object> { { "BookmarkName", "NonService Wakeup" }, { "Duration", TimeSpan.FromSeconds(100) } });
        Assert.NotEqual(Guid.Empty, id);

        Thread.Sleep(2000);//so the service may have time to persist.

        client.ResumeBookmark(id, "NonService Wakeup", "something");
    }
}

[Fact]
public void TestWaitForSignalOrDelayWithWrongBookmark()
{
    var endpointAddress = new EndpointAddress("net.tcp://localhost/nonservice/wakeup");
    var endpointBinding = new NetTcpBinding(SecurityMode.None);
    using (var host = CreateHost(new WaitForSignalOrDelayWorkflow(), endpointBinding, endpointAddress))
    {

        host.Open();
        Assert.Equal(CommunicationState.Opened, host.State);

        IWorkflowWithBookmark client = new ChannelFactory<IWorkflowWithBookmark>(endpointBinding, endpointAddress).CreateChannel();
        Guid id = client.Create(new Dictionary<string, object> { { "BookmarkName", "NonService Wakeup" }, { "Duration", TimeSpan.FromSeconds(100) } });
        Assert.NotEqual(Guid.Empty, id);

        Thread.Sleep(2000);//so the service may have time to persist.

        var ex = Assert.Throws<FaultException>(() =>
            client.ResumeBookmark(id, "NonService Wakeupkkk", "something"));

        Debug.WriteLine(ex.ToString());
    }
}
 
Hints
IWorkflowWithBookmark and CreateHost could be easily reused for workflows with bookmark. Just make sure they use different endpoint addresses.
 

Remarks

There are some limitations when using WorkflowHosting endpoint. Though many people said that WorkflowServiceHost is a wrapper around WorkflowApplication, I see little access points to access the WorkflowApplication instance, while what enabled by behaviors is rather limited.

WaitForSignalOrDelayWorkflow has outputs as dictionary of OutArgument. However, there seem to be no "official" way to access it. You might have to add logic to the workflow to return the result through SignalR or store in a buffer/DB then the client could pull it later. If you have other idea, please leave a comment.

So with all these troubles, it is better to use workflow service. Through correlation, you create an implicit mapping between session and workflow instance, so the client codes and the custom service codes have no need of knowing the workflow instance ID explicitly.

 

Workflow Service

In the samples above, I had demonstrated a few ways of hosting some types of workflows. However, Instance ID and bookmark are the implementation concepts of workflow, but for the sack of information hiding, generally the workflow service has better not to expose the fact that it uses Workflow Foundation for implementation, and client programs need not to have knowledge of such implementation details. In workflow service, the need for instance ID is handled by correlation, and the need for bookmark functionality is replaced by the Receive activity which is mapped to a service client call.

Conditions

You have some workflows that contain at least one Receive activity, so WCF will pass an incoming client call to the workflow hosted in WorkflowServiceHost.

Solutions

In previous article Learn Windows Workflow Foundation 4.5 through Unit Testing: WorkflowApplication and WorkflowServiceHost, you may have read that a workflow with Receive could become a WCF service through hosting the workflow (activity / XAML) in WorkflowServiceHost, and such construction could be good enough for self hosting solution like Windows services or desktop programs.

Workflow 4 provides WorkflowService to support such development model:

  1. Design workflow on top of WorkflowService/XAMLX.
  2. Run the workflow service to generate WSDL and XSD
  3. Craft client codes according to the WSDL

 

There are many tutorials around for building Workflow Services through crafting XAMLX:

I would presume you have read some of these articles or the similar, so I would introduce some tips that you might not have read.

In FonlowWorkflowDemo.sln, there's a project BasicWCFService which is created through the template "WCF Workflow Service Application".

Image 1

And Service1.xamlx has the following properties:

Image 2

You may notice that there is "CreateClientApi.bat".

BAT
start "Launch IIS Express" "C:\Program Files (x86)\IIS Express\iisexpress.exe" /site:"BasicWFService" /apppool:"Clr4IntegratedAppPool" /config:"C:\VsProjects\FonlowWorkflowDemo\.vs\config\applicationhost.config"

"C:\Program Files (x86)\Microsoft SDKs\Windows\v8.1A\Bin\NETFX 4.5.1 Tools\svcutil.exe" http://localhost:3065/Service1.xamlx?singleWsdl /noConfig /language:C# /n:http://fonlow.com/WorkflowDemo,Fonlow.WorkflowDemo.Clients /directory:..\BasicWFServiceClientApi /out:WFServiceClientApiAuto.cs
pause

Running this batch file will generate client API codes in WFServiceClientApiAuto.cs in the client API library "BasicWFServiceClientAPI".

Test the service

C#
const string realWorldEndpoint = "DefaultBinding_Workflow";

[Fact]
public void TestGetData()
{
    using (var client = new WorkflowProxy(realWorldEndpoint))
    {
        Assert.Equal("abcaaa",  client.Instance.GetData("abc"));
    }

}

Remarks

If you have read my article "WCF for the Real World, Not Hello World" in which the client API codes is generated from a contract library without implementation, you may be wondering why not run svcutil.exe against BasicWFService.dll. This is because the dll does not contain contract info, and the contract info is generated through contract inference when the Workflow WCF service is running. So I have to launch the service in order to acquire the contract info for generating client API codes. If you have many workflows and respective contracts, it is quite cumbersome to generate/updates client API codes and add/update service references every time you have new or updated contracts. So the contract first approach may be a good alternative.

 

Contract First Workflow Service

This chapter is inspired by "How to: Create a workflow service that consumes an existing service contract". And please also refer to "Contract First Workflow Service Development" as the basic tutorial which I am not going to repeat here.

And this example also demonstrate content based correlation, with customerId.

Step 1: Define the service contract of the workflow for buying a book

C#
[ServiceContract(Namespace = Constants.ContractNamespace)]
public interface IBookService
{
    [OperationContract]
    void Buy(Guid customerId, string bookName);

    [OperationContract]
    [return: System.ServiceModel.MessageParameterAttribute(Name = "CheckoutResult")]
    string Checkout(Guid customerId);

    [OperationContract]
    void Pay(Guid customerId, string paymentDetail);
}

Step 2: Import the IBookService contract

Image 3

Step 3: Create a new workflow service

After step 2, you may see the IBookServie component appear in the toolbox when editing an XAMLX file.

Image 4

Drag the operation contracts to the XAMLX file you will have a workflow of buying a book then checking out and pay.

Image 5Image 6

 

Step 4: Create correlation

When creating a new XAMLX file, Visual Studio will create a CorrelationHandle for you.

Image 7

Generally you just need one such handle for a sequential workflow.

For the Buy_Receive activity, make sure the CanCreateInstance property be true, and for the CheckOut_Receive, be false.

And in all Receive activities, make sure the CorrelationsWith property point to handle and use customerId  for content base correlation.

Image 8

 

Step 5: Establish persistence layer with SQL DB

In Web.config, setup the following settings for respective services, or all services if the host is hosting only workflow services.

XML
<system.serviceModel>
  <behaviors>
    <serviceBehaviors>
      <behavior>
        <!-- To avoid disclosing metadata information, set the values below to false before deployment -->
        <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
        <!-- To receive exception details in faults for debugging purposes, set the value below to true.  Set to false before deployment to avoid disclosing exception information -->
        <serviceDebug includeExceptionDetailInFaults="true"/>

        <sqlWorkflowInstanceStore
                  connectionString="Server =localhost; Initial Catalog = WF; Integrated Security = SSPI"
                  instanceEncodingOption="GZip"
                  instanceCompletionAction="DeleteAll"
                  instanceLockedExceptionAction="AggressiveRetry"
                  hostLockRenewalPeriod="00:00:02"
                  runnableInstancesDetectionPeriod="00:00:02"/>

      </behavior>
    </serviceBehaviors>

 

Remarks:

You may not need the instance store if all requests will go to the same host instance and the host instance will never be shutdown. Even if the workflow contains a few Persist activities, without an instance store, Persist will do nothing.

Test Case: All requests to the same host instance

C#
[Collection(TestConstants.IisExpressAndInit)]
public class WFServiceContractFirstIntegrationTests
{
    const string hostBaseAddress = "http://localhost:2327/BookService.xamlx";
    [Fact]
    public void TestBuyBook()
    {
        var client = ChannelFactory<IBookService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(hostBaseAddress));
        const string bookName = "Alice in Wonderland";
        var customerId = Guid.NewGuid();
        client.Buy(customerId, bookName);
        var checkOutBookName = client.Checkout(customerId);
        Assert.Equal(bookName, checkOutBookName);
        client.Pay(customerId, "Visa card");
    }

}

Test Case: Wrong order of running steps of workflow

C#
[Fact]
public void TestBuyBookInWrongOrderThrows()
{
    var client = ChannelFactory<IBookService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(hostBaseAddress));
    const string bookName = "Alice in Wonderland";
    var customerId = Guid.NewGuid();
    client.Buy(customerId, bookName);
    var ex = Assert.Throws<FaultException>(
        () => client.Pay(customerId, "Visa card"));
    Assert.Contains("correct order", ex.ToString());
}

Test Case: Non-existing correlation

C#
[Fact]
public void TestNonExistingSessionThrows()
{
    var client = ChannelFactory<IBookService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(hostBaseAddress));
    var customerId = Guid.NewGuid();
    var ex = Assert.Throws<FaultException>(
        () => { client.Checkout(customerId); });
    Assert.Contains("InstancePersistenceCommand", ex.ToString());
}

 

Test Cases: Requests of the same workflow instance are handled by 2 host instances

This test case simulate that the requests of the same workflow instance are handled by 2 host instances at different stages.

C#
public class WFServicePersistenceTests
{
    const string hostBaseAddress = "http://localhost:2327/BookService.xamlx";
    [Fact]
    public void TestBuyBook()
    {
        IisExpressAgent agent = new IisExpressAgent();
        const string bookName = "Alice in Wonderland";
        var customerId = Guid.NewGuid();
        agent.Start();
        var client = ChannelFactory<IBookService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(hostBaseAddress));
        client.Buy(customerId, bookName);
        agent.Stop();
        System.Threading.Thread.Sleep(5000);//WF runtime seems to be taking long time to persist data. And there may be delayed write, so closing the service may force writing data to DB.
        // The wait time has better to be hostLockRenewalPeriod + runnableInstancesDetectionPeriod + 1 second.
        agent.Start();
        client = ChannelFactory<IBookService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(hostBaseAddress));
        var checkOutBookName = client.Checkout(customerId);
        Assert.Equal(bookName, checkOutBookName);
        client.Pay(customerId, "Visa card");
        agent.Stop();
    }

}

And please refer to "Persistence Best Practices" for fine tuning your workflow services.

 

Points of Interest

Among different approached illustrated above, the Contract First approach is the most suitable for large enterprise projects with a lot complicated workflow to be hosted.

The persistence of the workflow in a workflow service demonstrated above does not persist the workflow definition, because the workflow and the service are in one piece. While a workflow may have multiple Receive activities, WCF runtime and WF runtime know inherently which workflow definition to use. I guess this is probably one of the reasons why WF 4 had removed built-in persistence of workflow definitions.

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)