Before learning how to enhance this WCF service to support distributed transactions, we will first confirm that the existing WCF service doesn't support distributed transactions. In this section, we will test the following scenarios:
TransactionScope
and redo the test.The first scenario to test is that within one method of the client application, two service calls will be made and one of them will fail. We then verify whether the update in the successful service call has been committed to the database. If it has been, this will mean that the two service calls are not within a single atomic transaction and will indicate that the WCF service does not support distributed transactions.
You can follow these steps to create a WPF client for this test case:
DistNorthwindWPF
as Name.Now the new test client should have been created and added to the solution. Let's follow these steps to customize this client so that we can call ProductService
twice within one method and test the distributed transaction support of this WCF service:
MainWindow
designer surface, add the following controls (you can double-click on the MainWindow.xaml item to open this window and make sure you are on the design mode, not the XAML mode):Your MainWindow design surface should be as shown in the following screenshot:
ProductServiceProxy
and the URL of the product service should be http://localhost/DistNorthwindService/ProductService.svc
.MainWindow.xaml
designer surface, double-click on the Get Product Details button to create an event handler for this button.MainWindow.xaml.cs
file, add the following using
statement:using DistNorthwindWPF.ProductServiceProxy;
MainWindow.xaml.cs
file, add the following two class members:Product product1, product2;
MainWindow.xaml.cs
file:private string GetProduct(TextBox txtProductID, ref Product product) { string result = ""; try { int productID = Int32.Parse(txtProductID.Text); var client = new ProductServiceClient(); product = client.GetProduct(productID); var sb = new StringBuilder(); sb.Append("ProductID:" + product.ProductID.ToString() + " "); sb.Append("ProductName:" + product.ProductName + " "); sb.Append("UnitPrice:" + product.UnitPrice.ToString() + " "); sb.Append("RowVersion:"); foreach (var x in product.RowVersion.AsEnumerable()) { sb.Append(x.ToString()); sb.Append(" "); } result = sb.ToString(); } catch (Exception ex) { result = "Exception: " + ex.Message.ToString(); } return result; }
This method will call the product service to retrieve a product from the database, format the product details to a string and return the string. This string will be displayed on the screen. The product object will also be returned so that later on we can re-use this object to update the price of the product.
txtProduct1Details.Text = GetProduct(txtProductID1, ref product1); txtProduct2Details.Text = GetProduct(txtProductID2, ref product2);
Now, we have finished adding code to retrieve products from the database through the Product WCF service. Set DistNorthwindWPF
as the startup project, press Ctrl + F5 to start the WPF Test Client, enter 30
and 31
as the product IDs, and then click on the Get Product Details button. You should get a window as shown in the following screenshot:
From the preceding screenshot, we see price of the product 30
is now 25.8900 and price of the product 31
is now 12.5000. Next, we will write code to update the prices of these two products to test the distributed transaction support of the WCF service.
To update the prices of these two products, follow these steps to add the code to the project:
MainWindow.xaml
design surface, double-click on the Update Price button to add an event handler for this button.MainWindow.xaml.cs
file:private string UpdatePrice( TextBox txtNewPrice, ref Product product, ref bool updateResult) { string result = ""; string message = ""; try { product.UnitPrice = Decimal.Parse(txtNewPrice.Text); var client = new ProductServiceClient(); updateResult = client.UpdateProduct(ref product, ref message); var sb = new StringBuilder(); if (updateResult == true) { sb.Append("Price updated to "); sb.Append(txtNewPrice.Text.ToString()); sb.Append(" "); sb.Append("Update result:"); sb.Append(updateResult.ToString()); sb.Append(" "); sb.Append("Update message:"); sb.Append(message); sb.Append(" "); sb.Append("New RowVersion:"); } else { sb.Append("Price not updated to "); sb.Append(txtNewPrice.Text.ToString()); sb.Append(" "); sb.Append("Update result:"); sb.Append(updateResult.ToString()); sb.Append(" "); sb.Append("Update message:"); sb.Append(message); sb.Append(" "); sb.Append("Old RowVersion:"); } foreach (var x in product.RowVersion.AsEnumerable()) { sb.Append(x.ToString()); sb.Append(" "); } result = sb.ToString(); } catch (Exception ex) { result = "Exception: " + ex.Message; } return result; }
This method will call the product service to update the price of a product in the database. The update result will be formatted and returned so that later on we can display it. The updated product object with the new RowVersion
will also be returned so that later on we can update the price of the same product repeatedly.
if (product1 == null) { txtUpdate1Results.Text = "Get product details first"; } else if (product2 == null) { txtUpdate2Results.Text = "Get product details first"; } else { bool update1Result = false, update2Result = false; txtUpdate1Results.Text = UpdatePrice( txtNewPrice1, ref product1, ref update1Result); txtUpdate2Results.Text = UpdatePrice( txtNewPrice2, ref product2, ref update2Result); }
Now, let's run the program to test the distributed transaction support of the WCF service. We will first update two products with two valid prices to make sure our code works with normal use cases. Then, we will update one product with a valid price and another with an invalid price. We will verify that the update with the valid price has been committed to the database, regardless of the failure of the other update.
Let's follow these steps for this test:
30
and 31
as product IDs in the top two textboxes and click on the Get Product Details button to retrieve the two products. Note that the prices for these two products are 25.89 and 12.5 respectively.26.89
and 13.5
as new prices in the New Price textboxes and click on the Update Price button to update these two products. The update results are True for both updates, as shown in the following screenshot:27.89
and -14.5
as new prices in the New Price textboxes and click on the Update Price button to update these two products. This time, the update result for product 30
is still True, but for the second update, the result is False. Click on the Get ProductDetails button again to refresh the product prices so that we can verify the update results.We know that the second service call should fail, so the second update should not be committed to the database. From the test result, we know this is true (the second product price did not change). However, from the test result we also know that the first update in the first service call has been committed to the database (the first product price has been changed). This means that the first call to the service is not rolled back even when a subsequent service call has failed. Therefore, each service call is in a separate standalone transaction. In other words, the two sequential service calls are not within one distributed transaction.
This test is not a complete distributed transaction test. On the client side, we didn't explicitly wrap the two updates in one transaction scope. We should test to see what will happen if we put the two updates within one transaction scope.
Follow these steps to wrap the two service calls in one transaction scope:
System.Transactions
in the client project.using
statement to the MainWindow.xaml.cs
file, as follows:using System.Transactions;
using
statement to put both updates within one transaction scope. Now, the click event handler for the Update Price button should be as follows:if (product1 == null) { txtUpdate1Results.Text = "Get product details first"; } else if (product2 == null) { txtUpdate2Results.Text = "Get product details first"; } else { bool update1Result = false, update2Result = false; using (var ts = new TransactionScope()) { txtUpdate1Results.Text = UpdatePrice( txtNewPrice1, ref product1, ref update1Result); txtUpdate2Results.Text = UpdatePrice( txtNewPrice2, ref product2, ref update2Result); if (update1Result == true && update2Result == true) ts.Complete(); } }
Run the client program again, still using 30
and 31
as product IDs, and enter 28.89
and -14.5
as new prices. You will find that even though we have wrapped both updates within one transaction scope, the first update is still committed to the database—it is not rolled back even though the outer transaction on the client side is not complete, and requests all participating parties to roll back. After the updates, price of the product 30
will be changed to 28.89
and price of the product 31
will remain as 13.5
.
At this point, we have proved that the WCF service does not support distributed transactions with multiple sequential service calls. Irrespective of whether the two sequential calls to the service have been wrapped in one transaction scope or not, each service call is treated as a standalone separate transaction and they do not participate in any distributed transaction.
In the previous
sections, we tried to call the WCF service sequentially to update records in the same database. We have proved that this WCF service does not support distributed transactions. In this section, we will do one more test, to add a new WCF service—DistNorthwindRemoteService
—to update records in another database on another computer. We will call the UpdateProduct
operation in this new service together with the original UpdateProduct
operation in the old service and then verify whether the two updates to the two databases will be within one distributed transaction or not.
This new service is very important for our distributed transaction support test because the distributed transaction coordinator will only be activated if more than two servers are involved in the same transaction. For test purposes, we cannot just update two databases on the same SQL server even though a transaction within a single SQL server that spans two or more databases is actually a distributed transaction. This is because the SQL server manages the distributed transaction internally—to the user it operates as a local transaction.
First, we will add a new WCF service to update a product in a remote database. We will re-use the same WCF service that we created for this solution, but just change the connection string to point to a remote database in a remote machine.
Follow these steps to add this new service:
Northwind
database to this SQL server on the remote machine. Make sure you add a new column RowVersion
, to the Products
table in this remote Northwind
database. This is all we need to do on the remote machine in this section.LINQNorthwindRemoteService
, under the DistNorthwind
solution folder C:SOAWithWCFandLINQProjectsDistNorthwind
.LINQNorthwindService
folder to the new LINQNorthwindRemoteService
folder:Web.config
bin
Web.config
file in the new service folder, change the Data Source part within the connectionString
node from localhost to the remote machine name.You may also need to adjust your login credentials to the remote database in the connection string, according to the security settings of the remote database on the remote machine.
If your computers are not within a domain, you may need to change the connection string to use SQL Server login, since connecting to the remote machine with integrated Windows authentication might not work in a work group environment.
DistNorthwindRemoteService
and set its physical path to the new LINQNorthwindRemoteService
folder. You can open the following address in Internet Explorer to verify that the new service is up and running:http://localhost/DistNorthwindRemoteService/ProductService.svc
LINQNorthwindRemoteService
to the solution and add the Web.config
file and the bin
folder of this new service to be under the new solution folder.copy
command:copy .*.* ......LINQNorthwindRemoteServicein
Again, this Post-build event command line will make sure the remote service folder will always contain the latest service binary files.
The new service is now up and running. Next, we will add a checkbox to the WPF client. If this checkbox is checked, when the Get Product Details button is clicked, we will get the second product from the remote database, using the new WCF service. When the Update Price button is clicked, we will also update its price in the remote database, using the new WCF service.
Now follow these steps to modify the WPF Client application to call the new service:
DistNorthwindRemoteService
. The namespace of this service reference should be RemoteProductServiceProxy
and the URL of the product service should be http://localhost/DistNorthwindRemoteService/ProductService.svc
.MainWindow.xaml
file, go to design mode, and add a checkbox to indicate we are going to get and update a product in the remote database by using the remote service. Set this checkbox's properties as follows:MainWindow.xaml.cs
file and add a new class member:RemoteProductServiceProxy.Product remoteProduct;
MainWindow.xaml.cs
file, copy the GetProduct
method and paste it as a new method, GetRemoteProduct
. Change the Product
type within this new method to be RemoteProductServiceProxy.Product
and change the client
type to RemoteProductServiceProxy.ProductServiceClient
. The new method should be as follows:private string GetRemoteProduct(TextBox txtProductID, ref RemoteProductServiceProxy.Product product) { string result = ""; try { int productID = Int32.Parse(txtProductID.Text); var client = new RemoteProductServiceProxy.ProductServiceClient(); product = client.GetProduct(productID); var sb = new StringBuilder(); sb.Append("ProductID:" + product.ProductID.ToString() + " "); sb.Append("ProductName:" + product.ProductName + " "); sb.Append("UnitPrice:" + product.UnitPrice.ToString() + " "); sb.Append("RowVersion:"); foreach (var x in product.RowVersion.AsEnumerable()) { sb.Append(x.ToString()); sb.Append(" "); } result = sb.ToString(); } catch (Exception ex) { result = "Exception: " + ex.Message.ToString(); } return result; }
btnGetProduct_Click
method to call the new service if the checkbox is checked, as follows:private void btnGetProduct_Click(object sender, RoutedEventArgs e) { txtProduct1Details.Text = GetProduct( txtProductID1, ref product1); if(chkRemote.IsChecked == true) txtProduct2Details.Text = GetRemoteProduct( txtProductID2, ref remoteProduct); else txtProduct2Details.Text = GetProduct( txtProductID2, ref product2); }
UpdatePrice
method and paste it as a new method UpdateRemotePrice
. Change the Product
type within this new method to RemoteProductServiceProxy.Product
and change the client
type to RemoteProductServiceProxy.ProductServiceClient
.The new method should be as follows:
private string UpdateRemotePrice( TextBox txtNewPrice, ref RemoteProductServiceProxy.Product product, ref bool updateResult) { string result = ""; string message = ""; try { product.UnitPrice = Decimal.Parse(txtNewPrice.Text); var client = new RemoteProductServiceProxy.ProductServiceClient(); updateResult = client.UpdateProduct(ref product, ref message); var sb = new StringBuilder(); if (updateResult == true) { sb.Append("Price updated to "); sb.Append(txtNewPrice.Text.ToString()); sb.Append(" "); sb.Append("Update result:"); sb.Append(updateResult.ToString()); sb.Append(" "); sb.Append("Update message:"); sb.Append(message); sb.Append(" "); sb.Append("New RowVersion:"); } else { sb.Append("Price not updated to "); sb.Append(txtNewPrice.Text.ToString()); sb.Append(" "); sb.Append("Update result:"); sb.Append(updateResult.ToString()); sb.Append(" "); sb.Append("Update message:"); sb.Append(message); sb.Append(" "); sb.Append("Old RowVersion:"); } foreach (var x in product.RowVersion.AsEnumerable()) { sb.Append(x.ToString()); sb.Append(" "); } result = sb.ToString(); } catch (Exception ex) { result = "Exception: " + ex.Message; } return result; }
btnUpdatePrice_Click
method to call the new service if the checkbox is checked.The new method should be as follows:
private void btnUpdatePrice_Click(object sender, RoutedEventArgs e) { if (product1 == null) { txtUpdate1Results.Text = "Get product details first"; } else if (chkRemote.IsChecked == false && product2 == null || chkRemote.IsChecked == true && remoteProduct == null) { txtUpdate2Results.Text = "Get product details first"; } else { bool update1Result = false, update2Result = false; using (var ts = new TransactionScope()) { txtUpdate1Results.Text = UpdatePrice( txtNewPrice1, ref product1, ref update1Result); if(chkRemote.IsChecked == true) txtUpdate2Results.Text = UpdateRemotePrice( txtNewPrice2, ref remoteProduct, ref update2Result); else txtUpdate2Results.Text = UpdatePrice( txtNewPrice2, ref product2, ref update2Result); if (update1Result == true && update2Result == true) ts.Complete(); } } }
Now let's run the program to test the distributed transaction support of the WCF service with two databases.
Follow these steps for this test:
30
and 31
as product IDs in the top two textboxes.To enable SQL Server port in firewall, go to Allow a program through Windows Firewall (type this command text in your Search programs and files box under Start), find SQL Server Port and make sure it is enabled (if it is not in the list, just add one):
30
and 31
. Note that details of the product 31
are now retrieved from the remote database. Price of the product 30
should be 28.89 and price of the product 31
should be still 12.5 in the remote database.If you get an exception in the second product's details textbox, make sure you have specified the correct connection string in the Web.config
file of the new WCF service and make sure you have added the RowVersion
column in the Products
table of the remote Northwind
database.
31
is not 12.5 but 13.5, it is likely that you did not check the remote database checkbox. For this test, we need to involve the remote database, so you need to check the remote database checkbox and again click on the Get Product Details button before you continue the test.29.89
and -14.5
as the new prices in the New Price textboxes and click on the Update Price button.Just as in the previous test, we know that the second service call fails due to the invalid price, so the second update is not committed to the database. From the refreshed product details, we know this is true (price of the product 31
did not change). However, from the refreshed product details we also know that the first update of the first service call has been committed to the remote database (price of the product 30
has been changed). This means that the first call to the service is not rolled back even when a subsequent service call has failed. Each service call is in a separate standalone transaction. In other words, the two sequential service calls are not within one distributed transaction.
If you debug the code and examine inner exception of the product service update product exception, you will see the error message is The UPDATE statement conflicted with the CHECK constraint. This is very important to us, as this proves the update was made to the remote database, then failed due to a constraint. In a following test, we need the remote database to get involved so that we can test the settings of MSDTC.