Introducing WF4.0: Building Distributed Apps with WF 4.0 and WF 4.0 Services
Introduction
In 2006, Microsoft released .NET Framework 3.0, which basically consisted of extensions to .NET Framework 2.0. WCF was the biggest hit back then and gained the most attention. That even continued with the 3.5 release of the framework.
With .NET Framework 4.0, Microsoft made WF the major focus, and with the many enhancements they have in place, it is evident the importance that WF 4.0 will have for building .NET-based applications.
What is WF?
Well, answering this question is not the scope of this article. Understanding what WF is and when to use it is something you can get from various online resources – or even books. For example, one of the great early publications on WF 4.0 is David Chappell’s paper on MSDN; reading it will be more than enough to understand what is and when to use WF 4.0. You can find it here: http://msdn.microsoft.com/en-us/library/dd851337.aspx.
So what is this article about?
In summary, this article has two main objectives:
- Gives a first look on WF 4.0 (beta1 as of this writing).
- Shows how we can use WF 4.0 to build distributed applications via WF 4.0 Services.
- Using .NET 4.0 Data Services for performing data access.
Note: A nice post about the differences between WF 4.0 and WF 3.5 can be found here: http://bloggingabout.net/blogs/pascal/archive/2009/05/20/wf-4-0-what-is-different-from-3-x.aspx.
Application architecture
The application demonstrates a demo-worthy Car Rental system. The image below shows the design of the application.
The scenario is as follows: A car rental company has its information in a SQL Server database. They need to build an application which allows employees to check car prices and then book cars to clients based on the client input. For that, a WF 4.0 Service which contains the business logic is hosted over HTTP where clients can ask it to do two operations: CheckPrice and BookCar. The WF 4.0 Service – being a business process – is not allowed to access the database directly; instead, it communicates with an ADO.NET Data Service which performs database CRUD operations (more on that later). What client types does the system support? The answer is just about any client. In this example, we will see a .NET Windows client, and more interestingly, a WF 4.0 client.
Any similarities spotted…?
Examining the above architecture should definitely trigger in your development sense the concept of n-tier applications. In fact, the above design is nothing more than the traditional and beloved three tier design. Let’s do the matching:
- Data Access Layer: ADO.NET Data Services is used instead of the traditional DAL class libraries with the famous
SQLHelper
class. - Business Layer: A WF 4.0 Service is used as the business layer instead of the BAL class libraries we used to write.
- Presentation Layer: A WF 4.0 console client application is used instead of a .NET console application. Now, this is tricky; in this example, no user interface is required, so a console WF application was sufficient. However, in cases where a GUI is needed, a Windows (or web) client application is used easily…after all, the business logic is hosted in a WF Service, and can be consumed by any client just as if you were using an XML Web Service, for example.
So in a more traditional design, the architecture would be like something in the figure below:
So, the advent of the new technologies allowed for better designs. How the new WF-based design tops the old traditional one is something you will see as we go in this article…
Source xode
The source code can be downloaded from the link above. I will use the solution components as we go through the article. It would have been impossible to show a step by step approach on how I built the application, so as we go, I will refer to the various components of the solution and explain each and every one of those components.
Prerequisites
The sample is built on top of .NET 4.0 and VS 2010 Beta1 release. You can download them from here: http://www.microsoft.com/downloads/details.aspx?familyid=3296BB4F-D8BA-4CFD-AA95-A424C5913F6B&displaylang=en. You will also need SQL Server 2000/2005. Let’s roll…
Part 1: The database
As with any data-centric approach, the application design usually starts with the database and then goes up through layers. We will follow the concept here. Luckily, for our example, the database is as simple as a single table called Cars, shown below:
The CarId column holds the names of some famous manufacturers like Honda and Mercedes. The Day_UnitPrice column holds the daily renting amount fee, and finally, the Quantity column holds the available number of cars for renting.
In the attached files, you will find a file called CarStore.bak; restore that to your SQL Server.
Part 2: The ADO.NET Data Service
Data Services follow the Representational State Transfer (REST) design paradigms. This design depends entirely on the plain HTTP verbs: POST, GET, PUT, and DELETE to perform Create, Read, Update, and Delete (CRUD) operations, respectively. With REST, you enjoy simplicity and ease, and is most suitable when you are in no need to the complexity (yet sometimes absolute must) of SOAP with XML Web Services or WCF. When you do not need SOAP encryption, WS-routing, WS-addressing, and any other WS-* standards, then Data Services can be the best choice for performing CRUD operations.
There are many great sources online debating the REST vs. SOAP subject, which I encourage you to read and get more insights about when to use each.
First thing to do when creating an ADO.NET Data Service is exposing the data source. I used the Entity Framework to do just that. So, follow these steps to set up the Data Service project:
- Create an empty VS 2010 solution and call it “CarRentalDemo”.
- In the attached files, you will find a folder “CarRentalDataService“. Copy this file into your intepub/wwwroot folder, and from within IIS, create its application.
- Add the “CarRentalDataService” project as an existing web site into the “CarRentalDemo” solution.
Now, let’s examine what makes the Data Service project. From within VS, you will see the following files:
- CarRental.edmx: This will create an object representation of the CarStore database. Before exposing a database via REST style in a Data Service, we need to have the object model of that database. This object model is best represented using the ADO.NET Entity Framework.
- CarRentalService.svc and CarRentalService.cs: Now with the object model ready, we need to create the Data Service itself. CarRentalService.cs is used to indicate what the Entity Model is which we are exposing, and CarRentalService.svc is the physical Data Service pointing to the
CarRentalService
class.
That’s all that is required in order to build a Data Service. Now, from within Visual Studio, you can right click CarRentalService.svc and select Browse, and you will be able to query the data source in a REST style. For example, to select the car with ID “Honda”, use the following URL: http://localhost/CarRentalDataService/CarRentalService.svc/Cars('Honda').
Using the browser is enough only for browsing; however, in order to make the Data Service usable in our sample, we will have to do the CRUD operations using .NET code. This will be shown next.
Part 3: Custom Activities
Following the architecture diagram, we should now be seeing the WF 4.0 Service. Prior to that, let's discuss the custom activities. Activities are the building blocks of WF. Every shape you drag from the toolbox into the WF designer is an Activity. Custom Activities allow the developer to extend the existing activities with new ones written especially for the problem in hand; it is a kind of a Domain Specific Language (DSL).
WF 4.0, in particular, encourages the use of custom activities. Prior to 4.0, WF had the Code Activity shape which allowed the developer to write code directly into the WF process. In WF 4.0, the Code Activity shape is gone, and now developers will have to create custom activities to build their components.
In our example, there are a set of custom activities that we will need to use in both the WF 4.0 Service and the WF 4.0 Client. In the attached files, you will find a project named “CustomActivities”; this is a Class Library were all activities are compiled. Add this project to your solution “CarRentalDemo”. Let’s examine its contents:
GetInput.cs: This activity collects input from a user via console, and assigns it to an output argument. Arguments are the way in WF 4.0 to get data in and out of a process. There are input arguments and output arguments. Below is the code of the activity:
public class GetInput : CodeActivity
{
OutArgument<string> data;
public OutArgument<string> Data
{
get { return data; }
set { data = value; }
}
protected override void Execute(CodeActivityContext context)
{
string input = Console.ReadLine();
context.SetValue(data, input);
}
}
First, notice how the activity inherits from the CodeActivity
class. Then, we define a special data of type OutArgument
indicating to WF that we need this data exposed as an output of the custom activity. Then, we implement the single method required, which is the “Execute
” method. Here, we write the code that we need – which is just getting data from the console – and then assign this data to the OutArgument
. This is done using the context of the activity via the CodeActivityContext
class.
CheckPrice.cs: This activity consumes the Data Service and asks for the price of a certain car. Let’s see the code of this activity:
public class CheckPrice : CodeActivity
{
InArgument<string> carId;
public InArgument<string> CarId
{
get { return carId; }
set { carId = value; }
}
OutArgument<decimal> carPrice;
public OutArgument<decimal> CarPrice
{
get { return carPrice; }
set { carPrice = value; }
}
protected override void Execute(CodeActivityContext context)
{
string carId = CarId.Get(context);
String urlstr = "http://mhalabi/CarRentalDataService" +
"/CarRentalService.svc";
CarRentalReference.CarStoreEntities proxy =
new CarRentalReference.CarStoreEntities(new Uri(urlstr));
var query = (from c in proxy.Cars
where c.CarId == carId
select c).First();
decimal? price = query.Day_UnitPrice;
context.SetValue(CarPrice, price);
}
}
For this activity, we want to give the CarId as an input and get back the Price as an output. For this, we defined an input argument (CarId) and an output argument (CarPrice). The input argument will be passed to the activity from the WF process itself; this will be seen later. In order to get the value from the input argument, we again use the CodeActivtyContext
class.
Now that we have the CarId, we need to use it to query the Data Service and get the price of that particular car. The Data Service – as you saw previously – is physically an SVC file (similar to an SVC file of WCF), so before querying it, we need to add a service reference. This is done in the project, and the code uses the generated proxy. In the code, we use LINQ to query the service and get the price of the car that we want. This LINQ shown here can be thought of as a LINQ to URI… Finally, we set the value of the output argument CarPrice
.
BookCar.cs: This activity also consumes the Data Service to book a certain car. I won’t show the complete code here since it’s similar to the previous activity, but in general, this activity defines an input argument CarId and then passes this argument to the Execute
method which consumes the Data Service in order to book the corresponding car.
Part 4: WF 4.0 Service
WF services are about exposing a WF process over a Web Service – WCF more specifically. With WF services, you can continue to use the powerful features of WF to build business processes with the additional power of hosting these processes over Web Services and making them available for consumption. This is for sure a great feature that would drastically enhance the design of distributed apps; much like the purpose of this article.
In the attached code, you will find a project called “CarRentalService”. This is a project of type Console Workflow Application. Let’s start dissecting its components:
Program.cs: as with any console application, an entry point is required, and WF console apps are no different. The special thing about this application – being a WF Service instead of a normal WF program – is the need to be hosted as a WCF service; as such, the below code hosts the WF program over HTTP as a normal WCF service.
class Program
{
static void Main(string[] args)
{
string baseAddress = "http://localhost:8089/CarRentalService";
using (WorkflowServiceHost host =
new WorkflowServiceHost(typeof(RentCar), new Uri(baseAddress)))
{
host.Description.Behaviors.Add(new
ServiceMetadataBehavior() { HttpGetEnabled = true });
host.AddDefaultEndpoints();
host.Open();
Console.WriteLine("Car rental service listening at: " +
baseAddress);
Console.WriteLine("Press ENTER to exit");
Console.ReadLine();
host.Close();
}
}
}
The WorkflowServiceHost
(System.ServiceMode.Activities.dll) class is responsible for hosting the WF 4.0 Service much like the well known ServiceHost
(System.ServiceMode.dll) class is responsible for WCF hosting. In our case, the WF service will be hosted on the following HTTP URI: http://localhost:8089/CarRentalService.
Now, this WF service can be referenced and consumed just like any other WCF Service.
With the hosting now on hand, let’s have our first look on the new WF 4.0 designer and see how the CarRentalService is actually built.
One of the coolest new features of WF 4.0 is the XAML-based designer. RentCar.xaml is the file representing the physical process. The first thing to see is the variables section:
Variables are another new feature in WF 4.0. They are used to store values through the lifetime of the program. In our example, we have five variables defined:
RequestInfo
: a variable of typeCarRentalDataContract
.CarRentalDataContract
is a class defined in the same project and holds the WCFDataContract
used in the message exchange. It is a simple contract holding values for the CarId under process and the UserId who wants to rent the car.CustomerId
: a variable of type string.CarId
: another variable of type string.UnitPrice
: a variable of type decimal.ContentHandle
: this is a special variable and needs a little explanation. First, as you can see, it is of typeCorrelationHandle
, and it represents a very common technique called Correlation. Correlation is the technique used to associate one or more messages to a single process instance. To better understand this, let's take our example as the case study: in our process, a client first submits the CarId of the car she wants to rent and then her UserId. The process checks the price of the car and displays it to the client. The client will take her time to think and then will submit a yes/no answer; the key here is that there will be many instances of the same WF process running for many users. So, the challenge is to “route” the answer of that specific client to the correct WF process instance. So, for example, we do not want the answer for client “A” to be routed to the WF instance which was created for client “B”. This type of design is called Correlation, and will be very much familiar to BizTalk developers. Note that Correlation is not required for synchronous messaging (i.e., Request-Response) since the response comes back on the same request channel. How Correlation was put in use will be much clearer shortly…
So now, with the variables defined, let’s see the first part of our WF Service:
To build the above section, I have dragged the “ReceiveAndSendReply” activity from the “Messaging” tab of the toolbox. And, in between, I have dragged the “Assign” activity from the “Procedural” tab of the toolbox, and finally, used the “CheckPrice” custom activity from the “CustomActivitiesComponents” tab. The “CustomActivitiesComponents” tab will appear in your toolbox once you reference the “CustomActivities” project and build the solution.
“Receive Check Price” receives the request for checking the price of a car. The operation name is set to be “CheckPrice”; this will be the Service operation to be consumed from clients. The value is set to the variable “RequestInfo
” which is the input parameter of the service operation “CheckPrice”. The “Correlated with” property is set to the variable “ContentHandle
”. Let’s see how this is configured:
Here, the correlation is associated with variable CustomerId which is set to the XPath value of the CustomerId in the Data Contract. This means that whenever a customer requests to check the price of a certain car, a new correlation will be initialized and associated with that client’s CustomerId. As we will see later in the second part of the WF Service, the correlation will be followed-up when the customer asks to book a car. More on this later…
Next, the “Assign” shape assigns the variable CarId to the corresponding CarId from the Data Contract. Then, we call in the custom activity “CheckPrice” and pass in to the input argument “CarId” the variable “CarId” while expecting back the output argument “CarPrice” and associating its return value to the variable “UnitPrice”.
Now, with the price in hands, we finally return the result back to the client via the “Send Price” part of the “ReceiveAndSendReply” activity.
The second part of the WF Service is shown below:
This part receives a request to book the car, books it using the “BookCar” custom activity, and then sends a boolean confirmation back to the client. The important thing to notice here is the “Correlates with” property: this property is configured to follow-up the correlation we set up in the previous part; so nowb each request to book a car will be correctly correlated to the correct instance of the WF process.
Note: this design will cause a problem if the same customer is issuing multiple orders because then for the same customer, we will have multiple possible WF instances to correlate to (for the same CustomerId). A more realistic design would have been to correlate based on an OrderId or GUID; however, this article kept things simple for the purpose of demonstration.
Part 5: Widows client
Now with the WF Service all set, we can consume it just like any other WCF Service. In the attached code, there is a project called “TestClient”; add this project to your solution. The below code shows how you can consume the WF Service:
CarRentalServiceReference.CarRentalDataContract contract =
new TestClient.CarRentalServiceReference.CarRentalDataContract();
contract.CarId = "Mercedes";
contract.CustomerId = "012";
CarRentalServiceReference.CarRentalServiceContractClient proxy =
new TestClient.CarRentalServiceReference.CarRentalServiceContractClient();
decimal? test = proxy.CheckPrice(contract);
bool? test1 = proxy.BookCar("012");
CarRentalServiceReference
is a normal Service Reference to a WCF Service. Recall that we have hosted our WF Service in the following URI: http://localhost:8089/CarRentalService.
We have simply added a service reference to that URI (note that you have to run the service before adding a service reference to it; you need to have the host of the service running). Notice how in the proxy we have two Service operations exposed: CheckPrice and BookCar; these are the operation names we configured in the two receive shapes of the WF service…
Part 6: The WF client
The “CarRentalWF” project in the attached code illustrates how to consume the WF Service using a WF console application. Below is a step by step description of the WF process:
- Add the CarRentalWF project into your solution.
- Add a service reference to the CarRentalService project (first, run the CarRentalService project to run the host). Build the solution, and you will get the CarRentalWFComponents tab in the toolbox.
- The activity “WriteLine” is used to display a text for the user asking him to enter a car ID.
- The custom activity “GetInput” reads the input and stores it in a variable “CarId”.
- The activity “WriteLine” is used to display a text asking for the user ID.
- The custom activity “GetInput” reads the input and stores it in a variable “UserId”.
- The activity “Assign” initializes the variable of type
CarRentalDataContract
. - The “Assign” activity is used to set the
CarId
property of the data contract instance to the CarId variable. - The “Assign” activity is used to set the
CustomerId
property of the data contract instance to the UserId variable. - The “CheckPrice” activity from the CarRentalWFComponents is then used and passed in the data contract as a parameter. The CheckPrice activity here represents a Service operation; much like the one we called explicitly in the Windows client. The return value is stored in the variable “
Price
”. - The “WriteLine” activity asks the user if he wants to proceed based on the returned price of the car.
- The “GetInput” custom activity collects the answer of the user and stores it in the variable “
IsContinue
”. - The “If” activity is then used as follows: if the answer from the user is “yes”, then the car is booked, else the process ends. Let’s see how the activity is configured:
If you double click on the “If” activity, it is expanded as follows:
The condition part tests the value of variable “IsContinue
”. If the value is “yes” then the “Then” portion is executed, else the “Else” portion is executed, which simple displays a message to the user. Now, let’s examine how the “Then” portion is configured; double click on the “Then” shape and you will see the following:
Here, we send a request to the Service operation “BookCar” and finally display a message.
Running the sample
Run the CarRentalService project. Now, you have the WF service running and ready to accept requests.
Next, run the CarRentalWF project. Now you have the WF client console asking you to enter the CarId you want to rent. Enter “Honda”, for example, and directly you will be prompted to enter the UserId; enter 10, for example.
At this time, the CarRentalWF instance will consume the CheckPrice operation of the WF service “CarRentalService” which in turn will query the Data Service via the Custom Activity “CheckPrice” and will display the price of the selected car. Keep in mind that now a new correlation is initialized at the WF Service. The user will now be prompted to continue or not. Enter “yes”. The CarRentalWF instance will consume the BookCar service operation of the WF service CarRentalService, which in turn will consume the Data Service via the Custom Activity “BookCar”. Since we have a Correlation set, the command to book the car will be correctly routed to the appropriate process instance… Below is a screenshot of the final console output:
posted on 2010-02-22 10:23 kaixingirl 阅读(701) 评论(0) 编辑 收藏 举报