Distributed Transaction Support of WCF - WCF Multi-layer Services Development with Entity Framework Fourth Edition (2014)

WCF Multi-layer Services Development with Entity Framework Fourth Edition (2014)

Chapter 10. Distributed Transaction Support of WCF

In the previous chapters, we created a WCF service using LINQ to Entities in the data access layer. Now, we will apply settings to make the service participate in distributed client transactions. Client applications will control the transaction scope and decide whether a service should commit or roll back its transaction.

In this chapter, we will first verify that the Northwind WCF service, which we built in the previous chapter, does not support distributed transaction processing. We will then learn how to enhance this WCF service to support distributed transaction processing and how to configure all related computers to enable the distributed transaction support. To demonstrate this, we will propagate a transaction from the client to the WCF service and verify that all sequential calls to the WCF service are within one single distributed transaction. We will also explore the multiple database support of the WCF service and discuss how to configure Microsoft Distributed Transaction Coordinator (MSDTC) and the firewall for the distributed WCF service.

We will cover the following topics in this chapter:

· Creating the DistNorthwind solution

· Hosting the WCF service in IIS

· Testing the transaction behavior of the existing WCF service

· Enabling distributed transaction support

· Understanding the distributed transaction support of a WCF service

· Testing the distributed transaction support of the new WCF service

· Trade-offs of distributed transactions

Creating the DistNorthwind solution

In this chapter, we will create a new solution based on the LINQNorthwind solution. We will copy all of the source code from the LINQNorthwind directory to a new directory and then customize it to suit our needs.

Follow these steps to create the new solution:

1. Create a new directory named DistNorthwind under the existing C:\SOAwithWCFandEF\Projects\ directory.

2. Copy all the files under the C:\SOAwithWCFandEF\Projects\LINQNorthwind directory to the C:\SOAwithWCFandEF\Projects\DistNorthwind directory.

3. Remove the LINQNorthwindClient folder. We will create a new client for this solution.

4. Change the solution file's name from LINQNorthwind.sln to DistNorthwind.sln.

Now, we have the file structures ready for the new solution. Here, we will reuse the old service to demonstrate how to enable transactions for the existing WCF service.

First, we need to make sure that this old service still works in the new solution, and we also need to make a small change to the service implementation so that we can test transaction support along with it. Once we have the service up and running, we will create a new client to test this new service using the following steps:

1. Start Visual Studio and open the C:\SOAwithWCFandEF\Projects\DistNorthwind\DistNorthwind.sln solution.

2. Click on the OK button to close the projects were not loaded correctly warning dialog.

3. From the Solution Explorer, remove the LINQNorthwindClient project.

4. Open the ProductService.cs file under the NorthwindService project in this new solution and remove the condition check for a negative price in the UpdateProduct method. Later, we will try and update a product's price to be negative in order to test the distributed transaction support of WCF.

Note

If this check is not removed, when we try to update a price to be negative later in this chapter, the operation will stop at this validation check. It won't be able to reach the data access layer where LINQ to Entities will be used and the remote database will get involved, which is the key part in a distributed transaction support test.

The following screenshot shows the final structure of the new solution, DistNorthwind:

Creating the DistNorthwind solution

Now, we have finished creating the solution. If you build the solution now, you should not see any errors. You can set the service project as the startup project, run the program, and the output should be the same as seen in the Testing the service with the WCF Test Client section inChapter 9, Applying LINQ to Entities to a WCF Service.

Hosting the WCF service in IIS

In the previous chapter, we hosted NorthwindService in IIS. Since we will change the service settings in this chapter to support distributed transaction, we need to create a different IIS application for the service.

Just open IIS manager, add a new DistNorthwindService application, and set its physical path to C:\SOAwithWCFandEF\Projects\DistNorthwind\NorthwindService. Choose the application pool, NorthwindAppPool, which was created in the last chapter, as this service's application pool.

Now, you have finished setting up the service to be hosted in IIS. Open Internet Explorer, go to http://localhost/DistNorthwindService/ProductService.svc, and you should see the ProductService description in the browser.

Testing the transaction behavior of the existing WCF service

Before learning how to enhance this WCF service to support distributed transactions, we will confirm whether the existing WCF service supports distributed transactions.

To do so, we will first create a Windows Presentation Foundation (WPF) client to call the same service twice in one method. We will make the first service call succeed and the second service call fail. After the two service calls, we will verify that the update in the first service call will be committed to the database, even though the second service call fails, which means that the WCF service does not support distributed transactions.

We will then wrap the two service calls in one transaction scope and redo the test. Again, we will verify that the update in the first service call will still be committed to the database, which means the WCF service does not support distributed transactions even if both the service calls are within the scope of one transaction.

Going one step further, we will add a second database support to the WCF service and modify the client to update both databases in one method. We will then verify that the update in the first service call will still be committed to the database, which means the WCF service does not support distributed transactions with multiple databases.

