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:
TransactionScope
and redo the test.The first scenario to test is that, within one method of the client application, two service calls will be made and one of them will fail. We then verify whether the update in the successful service call has been committed to the database. If it has been, 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:
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:
System.ServiceModel
to this DistributedClient
project. 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
using
statements to the Program.cs
file:using DistributedClient.ProductServiceProxy; using System.ServiceModel;
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?
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:
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.
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:
System.Transactions
in the client project. using
statement to the Program.cs
file like this:using System.Transactions;
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.
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:
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:
connections.config
file under the MyWCF.LINQNorthwind.Host project.<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;" />
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.
Northwind
database. RemoteNorthwind.dbml
. Northwind
database to the LINQ to SQL designer pane, and rename it to CategoryEntity. CategoryDAL.cs
to the DataAccess project.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; } } }
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.
In this section, we will customize the business logic layer to support the second database. Follow these steps to customize this layer:
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); } } }
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:
CategoryFault.cs
to the MyWCF.LINQNorthwind.FaultContracts project.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; } } }
ICategoryServiceContract.cs
to the MyWCF.LINQNorthwind.ServiceContracts project.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); } }
CategoryService.cs
to the project MyWCF.LINQNorthwind.ServiceImplementation.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 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:
CategoryService.svc
.<%@ ServiceHost language="c#" Debug="true" Service="MyWCF. LINQNorthwind.ServiceImplementation.CategoryService" %>
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>
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:
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.
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:
http://localhost:8080/MyWCF.LINQNorthwind.Host/ CategoryService.svc
Program.cs
file. using
statement to the class:using DistributedClient.CategoryServiceProxy;
static void Main(string[] args) { MultiCallTest(); MultiDBTest(); multiple database support testing, WCF serviceclient, modifying}
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?
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:
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
).