Testing the transaction behaviour of the WCF service

Before explaining 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 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, it will mean that the two service calls are not within a single atomic transaction, and will indicate that the WCF service doesn't support distributed transactions.

You can follow these steps to create a client for this test case:

  1. In the Solution Explorer, right-click on the Tests folder under the solution MyWCF.LINQNorthwind, and select Add | New Project from the context menu.
  2. Select Visual C# | Console Application as the template.
  3. Enter DistributedClient as the Name.
  4. Click 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. Add a reference System.ServiceModel to this DistributedClient project.
  2. Add a service reference of the product service to this DistributedClient project. The namespace of this service reference should be ProductServiceProxy, and the URL of the product service should be like this:
    http://localhost:8080/MyWCF.LINQNorthwind.Host/ProductServiceRef.svc
    
  3. Add the following using statements to the Program.cs file:
    using DistributedClient.ProductServiceProxy;
    using System.ServiceModel;
    
  4. Customize the Program.cs file like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using DistributedClient.ProductServiceProxy;
using System.ServiceModel;
namespace DistributedClient
{
class Program
{
static void Main(string[] args)
{
MultiCallTest();
}
static void MultiCallTest()
{
ProductServiceContractClient client = new ProductServiceContractClient();
GetProductRequest getRequest = new GetProductRequest();
UpdateProductRequest updateRequest = new UpdateProductRequest();
string exception = "";
StringBuilder sb = new StringBuilder();
sb.Append("Prices before update:");
Product product;
try
{
// update product 30
// first get the product from database
getRequest.ProductID = 30;
product = client.GetProduct(getRequest);
sb.Append(product.UnitPrice.ToString() + " ");
// then update its price by 1
product.UnitPrice += 1;
// submit to database
updateRequest.Product = product;
bool result1 = client.UpdateProduct(updateRequest);
WCF service, transaction behavior testingclient, creating// update product 31
// first get the product from database
getRequest.ProductID = 31;
product = client.GetProduct(getRequest);
sb.Append(product.UnitPrice.ToString() + "
");
// then update its price
product.UnitPrice = -10;
// submit to database -- this update will fail
updateRequest.Product = product;
bool result2 = client.UpdateProduct(updateRequest);
}
catch (TimeoutException ex)
{
exception = "The service operation timed out. " + ex.Message;
}
catch (FaultException<ProductFault> ex)
{
exception = "ProductFault returned: " + ex.Detail.FaultMessage;
}
catch (FaultException ex)
{
exception = "Unknown Fault: " + ex.ToString();
}
catch (CommunicationException ex)
{
exception = "There was a communication problem. " + ex.Message + ex.StackTrace;
}
catch (Exception ex)
{
exception = "Other excpetion: " + ex.Message + ex.StackTrace;
}
sb.Append("Prices after update:");
getRequest.ProductID = 30;
product = client.GetProduct(getRequest);
sb.Append(product.UnitPrice.ToString() + " ");
getRequest.ProductID = 31;
product = client.GetProduct(getRequest);
sb.Append(product.UnitPrice.ToString() + "
");
Console.WriteLine(sb.ToString() + exception);
}
WCF service, transaction behavior testingclient, creating}
}

In the above test function, we first create a client object to the service, then update the product 30's price by 1. We then try to update product 31's price to an invalid value. At the end of the method, we display the prices of both products, both before and after the update, so that they can be compared.

We know that the second update will fail due to a database constraint, but what about the first update? Will it be committed to database, or will it be rolled back due to the failure of the second update?

Testing the sequential calls to the WCF service

Let's run the program now, to find out. Set the solution to start with the Host and the DistributedClient, and then press F5 or Ctrl+F5 to run this program. We will get an error message saying "could not update product", as shown in the following image:

Testing the sequential calls to the WCF service

We know that the exception is due to the second service call, so the second update should not be committed to the database. From the test result, we know this is true (the second product price didn't 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 atomic transaction.

Wrapping the WCF service calls in one transaction scope

But this test is not a complete distributed transaction test. On the client side, we didn't explicitly wrap the two updates in one transaction. We should test to see what will happen if we put the two updates within once 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 Program.cs file like this:
    using System.Transactions;
    
  3. Add a using statement to put both updates within one transaction scope. Part of the source code should appear as shown here (we have omitted the try/catch blocks inside this method):
static void MultiCallTest()
{
ProductServiceContractClient client = new ProductServiceContractClient();
GetProductRequest getRequest = new GetProductRequest();
UpdateProductRequest updateRequest = new UpdateProductRequest();
string exception = "";
StringBuilder sb = new StringBuilder();
sb.Append("Prices before update:");
Product product;
using (TransactionScope ts = new TransactionScope())
{
// the original try/catch blocks in this method
}
sb.Append("Prices after update:");
getRequest.ProductID = 30;
product = client.GetProduct(getRequest);
sb.Append(product.UnitPrice.ToString() + " ");
getRequest.ProductID = 31;
product = client.GetProduct(getRequest);
sb.Append(product.UnitPrice.ToString() + "
");
Console.WriteLine(sb.ToString() + exception);
}

Run the client program again, and 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 fails and requests all participating parties to roll back.

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, that is, to add a new operation UpdateCategoryDesc to this WCF service, to update records in another database on another computer, and call this new operation together with the original UpdateProduct operation, and then verify whether the two updates to the two databases will be within one distributed transaction.

This new operation 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 can't 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.

We will follow these steps for this test:

  1. Modify the data access layer to update a second database.
  2. Modify the business logic layer to call the new data access layer methods.
  3. Modify the service interface layer to expose a new service contract with two new service operations.
  4. Modify the host application to add a new endpoint for the new service interface.
  5. Modify the client to call the existing and new WCF service operations to update two databases.
  6. One of the updates will fail.
  7. Verify that another update is committed to the database, which means that the WCF service does not support distributed transactions, even with multiple databases on different computers.

Modifying the data access layer for the second database support

We will start from the data access layer. We will add a second database support to the data access layer in this section. Follow these steps to add the necessary files to this layer:

  1. Discover another machine with the SQL server installed. We will refer to this machine as the remote machine, going forward.
  2. Install a Northwind database to this SQL server.
  3. Open the DistNorthwind solution in Visual Studio 2008.
  4. Open the connections.config file under the MyWCF.LINQNorthwind.Host project.
  5. Insert the following line to this file:
    <add name="RemoteNorthwindConnectionString" providerName="System. Data.SqlProvider" connectionString="server=remote_pc_name remote_db_instance;uid=your_db_user_name; pwd=your_db_password; database=Northwind;" />
    
    • This defines a connection string to another Northwind database on another computer, which we will use in the new operation.

      Remember that you need to change this connection string according to your real database environment, and you can also choose to use either Windows trusted or SSPI security connections.

  6. In the Solution Explorer, open Server Explorer, and add a connection to the remote Northwind database.
  7. Add a new LINQ to the SQL Class to the DataAccess project, with the name RemoteNorthwind.dbml.
  8. In the Server Explorer, drag the Categories table from the remote Northwind database to the LINQ to SQL designer pane, and rename it to CategoryEntity.
  9. Add a new class file named CategoryDAL.cs to the DataAccess project.
  10. Customize this new DAL class to look like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
namespace MyWCF.LINQNorthwind.DataAccess
{
public class CategoryDAL
{
string connectionString = ConfigurationManager.ConnectionS trings["RemoteNorthwindConnectionString"].ConnectionString;
public string GetCategoryDesc(int id)
{
RemoteNorthwindDataContext db = new RemoteNorthwindDat aContext(connectionString);
CategoryEntity categoryEntity = (from c in db.CategoryEntities
where c.CategoryID == id
select c).FirstOrDefault();
db.Dispose();
return categoryEntity.Description;
}
public bool UpdateCategoryDesc(int id, string desc)
{
// update a record in a remote database
RemoteNorthwindDataContext db = new RemoteNorthwindDat aContext(connectionString);
CategoryEntity categoryEntity = (from c in db.CategoryEntities
where c.CategoryID == id
select c).FirstOrDefault();
categoryEntity.Description = desc;
db.SubmitChanges();
db.Dispose();
return true;
}
}
}
  • As you can see, we have defined two methods in this new class. The first method will get the description of a category from the remote Northwind database, and the second one will update the description of a category in the remote Northwind database.

    To simplify the process, we didn't add error handling to these two methods. However, in a real project, error handling should be built in from the very beginning of the coding process.

Modifying the business logic layer for the second database support

In this section, we will customize the business logic layer to support the second database. Follow these steps to customize this layer:

  1. Add a new class file CategoryLogic.cs to the BusinessLogic project.
  2. Customize this new class to look like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MyWCF.LINQNorthwind.DataAccess;
namespace MyWCF.LINQNorthwind.BusinessLogic
{
public class CategoryLogic
{
CategoryDAL categoryDAL = new CategoryDAL();
public string GetCategoryDesc(int id)
{
return categoryDAL.GetCategoryDesc(id);
}
public bool UpdateCategoryDesc(int id, string desc)
{
return categoryDAL.UpdateCategoryDesc(id, desc);
}
}
}
  • In this layer, we just delegate the calls to the data access layer. In a real project, there might be lots of logic applied here.

Modifying the service interface layer for the second database support

Now, we can modify the service interface layer to expose two new operations. We need to add three files to the FaultContracts, ServiceContracts, and ServiceImplementation projects. Follow these steps to customize this layer:

  1. Add a new class file called CategoryFault.cs to the MyWCF.LINQNorthwind.FaultContracts project.
  2. Customize this new class to look like this:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using WcfSerialization = global::System.Runtime.Serialization;
    namespace MyWCF.LINQNorthwind.FaultContracts
    {
    /// <summary>
    /// Data Contract Class - CategoryFault
    /// </summary>
    [WcfSerialization::DataContract(Namespace = "http://mycompany. com", Name = "CategoryFault")]
    public class CategoryFault
    {
    private string faultMessage;
    [WcfSerialization::DataMember(Name = "FaultMessage", IsRequired = false, Order = 0)]
    public string FaultMessage
    {
    get { return faultMessage; }
    set { faultMessage = value; }
    }
    public CategoryFault(string message)
    {
    this.faultMessage = message;
    }
    }
    }
    
  3. Add a new interface file named ICategoryServiceContract.cs to the MyWCF.LINQNorthwind.ServiceContracts project.
  4. Customize this new interface to look like this:
    using System;
    using System.Net.Security;
    using WCF = global::System.ServiceModel;
    using System.ServiceModel;
    multiple database support testing, WCF serviceservice interface layer, modifyingnamespace MyWCF.LINQNorthwind.ServiceContracts
    {
    /// <summary>
    /// Service Contract Class - CategoryServiceContract
    /// </summary>
    [WCF::ServiceContract(Namespace = "http://mycompany.com", Name = "CategoryServiceContract", SessionMode = WCF:: SessionMode.Allowed, ProtectionLevel = ProtectionLevel.None)]
    public interface ICategoryServiceContract
    {
    [WCF::FaultContract(typeof(MyWCF.LINQNorthwind. FaultContracts.CategoryFault))]
    [WCF::OperationContract(IsTerminating = false, IsInitiating = true, IsOneWay = false, AsyncPattern = false, Action = "http://mycompany.com/ CategoryServiceContract/UpdateCategoryDesc", ReplyAction = "http://mycompany.com/CategoryServiceContract/ UpdateCategoryDesc", ProtectionLevel = ProtectionLevel. None)]
    bool UpdateCategoryDesc(int id, string desc);
    [WCF::OperationContract(IsTerminating = false, IsInitiating = true, IsOneWay = false, AsyncPattern = false, Action = "http://mycompany.com/ CategoryServiceContract/GetCategoryDesc", ReplyAction = "http://mycompany.com/CategoryServiceContract/ GetCategoryDesc", ProtectionLevel = ProtectionLevel.None)]
    string GetCategoryDesc(int id);
    }
    }
    
  5. Add a new class file named CategoryService.cs to the project MyWCF.LINQNorthwind.ServiceImplementation.
  6. Customize this new class to look like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MyWCF.LINQNorthwind.BusinessLogic;
using WCF = global::System.ServiceModel;
using System.ServiceModel;
using MyWCF.LINQNorthwind.FaultContracts;
namespace MyWCF.LINQNorthwind.ServiceImplementation
{
/// <summary>
/// Service Class - CategoryService
/// </summary>
[WCF::ServiceBehavior(Name = "CategoryService",
Namespace = "http://mycompany.com",
InstanceContextMode = WCF::InstanceContextMode.PerSession,
ConcurrencyMode = WCF::ConcurrencyMode.Single)]
public class CategoryService : MyWCF.LINQNorthwind. ServiceContracts.ICategoryServiceContract
{
multiple database support testing, WCF serviceservice interface layer, modifying#region CategoryServiceContract Members
CategoryLogic categoryLogic = new CategoryLogic();
public virtual bool UpdateCategoryDesc(int id, string desc)
{
bool result;
try
{
result = categoryLogic.UpdateCategoryDesc(id, desc);
}
catch (Exception e)
{
throw new FaultException<CategoryFault>( new CategoryFault("could not update category. Error message:" + e.Message));
}
return result;
}
public virtual string GetCategoryDesc(int id)
{
return categoryLogic.GetCategoryDesc(id);
}
#endregion
}
}
  • In the ServiceContracts and ServiceImplementation projects, we just added two service operations to get and update a category description. Here, we didn't introduce any new data contract or message contract, so that we could concentrate on the distributed transaction support test.

Modifying the service host for the second database support

In the previous sections, we modified the WCF service to expose one more service contract with two new operations. Now we need to modify the host application to publish this new service contract.

Follow these steps to publish this new service contract:

  1. Add a new text file to the MyWCF.LINQNorthwind.Host project, with the name CategoryService.svc.
  2. Type the following line into this new file:
    <%@ ServiceHost language="c#" Debug="true" Service="MyWCF. LINQNorthwind.ServiceImplementation.CategoryService" %>
    
  3. Open the file web.config and add the following node as a child node of<serviceBehaviors>:
    <behavior name="MyWCF.LINQNorthwind.ServiceImplementation. CategoryService_Behavior">
    <serviceDebug includeExceptionDetailInFaults="false" />
    <serviceMetadata httpGetEnabled="true" />
    </behavior>
    
  4. Still in the file web.config, add the following node as a child node of<services>:
<service behaviorConfiguration="MyWCF.LINQNorthwind. ServiceImplementation.CategoryService_Behavior"
name="MyWCF.LINQNorthwind.ServiceImplementation.CategoryService">
<endpoint address="" binding="basicHttpBinding" name="CategoryEndpoint"
bindingNamespace="http://mycompany.com" contract="MyWCF. LINQNorthwind.ServiceContracts.ICategoryServiceContract" />
<endpoint address="mex" binding="mexHttpBinding" contract= "IMetadataExchange" />
</service>

The above changes will publish the new service contract. You can follow these steps to confirm this:

  1. Save all of the files.
  2. Rebuild the solution.
  3. In the Solution Explorer, right-click on the project MyWCF.LINQNorthwind.Host.
  4. Select View in Browser from the context menu.

Now, when the ASP.NET Development Server is be started, and an Internet browser should pop up with the title of Directory Listing -- /MyWCF.LINQNOrthwind.Host. Within this browser, two svc files should be listed. Click on the CategoryService.svc, and you will see the introduction of this new service and the WSDL link of this service.

Modifying the client for the second database support

At this point, we have the new service implemented and hosted. Now, we can modify the client to test the multi-database support of the WCF service. We will prove that at this point the WCF service does not support distributed transactions among multiple databases. Later in this chapter, after we have enhanced the service, we will see that the WCF service supports distributed transactions among multiple databases.

Follow these steps to modify the client:

  1. Start the Host application in the ASP.NET Development Server.
  2. In the Solution Explorer, right-click on the project DistributedClient.
  3. Select Add Service Reference from the context menu.
  4. Select or type the following address in the Address list box of the Add Service Reference dialog window:
    http://localhost:8080/MyWCF.LINQNorthwind.Host/ CategoryService.svc
    
  5. You can also click the Discover button to discover this service.
  6. Type CategoryServiceProxy as the namespace of the service reference.
  7. Click OK to add the service reference.
  8. Open the Program.cs file.
  9. Add the following using statement to the class:
    using DistributedClient.CategoryServiceProxy;
    
  10. Add a new method call of MultiDBTest to the Main method. The Main method now should look like this:
    static void Main(string[] args)
    {
    MultiCallTest();
    MultiDBTest();
    multiple database support testing, WCF serviceclient, modifying}
    
  11. Add a new method called MultiDBTest to this file:
static void MultiDBTest()
{
ProductServiceContractClient productClient = new ProductServiceContractClient();
GetProductRequest getRequest = new GetProductRequest();
UpdateProductRequest updateRequest = new UpdateProductRequest();
CategoryServiceContractClient categoryClient = new CategoryServiceContractClient();
string exception = "";
StringBuilder sb = new StringBuilder();
sb.Append("Description and price before update:");
Product product;
using (TransactionScope ts = new TransactionScope())
{
try
{
// first get the category desc from database
sb.Append(categoryClient.GetCategoryDesc(4) + " ");
// first get the product from database
getRequest.ProductID = 30;
product = productClient.GetProduct(getRequest);
sb.Append(product.UnitPrice.ToString() + "
");
// update category description
// submit to database
bool result1 = categoryClient.UpdateCategoryDesc( 4,"Description updated at " + DateTime.Now. ToLongTimeString());
// update product price
product.UnitPrice = -10;
// submit to database -- this update will fail
updateRequest.Product = product;
bool result2 = productClient.UpdateProduct( updateRequest);
}
catch (TimeoutException ex)
{
exception = "The service operation timed out. " + ex.Message;
}
catch (FaultException<ProductFault> ex)
{
exception = "ProductFault returned: " + ex.Detail.
FaultMessage;
}
catch (FaultException<CategoryFault> ex)
{
exception = "CategoryFault returned: " + ex.Detail.FaultMessage;
}
catch (FaultException ex)
{
exception = "Unknown Fault: " + ex.ToString();
}
catch (CommunicationException ex)
{
exception = "There was a communication problem. " + ex.Message + ex.StackTrace;
}
catch (Exception ex)
{
exception = "Other excpetion: " + ex.Message + ex.StackTrace;
}
multiple database support testing, WCF serviceclient, modifying}
sb.Append("Description and price after update:");
sb.Append(categoryClient.GetCategoryDesc(4) + " ");
getRequest.ProductID = 30;
product = productClient.GetProduct(getRequest);
sb.Append(product.UnitPrice.ToString() + "
");
Console.WriteLine(sb.ToString() + exception);
}

In this method, we first call the CategoryService to update the description of category 4 in the remote Northwind database, then call the ProductService to update product 30's price to make it an invalid price. We know the second update will fail due to the database CHECK constraint, but what about the first service call? Will the update of the category description be committed to the remote database?

Testing the WCF service with two databases

Now, let's run the program to find out. Again, we will get an error message saying "could not update product", as shown in the following image:

Testing the WCF service with two databases

Just as in the previous test, we know that the exception is due to the second service call, so the second update is not committed to the database. From the test result, we know this is true (product 30's price didn't change). However, from the test result, we also know that the first update of the first service call has been committed to the remote database (category 4's description 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 atomic transaction.

From the output of the program, we also noticed that the price of product 30 has been updated by 1.00 in the first method call (MultiCallTest).

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

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