Now, let's start the test step by step.

Creating a client to call the WCF service sequentially

The first scenario that we will test is where within one method of the client application, two service calls will be made and one of them fails. We then check whether the update in the successful service call has been committed to the database. If it has been committed, it 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:

1. In the Solution Explorer, right-click on the solution item and select Add | New Project… from the context menu.

2. Click on Visual C# and then select WPF Application as the template.

3. Enter DistNorthwindWPF as Name.

4. Click on the OK button to create the new client project.

Now, the new test client should have been created and added to the solution. 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:

1. On WPF's 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 in the design mode, not the XAML mode):

o A label with Content Product ID

o Two textboxes named txtProductID1 and txtProductID2

o A button named btnGetProduct with the content Get Product Details

o A separator to separate the preceding controls from the following controls

o Two labels with the content Product1 Details and Product2 Details

o Two textboxes named txtProduct1Details and txtProduct2Details, with their AcceptsReturn properties as checked, VerticalScrollbarVisibility set to Auto, and IsReadOnly set to checked

o A separator to separate the preceding controls from the following controls

o A label with the content New Price

o Two textboxes named txtNewPrice1 and txtNewPrice2

o A button named btnUpdatePrice with the content Update Price

o A separator to separate the preceding controls from the following controls

o Two labels with the content Update1 Results and Update2 Results

o Two textboxes named txtUpdate1Results and txtUpdate2Results with the AcceptsReturn property as checked, VerticalScrollbarVisibility set to Auto, and IsReadOnly as checked

Your MainWindow design surface should be as shown in the following screenshot:

Creating a client to call the WCF service sequentially

2. In the Solution Explorer, right-click on the DistNorthwindWPF project item, select Add | Service Reference…, and add a service reference of the product service to the project. The namespace of this service reference should be ProductServiceProxy, and the URL of the product service should be http://localhost/DistNorthwindService/ProductService.svc.

Note

If you get an error saying An error (Details) occurred while attempting to find service and the error details are... Metadata contains a reference that cannot be resolved, you might need to give your IIS identity proper access rights to access your C:\Windows\Tempdirectory.

3. On the MainWindow.xaml designer surface, double-click on the Get Product Details button to create an event handler for this button.

4. In the MainWindow.xaml.cs file, add the following using statement:

using DistNorthwindWPF.ProductServiceProxy;

5. Again, in the MainWindow.xaml.cs file, add the following two class members:

Product product1, product2;

6. Now, add the following method to the MainWindow.xaml.cs file:

7. private string GetProduct(TextBox txtProductID,

8. ref Product product)

