Testing the transaction behavior of the WCF service

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:

  1. Create a WPF client to call the service twice in one method.
  2. The first service call should succeed and the second service call should fail.
  3. Verify that the update in the first service call has been committed to the database, which means that the WCF service does not support distributed transactions.
  4. Wrap the two service calls in one TransactionScope and redo the test.
  5. Verify that the update in the first service call has still been committed to the database, which means the WCF service does not support distributed transactions even if both service calls are within one transaction scope.
  6. Add a second database support to the WCF service.
  7. Modify the client to update both databases in one method.
  8. The first update should succeed and the second update should fail.
  9. Verify that the first update has been committed to the database, which means the WCF service does not support distributed transactions with multiple databases.

Creating a client to call the WCF service sequentially

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:

  1. In Solution Explorer, right-click on the solution item and select Add | New Project… from the context menu.
  2. Select Visual C# | 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. 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:

  1. On the WPF 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):
    • A label with Content Product ID
    • Two textboxes named txtProductID1 and txtProductID2
    • A button named btnGetProduct with the content Get Product Details
    • A separator to separate the preceding controls from the following controls
    • Two labels with content Product1 Details and Product2 Details
    • Two textboxes named txtProduct1Details and txtProduct2Details, with the following properties:
      • AcceptsReturn: checked
      • HorizontalScrollbarVisibility: Auto
      • VerticalScrollbarVisibility: Auto
      • IsReadOnly: checked
    • A separator to separate the preceding controls from the following controls
    • A label with content New Price
    • Two textboxes named txtNewPrice1 and txtNewPrice2
    • A button named btnUpdatePrice with the content Update Price
    • A separator to separate the preceding controls from the following controls
    • Two labels with content Update1 Results and Update2 Results
    • Two textboxes named txtUpdate1Results and txtUpdate2Results with the following properties:
      • AcceptsReturn: checked
      • HorizontalScrollbarVisibility: Auto
      • VerticalScrollbarVisibility: Auto
      • IsReadOnly: 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 may need to give your IIS identity proper access rights to your windows emp directory.

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

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

Creating a client to call the WCF service sequentially

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:

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

  3. Inside the event handler of the Update Price button, add the following code to update the product prices:
    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);
    }

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

  1. Press Ctrl + F5 to start the program.
  2. Enter 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.
  3. Enter 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:
    Testing the sequential calls to the WCF service
  4. Now enter 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.
    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 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.

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.

    Note

    System.Transactions may not be in your .NET reference list. In this case, you can browse to the file and add it. The file should be in the C:WindowsMicrosoft.NETFramework folder (or Framework64). On my machine, this file is in the C:WindowsMicrosoft.NETFrameworkv4.0.30319 folder.

  2. Add a using statement to the MainWindow.xaml.cs file, as follows:
    using System.Transactions; 
  3. Add a 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.

Testing 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—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.

Creating a new WCF service

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:

  1. Discover another machine with the SQL server installed (you can use a virtual machine, if you don't have a physical one). We will refer to this machine as the remote machine from now on.
  2. Install a 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.
  3. On the local server, in Windows Explorer, create a new folder LINQNorthwindRemoteService, under the DistNorthwind solution folder C:SOAWithWCFandLINQProjectsDistNorthwind.
  4. Copy the following items from the LINQNorthwindService folder to the new LINQNorthwindRemoteService folder:
    • Web.config
    • bin
  5. 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 name.

    Note

    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.

  6. In IIS Manager, add a new application 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
  7. To make it easier to maintain this new service, from Visual Studio in Solution Explorer, add a new solution folder 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.
  8. Also from Visual Studio, in the Solution Explorer, right-click on the project item LINQNorthwindService, 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 .*.* ......LINQNorthwindRemoteServicein

Again, this Post-build event command line will make sure 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, 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:

  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 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:
    • Content: Get and Update 2nd Product in Remote Database
    • Name: chkRemote
  3. Open the MainWindow.xaml.cs file and add a new class member:
    RemoteProductServiceProxy.Product remoteProduct;
  4. Still in the 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;
    }
  5. Change the 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);
    }
  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)
    {
      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;
    }
  7. Change the 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();
        }
      }
    }

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 for 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 product IDs in the top two textboxes.
  4. If there is a firewall on the remote machine, make sure you have the SQL Server port open before you go to next step.

    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):

    Testing the WCF service with two databases
  5. Verify the port properties are as shown in the following screenshot (click on the Details button in the dialog window shown in the preceding screenshot):
    Testing the WCF service with two databases
  6. You also need to enable TCP on the remote SQL database server (SQL Server Configuration Manager) and make sure it allows remote connections (SQL Server Management Studio | Database properties | Connections). You should first try to connect to the remote database server, using SQL Server Management Studio from another machine to make sure this remote SQL Server is set up properly.
  7. Once the remote database server is ready, click on the Get Product Details button to get product details for product ID 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.

  8. If you see the price for 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 Details button before you continue the test.
  9. Now enter 29.89 and -14.5 as the new prices in the New Price textboxes and click on the Update Price button.
  10. The update result for the first product should be True and for the second product should be False. This means the second product in the remote database has not been updated.
  11. 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 (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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset