git clone https://toreaurstad@bitbucket.org/toreaurstad/wcfdemotransactions.git
The repository with the code sample is available as a public repository on Bitbucket where you can view the code here: https://bitbucket.org/toreaurstad/wcfdemotransactions/src/master/ The code sample is a full-stack WPF application with a backend implemented in WCF serviced under WAS / IIS and the data layer uses Entity Framework and Model first (EDMX). This scenario will display that we can implement transactions that span multiple WCF calls and be able to either commit the update of data in the database that all these WCF calls inflict, or abort them all, i.e. a transaction. The GUI looks like this:
Enabling transactions for the WCF service
First off, enable transactionflow on the binding of the service (web.config)<system.serviceModel> <bindings> <wsHttpBinding> <binding name="wsHttpBindingWithTransactionFlow" transactionFlow="true" > <security> <transport clientCredentialType="None"></transport> </security> </binding> </wsHttpBinding> </bindings> <services> <service name="WcfTransactionsDemo.ServiceImplementation.SampleServiceImplementation" behaviorConfiguration="SampleServiceBehavior"> <endpoint bindingConfiguration="wsHttpBindingWithTransactionFlow" binding="wsHttpBinding" address="http://localhost/WcfTransactionsDemo.Host/sampleservice.svc" contract="WcfTransactionsDemo.Common.ServiceContract.ISampleServiceContract"></endpoint> </service> </services>Next, define that the transactionflow from the client is mandatory in the WCF methods that will support this in the Service Contract of the WCF service: (this is done setting the TransactionFlow attribute to Mandatory on the WCF service methods (operations) that will join a transaction flowed downstream from the client.
[ServiceContract(Namespace = Constants.ServiceContractsNamespace)] public interface ISampleServiceContract { [OperationContract] [FaultContract(typeof(FaultDataContract))] [TransactionFlow(TransactionFlowOption.NotAllowed)] ListNext, specify the transaction isolation level of the WCF service implementation, using a ServiceBehavior attribute.GetAllCustomers(); [OperationContract] [FaultContract(typeof(FaultDataContract))] [TransactionFlow(TransactionFlowOption.NotAllowed)] List GetAllProducts(); [OperationContract] [FaultContract(typeof(FaultDataContract))] [TransactionFlow(TransactionFlowOption.Mandatory)] string PlaceOrder(OrderDataContract order); [OperationContract] [FaultContract(typeof(FaultDataContract))] [TransactionFlow(TransactionFlowOption.Mandatory)] string AdjustInventory(int productId, int quantity); [OperationContract] [FaultContract(typeof(FaultDataContract))] [TransactionFlow(TransactionFlowOption.Mandatory)] string AdjustBalance(int customerId, decimal amount); }
[ServiceBehavior(TransactionIsolationLevel = IsolationLevel.Serializable, TransactionTimeout = "00:00:30")] public class SampleServiceImplementation : ISampleServiceContract {Serializable is default in .NET and provides the consistency, it is though not recommended in high traffic scenarios as it causes too much database locking. (ReadCommitted can then be used for instance instead) Each WCF method in the service implementation that will join a transaction flowing from the client now specifies this with an OperationBehavior attribute, for example as the sample solution's AdjustBalance method:
[OperationBehavior(TransactionScopeRequired = true, TransactionAutoComplete = true)] public string AdjustBalance(int customerId, decimal amount) {
Setting up a transaction at the client side
The WCF service is now configured to support transactions in our sample demo. Moving on next to the app.config file, updating service reference should set the transaction flow attribute correct. Note that the servicePrincipalName in this demo must be adjusted to match your computer's name (or use localhost).<system.serviceModel> <bindings> <wsHttpBinding> <binding name="WSHttpBinding_ISampleServiceContract" transactionFlow="true" /> </wsHttpBinding> </bindings> <client> <endpoint address="http://localhost/WcfTransactionsDemo.Host/sampleservice.svc" binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_ISampleServiceContract" contract="SampleService.ISampleServiceContract" name="WSHttpBinding_ISampleServiceContract"> <identity> <servicePrincipalName value="host/AlienHivemind" /> </identity> </endpoint> </client> </system.serviceModel>The demo of this article works in the following manner :
- Client selects a row from the list of customers in the GUI
- Client selects a row from the list of products in the GUI
- Client enters a quantity of the selected product to order
- Client clicks Place Order to place the order
Now try to do the following: Do not select a customer but select a product. Also enter a quantity such as 10. Then click the button to place an order. What happens is that the WCF service expects the client to have selected a customer and a product. Since the client has not selected a customer, the AdjustBalance() methods throws a FaultException at the WCF service. The method AdjustInventory() however succeeds. If there was no transaction scope at the client side, you would see that the InStock / OnHand value is reduced, but there are no Balance reduction on either Customer, since the client forgot to select a Customer. Actually, using WCF transaction, it is possible to roll back data and get consistent data still - since the client defines a transaction. The client does it in the following manner:
private void PlaceOrderCommandExecute(object obj) { using (var client = new SampleServiceContractClient()) { using (var transactionScope = new TransactionScope()) { try { string orderPlacement = client.PlaceOrder(new OrderDataContract { CustomerId = SelectedCustomer != null ? SelectedCustomer.CustomerId : 0, ProductId = SelectedProduct != null ? SelectedProduct.ProductId : 0, Quantity = Quantity }); MessageBox.Show("Placing order: " + orderPlacement); string adjustedInventory = client.AdjustInventory(SelectedProduct != null ? SelectedProduct.ProductId : 0, -1 * Quantity); MessageBox.Show("Adjusting inventory: " + adjustedInventory); string adjustedBalance = client.AdjustBalance(SelectedCustomer != null ? SelectedCustomer.CustomerId : 0, -1 * (SelectedProduct != null && SelectedProduct.Price > 0 ? SelectedProduct.Price.Value : 0) * Quantity); MessageBox.Show("Adjusting balance: " + adjustedBalance); transactionScope.Complete(); } catch (FaultException err) { MessageBox.Show("FaultException: " + err.Message); } catch (ProtocolException perr) { MessageBox.Show("ProtocolException: " + perr.Message); } } try { LoadCustomers(); LoadProducts(); } catch (Exception err) { Console.WriteLine(err.Message); } } }As the client code shows, there are multiple WCF calls (three WCF calls) and the second call gave a FaultException when the client did not enter a Customer. The change inflicted was not persisted to the database and we managed to keep a consistent content of our two tables and the transaction rolled back the persistence of data data Entity Framework was about to inflict - accross multiple WCF calls. Transaction support is rather easy to add to your WCF services and at the client side there is little code that must be writted to ensure that multiple WCF calls inflict consistent change in data. Adding WCF transactions are a much more elegant way to add transaction support to your API accross multiple WCF methods than manually trying to undo WCF operations or refactor / rewrite much code to achieve what you always want with your API - to persist data supporting all four ACID principles. Note that you must use SQL Server database (I have used SqlExpress) and give access to the database I have added a SQL script for (Transactionsdemo.sql) to the app pool user so that the database TransactionsDemo can be accessed and updated (grant db_datareader and db_datawriter access in SQL Management Studio). Hope you found this sample interesting.
No comments:
Post a Comment