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: WorkflowApplication and WorkflowServiceHost

5.00/5 (5 votes)
25 Mar 2016CPOL6 min read 13K  
WorkflowApplication and WorkflowServiceHost

Background

Recently, I had started to learn Windows Workflow Foundation. I prefer to learn a major technology framework through systematic study rather than Googling around. However, I found that most well written books and articles were published between 2006-2009, so outdated, particularly missing new features in .NET 4; and a few books published in recent years for WF 4.0 and 4.5 were poorly written. I generally prefer systematic, dry and abstract study, this time I would make some wet materials for studying.

Introduction

And this article is focused on WorkflowApplication and WorkflowServiceHost, and introducing some basic behaviors of these 2 classes.

This is the 3rd article in the series. And the source code is available at https://github.com/zijianhuang/WorkflowDemo.

This article presumes you have read articles, tutorials and examples etc. for WorkflowApplication and WorkflowServiceHost, and is focused more on the error scenarios. When a workflow is running in fire and forget mode or remotely, you do have to take much more care of errors which could not be popup in your computer screen like a desktop program does.

Other articles in this series are given below:

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 3
  2. xUnit (included)
  3. EssentialDiagnostics (included)
  4. Workflow Persistence SQL database, with default local database WF.

Examples in this article are from a test classe: WorkflowApplicationTests, WorkflowServiceHostTests.

WorkflowApplication

Some test cases below show the behaviors of WorkflowApplication under error circumstances, so you may have better ideas about how to handle similar situations in your applications.

Example 1

C#
[Fact]
public void TestWorkflowApplication()
{
    AutoResetEvent syncEvent = new AutoResetEvent(false);
    var a = new System.Activities.Statements.Sequence()
    {
        Activities =
        {
            new System.Activities.Statements.Delay()
            {
                Duration= TimeSpan.FromSeconds(2),
            },

            new Multiply()
            {
                X = 3,
                Y = 7,
            }
        },
    };

    var app = new WorkflowApplication(a);
    int mainThreadId = Thread.CurrentThread.ManagedThreadId;
    int workFlowThreadId = -1;

    app.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
    {
        workFlowThreadId = Thread.CurrentThread.ManagedThreadId;
        syncEvent.Set();
    };

    var dt = DateTime.Now;
    app.Run();
    Assert.True((DateTime.Now - dt).TotalSeconds < 1, "app.Run() should not be blocking");
    syncEvent.WaitOne();
    Assert.NotEqual(mainThreadId, workFlowThreadId);
}

This test case demonstrates that a WorkflowApplication instance is running activity Sequence in a new thread, and app.Run is not blocking.

Remarks:

According to MSDN about WorkflowApplication.Run():

Call this method to initiate execution of a newly created workflow instance.

If the run operation does not complete within 30 seconds, a TimeoutException is thrown.

This actually means that if the .NET runtime takes over 30 seconds to allocate resources for running the WorkflowApplication, a TimeoutException is thrown. And apparently the first WorkflowApplication to run in a process may take over 1 second to initiate execution, and the subsequent instances may take no time. If the resources are stressed out, it is possible that .NET runtime may take longer time to execute Run().

Example 2

C#
public class ThrowSomething : CodeActivity
{
    protected override void Execute(CodeActivityContext context)
    {
        throw new NotImplementedException("nothing");
    }
}

   [Fact]
    public void TestWorkflowApplicationCatchException()
    {
        AutoResetEvent syncEvent = new AutoResetEvent(false);
        var a = new ThrowSomething();

        var app = new WorkflowApplication(a);
        bool exceptionHandled = false;
        bool aborted = false;
        int mainThreadId = Thread.CurrentThread.ManagedThreadId;
        int workFlowThreadId = -1;
        app.OnUnhandledException = (e) =>
        {
            Assert.IsType<NotImplementedException>(e.UnhandledException);
            exceptionHandled = true;
            workFlowThreadId = Thread.CurrentThread.ManagedThreadId;
            return UnhandledExceptionAction.Abort;
        };

        app.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
        {
            Assert.True(false, "Never completed");
            syncEvent.Set();
        };

        app.Aborted = (eventArgs) =>
        {
            aborted = true;
            syncEvent.Set();
        };
        app.Run();
        syncEvent.WaitOne();
        Assert.True(exceptionHandled);
        Assert.True(aborted);
        Assert.NotEqual(mainThreadId, workFlowThreadId);
    }

