Content Based Correlation In WF 4 ---a Samlpe
How to define and host workflow services with multiple receives
First off let me apologise for the length of this post. If you just want the code, skip to the bottom.
I don’t know about you but some words just don’t seem to make their way into my skull very easily, and correlation is one of them. It doesn’t matter how many times I say it in my head, even reallyslowly “c-o-r-r-e-l-a-t-i-o-n”, it still makes little sense.
So, in the spirit of sharing I thought I’d post what I know about correlation (in as far as it’s used in Workflow 4), and add in some code to cement the subject. First off though I thought I’d pop upstairs and look at the dictionary definition of correlation so here goes…
- Correlate – have a relationship or connection in which one thing affects or depends on another
- Correlation – connection, association, link, tie-in, tie-up, relationship, interrelationship, interdependence, interconnection, interaction
Thanks to the “Concise Oxford English Dictionary” for the former definition, and the “Oxford Paperback Thesaurus” for the latter. I hope they won’t mind me using their definitions here but if they do then someone else wrote this article, it wasn’t me, honest. As an aside, the “Concise OED” is a bit of an oxymoron if you see the physical size of it, but then again if you saw the full OED then you’d realise why this was called the concise version. Anyhow, enough English language for now!
So, how does correlation apply to a workflow then?
In the old days (well, pre Workflow 4), the common way to deal with a workflow instance was to know it’s instance ID which was a GUID. You had a workflow definition (i.e. some XML or a coded workflow), and when you created an instance of that workflow definition you had a workflow instance, and it was uniquely identifiable by its workflow instance ID. Simple.
If I wanted to do anything with a WF 3.x workflow, all I needed to know was it’s GUID and I could load it up, send messages to it, find it in the persistence database and so on. When I created a workflow instance I could optionally choose to assign my own GUID to it rather than having the system generate one for me. Life was good.
Now, lets say you were exposing a workflow as a service. You might have a few operations defined on that service and want to call these in whatever order suited you. You’ll define the first operation (which effectively kicks off the workflow), then subsequent operations will be called on the same instance of the workflow. In traditional programming you would maybe implement an interface something like the following…
View Code[ServiceContract]
public interface IStudent
{
[OperationContract]
Guid EnrollStudent(string name, DateTime dob);
[OperationContract]
void AddExamResults(Guid studentId, string examName, int mark);
[OperationContract]
void Graduate(Guid studentId);
So the initial operation is EnrollStudent, and this returns the GUID of that student. Then some time later (days/weeks/months/years) you call AddExamResults and pass through the GUID in order to hook up the results with the right student. Finally at some point in the future the Graduate method is called (OK, maybe they flunked, but I’ll keep this simple). That’s all fairly easy to understand.
Now, if you were doing this in the workflow world things would be a little different. As of now (Workflow 4 Beta 1) you can’t do contract first workflow services, so you need to define the service contract within the workflow itself. Hopefully this limitation will be fixed by the time Beta 2 drops as I quite like contract first development these days.
In the service contract I defined above, the thing that identifies one student from another is the GUID returned from the initial EnrollStudent method. You can conceptually see that this key uniquely defines the student instance, and as long as you use that every time you want to ‘talk’ to that student instance then everything will be just fine.
Moving this service into the world of Workflow 4, we need some identifier that does the same job – it ties my requests to an instance of a workflow. Suppose I have two clients. The first calls EnrollStudent and this constructs an instance of my workflow and then returns the unique ID back to the caller. The second client can call EnrollStudent and will get a different ID and so now there will be two distinct workflow instances ‘running’ (they may be persisted to the database). Now when I get calls to the AddExamResults method I need to have these go to the appropriate workflow instance, and it’s correlation that does that. You don’t have to use GUIDs by the way – correlation in WF 4 allows you to define anything in the message as the correlation ‘handle’ – indeed you can use multiple items in the message as this handle which is very cool indeed. However for this example I’m using just one thing – a GUID.
So, finally, we have my definition of correlation as it applies to WF 4…
- Correlation – hooks messages to the workflows they are intended to reach
OK, so that’s a fairly poor definition and I don’t think the OED will be knocking on the door any time soon asking me to write for them, but hopefully you get the drift. Enough waffle though, lets get to some code. I’ll try to emulate the above service contract with WF 4, and use code to call that service.
Example – Defining the Workflow
In this example I’ll use code throughout – no XAML here folks! First off I need to define some data contracts for the arguments I’m passing in to the service methods. These mimic the parameters I’m passing to the service contract I outlined above…
View Code[DataContract(Namespace = "http://www.morganskinner.com")]
public class EnrollStudentArgs
{
[DataMember]
public string Name { get; set; }
[DataMember]
public DateTime DateOfBirth { get; set; }
}
[DataContract(Namespace="http://www.morganskinner.com")]
public class AddExamResultsArgs
{
[DataMember]
public Guid StudentId { get; set; }
[DataMember]
public string ExamName { get; set; }
[DataMember]
public int Mark { get; set; }
So here I have two data contracts, one for the EnrollStudent operation and another for the AddExamResults operation. I need to do it this way as there’s no way to define multiple arguments to the Receive activity at present (this will be coming in Beta 2). You would currently need to do this for every method that takes more than a single argument, and whilst it’s a bit of a pain to have to do it that’s just the way it is for now. Well actually that’s a bit of a lie, there is an activity that will do this in Beta 1 but some changes are afoot for Beta 2 so I decided to show you this method which works for now.
Now we need a workflow. For this example I’ve chosen to build the workflow up as follows…
- An initial call to the EnrollStudent operation is made. This kicks off a new workflow and passes in the EnrollStudentArgs, consisting of the Name and DateOfBirth. This data is stored away within the workflow and a new GUID returned to the caller.
- The workflow then sits in a loop, waiting for a call to AddExamResults or a call to Graduate.
- If AddExamResults is called the data is output to the console
- If Graduate is called then the loop will exit and the workflow will complete
So, the overall workflow will be defined roughly as follows …
I’ve simplified it a bit as in the workflow there are actually many more activities, but hopefully this image conveys the main structure. I’ll build the code up step by step and explain what the bits are as I go along.
The first thing you’ll need to define are some variables and the initial Receive activity…
View Code
What we have defined is now nearly enough to so the first stage of the workflow – the initial Receive, the computation of the Guid and the SendReply to pass this Guid back to the client. I say nearly enough – there is one more thing we need to define up front but I’m going to gloss over that just for a moment and show the workflow up to this point…View Code
So, in order to get this to compile I need to define the following before defining the Sequence shown above…
View Code
I’ve defined the message context which is used when defining the XPathMessageQuery objects that are used within the MessageQuerySet. I’ve defined a namespace prefix of “local” to refer to the namespace of my classes – we’ll need this again later in the example when retrieving values passed up from the client.
The MessageQuerySet object is used to define which bits are stripped from the message. In this case I have just one named value - “StudentId”, and this comes from the rather cryptic “sm:body()/ser:guid” XPath statement.
Now I’m not here to give you a lesson in XPath (and indeed you wouldn’t want one from me either!). If like me you think that XPath is akin to Voodoo Magic then I wouldn’t blame you.
The sm:body()/ser:guid implies that I’m looking throughout the ‘body’ of the message message for the ‘guid’ element from the ‘ser’ namespace and returning that. If you look at the actual data returned from the SendReply activity on the wire you would see something like the following (I captured this using the most excellent Fiddler2 tool)…
HTTP/1.1 200 OK
Content-Length: 203
Content-Type: text/xml; charset=utf-8
Server: Microsoft-HTTPAPI/2.0
Date: Fri, 04 Sep 2009 09:15:52 GMT
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<guid xmlns="http://schemas.microsoft.com/2003/10/Serialization/">
ddafc236-bec4-4745-bc6d-882f5ab3b051
</guid>
</s:Body>
</s:Envelope>
Here you can probably infer that we’re using ‘ser’ as a prefix for the Serialization namespace. It makes sense now, but I don’t much like magic prefixes – there are a bunch that are currently defined and used within the framework but are not currently documented. The documentaton should be sorted before WF 4 is released. If you’re interested these constants currently live on the XPathMessageContext class.
With all that defined we’re at the point where we should have a working Receive and SendReply (and of course a few other activities). Now it gets even more interesting.
Now I want to be able to call either the AddExamResults or the Graduate methods. These both pass the Guid along on the wire, but the former also passes other information too. In order to get to the right workflow instance I therefore need to extract the student Id from these calls and match it up to my earlier extract.
For this to work I’ll need another couple of message query sets…
View Code
The first message query set defines the XPath that will strip out the value of the StudentId property from the AddExamResultsArgs class. You’ll see that I’ve included the ‘local’ namespace prefix for both of these elements – it’s necessary, you won’t get anywhere without it.
The second message query set is used for the call to the Graduate method – here I’m just passing up a simple Guid and so need to strip just that Guid out of the message.
With a couple more variables defined (a boolean flag ‘finished’ and a variable to record the exam results) and two more receive activities we’re pretty much sorted…
Variable<AddExamResultsArgs> examResultArgs =
new Variable<AddExamResultsArgs> ( ) ;
// Flag indicating that we're done
Variable<bool> finished = new Variable<bool> { Default = false };
// This recieve activity waits for the AddExamResults operation to be called
Receive receiveAdd = new Receive
{
OperationName = "AddExamResults",
ServiceContractName = XName.Get("IStudent", ns),
content= new OutArgument<AddExamResultsArgs>(examResultArgs),
CorrelatesWith = errolHandle,
CorrelationQuery = studentIdQuery
};
// And this activity waits for the Graduate operation to be called
Receive receiveStop = new Receive
{
OperationName = "Graduate",
ServiceContractName = XName.Get("IStudent", ns),
CorrelatesWith = errolHandle,
CorrelationQuery = graduateId
content= new OutArgument<Guid>(studentId)
In the above I’ve defined the other receive operations that my workflow will process. These again need to define their operation names and the name of the service contract to which they belong, and the only other tricky part is to define the correlation information. The AddExamResults operation is passed an argument of the AddExamResultsArgs type, so I need to strip out the AddExamResultsArgs.StudentId in order to correlate this message with the workflow. That’s just what the CorrelatesWith and CorrelationQuery are doing.
The second operation is just passed a Guid, so again I’m using a query that will retrieve the Guid passed to the Graduate operation.
Finally we get to the rest of the workflow…
View Code
any number of child branches and will wait until one of the branches completes. In this case my branches are awaiting calls to WCF methods that I’ve defined by using Receive activities.
The first branch schedules the Receive activity that awaits a call on the AddExamResults method (and writes this data out to the console when it arrives), and the second branch waits for the Graduate call to be made and then sets the boolean ‘finished’ flag so that the workflow completes.
Example – Hosting the Workflow
Now we have a workflow we need to host it. Again I’m just using code rather than XAML to show you all the stuff you need.
View Code
Here I’ve defined the base address for the service to include the physical name of the machine I’m running on (dev10-vpc). If you use ‘localhost’ it will still work fine, however Fiddler2 won’t be able to pickup the traffic and display it for you.
I then construct a WorkflowServiceHost and add an appropriate endpoint to it. After that I’ve defined a metadata behaviour to permit clients to get the service metadata over Http (I could have added a metadata endpoint/binding instead). After that it’s just a case of opening the service and waiting for calls. You don’t need a .config file as all of the configuration has been done in code.
The last (and you’ll be glad to know, easiest!) part is creating the client.
Example – Creating a client
Construction of the client is simple. Add a service reference to the service (in my instancehttp://dev10-vpc/StudentService) and then call it. I’ve deliberately used different client instances here to prove that
View Code
note: this article originates from here, but it is for wf beta 1. but i converted to wf beta2 . so you can find difference between both version . the converted code here.
any comments are welcome. thanks.
posted on 2012-06-20 21:59 malaikuangren 阅读(382) 评论(0) 编辑 收藏 举报