I am sure that transactions are not a new thing for you. For WCF Transactions specifically, let's think of a scenario of two tables in a database, Login and LoginTrans.
And in our service there are two functions, INSERT that takes one string parameter and inserts it into the LOGIN table and the UPDATE function that updates the LoginTrans table. The client will send his / her user id to the insert method and it will insert the user id into the login table and update the LoginTrans table. The problem however is the update code is written in a different function so the client will first call the insert method then the update method so the question becomes, how to maintain the atomicity, in other words either both function should execute and make changes to the database or neither. Let's see now how to use a WCF Transaction to deal with this. In the Service Contract use a TransactionFlow attribute on the functions that you want to run under the transaction, like this:
[ServiceContract]
public interface IAhmarService
{
[FaultContract(typeof(string))]
[OperationContract]
[TransactionFlow(TransactionFlowOption.Allowed)]
void Insert(string UserId);
[FaultContract(typeof(string))]
[OperationContract]
[TransactionFlow(TransactionFlowOption.Allowed)]
string Update();
}
The Transaction flow attribute has a parameter, TransactionFlowOption that has the following three values:
- Allowed: Implies if client has transaction, it will be accepted otherwise not a problem.
- NotAllowed: Client cannot send transaction to server while calling a method.
- Mandatory: Client has to create a transaction otherwise the method cannot be called.
In the service class decorate the functions with[OperationBehavior(TransactionScopeRequired = true)]
It will look like:
public class AhmarService : IAhmarService
{
[OperationBehavior(TransactionScopeRequired = true)]
public void Insert(string uid)
{
try
{
string constring = @"Data Source=AHMAR-PC\SQLEXPRESS;Initial Catalog=DEVDB;Integrated Security=True;User ID=Ahmar-PC\Ahmar";
SqlConnection con = new SqlConnection(constring);
con.Open();
SqlCommand cmd = new SqlCommand("insert into Login values(@uid)", con);
cmd.Parameters.AddWithValue("@uid", uid);
cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
throw new FaultException("error");
}
}
[OperationBehavior(TransactionScopeRequired = true)]
public string Update()
{
try
{
string constring = @"Data Source=AHMAR-PC\SQLEXPRESS;Initial Catalog=DEVDB;Integrated Security=True;User ID=Ahmar-PC\Ahmar";
SqlConnection con = new SqlConnection(constring);
//throw new Exception("asa");
con.Open();
SqlCommand cmd = new SqlCommand("Update LoginTrans set Islogin='1'", con);
cmd.ExecuteNonQuery();
return "1";
}
catch (Exception ex)
{
throw new FaultException("error");
}
}
}
The code is as simple as I described earlier. The Insert method will insert a userid into the login table and the update method will update the logintrans table. Now the most important thing is to allow the transaction flow in the web config of the service.
<bindings>
<wsHttpBinding>
<binding name="httpBin" ="" transactionFlow="true"/>
</wsHttpBinding >
</bindings>
The entire web config will look like this:
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="httpBin" ="" transactionFlow="true"/>
</wsHttpBinding >
</bindings>
<services>
<service name="WcfService2.AhmarService">
<endpoint address="" behaviorConfiguration="webby" binding="wsHttpBinding" bindingConfiguration="httpBin"
contract="WcfService2.IAhmarService" />
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<endpointBehaviors>
<behavior name="webby" >
<!--<webHttp helpEnabled="true"/>-->
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
Our service is ready; let's move to the client application. Open a console application; the target framework should be .NET Framework 4.0 and add a Service Reference to it. That will generate an app.config and Proxy class. If you want to generate it using SVCUTIL.EXE then you can do that.
Add a namespace as "using System.Transactions",the code will look like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Transactions;
using System.ServiceModel;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
using (TransactionScope ts = new TransactionScope())
{
try
{
ServiceReference1.AhmarServiceClient sc = new ServiceReference1.AhmarServiceClient();
sc.Insert("ahmar");
sc.Update();
ts.Complete();
}
catch (FaultException ex)
{
ts.Dispose();
Console.WriteLine("Data RollBacked error in service");
}
catch (Exception ex)
{
}
}
}
}
}
It is important to call the methods in the Transaction Scope because if any error occurs from the service then control will execute the catch block of the fault exception where ts.dispose will roll back the data.