How to: Host a non-service workflow in IIS
This topic applies to Windows Workflow Foundation 4 (WF4).
Workflows that are not workflow services can be hosted under IIS/WAS. This is useful when you need to host a workflow written by somebody else. For example, if you rehost the workflow designer and allow users to create their own workflows. Hosting non-service workflows in IIS provides support for features like process recycling, idle shutdown, process health monitoring, and message-based activation. Workflow services hosted in IIS contain Receive activities and are activated when a message is received by IIS. Non-service workflows do not contain messaging activities, and by default cannot be activated by sending a message. You must derive a class from WorkflowHostingEndpoint and define a service contract that contains operations to create an instance of the workflow. This topic will walk you through creating a simple workflow, defining a service contract a client can use to activate the workflow, and deriving a class from WorkflowHostingEndpoint which uses the service contract to listen for workflow creating requests.
Create a simple workflow
Create a new Visual Studio 2010 empty solution called
CreationEndpointTest
.Add a new WCF Workflow Service Application project called
SimpleWorkflow
to the solution. The workflow designer will open.Delete the ReceiveRequest and SendResponse activities. These activities are what makes a workflow a workflow service. Since we are not working with a workflow service, we no longer need them.
Set the DisplayName for the sequence activity to “Sequential Workflow”.
Rename Service1.xamlx to Workflow1.xamlx.
Click the designer outside of the sequence activity, and set the Name and ConfigurationName properties to “Workflow1”
Drag a WriteLine activity into the Sequence. The WriteLine activity can be found in the Primitives section of the toolbox. Set the Text property of the WriteLine activity to “Hello, world”.
The workflow should now look like the following diagram.
Create the workflow creation service contract
Add a new class library project called
Shared
to theCreationEndpointTest
solution.Add a reference to System.ServiceModel.dll, System.Configuration, and System.ServiceModel.Activities to the
Shared
project.Rename the Class1.cs file to IWorkflowCreation.cs and the following code to the file.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ServiceModel; namespace Shared { //service contract exposed from the endpoint [ServiceContract(Name = "IWorkflowCreation")] public interface IWorkflowCreation { [OperationContract(Name = "Create")] Guid Create(IDictionary<string, object> inputs); [OperationContract(Name = "CreateWithInstanceId", IsOneWay = true)] void CreateWithInstanceId(IDictionary<string, object> inputs, Guid instanceId); } }
This contract defines two operations both create a new instance of the non-service workflow you just created. One creates a new instance with a generated instance ID and the other allows you to specify the instance ID for the new workflow instance. Both methods allow you to pass in parameters to the new workflow instance. This contract will be exposed by the WorkflowHostingEndpoint to allow clients to create new instances of a non-service workflow.
Derive a class from WorkflowHostingEndpoint
Add a new class called
CreationEndpoint
derived from WorkflowHostingEndpoint to theShared
project.using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.ServiceModel; using System.ServiceModel.Activities; using System.ServiceModel.Channels; namespace Shared { public class CreationEndpoint : WorkflowHostingEndpoint { } }
Add a local static Uri variable called
defaultBaseUri
to theCreationEndpoint
class.public class CreationEndpoint : WorkflowHostingEndpoint { static Uri defaultBaseUri; }
Add the following constructor to the
CreationEndpoint
class. Notice we specify theIWorkflowCreation
service contract in the call to the base constructor.public CreationEndpoint(Binding binding, EndpointAddress address) : base(typeof(IWorkflowCreation), binding, address) { }
Add the following default constructor to the
CreationEndpoint
class.public CreationEndpoint() : this(GetDefaultBinding(), new EndpointAddress(new Uri(DefaultBaseUri, new Uri(Guid.NewGuid().ToString(), UriKind.Relative)))) { }
Add a static
DefaultBaseUri
property to theCreationEndpoint
class. This property will be used to hold a default base URI if one is not provided.static Uri DefaultBaseUri { get { if (defaultBaseUri == null) { defaultBaseUri = new Uri(string.Format(CultureInfo.InvariantCulture, "net.pipe://localhost/workflowCreationEndpoint/{0}/{1}", Process.GetCurrentProcess().Id, AppDomain.CurrentDomain.Id)); } return defaultBaseUri; } }
Create the following method to get the default binding to use for the creation endpoint.
//defaults to NetNamedPipeBinding public static Binding GetDefaultBinding() { return new NetNamedPipeBinding(NetNamedPipeSecurityMode.None) { TransactionFlow = true }; }
Override the OnGetInstanceId method to return the workflow instance ID. If the Action header ends with “Create” return an empty GUID, if the Action header ends with “CreateWithInstanceId” return the GUID passed into the method. Otherwise, throw an InvalidOperationException. These Action headers correspond to the two operations defined in the
IWorkflowCreation
service contract.protected override Guid OnGetInstanceId(object[] inputs, OperationContext operationContext) { //Create was called by client if (operationContext.IncomingMessageHeaders.Action.EndsWith("Create")) { return Guid.Empty; } //CreateWithInstanceId was called by client else if (operationContext.IncomingMessageHeaders.Action.EndsWith("CreateWithInstanceId")) { return (Guid)inputs[1]; } else { throw new InvalidOperationException("Invalid Action: " + operationContext.IncomingMessageHeaders.Action); } }
Override the OnGetCreationContext method to create a WorkflowCreationContext and add any arguments for the workflow, send the instance ID to the client, and then return the WorkflowCreationContext.
protected override WorkflowCreationContext OnGetCreationContext(object[] inputs, OperationContext operationContext, Guid instanceId, WorkflowHostingResponseContext responseContext) { WorkflowCreationContext creationContext = new WorkflowCreationContext(); if (operationContext.IncomingMessageHeaders.Action.EndsWith("Create") || (operationContext.IncomingMessageHeaders.Action.EndsWith("CreateWithInstanceId"))) { Dictionary<string, object> arguments = (Dictionary<string, object>)inputs[0]; if (arguments != null && arguments.Count > 0) { foreach (KeyValuePair<string, object> pair in arguments) { //arguments to pass to the workflow creationContext.WorkflowArguments.Add(pair.Key, pair.Value); } } //reply to client with instanceId responseContext.SendResponse(instanceId, null); } else { throw new InvalidOperationException("Invalid Action: " + operationContext.IncomingMessageHeaders.Action); } return creationContext; }
Create a standard endpoint element to allow you to configure the WorkflowCreationEndpoint
Add a reference to Shared in the
CreationEndpoint
projectAdd a new class called
CreationEndpointElement
, derived from StandardEndpointElement to theCreationEndpoint
project. This class will represent aCreationEndpoint
in a web.config file.using System; using System.Configuration; using System.ServiceModel.Activities; using System.ServiceModel.Configuration; using System.ServiceModel.Description; using Shared; namespace CreationEndpointTest { //config element for CreationEndpoint public class CreationEndpointElement : StandardEndpointElement { }
Add a property called
EndpointType
to return the type of the endpoint.protected override Type EndpointType { get { return typeof(CreationEndpoint); } }
Override the CreateServiceEndpoint method and return a new
CreationEndpoint
.protected override ServiceEndpoint CreateServiceEndpoint(ContractDescription contractDescription) { return new CreationEndpoint(); }
Overload the OnApplyConfiguration, OnApplyConfiguration, OnInitializeAndValidate, and OnInitializeAndValidate methods. These methods just need to be defined, you do not need to add any code to them.
protected override void OnApplyConfiguration(ServiceEndpoint endpoint, ChannelEndpointElement channelEndpointElement) { } protected override void OnApplyConfiguration(ServiceEndpoint endpoint, ServiceEndpointElement serviceEndpointElement) { } protected override void OnInitializeAndValidate(ChannelEndpointElement channelEndpointElement) { } protected override void OnInitializeAndValidate(ServiceEndpointElement serviceEndpointElement) { }
Add the collection class for
CreationEndpoint
to the CreationEndpointElement.cs file in theCreationEndpoint
project. This class is used by configuration to hold a number ofCreationEndpoint
instances in a web.config file.public class CreationEndpointCollection : StandardEndpointCollectionElement<CreationEndpoint, CreationEndpointElement> { }
Build the solution.
Host the workflow in IIS
Create a new application called
MyCreationEndpoint
in IIS.Copy the workflow1.xaml file generated by the workflow designer to the application directory and rename it to workflow1.xamlx.
Copy the shared.dll and CreationEndpoint.dll files to the application’s bin directory (create the bin directory if it is not present).
Replace the contents of the Web.config file in the
CreationEndpoint
project with the following code.<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.web> <compilation debug="true" targetFramework="4.0" /> </system.web> </configuration>
After the
<system.web>
element, registerCreationEndpoint
by adding the following configuration code.<system.serviceModel> <!--register CreationEndpoint--> <serviceHostingEnvironment multipleSiteBindingsEnabled="true" /> <extensions> <endpointExtensions> <add name="creationEndpoint" type="CreationEndpointTest.CreationEndpointCollection, CreationEndpoint, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> </endpointExtensions> </extensions> </system.serviceModel>
This registers the
CreationEndpointCollection
class so you can configure aCreationEndpoint
in a web.config file.Add a
<service>
element (after the </extensions> tag) with aCreationEndpoint
which will listen for incoming messages.<services> <!-- add endpoint to service--> <service name="Workflow1" behaviorConfiguration="basicConfig" > <endpoint kind="creationEndpoint" binding="basicHttpBinding" address=""/> </service> </services>
Add a <behaviors> element (after the </services> tag) to enable service metadata.
<behaviors> <serviceBehaviors> <behavior name="basicConfig"> <serviceMetadata httpGetEnabled="true" /> </behavior> </serviceBehaviors> </behaviors>
Copy the web.config to your IIS application directory.
Test to see if the creation endpoint is working by starting Internet Explorer and browsing to https://localhost/MyCreationEndpoint/Workflow1.xamlx. Internet Explorer should display the following screen:
Create a client that will call the CreationEndpoint.
Add a new Console application to the
CreationEndpointTest
solution.Add references to System.ServiceModel.dll, System.ServiceModel.Activities, and the
Shared
project.In the
Main
method create a ChannelFactory of typeIWorkflowCreation
and call CreateChannel. This will return a proxy. You can then callCreate
on that proxy to create the workflow instance hosted under IIS:using System.Text; using Shared; using System.ServiceModel; namespace CreationEndpointClient { class Program { static void Main(string[] args) { try { //client using BasicHttpBinding IWorkflowCreation client = new ChannelFactory<IWorkflowCreation>(new BasicHttpBinding(), new EndpointAddress("https://localhost/CreationEndpoint/Workflow1.xamlx")).CreateChannel(); Console.WriteLine("Workflow Instance created using CreationEndpoint added in config. Instance Id: {0}", client.Create(null)); Console.WriteLine("Press return to exit ..."); Console.ReadLine(); } catch (Exception ex) { Console.WriteLine(ex); Console.ReadLine(); } } } }
Run the CreationEndpointClient. The output should look like the following:
Workflow Instance created using CreationEndpoint added in config. Instance Id: 0875dac0-2b8b-473e-b3cc-abcb235e9693
Press return to exit ...
> [!NOTE]
> You will not see the output of the workflow because it is running under IIS which has no console output.
> <p></p>
Example
The following is the complete code for this sample.
<!-— workflow1.xamlx -->
<WorkflowService mc:Ignorable="sap"
ConfigurationName="Workflow1"
sap:VirtualizedContainerService.HintSize="263,230"
Name="Workflow1"
mva:VisualBasic.Settings="Assembly references and imported namespaces serialized as XML namespaces"
xmlns="https://schemas.microsoft.com/netfx/2009/xaml/servicemodel"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mv="clr-namespace:Microsoft.VisualBasic;assembly=System"
xmlns:mva="clr-namespace:Microsoft.VisualBasic.Activities;assembly=System.Activities"
xmlns:p="https://schemas.microsoft.com/netfx/2009/xaml/activities"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:s1="clr-namespace:System;assembly=System"
xmlns:s2="clr-namespace:System;assembly=System.Xml"
xmlns:s3="clr-namespace:System;assembly=System.Core"
xmlns:sad="clr-namespace:System.Activities.Debugger;assembly=System.Activities"
xmlns:sap="https://schemas.microsoft.com/netfx/2009/xaml/activities/presentation"
xmlns:scg="clr-namespace:System.Collections.Generic;assembly=System"
xmlns:scg1="clr-namespace:System.Collections.Generic;assembly=System.ServiceModel"
xmlns:scg2="clr-namespace:System.Collections.Generic;assembly=System.Core"
xmlns:scg3="clr-namespace:System.Collections.Generic;assembly=mscorlib"
xmlns:sd="clr-namespace:System.Data;assembly=System.Data"
xmlns:sl="clr-namespace:System.Linq;assembly=System.Core"
xmlns:st="clr-namespace:System.Text;assembly=mscorlib"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
<p:Sequence DisplayName="Sequential Service"
sad:XamlDebuggerXmlReader.FileName="c:\projects\CreationEndpointTest\CreationEndpoint\Service1.xamlx"
sap:VirtualizedContainerService.HintSize="233,200"
mva:VisualBasic.Settings="Assembly references and imported namespaces serialized as XML namespaces">
<p:Sequence.Variables>
<p:Variable x:TypeArguments="CorrelationHandle" Name="handle" />
<p:Variable x:TypeArguments="x:Int32" Name="data" />
</p:Sequence.Variables>
<sap:WorkflowViewStateService.ViewState>
<scg3:Dictionary x:TypeArguments="x:String, x:Object">
<x:Boolean x:Key="IsExpanded">True</x:Boolean>
</scg3:Dictionary>
</sap:WorkflowViewStateService.ViewState>
<p:WriteLine sap:VirtualizedContainerService.HintSize="211,61" Text="Hello, world" />
</p:Sequence>
</WorkflowService>
// CreationEndpointElement.cs
using System;
using System.Configuration;
using System.ServiceModel.Activities;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using Shared;
namespace CreationEndpointTest
{
//config element for CreationEndpoint
public class CreationEndpointElement : StandardEndpointElement
{
protected override Type EndpointType
{
get { return typeof(CreationEndpoint); }
}
protected override ConfigurationPropertyCollection Properties
{
get
{
ConfigurationPropertyCollection properties = base.Properties;
properties.Add(new ConfigurationProperty("name", typeof(String), null, ConfigurationPropertyOptions.IsRequired));
return properties;
}
}
protected override ServiceEndpoint CreateServiceEndpoint(ContractDescription contractDescription)
{
return new CreationEndpoint();
}
protected override void OnApplyConfiguration(ServiceEndpoint endpoint, ChannelEndpointElement channelEndpointElement)
{
}
protected override void OnApplyConfiguration(ServiceEndpoint endpoint, ServiceEndpointElement serviceEndpointElement)
{
}
protected override void OnInitializeAndValidate(ChannelEndpointElement channelEndpointElement)
{
}
protected override void OnInitializeAndValidate(ServiceEndpointElement serviceEndpointElement)
{
}
}
public class CreationEndpointCollection : StandardEndpointCollectionElement<CreationEndpoint, CreationEndpointElement>
{
}
}
<!-- web.config -->
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<!--register CreationEndpoint-->
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
<extensions>
<endpointExtensions>
<add name="creationEndpoint" type="CreationEndpointTest.CreationEndpointCollection, Shared, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</endpointExtensions>
</extensions>
<services>
<!-- add endpoint to service-->
<service name="Workflow1" behaviorConfiguration="basicConfig" >
<endpoint kind="creationEndpoint" binding="basicHttpBinding" address=""/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="basicConfig">
<serviceMetadata httpGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
</configuration>
// IWorkflowCreation.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
namespace Shared
{
//service contract exposed from the endpoint
[ServiceContract(Name = "IWorkflowCreation")]
public interface IWorkflowCreation
{
[OperationContract(Name = "Create")]
Guid Create(IDictionary<string, object> inputs);
[OperationContract(Name = "CreateWithInstanceId", IsOneWay = true)]
void CreateWithInstanceId(IDictionary<string, object> inputs, Guid instanceId);
}
}
// CreationEndpoint.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel.Activities;
using System.ServiceModel.Channels;
using System.ServiceModel;
using System.Globalization;
using System.Diagnostics;
namespace Shared
{
public class CreationEndpoint : WorkflowHostingEndpoint
{
static Uri defaultBaseUri;
public CreationEndpoint(Binding binding, EndpointAddress address)
: base(typeof(IWorkflowCreation), binding, address) { }
public CreationEndpoint()
: this(GetDefaultBinding(),
new EndpointAddress(new Uri(DefaultBaseUri, new Uri(Guid.NewGuid().ToString(), UriKind.Relative)))) { }
static Uri DefaultBaseUri
{
get
{
if (defaultBaseUri == null)
{
defaultBaseUri = new Uri(string.Format(CultureInfo.InvariantCulture, "net.pipe://localhost/workflowCreationEndpoint/{0}/{1}",
Process.GetCurrentProcess().Id,
AppDomain.CurrentDomain.Id));
}
return defaultBaseUri;
}
}
//defaults to NetNamedPipeBinding
public static Binding GetDefaultBinding()
{
return new NetNamedPipeBinding(NetNamedPipeSecurityMode.None) { TransactionFlow = true };
}
protected override Guid OnGetInstanceId(object[] inputs, OperationContext operationContext)
{
//Create was called by client
if (operationContext.IncomingMessageHeaders.Action.EndsWith("Create"))
{
return Guid.Empty;
}
//CreateWithInstanceId was called by client
else if (operationContext.IncomingMessageHeaders.Action.EndsWith("CreateWithInstanceId"))
{
return (Guid)inputs[1];
}
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 to pass to the workflow
creationContext.WorkflowArguments.Add(pair.Key, pair.Value);
}
}
//reply to client with instanceId
responseContext.SendResponse(instanceId, null);
}
else if (operationContext.IncomingMessageHeaders.Action.EndsWith("CreateWithInstanceId"))
{
Dictionary<string, object> arguments = (Dictionary<string, object>)inputs[0];
if (arguments != null && arguments.Count > 0)
{
foreach (KeyValuePair<string, object> pair in arguments)
{
//arguments to pass to workflow
creationContext.WorkflowArguments.Add(pair.Key, pair.Value);
}
}
}
else
{
throw new InvalidOperationException("Invalid Action: " + operationContext.IncomingMessageHeaders.Action);
}
return creationContext;
}
}
}
// CreationEndpointClient.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Shared;
using System.ServiceModel;
namespace CreationClient
{
class Program
{
static void Main(string[] args)
{
try
{
//client using BasicHttpBinding
IWorkflowCreation client = new ChannelFactory<IWorkflowCreation>(new BasicHttpBinding(), new EndpointAddress("https://localhost/MyCreationEndpoint/Workflow1.xamlx")).CreateChannel();
Console.WriteLine("Workflow Instance created using CreationEndpoint added in config. Instance Id: {0}", client.Create(null));
Console.WriteLine("Press return to exit ...");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine(ex);
Console.ReadLine();
}
}
}
}
This example may seem confusing because you never implement a service that implements IWorkflowCreation
. This is because the CreationEndpoint
does this for you.
See Also
Tasks
Instance creation using WorkflowHostingEndpoint
Concepts
Windows Workflow Architecture
Windows Workflow Overview
Other Resources
Workflow Services
Hosting in Internet Information Services
Internet Information Services Hosting Best Practices
Internet Information Services Hosting Instructions
Rehosting the Workflow Designer