9. {

10. var result = "";

11.

12. try

13. {

14. var productID = Int32.Parse(txtProductID.Text);

15. var client = new ProductServiceClient();

16. product = client.GetProduct(productID);

17.

18. var sb = new StringBuilder();

19. sb.Append("ProductID:" +

20. product.ProductID.ToString() + "\n");

21. sb.Append("ProductName:" +

22. product.ProductName + "\n");

23. sb.Append("UnitPrice:" +

24. product.UnitPrice.ToString() + "\n");

25. sb.Append("RowVersion:");

26. foreach (var x in product.RowVersion.AsEnumerable())

27. {

28. sb.Append(x.ToString());

29. sb.Append(" ");

30. }

31. result = sb.ToString();

32. }

33. catch (Exception ex)

34. {

35. result = "Exception: " + ex.Message.ToString();

36. }

37.

38. 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 reuse this object to update the price of the product.

7. Inside the event handler of the Get Product Details button, add the following two lines of code to get and display the product details:

8. 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 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:

Creating a client to call the WCF service sequentially

From the preceding screenshot, we can see that the price of the product 30 is now 25.8900, and the 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:

1. On the MainWindow.xaml design surface, double-click on the Update Price button to add an event handler for this button.

2. Add the following method to the MainWindow.xaml.cs file:

3. private string UpdatePrice(

4. TextBox txtNewPrice,

5. ref Product product,

6. ref bool updateResult)

7. {

8. var result = "";

9. var message = "";

10.

11. try

12. {

13. product.UnitPrice =

14. Decimal.Parse(txtNewPrice.Text);

15.

16. var client =

17. new ProductServiceClient();

18. updateResult =

19. client.UpdateProduct(ref product, ref message);

20. var sb = new StringBuilder();

21.

22. if (updateResult == true)

23. {

24. sb.Append("Price updated to ");

25. sb.Append(txtNewPrice.Text.ToString());

26. sb.Append("\n");

27. sb.Append("Update result:");

28. sb.Append(updateResult.ToString());

29. sb.Append("\n");

30. sb.Append("Update message:");

31. sb.Append(message);

32. sb.Append("\n");

33. sb.Append("New RowVersion:");

34. }

35. else

36. {

37. sb.Append("Price not updated to ");

38. sb.Append(txtNewPrice.Text.ToString());

39. sb.Append("\n");

40. sb.Append("Update result:");

41. sb.Append(updateResult.ToString());

42. sb.Append("\n");

43. sb.Append("Update message:");

44. sb.Append(message);

45. sb.Append("\n");

46. sb.Append("Old RowVersion:");

47. }

48. foreach (var x in product.RowVersion.AsEnumerable())

49. {

50. sb.Append(x.ToString());

51. sb.Append(" ");

52. }

53.

54. result = sb.ToString();

55. }

56. catch (Exception ex)

57. {

58. result = "Exception: " + ex.Message;

59. }

60.

61. 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 row version will also be returned so that later on, we can update the price of the same product repeatedly.

3. Inside the event handler of the Update Price button, add the following code to update the products' prices:

4. if (product1 == null)

5. {

6. txtUpdate1Results.Text = "Get product details first";

7. }

8. else if (product2 == null)

9. {

10. txtUpdate2Results.Text = "Get product details first";

11.}

12.else

13.{

14. bool update1Result = false, update2Result = false;

15.

16. txtUpdate1Results.Text = UpdatePrice(

17. txtNewPrice1, ref product1, ref update1Result);

18. txtUpdate2Results.Text = UpdatePrice(

19. txtNewPrice2, ref product2, ref update2Result);

}

Testing the sequential calls to the WCF service

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 that 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 to perform the test:

1. Press Ctrl + F5 to start the program.

2. Enter 30 and 31 as the 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.

3. Enter 26.89 and 13.5 as the new prices in the New Price textboxes and click on the Update Price button to update the values for these two products. The update results are True for both the updates, as shown in the following screenshot:

Testing the sequential calls to the WCF service

4. Now, enter 27.89 and -14.5 as the new prices in the New Price textboxes and click on the Update Price button to update the values for 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 Product Details button again to refresh the product prices so that we can verify the update results, as shown in the following screenshot:

Testing the sequential calls to the WCF service

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 that this is true (the second product's 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's price has changed). This means that the first call to the service is not rolled back even when a subsequent service call fails. Therefore, each service call is in a separate standalone transaction. In other words, two sequential service calls are not within one distributed transaction.

Wrapping the WCF service calls in one transaction scope

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:

1. Add a reference to System.Transactions in the client project.

2. Add a using statement to the MainWindow.xaml.cs file, as follows:

using System.Transactions;

3. Add a using statement to put both the updates within one transaction scope. Now, the click event handler for the Update Price button should be as follows:

4. if (product1 == null)

5. {

6. txtUpdate1Results.Text = "Get product details first";

7. }

8. else if (product2 == null)

9. {

10. txtUpdate2Results.Text = "Get product details first";

11.}

12.else

13.{

14. bool update1Result = false, update2Result = false;

15.

16. using (var ts = new TransactionScope())

17. {

18. txtUpdate1Results.Text = UpdatePrice(

19. txtNewPrice1, ref product1, ref update1Result);

20. txtUpdate2Results.Text = UpdatePrice(

21. txtNewPrice2, ref product2, ref update2Result);

22. if (update1Result == true && update2Result == true)

23. ts.Complete();

24. }

}

Run the client program again, still using 30 and 31 as the product IDs, and enter 28.89 and -14.5 as the new prices. You will find that even though we have wrapped both the updates within one transaction scope, the first update is still committed to the database—it is not rolled back even though the transaction scope is not completed—and requests all the participating parties to roll back. After the updates, the price of the product 30 will change to 28.89 and the price of the product 31 will remain 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 it does not participate in any distributed transactions.

Testing the multiple database support of the WCF service

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—in order 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 from the old service and then check 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 SQL Server manages the distributed transactions internally, but for the user, it operates as a local transaction.

Configuring a remote database on a remote machine

To test the multiple database support, we first need to install a database on another machine and configure this machine properly. Going forward, we will call this machine as the remote machine or remote server and call the previous development machine as the local machine or local server.

Follow these steps to configure this remote machine:

1. Install a fresh Northwind database for SQL Server on the remote machine. Add a new RowVersion column to the Products table in this remote Northwind database.

2. Make sure that Allow remote connections to this server is enabled for this remote database. In the following section, we will create a new WCF service to connect to in order to get product details and update the product information in this remote database; so, this option needs to be enabled. You can open SQL Server Management Studio, go to Properties of the database server, and verify this in the Connections tab of the Server Properties window.

Note

Allow remote connections to this server is a property of the database server, not of the Northwind database.

3. Enable TCP on the remote SQL database server. You can open Sql Server Configuration Manager, expand the SQL Server Network Configuration node, select Protocols for MSSQLSERVER, and then enable the TCP/IP protocol, as follows:

Configuring a remote database on a remote machine

4. Allow the SQL Server process through firewall if the firewall is turned on on the remote machine. To enable the SQL Server port in firewall, open the Allow a program through Windows Firewall window, find the SQL Server Windows NT program and make sure it is enabled for your network:

Note

If SQL Server is not in the list, click on the Add another app… button and then browse to and add the SQL Server process to the list. The process file should be located in a folder such as C:\Program Files\Microsoft SQL Server\MSSQL12.MSSQLSERVER\MSSQL\Binn\sqlservr.exe.

Configuring a remote database on a remote machine

5. If your computers are in a home group instead of a domain network, add a SQL login to the remote database. This login should have full access to the Northwind database. Later, we will use this login to connect to this remote database as integrated security is not supported in a home group.

6. Now, on your local machine, open the SQL Server Management Studio and try to connect to the remote database. Write a query such as select * from Products to verify that you have set up the remote database properly. You should also verify that the RowVersion column has been added to the Products table. If you cannot connect to the remote database from your local machine or the RowVersion column has not been added to the Products table, you should fix it right now before moving on to the next section, following the instructions in this section.

Creating a new WCF service

Once you have the remote database set up properly, we will add a new WCF service to update a product in the remote database. We will reuse the same WCF service that we created for the DistNorthwind solution, but we will change the connection string to point to the remote database on the remote machine.

Follow these steps to add this new service:

1. On the local server, in Windows Explorer, create a new folder named NorthwindRemoteService under the DistNorthwind solution folder C:\SOAwithWCFandEF\Projects\DistNorthwind.

2. Copy Web.config and bin from the NorthwindService folder to the new NorthwindRemoteService folder.

3. Open the Web.config file in the new service folder, change the Data Source part within the connectionString node from localhost to the remote machine's name.

4. If your computers are in a home group instead of a domain network, change the connection string to use the SQL Server login instead of integrated security since connecting to the remote machine with integrated security is not supported in a home group.

5. Now, open IIS Manager and add a new application pool, NorthwindRemoteAppPool. Make sure it is a .NET 4.0 application pool.

6. Change the identity of this new application pool to a user that has appropriate access to the remote Northwind database. The service that is going to be created in the next step will use this identity to access the remote Northwind database.

Note

If your computers are in a home group instead of a domain network, your app pool's identity doesn't need to have access to the remote database since the specified SQL Server login in the connection string will be used to connect to the remote database.

In IIS Manager, add a new application, DistNorthwindRemoteService, andset its physical path to the new NorthwindRemoteService folder. Choose the newly created application pool as this service's application pool. You can openhttp://localhost/DistNorthwindRemoteService/ProductService.svc in Internet Explorer to verify that the new service is up and running.

Note

If you get the error Cannot read configuration file due to insufficient permissions, you should give the identity of the new app pool read access to the NorthwindRemoteService folder.

1. To make it easier to maintain this new service, in the Solution Explorer, add a new solution folder, NorthwindRemoteService, to the solution and add the Web.config file and the bin folder of this new service to be under the new solution folder.

2. Also, in the Solution Explorer, right-click on the NorthwindService project item, select Properties, then click on the Build Events tab, and add the following to the Post-build event command line box below the original line of the copy command:

copy .\*.* ..\..\..\NorthwindRemoteService\bin

Again, this post-build event command line will make sure that the remote service folder will always contain the latest service binary files.

Calling the new WCF service in the client application

The new service is now up and running. Next, we will add a checkbox to the WPF client. If this checkbox is checked and the Get Product Details button is clicked on, we will get the second product from the remote database using the new WCF service. When the Update Pricebutton is clicked, we will also update the price of the product in the remote database using the new WCF service.

Now, follow these steps to modify the WPF client application to call the new service:

1. Within Visual Studio, in the Solution Explorer, right-click on the DistNorthwindWPF project item and add a service reference to the new WCF 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.

2. Open the MainWindow.xaml file, go to the design mode, and add a checkbox to indicate that 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:

o Content: Get and Update 2nd Product in Remote Database

o Name: chkRemote

3. Open the MainWindow.xaml.cs file and add a new class member:

RemoteProductServiceProxy.Product remoteProduct;

4. In the MainWindow.xaml.cs file, copy the GetProduct method and paste it as a new method, GetRemoteProduct, in the same file. Change the Product type within this new method to be RemoteProductServiceProxy.Product, and change the client type toRemoteProductServiceProxy.ProductServiceClient. The new method should be as follows:

5. private string GetRemoteProduct(TextBox txtProductID,

6. ref RemoteProductServiceProxy.Product product)

7. {

8. var result = "";

9.

10. try

11. {

12. var productID = Int32.Parse(txtProductID.Text);

13. var client =

14. new RemoteProductServiceProxy.ProductServiceClient();

15. product = client.GetProduct(productID);

16.

17. var sb = new StringBuilder();

18. sb.Append("ProductID:" +

19. product.ProductID.ToString() + "\n");

20. sb.Append("ProductName:" +

21. product.ProductName + "\n");

22. sb.Append("UnitPrice:" +

23. product.UnitPrice.ToString() + "\n");

24. sb.Append("RowVersion:");

25. foreach (var x in product.RowVersion.AsEnumerable())

26. {

27. sb.Append(x.ToString());

28. sb.Append(" ");

29. }

30. result = sb.ToString();

31. }

32. catch (Exception ex)

33. {

34. result = "Exception: " + ex.Message.ToString();

35. }

36.

37. return result;

}

5. Change the btnGetProduct_Click method to call the new service if the checkbox is checked, as follows:

6. private void btnGetProduct_Click(object sender, RoutedEventArgs e)

7. {

8. txtProduct1Details.Text = GetProduct(

9. txtProductID1, ref product1);

10. if(chkRemote.IsChecked == true)

11. txtProduct2Details.Text = GetRemoteProduct(

12. txtProductID2, ref remoteProduct);

13. else

14. txtProduct2Details.Text = GetProduct(

15. txtProductID2, ref product2);

}

6. Copy the 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)

{

var result = "";

var 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("\n");

sb.Append("Update result:");

sb.Append(updateResult.ToString());

sb.Append("\n");

sb.Append("Update message:");

sb.Append(message);

sb.Append("\n");

sb.Append("New RowVersion:");

}

else

{

sb.Append("Price not updated to ");

sb.Append(txtNewPrice.Text.ToString());

sb.Append("\n");

sb.Append("Update result:");

sb.Append(updateResult.ToString());

sb.Append("\n");

sb.Append("Update message:");

sb.Append(message);

sb.Append("\n");

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;

}

7. Change the btnUpdatePrice_Click method to call the new service, if the checkbox is checked. The new method should be as follows:

8. private void btnUpdatePrice_Click(object sender,

9. RoutedEventArgs e)

10.{

11. if (product1 == null)

12. {

13. txtUpdate1Results.Text = "Get product details first";

14. }

15. else if (chkRemote.IsChecked == false

16. && product2 == null

17. || chkRemote.IsChecked == true

18. && remoteProduct == null)

19. {

20. txtUpdate2Results.Text = "Get product details first";

21. }

22. else

23. {

24. bool update1Result = false, update2Result = false;

25.

26. using (var ts = new TransactionScope())

27. {

28. txtUpdate1Results.Text = UpdatePrice(

29. txtNewPrice1,

30. ref product1,

31. ref update1Result);

32. if(chkRemote.IsChecked == true)

33. txtUpdate2Results.Text = UpdateRemotePrice(

34. txtNewPrice2,

35. ref remoteProduct,

36. ref update2Result);

37. else

38. txtUpdate2Results.Text = UpdatePrice(

39. txtNewPrice2,

40. ref product2,

41. ref update2Result);

42. if (update1Result == true && update2Result == true)

43. ts.Complete();

44. }

45. }

}

Testing the WCF service with two databases

Now, let's run the program to test the distributed transaction support of the WCF service with two databases. Follow these steps to perform this test:

1. Press Ctrl + F5 to start the client application.

2. Check the Get and Update 2nd Product in Remote Database checkbox.

3. Enter 30 and 31 as the product IDs in the top two textboxes.

4. Click on the Get Product Details button to get the product details for the product IDs 30 and 31. Note that the details of the product 31 are now retrieved from the remote database. The price of the product 30 should be 28.89 and the price of the product 31 should still be 12.5 in the remote database.

If you get an exception in the second product results' textbox, make sure that you have specified the correct connection string in the Web.config file of the new WCF service and you have added the RowVersion column in the Products table of the remote Northwind database.

5. If you see the price for the product 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 Detailsbutton before you continue the test.

6. Now, enter 29.89 and -14.5 as the new prices in the New Price textboxes and click on the Update Price button.

7. The updated result for the first product should be True and for the second product should be False. This means that the second product in the remote database has not been updated.

8. Click on the Get Product Details button to refresh the product details so that we can verify the update results.

Testing the WCF service with two databases

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 (the 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 (the price of the product 30 has changed). This means that the first call to the service is not rolled back even when a subsequent service call fails. 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 the inner exception of the product service update exception, you will see that the error message is The UPDATE statement conflicted with the CHECK constraint. This is very important for us, as this proves that the update was made to the remote database but then it failed due to a constraint. In the Testing the distributed transaction support with two databases section, we need the remote database to get involved so that we can test the settings of MSDTC.

Enabling distributed transaction support

In the previous sections, we verified that the WCF service currently does not support the distributed transactions irrespective of whether there are two sequential calls to the same service or two sequential calls to two different services, with one database or with two databases.

In the following sections, we will learn how to allow this WCF service to support distributed transactions. We will allow the WCF service to participate in the client transaction. From another point of view, we will learn how to propagate a client transaction across the service boundaries so that the client can include service operation calls on multiple services in the same distributed transaction.

Enabling transaction flow in service binding

The first thing that we need to pay attention to is the bindings. As we learned in Chapter 1, Implementing a Basic HelloWorld WCF Service, the three elements of a WCF service endpoint are the address, binding, and contract (WCF ABC). Although the address has nothing to do with the distributed transaction support, the other two elements do.

We know that WCF supports several different bindings. Most of them support transactions except some HTTP bindings, such as BasicHttpBinding. In this chapter, we will use wsHttpBinding for our example.

However, using a transaction-aware binding doesn't mean that a transaction will be propagated to the service. The transaction propagation is disabled by default, and we have to enable it manually. Unsurprisingly, the attribute to enable the transaction flow in the bindings is calledtransactionFlow.

In the following section, we will do the following to enable the transaction propagation:

· Use wsHttpBinding on the host application as a binding

· Set the value of the transactionFlow attribute to true on the host application binding configuration

Enabling transaction flow on the service hosting the application

In this section, we will enable the transaction flow in bindings for both ProductService and RemoteProductService:

1. In the Solution Explorer, open the Web.config file under the C:\SOAwithWCFandEF\Projects\DistNorthwind\NorthwindService folder. Refer to the following line:

2. <endpoint address="" binding="basicHttpBinding"

contract="NorthwindService.IProductService">

Change it to this line:

<endpoint address="" binding="wsHttpBinding"

contract="NorthwindService.IProductService"

bindingConfiguration="transactionalWsHttpBinding">

2. Add the following node to the Web.config file inside the system.serviceModel node and in parallel with node services:

3. <bindings>

4. <wsHttpBinding>

5. <binding name="transactionalWsHttpBinding" transactionFlow="true" receiveTimeout="00:10:00" sendTimeout="00:10:00" openTimeout="00:10:00" closeTimeout="00:10:00" />

6. </wsHttpBinding>

</bindings>

In this configuration section, we have set the transactionFlow attribute to true for the binding, so a transaction is allowed to flow from the client to the service.

3. Make the same changes to the Web.config file under the C:\SOAwithWCFandEF\Projects\DistNorthwind\NorthwindRemoteService folder.

In the preceding configuration file, we have verified and left the bindings for both ProductService and RemoteProductService to wsHttpBinding and set the transactionFlow attribute of the binding to true. This will enable distributed transaction support from the WCF service binding side.

Modifying the service operation contract to allow a transaction flow

Now, the service is able to participate in a propagated transaction from the client application, but the client is still not able to propagate a distributed transaction into the service. Before we enable the distributed transaction support from the client side, we need to make some more changes to the service-side code, that is, modify the service operation to opt in to participate in a distributed transaction. By default, it is opted out.

Two things need to be done in order to allow an operation to participate in a propagated transaction. The first thing is to enable the transaction flow in operation contracts. Follow these steps to enable this option:

1. Open the IProductService.cs file under the NorthwindService project.

2. Add the following attribute definition before the UpdateProduct method:

[TransactionFlow(TransactionFlowOption.Allowed)]

In the preceding code line, we set TransactionFlowOption in the UpdateProduct operation to be Allowed. This means that a transaction is allowed to be propagated from the client to this operation.

The three transaction flow options for a WCF service operation are Allowed, NotAllowed, and Mandatory, as shown in the following table:

Option

Description

NotAllowed

Transaction flow is not allowed; this is the default value

Allowed

Transaction flow is allowed

Mandatory

Transaction flow must happen

Modifying the service operation implementation to require a transaction scope

The second thing that we need to do is to specify the TransactionScopeRequired behavior for the service operation. This has to be done on the service implementation project.

1. Open the ProductService.cs file under the NorthwindService project.

2. Add the following line before the UpdateProduct method:

[OperationBehavior(TransactionScopeRequired = true)]

The TransactionScopeRequired attribute means that for the UpdateProduct method, the whole service operation will always be executed inside one transaction. If a transaction is propagated from the client application, this operation will participate in the existing distributed transaction. If no transaction is propagated, a new transaction will be created and this operation will be run within the new transaction.

Note

At end of this chapter, after you have finished setting up everything for transaction support and run the program, you can examine the ambient transaction inside the WCF service (Transaction.Current) and compare it with the ambient transaction of the client to see if they are the same. You can also examine the TransactionInformation property of the ambient transaction object to see if it is a local transaction (TransactionInformation.LocalIdentifier) or a distributed transaction (TransactionInformation.DistributedIdentifier).

We now need to regenerate the service proxy and the configuration files on the client project because we have changed the service interfaces. Remember that in your real project, you should avoid making any nonbackward compatible service interface changes. Once the service goes live, if you have to make changes to the service interface, you should version your service and allow the client applications to migrate to the new versions of the service when they are ready to do so. To simplify our example, we will just update the proxy and the configuration files and recompile our client application.

These are the steps to regenerate the configuration and proxy files:

1. Rebuild the solution. As we have set up the post-build event for the NorthwindService project to copy all the assembly files to two IIS directories, both NorthwindService and NorthwindRemoteService should now contain the latest assemblies with distributed transaction support enabled.

2. In the Solution Explorer, right-click on RemoteProductServiceProxy under the Service References directory of the DistNorthwindWPF project.

3. Select Update Service Reference from the context menu.

4. Right-click on ProductServiceProxy under the Service References directory of the DistNorthwindWPF project.

5. Select Update Service Reference from the context menu.

Open the App.config file under the DistNorthwindWPF project. You will find that the transactionFlow attribute is now populated as true because the code generator finds that some operations in the service now allow transaction propagation.

Understanding the distributed transaction support of a WCF service

As we have seen, the distributed transaction support of a WCF service depends on the binding of the service, the operation contract attribute, the operation implementation behavior, and the client applications.

The following table shows some possible combinations of the WCF-distributed transaction support:

Binding permits transaction flow

Client has transaction flow enabled

Service contract opts in transaction

Service operation requires a transaction scope

Possible result

True

Yes

Allowed or Mandatory

True

Service executes under the flowed-in transaction

True or false

No

Allowed

True

Service creates and executes within a new transaction

True

Yes or no

Allowed

False

Service executes without a transaction

True or false

No

Mandatory

True or false

SOAP exception

True

Yes

NotAllowed

True or false

SOAP exception

Testing the distributed transaction support of the new WCF service

Now that we have changed the service to support distributed transactions and let the client propagate the transaction to the service, we will test this. We will first change the distributed transaction coordinator and firewall settings for the distributed transaction support of the WCF service, then propagate a transaction from the client to the service, and test the WCF service for multiple database support.

Configuring the Microsoft Distributed Transaction Coordinator

In a subsequent section, we will call two services to update two databases on two different computers. As these two updates are wrapped within one distributed transaction, Microsoft Distributed Transaction Coordinator (MSDTC) will be activated to manage this distributedtransaction. If MSDTC is not started or not configured properly, the distributed transaction will not be successful. In this section, we will learn how to configure MSDTC on both the machines.

You can follow these steps to configure MSDTC on your local and remote machines:

1. Open Component Services by going to Control Panel | Administrative Tools (or start dcomcnfg.exe from the Run dialog box (Windows + R)).

2. In the Component Services window, expand Component Services, then Computers, and then right-click on My Computer.

3. Select Properties from the context menu.

4. In the My Computer Properties window, click on the MSDTC tab.

5. Verify that Use local coordinator is checked and then close the My Computer Properties window.

6. Expand Distributed Transaction Coordinator under the My Computer node, right-click on Local DTC, select Properties from the context menu, and then from the Local DTC Properties window, click on the Security tab.

7. You should now see the security configuration for DTC on this machine. Set it as shown in the following screenshot:

Configuring the Microsoft Distributed Transaction Coordinator

Note

You have to restart the MSDTC service after you have changed your MSDTC settings for the changes to take effect. You might also need to reboot your machine to let these changes take effect.

Also, to simplify our example, we have chosen the No Authentication Required option. You should be aware that not needing authentication is a serious security issue in a production environment.

Remember that you have to make these changes to both your local and remote machines.

Configuring the firewall

Even though the distributed transaction coordinator has been enabled, the distributed transaction may still fail if the firewall is turned on and hasn't been set up properly for MSDTC.

To set up the firewall for MSDTC, follow these steps:

1. Open the Windows Firewall window from the Control Panel.

2. If the firewall is not turned on, you can skip this section.

3. Go to the Allow a program or feature through Windows Firewall window.

4. Add Distributed Transaction Coordinator to the program list (Windows\System32\msdtc.exe), if it is not already in the list. Make sure the checkbox before this item is checked.

5. You need to change your firewall settings for both your local and remote machines.

6. You can also configure this through Windows Firewall with Advanced Security under Control Panel | System and Security | Windows Firewall | Advanced Settings.

Configuring the firewall

Now, the firewall will allow msdtc.exe to go through, so our next test won't fail due to the firewall restrictions.

Note

You might have to restart IIS after you have changed your firewall settings. In some cases, you might also have to stop and then restart your firewall for the changes to take effect.

Propagating a transaction from the client to the WCF service

Now, we have the services and MSDTC ready. In this section, we will rerun the distributed test client and verify the distributed transaction support of the enhanced WCF service.

Testing the distributed transaction support with one database

First, we will test the distributed transaction support of the WCF service within one database. We will try to update two products (30 and 31). The first update will succeed, but the second update will fail. Both the updates are wrapped in one client transaction, which will be propagated into the service, and the service will participate in this distributed transaction. Due to the failure of the second update, the client application will roll back this distributed transaction at the end, and the service should also roll back every update that is within this distributed transaction. So, in the end, the first update should not be committed to the database.

Now, follow these steps to do this test:

1. Press Ctrl + F5 to start the client application.

2. Enter 30 and 31 as the product IDs in the top two textboxes.

3. Make sure Get and Update 2nd Product in Remote Database is not checked.

4. Click on the Get Product Details button. The prices for these two products should be 29.89 and 13.5, respectively.

5. Enter 30.89 and -14.5 as the new prices in the New Price textboxes.

6. Click on the Update Price button.

7. Click on the Get Product Details button to refresh the product details so that we can verify the results, as follows:

Testing the distributed transaction support with one database

From the output window, we can see that the prices of both the products remain unchanged, which proves that the first update has been rolled back. From this output, we know that both the service calls are within a distributed transaction and the WCF service now fully supports the distributed transaction within one database.

Testing the distributed transaction support with two databases

Next, we will test the distributed transaction support of the WCF service with two databases or machines involved. As mentioned before, this is a true distributed transaction test, as MSDTC will be activated only when the machine boundary is crossed.

In this test, we will try to update two products (product 30 and 31). However, this time, the second product (product 31) is in a remote database on another machine. As in the previous test, the first update will succeed, but the second update will fail. Both the updates are wrapped in one client transaction, which will be propagated into the service, and the service will participate in this distributed transaction. Due to the failure of the second update, the client application will roll back this distributed transaction at the end, and the service should also roll back every update that is within this distributed transaction. The first update should finally not be committed to the database.

Now, follow these steps to carry out this test:

1. Press Ctrl + F5 to start the client application.

2. Enter 30 and 31 as the product IDs in the top two textboxes.

3. Make sure Get and Update 2nd Product in Remote Database is checked.

4. Click on the Get Product Details button. The prices for these two products should be 29.89 and 12.5, respectively.

5. Enter 30.89 and -14.5 as the new prices in the New Price textboxes.

6. Click on Update Price.

7. Click on the Get Product Details button to refresh the product details so that you can verify the results, as shown in the following screenshot:

Testing the distributed transaction support with two databases

From the output window, we can see that the prices of both the products remain unchanged, which proves that the first update has been rolled back. From this output, we know that both service calls are within a distributed transaction and the WCF service now fully supports the distributed transaction with multiple databases involved.

Now, to prove that the distributed transaction support works, click on the Get Product Details button to refresh product1 (make sure Get and Update 2nd Product in Remote Database is still checked), enter two valid prices, such as 30.89 and 14.5, and then click on the Update Price button. This time you should see that both products' prices are updated successfully.

Testing the distributed transaction support with two databases

To see the status of all your distributed transactions, you can go to Component Services and select Transaction Statistics, as shown in the following screenshot:

Testing the distributed transaction support with two databases

For the last successful test, you might not get an output as shown in the preceding screenshot, but instead still get an exception. If you debug your code inside the UpdatePrice operation of your product service (note that you need to step into your service code from the client'sUpdateRemotePrice method), when you examine the update product exception, you might see one of the following error messages:

· MSDTC on server 'xxxxxx' is unavailable

· Network access for Distributed Transaction Manager (MSDTC) has been disabled

· The transaction has already been implicitly or explicitly committed or aborted

· The MSDTC transaction manager was unable to push the transaction to the destination transaction manager

· The partner transaction manager has disabled its support for remote/network transactions

This might be because you have not set your distributed transaction coordinator or firewall correctly. In this case, you need to follow the instructions in the previous sections to configure these settings and then come back and redo these tests. Remember that you may need to restart your machines (both the local machine and the remote machine) to let the changes take effect.

If you get an exception of the OptimisticConcurrencyException type inside the UpdateRemotePrice method, double-check your client proxy object within the UpdateRemotePrice method. You might have forgotten to change the client proxy fromProductServiceProxy.ProductServiceClient to RemoteProductServiceProxy.ProductServiceClient. Once you have corrected it, you should be able to perform this test successfully.

If you get a timeout exception while you are debugging, increase your client application's service binding settings of sendTimeout and receiveTimeout to a larger value such as 5 minutes and try again.

Trade-offs of distributed transactions

Now, you have learned how to turn on distributed transaction support for a WCF service. Before you dive into the world of distributed transaction in your real-work environment, you need to be aware that distributed transaction support will impact your applications in a few areas, sometimes maybe significantly.

The following is a list of some of the impacts of using distributed transactions:

· Distributed transactions might introduce more complexity to your applications

· Distributed transactions might decrease the performance of your applications

· Distributed transactions might increase the deadlock probability of your processes

You should analyze your requirements, consider all the pros and cons of turning the distributed transaction support on, and then make your own judgment for your applications.

Summary

In this chapter, we discussed how to enable distributed transaction support for a WCF service. We learned how to choose a binding for a transactional WCF service, how to enable transaction support for the binding, and how to opt in an operation to a transactional WCF service. Now, we can wrap the sequential WCF service calls within one transaction scope and include the distributed transaction into the WCF services. We can also update multiple databases on different computers, all within one single distributed transaction.

In the next chapter, we will learn how to convert an existing SOAP WCF service to a RESTful WCF service.