by community-syndication | Feb 26, 2010 | BizTalk Community Blogs via Syndication
BizTalk: Compensation Model
There are still many questions in using Compensation in BizTalk.
%u00b7 What is the proper order of the Exception and Compensating blocks (handlers), Compensate shapes?
%u00b7 Do we have to rethrow the Exceptions?
%u00b7 Do we have to use Compensate shape for the current transaction or for the internal transactions?
%u00b7 What exactly does the Compensate shape?
%u00b7 What is the difference between Atomic and Long-Running transactions from the Compensation point of view?
Let’s start from the theory.
Compensation and Rollback
I have to use term the Rollback in this document. It is not from the BizTalk Documentation.
Data_1–> [Transaction: Data_1to2]–> Data_2
I use the Rollback term like the same term in the database transactions. Rollback means restoring all data to the point before transaction. It is completely automatic, atomic process. We cannot control rollback. Only one operation is possible, Start Rollback. It rolls back only the local orchestration data, orchestration variables and parameters.
The local orchestration data outside the transaction could be only Data_1 or Data_2. When Transaction is Committed, the data is Data_2. When Transaction is Rollbacked, the data automatically returns to the pre-transaction state, to the Data_1. We never get the Data_1to2 outside the Transaction. We all know this; it is ABC of the Transaction model.
But BizTalk is not a data base server as SQL Server and transaction in BizTalk is not a data base transaction. For example the Send shape inside transaction scope can send message to the external system and change data in this system. In general case BizTalk cannot automatically roll back this operation and won’t do this. We have to manually create a code to roll back the external changes. Atomic transaction might change the external system in the same way as Long-Running transaction. BizTalk helps us with managing this roll back procedure. We include the custom roll back code in the Compensation block. This code is invoked automatically by BizTalk or manually by a Compensate shape and the whole process called the Compensation.
Compensation it is not a Rollback. It is another process.
Compensation is a controlled process. Here is a typical example: code inside BizTalk transaction inserts rows in the database. BizTalk does not automatically rollback this operation. But it gives us possibility to implement [maybe] complex business process of deleting inserted rows. Plus, the last uncontrolled step of the Compensation is a Rollback. We implement some custom business process of compensation and then BizTalk invoke Rollback.
All Rollbacks and Compensations are initiated by an Exception. If an Exception is fired inside transaction, BizTalk invokes Rollback, but Compensation for this transaction is usually prohibited.
BizTalk invokes Rollback, but before that BizTalk invokes the Exception block of this transaction, if there is such block and if this block catches the Exception.
Note: Compensation block and Compensation shape are different things. Compensation shape is a method to call the Compensation block.
Atomic and Long-Running transactions
By design Atomic transaction should be used for the fast and simple procedures. The data before Atomic transaction is not persisted on the disk. Atomic transaction is quick that’s why it does not damage the total reliability. You can mention that Atomic transaction property the Timeout automatically set up to 60 sec, but not for the Long-Running transaction. Moreover only the Atomic transaction has the Property the Retry because it makes sense to automatically retry only the short process not the long one.
The likelihood of the system crash is bigger in the durations of hours and days than in seconds. That’s why data before Long-Running transaction persisted to disk. That’s why the orchestration data used in the Long- Running transaction must be serializable. That’s why the Long-Running transaction theoretically slower than Atomic transaction.
Note: Atomic transaction in BizTalk is not atomic in strict sense. It can include requests to the external processes, calls to the .NET classes, etc. Each of these processes could change something outside the orchestration and the transaction Rollback does not automatically roll back all these changes. It would be dangerous.
The Atomic transaction is short by definition. It cannot nest other transactions inside. The Atomic transaction is processed as a single unit of work. If Exception is fired inside the Atomic transaction, all we have to do is to roll back this transaction. And BizTalk makes this automatically. We cannot change this roll back process no matter what sort of Exception was fired.
Seems this is the main difference between n the Atomic and Long-Running transactions in BizTalk, how they behave in case of the internal Exception. Atomic transaction always automatically performs Rollback. We cannot avoid this Rollback and cannot do something else. Long-Running transaction could catch the Exception in the optional Exception block and process the rollback in specific custom code. But the Exception block cannot prevent the automatic Rollback. The Rollback occurs after executing the Exception block. If a Long-Running transaction does not have the Exception blocks it processes an Exception the same way as an Atomic transaction.
Compensations and Exceptions
Rule 1: Transaction block can get only one custom Compensation block.
Rule 2: If the Compensation block is not defined, the Transaction uses the default Compensation. Default Compensation is the Compensation for the current Transaction. That means the unwiring the Compensations for the nested Transactions and Rollback (restoring the Orchestration variables).
Rule 3: Firing an Exception is the only way to initialize the Compensation process.
Rule 4: Atomic transaction block cannot get the Exception block.
Rule 5: Long-Running transaction use the default Exception block for the “General Exception” if the custom Exception block for the “General Exception” is not defined. Default Exception block invokes the Compensation shape for current transaction and rethrow the Exception.
Rule 6: Compensation shape can be inserted only inside Exception block or Compensation blocks.
It seems obvious but anyway
We can include the Compensate shape nowhere but only in the Exception and Compensation blocks. We cannot start the compensation process arbitrary. No exception – no compensation! Sometimes we need to utilize the automatic compensation ability of the BizTalk in the ordinary business process. Firing Exception is not the ordinary situation; an exception should be always indicator of the error in process. But here we have to use exception, there are no other ways.
Rule 7: Transaction that fired the Exception processes a Rollback right after this Exception. We cannot compensate this transaction.*
*) As Charles Young mentioned here is a workaround for this rule (see the “Using Compensation Blocks on L-R Transactions” topic in his article), but you have to use undocumented features of the BizTalk. BizTalk is wisely trying to prevent compensating uncommitted transaction.
Standard compensation process (Example)
Let’s cover the standard compensation process.
Below is the simple orchestration with one LR nesting transaction (1—) and two nested transactions (11 and 12).
Second nested transaction fired the Exception.
Let see what is going on here:
1. Code generates the DivideByZeroException
2. The Exception is caught in the Exception block of the same Transaction.
3. The Exception is rethrown.
4. BizTalk rolls back the Transaction, that means it restore the orchestration variables and parameters.
5. The Exception is caught in the Exception block of the nesting, outer Transaction.
6. Calls to the Compensating of the nested Transactions in the revers order. Compensation of the 12 cannot be executed because this transaction was not Committed (completed).
7. Compensating of the 11 Transaction.
8. Exception is rethrown. Orchestration is suspended.
Why the Compensation blocks the Comp12 and Comp 1 were not executed? For different reasons.
Comp12 was not executed because Transaction the 12 was not committed but rollbacked and a try to call the Compensation in the C_Ex 1 block was skiped.
Comp1 was not executed because it could be called only from the outer, nesting transaction. We don’t have such transaction here.
The trace log was:
1 —
1 — 11…
1 — 11… 12…(1)
1 — 11… 12… >>> Exc 12 (2)
1 — 11… 12… <<< Exc 12 Rethrow Exception ******** (3) (4)
1 — 11… >>> C_Ex 1 (5)
1 — 11… >>> Comp 11 (6)
1 — 11… <<< Comp 11 (7)
1 — 11… <<< C_Ex 1Rethrow Exception ******** (8)
Below is the XLANGs code generated by BizTalk Orchestration Editor:
module GLD.Samples.Compensation
{
internal service longrunning transaction StandardCompensation
{
port implements PortType_1 Control_R;
message System.Xml.XmlDocument msg_XmlDocument;
System.Int32 var_int;
System.String var_State;
body ()
{
activate receive (Control_R.Operation_1, msg_XmlDocument);
var_int = 0;
var_State = “”;
scope longrunning transaction tx_1
{
System.Int32 var_IntLevel1;
body
{
var_IntLevel1 = 0;
var_State = “1 — “;
System.Diagnostics.Trace.WriteLine(var_State);
scope longrunning transaction tx_11
{
body
{
var_State = var_State + “11… “;
System.Diagnostics.Trace.WriteLine(var_State);
}
compensation ()
{
System.Diagnostics.Trace.WriteLine(var_State + “>>> Comp 11”);
System.Diagnostics.Trace.WriteLine(var_State + “<<< Comp 11”);
}
}
scope longrunning transaction tx_12
{
body
{
var_State = var_State + “12… “;
System.Diagnostics.Trace.WriteLine(var_State);
// Exception!!!
var_int = var_int / System.Convert.ToInt32(“0”);
}
exceptions
{
catch
{
System.Diagnostics.Trace.WriteLine(var_State + “>>> C_Ex 12”);
System.Diagnostics.Trace.WriteLine(var_State + “<<< C_Ex 12 Rethrow Exception ********”);
throw;
}
}
compensation ()
{
System.Diagnostics.Trace.WriteLine(var_State + “>>> Comp 12”);
System.Diagnostics.Trace.WriteLine(var_State + “<<< Comp 12”);
}
}
}
exceptions
{
catch
{
System.Diagnostics.Trace.WriteLine(var_State + “>>> C_Ex 1”);
if (succeeded(tx_12))
{
compensate tx_12 ();
}
if (succeeded(tx_11))
{
compensate tx_11 ();
}
System.Diagnostics.Trace.WriteLine(var_State + “C_Ex 1Rethrow Exception ******** “);
throw;
}
}
compensation ()
{
System.Diagnostics.Trace.WriteLine(var_State + “>>> Comp 1”);
compensate tx_1 ();
System.Diagnostics.Trace.WriteLine(var_State + “<<< Comp 1”);
}
}
}
}
The uncear points in the Compensation Model
To me there are several confusing things in the Compensation model.
First, the Compensation blocks are called outside. Event to call the Compensation comes outside! It is pretty unusual from the developer experience. One more time: the Compensation block is called outside. We are waiting that methods start from the first operator, and the exceptions catch blocks are placed in the end of code. The Exception blocks in the Orchestration are placed as we are waiting for, in the end of the code. Compensation blocks placed in the same place but worked in opposite manner. It is unintuitive. Now the similar graphic presentation of the Exception and Compensation blocks mixed with different behaviour of the Exception and Compensation blocks. It is misleading.
Second, Rollbacks and Compensation are kind of unsynchronized. I will tell about this in the next sample.
Compensation Model process (Example)
Let’s cover the more complex compensation process.
This is the transaction tree:
Below is the orchestration picture. Nesting transaction was implemented in nested loops.
Exception was fired in the 1222 transaction.
The trace log was:
——————————- 5 —————————-
1000***
1000*** 1100===
1000*** 1100=== 1110—
1000*** 1100=== 1110— 1111…
1000*** 1100=== 1110— 1111… 1112…
1000*** 1100=== 1110— 1111… 1112… 1120—
1000*** 1100=== 1110— 1111… 1112… 1120— 1121…
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122…
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200===
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210—
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211…
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212…
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… 1220—
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… 1220— 1221…
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… 1220— 1221… 1222…
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… 1220— 1221… 1222…******* Throw exception(1)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… 1220— 1221… >>> C_Ex 3(2)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… 1220— 1221… >>> Comp 4(3)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… 1220— 1221… <<< Comp 4(4)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… 1220— 1221… <<< C_Ex 3 ******* ReThrow exception(5)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… >>> C_Ex 2(6)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… >>> Comp 3(7)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… >>>Comp 4(8)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… <<< Comp 4(9)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… >>> Comp 4(10)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… <<< Comp 4 (11)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… <<<Comp 3(12)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… 1200=== 1210— 1211… 1212… <<< C_Ex 2 ******* ReThrow exception(13)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… >>> C_Ex 1(14)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… >>> Comp 2(15)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… >>> Comp 3(16)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… >>> Comp 4(17)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… <<< Comp 4(18)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… >>> Comp 4(19)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… <<< Comp 4(20)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… <<< Comp 3(21)
1000*** 1100=== 1110— 1111… 1112… >>> Comp 3(22)
1000*** 1100=== 1110— 1111… 1112… >>> Comp 4(23)
1000*** 1100=== 1110— 1111… 1112… <<< Comp 4(24)
1000*** 1100=== 1110— 1111… >>> Comp 4(25)
1000*** 1100=== 1110— 1111… <<< Comp 4(26)
1000*** 1100=== 1110— 1111… 1112… <<< Comp 3(27)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… <<< Comp 2(28)
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122… <<< C_Ex 1(29)
……………………………………………………………..
1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122…
Picture of whole compensation process:
As an initial exception (1) is fired, there are several stages in the compensation process. A catch block follows after each exception, then compensations (blue arrow) and rollbacks (green arrow) for nested transaction(s). Each compensation is followed by a rollback with nested compensations and rollbacks in between.
For example, an Exception (5) in the Catch Exception block the “C_Ex 3” of the transaction 1220 is caught into the Catch Exception block (6) of the Transaction 1200, then follows the Compensation “Comp3” for the Transaction 1210 (7), the Compensation (8) and the Rollback (9) for the Transaction 1212, the Compensation (10) and the Rollback (11) for the Transaction 1211, the Rollback (12) for the Transaction 1210.
You can mention one interesting detail in the whole compensation process: All compensation blocks were called and all compensation code inside was invoked. But the orchestration data was not returned to the initial state. The orchestration data was roll backed only for the transaction branch where exceptions were fired, for the transaction 1200 and nested transactions. We could predict the roll backed data would be the “” but it is “1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122…”
Is it a bug in BizTalk?
Seems the compensation process is tuned up only for the “default” compensation model for some reason. In this model each implicit Exception block is finished by a Throw Exception operator. As you can remember the Default Exception block invokes the Compensation shape with current transaction and rethrow an initial Exception. If we follow this model we have got a predictable rollback process.
In the preceding example we have to nest the Transaction 1000 in outer Transaction and add a Throw Exception operator in “C-Ex 1” Exception block. In this case the data would be roll backed to the “” value in this outer transaction.
Rule 7: The orchestration data is roll backed to the initial point of the transaction where the last exception is rethrown.
For example, the last exception was rethrown in the Transaction 1200. The initial data for this Transaction was “1000*** 1100=== 1110— 1111… 1112… 1120— 1121… 1122…”. The orchestration data was roll backed to this value, not to the initial data for the outer compensated Transaction 1000.
Conclusion
Let’s summarize how to create the standard compensation process.
1. Define the Compensation blocks for all transaction you want to be compensated. This is the most chalenging task.
2. Define the Exception blocks where the Exception(s) should be cought.
3. Add the Compensate shape(s) to this Exception block. Define what transaction you want to compensate. The standard way is to define a current transaction. All nested transactions would be compensated automaticaly in the right order.
4. If you need a predictable rollback process for the orchestration data, rethrow an Exception in each Exception block where you added the Compensate shape.
by community-syndication | Feb 25, 2010 | BizTalk Community Blogs via Syndication
This is part ten of a twelve part series that introduces the features of WCF WebHttp Services in .NET 4. In this post we will cover:
- Generating and including ETags in responses from a WCF WebHttp Service
- Adding Conditional GET support to a WCF WebHttp Service using ETags
Over the course of this blog post series, we are building a web service called TeamTask. TeamTask allows a team to track tasks assigned to members of the team. Because the code in a given blog post builds upon the code from the previous posts, the posts are intended to be read in-order.
Downloading the TeamTask Code
At the end of this blog post, you’ll find a link that will allow you to download the code for the current TeamTask Service as a compressed file. After extracting, you’ll find that it contains “Before” and “After” versions of the TeamTask solution. If you would like to follow along with the steps outlined in this post, download the code and open the “Before” solution in Visual Studio 2010. If you aren’t sure about a step, refer to the “After” version of the TeamTask solution.
Note: If you try running the sample code and see a Visual Studio Project Sample Loading Error that begins with “Assembly could not be loaded and will be ignored”, see here for troubleshooting.
Getting Visual Studio 2010
To follow along with this blog post series, you will need to have Microsoft Visual Studio 2010 and the full .NET 4 Framework installed on your machine. (The client profile of the .NET 4 Framework is not sufficient.) At the time of this posting, the Microsoft Visual Studio 2010 Ultimate Beta 2 is available for free download and there are numerous resources available regarding how to download and install, including this Channel 9 video.
Step 1: Adding ETags to Responses
Entity Tags (or ETags) are a crucial aspect of the caching mechanisms in HTTP. An ETag is an opaque quoted string that may be returned along with a response in an ETag HTTP header. ETags are used to indicate whether or not a resource has changed state. If two requests for the same resource result in responses with the same ETag then it can be assumed that the state of the resource has not changed between the two requests.
As we’ll see, ETags can be used to implement conditional GET. With conditional GET the client sends the ETag from a previous response and effectively asks the server to only send the content if the resource state has changed. Using conditional GET reduces the bandwidth demands on the service and client since the content need not be sent if the client already has the most recent version.
To demonstrate the ETag support in WCF WebHttp Services for .NET 4 we’ll update the TeamTask service to send responses with ETags for all requests of a single task. The GetTask() service operation on the TaskService class is responsible for returning individual tasks, so it’s the operation we’ll be modifying.
We’ll need to do two things in order to provide ETags with responses from the GetTask() operation. First, we’ll need to specify that a given ETag should be included with a given response. As you’ll see below, this is easily done by calling the new SetETag() method from the WebOperationContext within the service operation. But we also need to generate the ETag in the first place. Reliably and efficiently generating Etags for our tasks could be a messy job, but since we’re using an SQL database to store our tasks, we can delegate the ETag generation to the database and make it easy for ourselves.
We’ll be using the Version property of the Task type for our ETags. The Version property has existed on the Task type from the very first post of this series but you might not have noticed it since we haven’t been serializing it when generating responses. The Version property on the Task type is mapped to the Version column of the Tasks table in the TeamTask.mdf database file. If you open the Tasks table definition in the Server Explorer window (Ctrl+W, L), you’ll see that the Version column is a timestamp data type that is updated by SQL anytime the task is updated in the database.
The Version property of the Task type is an eight byte array, so the first thing we’ll do is create a an extension method that will encode the eight byte array as a string representing a hexadecimal number.
If you haven’t already done so, download and open the “Before” solution of the code attached to this blog post.
In the Solution Explorer (Ctrl+W, S) right click on the TeamTask.Service project and select “Add”->”Class” from the context menu that appears. In the Add New Item window enter the name “Extensions.cs” and hit enter.
Make the Extensions class static and add the following ToHexString() extension method as shown below. You’ll also need to add “using System.Linq;” to the code file:
public static class Extensions
{
public static string ToHexString(this byte[] byteArray)
{
return string.Join(string.Empty, byteArray.Select(b => b.ToString(“X”)));
}
}
Now open the TaskService.cs file in the code editor and add the following two lines of code to the GetTask() operation directly before returning the task within the using code block:
string etag = task.Version.ToHexString();
WebOperationContext.Current.OutgoingResponse.SetETag(etag);
By calling the SetETag() method for the outgoing response we ensure that an HTTP ETag header with the task version value is included with the response.
The complete GetTask() operation should look like the following:
[Description(“Returns the details of a single task.”)]
[WebGet(UriTemplate = “{id}”)]
public Task GetTask(string id)
{
int parsedId = ParseTaskId(id);
using (TeamTaskObjectContext objectContext =
new TeamTaskObjectContext())
{
var task = objectContext.Tasks
.FirstOrDefault(t => t.Id == parsedId);
if (task == null)
{
ThrowNoSuchTaskId(parsedId);
}
string etag = task.Version.ToHexString();
WebOperationContext.Current.OutgoingResponse.SetETag(etag);
return task;
}
}
Helpful Tip: The SetETag() method is found on the OutgoingResponse of the WebOperationContext, which also provides access to the response headers and even an ETag property. However, the SetETag() method will ensure that the ETag is correctly quoted as per the HTTP specification so you should use it instead of the headers collection or the ETag property. |
Step 2: Adding Conditional GET Support
In step one of this blog post we briefly discusses the concept of a conditional GET. If a client has already sent a previous request for a resource and received a response along with an ETag, the client can send that ETag in an HTTP If-None-Match header on future requests. If the state of the resource hasn’t changed since the previous request, the server can send back a response without a body and an HTTP status code of 304 (Not Modified). Of course, if the state of the resource has changed since the previous request, the server will respond with the new state of the resource and a new ETag.
In the next step, we’ll update our client to use ETags and send conditional GET requests so we’ll see conditional GET in action. But first we need to edit the GetTasks() operation of the TaskService service class so that it checks for the HTTP If-None-Match header. We’ll add this check for the If-None-Match header with a single call to the new CheckConditionalRetrieve() method from the WebOperationContext.
The CheckConditionalRetrieve() method takes the current ETag as an argument and tries to match it against the ETags in the If-None-Match header of the request. If there is a match then the client already has the current version of the resource so the CheckConditionalRetrieve() method throws a WebFaultException with an HTTP status code of 304 (Not Modified) just as the HTTP specification prescribes. If there is no If-None-Match header in the request or if none of the ETags in the header match the current ETag, then the CheckConditionalRetrieve() method returns so that service operation can complete as normal.
Now open the TaskService.cs file in the code editor and add a call to the CheckConditionalRetrieve() method in the GetTask() operation like so:
string etag = task.Version.ToHexString();
WebOperationContext.Current.IncomingRequest
.CheckConditionalRetrieve(etag);
WebOperationContext.Current.OutgoingResponse.SetETag(etag);
Helpful Tip: In part seven of this blog post series we discussed how ASP.NET server and client-side caching functionality can be leveraged by a WCF WebHttp service in .NET 4. It’s important to consider how ETags and conditional GET interacts with server and client-side caching.
For example, conditional GET support requires checking the ETag from the request within the service operation. However, if server-side caching has naively been enabled, two requests with different ETags (one current, the other stale) may both receive the same response from the ASP.NET output cache. The correct way to compose server-side caching and conditional GET is to cache different responses based on the value of the HTTP If-Modified-Since header from the requests. Therefore when a request with a new ETag value is received there will be a cache-miss and the service operation will execute. In step two of part seven of this blog post series we discussed how to cache based on request headers.
The right caching mechanisms for your service will depend on the type of resources your service exposes. If a resource changes rarely (or at known intervals) and you expect lots of different clients to only request the resource once, then server-side caching makes sense because you can serve those requests from the cache. If you expect clients to poll an often-changing resource for updates, then you’d probably want to include conditional GET support. |
Step 3: Updating the Client to use ETags
Now that we’ve implemented the GetTask() service operation to both return ETags and support conditional GET requests, let’s update the client to take advantage of this new functionality.
We’ll first have the client send a GET request for a task. The client won’t yet have an ETag for the task, so it won’t include one. However, we’ll capture the ETag from the response and send a second request with the same ETag in an If-Modified-Since header. With this second request we’ll see that the TeamTask service will return a 304 (Not Modified) response and no message body. We’ll then update the task and send a third request with the original ETag. Since we’ve updated the task, the original ETag is stale and the response to the third request will include the updated task and a new ETag.
Open the Program.cs file of the TeamTask.Client project in the code editor and replace the current implementation of the static GetTask() method with the implementation below. You’ll also need to add “using Microsoft.Http.Headers;” and “using System.Net;” to the code file:
static Task GetTask(HttpClient client, int id, ref EntityTag etag)
{
Console.WriteLine(“Getting task ‘{0}’:”, id);
using (HttpRequestMessage request =
new HttpRequestMessage(“GET”, “Tasks/” + id))
{
request.Headers.Accept.AddString(“application/json”);
if (etag != null)
{
request.Headers.IfNoneMatch.Add(etag);
}
using (HttpResponseMessage response = client.Send(request))
{
if (response.StatusCode == HttpStatusCode.NotModified)
{
Console.WriteLine(
“The task with id ‘{0}’ has not been modified.”, id);
Console.WriteLine();
return null;
}
else
{
etag = response.Headers.ETag;
response.EnsureStatusIsSuccessful();
Console.WriteLine(“Etag: {0}”, etag.Tag);
WriteOutContent(response.Content);
return response.Content.ReadAsJsonDataContract<Task>();
}
}
}
}
This new implementation of the GetTask() method is similar to the client code that we created in part two of this blog post series except that we’ve added the conditional GET functionality. Before sending the request, we include the If-None-Match header by calling the IfNoneMatch.Add() method on the headers collection. After receiving the response we check the status code for a value of 304 (Not Modified).
Replace the Main() method implementation with the following code:
using (HttpClient client = new HttpClient(“http://localhost:8080/TeamTask/”))
{
EntityTag etag = null;
// Get the task and the etag
Task task = GetTask(client, 2, ref etag);
// Get the task a second time and include the etag
GetTask(client, 2, ref etag);
// Update the task;
task.Status = (task.Status == TaskStatus.Completed) ?
TaskStatus.InProgress :
TaskStatus.Completed;
UpdateTask(client, task);
// Get the task again… the etag should be stale
GetTask(client, 2, ref etag);
Console.ReadKey();
}
Start without debugging (Ctrl+F5) to get the TeamTask service running and then start the client by right clicking on the TeamTask.Client project in the “Solution Explorer” window and selecting “Debug”->”Start New Instance”. The console should contain the following output:
Notice that the second response for task ‘2’ has a status code of 304 (Not Modified) and that the ETag returned for the third request is different than the ETag returned from the first request because of the update that occurred between the two requests.
Next Steps: Optimistic Concurrency Support in WCF WebHttp Services
In this part of the series we’ve shown how easy it is to add ETag and conditional GET functionality to a WCF WebHttp service in .NET 4. In part eleven of this blog post series we’ll explore how ETags can be used to implement optimistic concurrency in a WCF WebHttp service in order to prevent clients from unknowingly updating data that has changed since they last retrieved it.
Randall Tombaugh
Developer, WCF WebHttp Services