WorkflowApplication has built-in mechanism of handling uncaught exceptions through OnUnhandledException, so the exception uncaught won't be propagated to the caller thread.

Remarks:

I had found in legacy codes that some programmers would preserve the exception object through a variable outside the scope of WorkflowApplication, and after the blocking call syncEvent.WaitOne(), re-throw the exception. Such practice actually destroys the design purpose of WorkflowApplication.

Example 3

C#
[Fact]
public void TestWorkflowApplicationWithoutOnUnhandledException()
{
    AutoResetEvent syncEvent = new AutoResetEvent(false);
    var a = new ThrowSomething();

    var app = new WorkflowApplication(a);
    bool aborted = false;
    bool completed = false;

    app.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
    {
        completed = true;
        syncEvent.Set();
    };

    app.Aborted = (eventArgs) =>
    {
        aborted = true;
        syncEvent.Set();
    };
    app.Run();
    syncEvent.WaitOne();
    Assert.True(completed);
    Assert.False(aborted);
}

In the test case, the workflow being executed throws an exception, however, the Completed delegate get called, and the Aborted is not called. This is a designed behavior, as stated in MSDN.

  Member name Description
  Abort Specifies that the WorkflowApplication should abort the workflow.

This results in Aborted being called when the abort process is complete. The unhandled exception is used as the abort reason.

  Cancel Specifies that the WorkflowApplication should schedule the cancellation of the root activity and resume execution.

This results in Completed being called when the cancellation process is complete.

  Terminate Specifies that the WorkflowApplication should schedule termination of the root activity and resume execution.

This results in Completed being called when the termination process is complete. The unhandled exception is used as the termination reason. Terminate is the default action if no OnUnhandledException handler is specified.

Example 4

C#
[Fact]
public void TestWorkflowApplicationNotCatchExceptionWhenValidatingArguments()
{
    var a = new Multiply()
    {
        Y = 2,
    };

    var app = new WorkflowApplication(a);

    //None of the handlers should be running
    app.OnUnhandledException = (e) =>
    {
        Assert.True(false);
        return UnhandledExceptionAction.Abort;
    };

    app.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
    {
        Assert.True(false);
    };

    app.Aborted = (eventArgs) =>
    {
        Assert.True(false);
    };

    app.Unloaded = (eventArgs) =>
    {
        Assert.True(false);
    };

    Assert.Throws<ArgumentException>(() => app.Run());//exception occurs during
    //validation and in the same thread of the caller, before any activity runs.
}

In this case, the workflow is not executed at all in WorkflowApplication. The activity to be executed has a missing argument X. The validation of the arguments occurs in the caller thread, so the ArgumentException is thrown in the caller thread.

WorkflowServiceHost

If you have done a lot development on WCF, you should be getting well easily with WorkflowServiceHost. However, in WF, there are 2 classes named as WorkflowServiceHost.

WorkflowServiceHost Class in WF3.5

Note: This API is now obsolete.

Inheritance Hierarchy

System. Object
   System.ServiceModel.Channels. CommunicationObject
     System.ServiceModel. ServiceHostBase
       System.ServiceModel. WorkflowServiceHost

Namespace: System.ServiceModel
Assembly:System.WorkflowServices (in System.WorkflowServices.dll)

WorkflowServiceHost Class in WF4 and WF4.5

Inheritance Hierarchy

System. Object
   System.ServiceModel.Channels. CommunicationObject
     System.ServiceModel. ServiceHostBase
       System.ServiceModel.Activities. WorkflowServiceHost

Namespace: System.ServiceModel.Activities
Assembly:System.ServiceModel.Activities (in System.ServiceModel.Activities.dll)

Therefore, please make sure you reference to the correct assembly and namespace for the right WorkflowServiceHost class in WF4.5.

Example 1: Simple one-way operation to kick start a workflow

C#
    [ServiceContract]
    public interface ICountingWorkflow
    {
        [OperationContract(IsOneWay = true)]
        void start();
    }

const string connectionString = "Server =localhost; Initial Catalog = WF; Integrated Security = SSPI";
const string hostBaseAddress = "net.tcp://localhost/CountingService";
[Fact]
public void TestOpenHost()
{
	// Create service host.
	WorkflowServiceHost host = new WorkflowServiceHost
	(new Microsoft.Samples.BuiltInConfiguration.CountingWorkflow(), new Uri(hostBaseAddress));

	// Add service endpoint.
	host.AddServiceEndpoint("ICountingWorkflow", new NetTcpBinding(), "");

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

	// Create a client that sends a message to create an instance of the workflow.
	ICountingWorkflow client = ChannelFactory<ICountingWorkflow>.CreateChannel
	(new NetTcpBinding(), new EndpointAddress(hostBaseAddress));
	client.start();

host.Close();
}

