Peter Goodman bio photo

Peter Goodman

A software engineer and leader living in Auckland building products and teams. Originally from Derry, Ireland.

Twitter Google+ LinkedIn Github

This post is the third in a series on building an Enterprise Workflow System using WF4. Previously we discussed how to get the activity authoring experience for workflow service authors by using a custom WorkflowServiceHostFactory to tell the WorkflowServiceHost to host a compiled activity instead of a xamlx file.

Now we have a problem in that our workflow service cannot be started because it does not have a Receive Activity with a “CanCreateInstance” flag. Well, this is where the WorkflowCreationEndpoint steps in. This little beauty is an endpoint that can sit on our workflow service and when a request is processed by this endpoint, it will start a new instance of our workflow.

The only strange thing about this class is that it is a little different to implement than a standard WCF service. In WCF we would normally have an interface that represented our contract and we would only have to write a class that implements that contract. The WorkflowCreationEndpoint works slightly differently, before we get to that, lets create the interface that our clients will use to start a new instance of our workflow. Remember that we are building a Magic Eight Ball service such that we can ask a question, provide an email address and we will be emailed the response. Therefore our contract looks like this:

 

[ServiceContract(Name = "IEightBallContract")]
public interface IEightBallContract {
    [OperationContract]
    Guid Ask(string question, string email);
}

Notice that we have an operation called Ask that takes a question and an email address. The Guid return value will be our Workflow Instance Id.

Now for the implementation of the WorkflowCreationEndpoint:

internal class EnterpriseWorkflowCreationEndpoint : WorkflowHostingEndpoint  {
    public EnterpriseWorkflowCreationEndpoint(Binding binding, EndpointAddress endpointAddress)
        : base(typeof(IEightBallContract), binding, endpointAddress) {}

    protected override WorkflowCreationContext OnGetCreationContext(object[] inputs, OperationContext operationContext, Guid instanceId, WorkflowHostingResponseContext responseContext) {

        if(!operationContext.IncomingMessageHeaders.Action.EndsWith("Ask")) {
            throw new InvalidOperationException();
        }

        EnterpriseWorkflowCreationContext workflowCreationContext = new EnterpriseWorkflowCreationContext();

        string question = inputs[0] as string;
        string email = inputs[1] as string;

        workflowCreationContext.WorkflowArguments.Add("Question", question);
        workflowCreationContext.Email = email;

        responseContext.SendResponse(instanceId, null);

        return workflowCreationContext;
    }
}

 

In the creation endpoint, we override the OnGetCreationContext method and first validate that we are dealing with the “Ask” operation that we expect. We will cover the WorkflowCreationContext in more detail in the next post but notice that we can take the inputs, in this case only the question, and pass them as arguments to our Workflow using the creation context. We then send a response to the user containing the instance id and we are done.

The last step here is to wire up the creation endpoint on our WorkflowServiceHostFactory. For this we override the CreateWorkflowServiceHost method in the factory and add the endpoints as follows:

protected override WorkflowServiceHost CreateWorkflowServiceHost(System.Activities.Activity activity, Uri[] baseAddresses) {
    WorkflowServiceHost workflowServiceHost = base.CreateWorkflowServiceHost(activity, baseAddresses);
    AddBehaviorsAndEndpoints(workflowServiceHost);
    return workflowServiceHost;
}

/// <summary>
/// Adds the standard behaviors and endpoints to our workflow service host.
/// </summary>
/// <param name="workflowServiceHost">The workflow service host.</param>
private void AddBehaviorsAndEndpoints(WorkflowServiceHost workflowServiceHost) {
    // Check whether we have already initialised the service host
    if (workflowServiceHost.Description.Endpoints.Where(endpoint => endpoint is EnterpriseWorkflowCreationEndpoint).Any()) {
        return;
    }

    // Add endpoints for any services that have been defined in the workflow
    workflowServiceHost.AddDefaultEndpoints();

    ServiceEndpoint firstEndpoint = (from endpoint in workflowServiceHost.Description.Endpoints
                         where endpoint.IsSystemEndpoint == false
                         select endpoint).FirstOrDefault();

    BasicHttpBinding binding = new BasicHttpBinding();
    EndpointAddress endpointAddress = new EndpointAddress(workflowServiceHost.BaseAddresses[0]);

    // Add the creation endpoint
    EnterpriseWorkflowCreationEndpoint creationEndpoint = new EnterpriseWorkflowCreationEndpoint(firstEndpoint != null ? firstEndpoint.Binding : binding, firstEndpoint != null ? firstEndpoint.Address : endpointAddress);

    workflowServiceHost.AddServiceEndpoint(creationEndpoint);

}

 

There is a lot more that can be done here but the important things to know are that we are using AddDefaultEndpoints to first add any endpoints that are specified by Receive activities on our workflow activity, this is important if we want to support a workflow that can be called from outside while in process.

Next we create a default endpoint address and binding in case we are going to need it and then we try to reuse the address and binding of a previous endpoint if one exists otherwise use our new ones. Finally we simply call AddServiceEndpoint to add the WorkflowCreationEndpoint to our host. Now we have a workflow that can be started by calling Ask. You can test this using the WCF Test Client as below:

CropperCapture[8]

We can also write client code to access our service easily, either by using the Add Service Reference in Visual Studio or using the ChannelFactory.

// Start the EightBall workflow
IEightBallContract eightBall = ChannelFactory<IEightBallContract>.CreateChannel(
    new BasicHttpBinding(),
    new EndpointAddress(
       "http://localhost/HardcoreWorkflow/BasicMagicEightBall/EightBall.svc"));

eightBall.Ask(model.Question, model.Email);

 

In the next post we will look at the WorkflowCreationContext and how we can use it to store state about how our instance was started without requiring our workflow to know about it. We will also look at how we can respond to the completion of the workflow and perform an action once it is complete, cancelled or terminated.

Download the Code EnterpriseWorkflowDemo.zip