How to: Create a Transactional Service
This sample demonstrates various aspects of creating a transactional service and the use of a client-initiated transaction to coordinate service operations.
Creating a transactional service
Create a service contract and annotate the operations with the desired setting from the TransactionFlowOption enumeration to specify the incoming transaction requirements. Note that you can also place the TransactionFlowAttribute on the service class being implemented. This allows for a single implementation of an interface to use these transaction settings, instead of every implementation.
[ServiceContract] public interface ICalculator { [OperationContract] // Use this to require an incoming transaction [TransactionFlow(TransactionFlowOption.Mandatory)] double Add(double n1, double n2); [OperationContract] // Use this to permit an incoming transaction [TransactionFlow(TransactionFlowOption.Allowed)] double Subtract(double n1, double n2); }
Create an implementation class, and use the ServiceBehaviorAttribute to optionally specify a TransactionIsolationLevel and a TransactionTimeout. You should note that in many cases, the default TransactionTimeout of 60 seconds and the default TransactionIsolationLevel of
Unspecified
are appropriate. For each operation, you can use the OperationBehaviorAttribute attribute to specify whether work performed within the method should occur within the scope of a transaction scope according to the value of the TransactionScopeRequired attribute. In this case, the transaction used for theAdd
method is the same as the mandatory incoming transaction that is flowed from the client, and the transaction used for theSubtract
method is either the same as the incoming transaction if one was flowed from the client, or a new implicitly and locally created transaction.[ServiceBehavior( TransactionIsolationLevel = System.Transactions.IsolationLevel.Serializable, TransactionTimeout = "00:00:45")] public class CalculatorService : ICalculator { [OperationBehavior(TransactionScopeRequired = true)] public double Add(double n1, double n2) { // Perform transactional operation RecordToLog($"Adding {n1} to {n2}"); return n1 + n2; } [OperationBehavior(TransactionScopeRequired = true)] public double Subtract(double n1, double n2) { // Perform transactional operation RecordToLog($"Subtracting {n2} from {n1}"); return n1 - n2; } private static void RecordToLog(string recordText) { // Database operations omitted for brevity // This is where the transaction provides specific benefit // - changes to the database will be committed only when // the transaction completes. } }
Configure the bindings in the configuration file, specifying that the transaction context should be flowed, and the protocols to be used to do so. For more information, see ServiceModel Transaction Configuration. Specifically, the binding type is specified in the endpoint element’s
binding
attribute. The <endpoint> element contains abindingConfiguration
attribute that references a binding configuration namedtransactionalOleTransactionsTcpBinding
, as shown in the following sample configuration.<service name="CalculatorService"> <endpoint address="net.tcp://localhost:8008/CalcService" binding="netTcpBinding" bindingConfiguration="transactionalOleTransactionsTcpBinding" contract="ICalculator" name="OleTransactions_endpoint" /> </service>
Transaction flow is enabled at the configuration level by using the
transactionFlow
attribute, and the transaction protocol is specified using thetransactionProtocol
attribute, as shown in the following configuration.<bindings> <netTcpBinding> <binding name="transactionalOleTransactionsTcpBinding" transactionFlow="true" transactionProtocol="OleTransactions"/> </netTcpBinding> </bindings>
Supporting multiple transaction protocols
For optimal performance, you should use the OleTransactions protocol for scenarios involving a client and service written using Windows Communication Foundation (WCF). However, the WS-AtomicTransaction (WS-AT) protocol is useful for scenarios when interoperability with third-party protocol stacks is required. You can configure WCF services to accept both protocols by providing multiple endpoints with appropriate protocol-specific bindings, as shown in the following sample configuration.
<service name="CalculatorService"> <endpoint address="http://localhost:8000/CalcService" binding="wsHttpBinding" bindingConfiguration="transactionalWsatHttpBinding" contract="ICalculator" name="WSAtomicTransaction_endpoint" /> <endpoint address="net.tcp://localhost:8008/CalcService" binding="netTcpBinding" bindingConfiguration="transactionalOleTransactionsTcpBinding" contract="ICalculator" name="OleTransactions_endpoint" /> </service>
The transaction protocol is specified using the
transactionProtocol
attribute. However, this attribute is absent from the system-providedwsHttpBinding
, because this binding can only use the WS-AT protocol.<bindings> <wsHttpBinding> <binding name="transactionalWsatHttpBinding" transactionFlow="true" /> </wsHttpBinding> <netTcpBinding> <binding name="transactionalOleTransactionsTcpBinding" transactionFlow="true" transactionProtocol="OleTransactions"/> </netTcpBinding> </bindings>
Controlling the completion of a transaction
By default, WCF operations automatically complete transactions if no unhandled exceptions are thrown. You can modify this behavior by using the TransactionAutoComplete property and the SetTransactionComplete method. When an operation is required to occur within the same transaction as another operation (for example, a debit and credit operation), you can disable the autocomplete behavior by setting the TransactionAutoComplete property to
false
as shown in the followingDebit
operation example. The transaction theDebit
operation uses is not completed until a method with the TransactionAutoComplete property set totrue
is called, as shown in the operationCredit1
, or when the SetTransactionComplete method is called to explicitly mark the transaction as complete, as shown in the operationCredit2
. Note that the two credit operations are shown for illustration purposes, and that a single credit operation would be more typical.[ServiceBehavior] public class CalculatorService : IAccount { [OperationBehavior( TransactionScopeRequired = true, TransactionAutoComplete = false)] public void Debit(double n) { // Perform debit operation return; } [OperationBehavior( TransactionScopeRequired = true, TransactionAutoComplete = true)] public void Credit1(double n) { // Perform credit operation return; } [OperationBehavior( TransactionScopeRequired = true, TransactionAutoComplete = false)] public void Credit2(double n) { // Perform alternate credit operation OperationContext.Current.SetTransactionComplete(); return; } }
For the purposes of transaction correlation, setting the TransactionAutoComplete property to
false
requires the use of a sessionful binding. This requirement is specified with theSessionMode
property on the ServiceContractAttribute.[ServiceContract(SessionMode = SessionMode.Required)] public interface IAccount { [OperationContract] [TransactionFlow(TransactionFlowOption.Allowed)] void Debit(double n); [OperationContract] [TransactionFlow(TransactionFlowOption.Allowed)] void Credit1(double n); [OperationContract] [TransactionFlow(TransactionFlowOption.Allowed)] void Credit2(double n); }
Controlling the lifetime of a transactional service instance
WCF uses the ReleaseServiceInstanceOnTransactionComplete property to specify whether the underlying service instance is released when a transaction completes. Since this defaults to
true
, unless configured otherwise, WCF exhibits an efficient and predictable "just-in-time" activation behavior. Calls to a service on a subsequent transaction are assured a new service instance with no remnants of the previous transaction's state. While this is often useful, sometimes you may want to maintain state within the service instance beyond the transaction completion. Examples of this would be when required state or handles to resources are expensive to retrieve or reconstitute. You can do this by setting the ReleaseServiceInstanceOnTransactionComplete property tofalse
. With that setting, the instance and any associated state will be available on subsequent calls. When using this, give careful consideration to when and how state and transactions will be cleared and completed. The following sample demonstrates how to do this by maintaining the instance with therunningTotal
variable.[ServiceBehavior(TransactionIsolationLevel = [ServiceBehavior( ReleaseServiceInstanceOnTransactionComplete = false)] public class CalculatorService : ICalculator { double runningTotal = 0; [OperationBehavior(TransactionScopeRequired = true)] public double Add(double n) { // Perform transactional operation RecordToLog($"Adding {n} to {runningTotal}"); runningTotal = runningTotal + n; return runningTotal; } [OperationBehavior(TransactionScopeRequired = true)] public double Subtract(double n) { // Perform transactional operation RecordToLog($"Subtracting {n} from {runningTotal}"); runningTotal = runningTotal - n; return runningTotal; } private static void RecordToLog(string recordText) { // Database operations omitted for brevity } }
Note
Since the instance lifetime is a behavior that is internal to the service, and controlled through the ServiceBehaviorAttribute property, no modification to the service configuration or service contract is required to set the instance behavior. In addition, the wire will contain no representation of this.