Remarks:

Microsoft's Workflow Samples use HttpBinding. However, HttpBinding listening requires admin privilege while I prefer to run Visual Studio as a normal user. So I generally use NetTcpBinding for the service host during unit testing. When running a test suit for the first time which will create and open a service host, Windows Firewall will prompt for opening a port for the test suit.

Image 1

After you clicking on "Allow Access", you will have 2 new rules and the test suits will continue to run.

Image 2

Such configuration is one-off in your development PC.

Example 2: Mismatch between contract and workflow

C#
[Fact]
public void TestOpenHostWithoutContractImpThrows()
{
    // Create service host.
    using (WorkflowServiceHost host = new WorkflowServiceHost(new Plus(), new Uri(hostBaseAddress)))
    {

        Assert.Throws<InvalidOperationException>(()
            => host.AddServiceEndpoint("ICountingWorkflow", new NetTcpBinding(), ""));

        Assert.Equal(CommunicationState.Created, host.State);
    }
}

During the creation of a WorkflowServiceHost instance, if the contract of an endpoint to be added does not match the workflow being hosted, there will be InvalidOperationException. However, the service host is still created. Obviously then you have to have some design to dispose a invalid or faulted service host object.

Example 3: OperationContract to return the result of a workflow

C#
[ServiceContract(Namespace ="http://fonlow.com/workflowdemo/")]
public interface ICalculation
{
    [OperationContract]
    [return: MessageParameter(Name = "Result")]
    long MultiplyXY(int parameter1, int parameter2);

}

    [Fact]
    public void TestMultiplyXY()
    {
        // Create service host.
        using (WorkflowServiceHost host = new WorkflowServiceHost(new Fonlow.Activities.MultiplyWorkflow2(), new Uri(hostBaseAddress)))
        {
            Debug.WriteLine("host created.");
            // Add service endpoint.
            host.AddServiceEndpoint("ICalculation", new NetTcpBinding(), "");

            host.Open();
            Debug.WriteLine("host opened");
            Assert.Equal(CommunicationState.Opened, host.State);

            // Create a client that sends a message to create an instance of the workflow.
            var client = ChannelFactory<ICalculation>.CreateChannel(new NetTcpBinding(), new EndpointAddress(hostBaseAddress));
            var r = client.MultiplyXY(3, 7);

            Assert.Equal(21, r);
        }
    }

Remarks

The operation contract used in the client must match the parameters and the respond defined in Receive and SendReply of the workflow, otherwise the data binding in the service side will fail. Typically you have to make sure that the [return: MessageParameter...] match the parameter name in the response of the SendReply content. Thus, in a real world project, it is better to generate client API codes to ensure the exact match.

Image 3

Image 4

 

General steps

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:

  1. You have a service contract, and make sure [return ...] is available to operation contract that returns data.
  2. Craft workflows that implement the service contract.
  3. Launch one WorkflowServiceHost for each workflow
  4. Craft client codes using the same service contract.

 

Points of Interests

You might have noticed that WorkflowApplication also has BeginRun() and EndRun(). I found they have little use in real world applications. If you call this pair instead of Run(), callbacks like Completed and OnUnhandledException will never get called, thus lost the advantage of accessing more of WF runtime. You may check test cases: BasicTests.WorkflowApplicationTests.TestWorkflowApplicationBeginRun() and TestWorkflowApplicationBeginRunWithException(). And If I don't care about more of WF runtime and would just run a workflow in the Begin/End manner, I may simply use delegate BeginInvoke(). If you have different ideas about BeingRun() and EndRun(), please leave a comment.

While both WorkflowApplication and WorkflowServiceHost could access WF runtime, only WorkflowServiceHost could listen to remote requests. For example, if a workflow contains Receive, you will have to run it through WorrkflowServiceHost, and there's no use to run such workflow in WorkflowApplication which could not listen and pass the message to Receive.

And if an activity/workflow does not contain Receive, there's no point to host it in WorkflowServiceHost, since a ServiceHost is useful only with at least one endpoint consisting of contract, binding and address. Though WorkflowServiceHost could take any activity, only after adding an endpoint with a contract matching what available in the workflow the servicehost will become communicative, and the workflow hosted could run.

 

References:

 

 

License

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