Saturday, June 14, 2008

How to create WF as WCF service hosted in IIS?

Problem

Last few days I been struggling to create Microsoft WF(http://netfx3.com/content/WFHome.aspx) as WCF (http://netfx3.com/content/WCFHome.aspx) service hosted in IIS. Followings are the problems I resolved:

1) How to host WF in WCF
2) How to host WF + WCF in IIS and expose WF as service
3) How to write self contained unit test to test WF + WCF
4) How to pass data from WCF service to WF and bind data sent from WCF to WF DependencyProperty
5) How to configure WF + WCF

References used to research and learn

http://www.dinnernow.net/
http://www.west-wind.com/WebLog/posts/9323.aspx
http://community.bartdesmet.net/blogs/bart/archive/2006/08/28/4322.aspx
http://blogs.microsoft.co.il/blogs/bursteg/archive/2007/04/20/WF-and-WCF-Integration-in-_2200_Orcas_2200_-_2D00_-Part-1-_2D00_-Workflow-Enabled-Services.aspx

About the project

Workflow receives file path and name through WCF service
void StartWorkflow(string filePathAndName). It will write to file
"codeActivity1_Handler invoked from Workflow1".

On first test, WF + WCF is self hosted programmatically, and invokes StartWorkflow.
On second test, ChannelFactory.CreateChannel is used to create IService1 hosted on IIS.

Download Workflow_WCF_IIS_Demo.zip

Requirements:

.Net 3.5
VS2008
NUnit (http://www.nunit.com/)

Step by step:

Adding projects to solution

- Add "Sequential Workflow Library" under Workflow teamplates
- Add "WCF Service Application" for hosting WCF in IIS
- Add "Class Library" called ClassLibrary1 for NUnit
- Add "Class Library" called WcfServiceLibrary1 For WCF contract
- Project structure should looks like below



Setup WcfServiceLibrary1

- Add references to System.ServiceModel and System.Runtime.Serialization
- Add interface IService.cs and add fllowing lines of code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
 
namespace WcfServiceLibrary1
{
    [ServiceContract]
    public interface IService1
    {
        [OperationContract]
        void StartWorkflow(string filePathAndName);
    }
}


Setup WorkflowLibrary1

- Add reference to WcfServiceLibrary1
- Double click Workflow1.cs
- Drag and drop ReceiveActivity



- Select receiveActivity1
- On Properties "CanCreateInstance" to true
- On Properties click browse button on "ServiceOperationInfo"
- On Choose Operation window click "Impot..."
- Browse to WcfServiceLibrary1.IService1 and click OK



- On Choose Operation window choose StartWorkflow and click OK
Notice on Properties windows filePathAndName appears. filePathAndName is OperationContract parameter defined in IService.StartWorkflow.
- Click Workflow1.cs and click F7 to go to code behind of Workflow1.cs
- Add following lines of code

public static DependencyProperty ReceiveDataProperty
    = DependencyProperty.Register("ReceiveData", typeof(string), typeof(Workflow1));
 
[Description("ReceiveData")]
[Category("ReceiveData")]
[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public string ReceiveData
{
    get
    {
        return ((string)(base.GetValue(Workflow1.ReceiveDataProperty)));
    }
    set
    {
        base.SetValue(Workflow1.ReceiveDataProperty, value);
    }
}


- Bind filePathAndName received from StartWorkflow method to ReceiveData property of Workflow1
- Drag and drop Code Activity
- Add "codeActivity1_Handler" to ExecuteCode property of codeActivity1
- Add following lines of code to Workflow1.cs code behind


private void codeActivity1_Handler(object sender, EventArgs e)
{
    System.IO.File.WriteAllText(ReceiveData, "codeActivity1_Handler invoked from Workflow1");
}


Setup NUnit and Run Workflow test

- Add references to NUnit
- Add project references to WorkflowLibrary1 and WcfServiceLibrary1
- Add references System.Runtime.Serialization, System.Workflow.Activities, System.Workflow.ComponentModel, System.Workflow.Runtime, System.WorkflowServices, System.Runtime.Serialization



- Copy and paste following lines of code to Class1.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
using System.Workflow.Runtime;
using System.ServiceModel;
using System.ServiceModel.Description;
 
using NUnit.Core;
using NUnit.Framework;
 
 
namespace ClassLibrary1
{
    [TestFixture]
    public class Class1
    {
        [Test]
        public void TestWorkflow()
        {
            // Setup test.
            string testDir = @"c:\WorkflowTestDIrectory";
            string testFile = testDir + @"\" + "moo.txt"; 
            if (!System.IO.Directory.Exists(testDir))
            {
                System.IO.Directory.CreateDirectory(testDir);
            }
            if (System.IO.File.Exists(testFile))
            {
                System.IO.File.Delete(testFile);
            }
 
            // Setup WCF + WF host
            Uri baseAddress = new Uri("http://localhost:8999/UnitTestHosting");
            WorkflowServiceHost host 
                = new WorkflowServiceHost(typeof(WorkflowLibrary1.Workflow1), baseAddress);
            host.AddServiceEndpoint(typeof(WcfServiceLibrary1.IService1)
                                        , new WSHttpContextBinding()
                                        , "SomeEndPoint");
            ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
            smb.HttpGetEnabled = true;
            host.Description.Behaviors.Add(smb);
 
 
            try
            {
                host.Open();
                EndpointAddress address 
                    = new EndpointAddress("http://localhost:8999/UnitTestHosting/SomeEndPoint");
                WcfServiceLibrary1.IService1 service 
                    = ChannelFactory<WcfServiceLibrary1.IService1>.CreateChannel(new WSHttpContextBinding(), address);
                service.StartWorkflow(testFile);
                Assert.AreEqual("codeActivity1_Handler invoked from Workflow1", System.IO.File.ReadAllText(testFile));
            }
            catch (Exception)
            {
                throw;
            }
            finally
            {
                if (host != null && host.State == CommunicationState.Opened)
                {
                    host.Close();
                }
            }
 
        }
    }
}


Setting Up WF + WCF + IIS and running NUnit test

- Add references to WcfServiceLibrary1, WorkflowLibrary1, System.Workflow.Runtime
- Modify Service.svc as follows

<%@ ServiceHost Language="C#" Debug="true" Factory="System.ServiceModel.Activation.WorkflowServiceHostFactory" Service="WorkflowLibrary1.Workflow1" %>


- Add or modify System.serviceModel as follows

<system.serviceModel>
    <services>
      <service name="WorkflowLibrary1.Workflow1" behaviorConfiguration="WorkflowLibrary1.Workflow1.Service1Behavior">
 
        <endpoint address="" binding="wsHttpContextBinding" contract="WcfServiceLibrary1.IService1">
          <identity>
            <dns value="localhost"/>
          </identity>
        </endpoint>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="WorkflowLibrary1.Workflow1.Service1Behavior">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>


- Create virtual directory called wftest pointing to the location of WcfService1
- Remove IService1 created by default from WcfService1
- Check to make sure WCF service is up on http://localhost/wftest/Service1.svc
- Add following NUnit test to ClassLibrary1

[Test]
public void TestWorkflowHostedOnIIS()
{
    // Setup test.
    string testDir = @"c:\WorkflowTestDIrectory";
    string testFile = testDir + @"\" + "moo.txt";
    if (!System.IO.Directory.Exists(testDir))
    {
        System.IO.Directory.CreateDirectory(testDir);
    }
    if (System.IO.File.Exists(testFile))
    {
        System.IO.File.Delete(testFile);
    }
 
    EndpointAddress address
        = new EndpointAddress("http://localhost/wftest/Service1.svc");
    WcfServiceLibrary1.IService1 service
        = ChannelFactory<WcfServiceLibrary1.IService1>.CreateChannel(new WSHttpContextBinding(), address);
    service.StartWorkflow(testFile);
    Assert.AreEqual("codeActivity1_Handler invoked from Workflow1", System.IO.File.ReadAllText(testFile));
}

4 comments:

Leonard said...

Wondering if you considered using WFServiceLibrary which contains both the service and the workflow in one project, if that would avoid some work.

NewAgeSolution said...

Just my force of habit of seperating common stuffs like interfaces and data transfer objects out to its own projects so it can be reused for whatever reasons. Good use would be for example, if I wanted to use some kind of mock objects like nMock and nMock unit test will only required to reference project that contains interfaces and I do not have to necessarily have dependencies to System.Workflow namespaces.

Anonymous said...

On the host app, what's my entry point? Where to I put app-specific initialization code?

NewAgeSolution said...

Entry point on host app should be through Service1.svc from IIS.