Introduction
Who likes to write boiler plate code? Anyone? Bueler?
This article is a continuation of my journey with arguably the best Aspect Oriented Programming framework,
PostSharp from SharpCrafters.
Disclaimer: I am only an extremely satisfied customer of SharpCrafter, not an employee, partner,
or in any other way related to the founders. However, my satisfaction level has been recently affected by PostSharp licensing changes.
Background
The System.Transactions infrastructure
provides both an explicit programming model based on the Transaction class, as well as an implicit programming model using the
TransactionScope
class,
in which transactions are automatically managed by the infrastructure. TransactionScope
can be used with your database connection, WCF Services,
and any other class which is recognized by transaction manager as in scope. For more information on this model,
see the Implementing An Implicit Transaction Using Transaction Scope topic,
and on Writing A Transactional Application. In this example we are going
to skip over these details and concentrate on the solution itself.
Transaction Scope Attribute
This attribute, like the others I've written before, is a MethodInterceptionAspect
, which means that it will applied at runtime, during method invocation.
So to support capturing various exceptions encountered during the transaction, we must initialize a list, which will contain them all. This should cover
scenarios, where during multiple retries, the nature of the exceptions might change from a timeout to invalid argument value for example. To allow for specifying of the number
of retries as well as delay between consecutive retries, I have created two public properties,
MaxRetries
(default is infinite number of retries) and RetryDelay
(in seconds).
namespace TransactionAspect
{
using System;
using System.Collections.Generic;
using System.Threading;
using System.Transactions;
using PostSharp.Aspects;
[Serializable]
public class TransactionScopeAttribute : MethodInterceptionAspect
{
#region Fields
private int maxRetries = int.MaxValue;
private int retryDelay = 30;
#endregion
#region Public Properties
public int MaxRetries
{
get
{
return this.maxRetries;
}
set
{
this.maxRetries = value;
}
}
public int RetryDelay
{
get
{
return this.retryDelay;
}
set
{
this.retryDelay = value;
}
}
#endregion
#region Public Methods and Operators
public override void OnInvoke(MethodInterceptionArgs args)
{
var aggregateExceptions = new List<Exception>();
int retries = 0;
while (retries++ < this.maxRetries)
{
try
{
using (var scope = new TransactionScope())
{
args.Proceed();
scope.Complete();
}
break;
}
catch (Exception ex)
{
aggregateExceptions.Add(ex);
if (retries >= this.maxRetries)
{
throw new AggregateException(string.Format("Trasaction failed after {0} attempts", this.maxRetries), aggregateExceptions);
}
Thread.Sleep(TimeSpan.FromSeconds(this.retryDelay));
}
}
}
#endregion
}
}
Than the rest is simple. Any time you want a method to be retried under a
transaction, simply decorate it with the TransactionScope
attribute.
[TransactionScope(MaxRetries = 3, RetryDelay = 1)]
private void QueryDB(string sql)
{
var sqlConnectionStringBuilder = new SqlConnectionStringBuilder { DataSource = "localhost",
InitialCatalog = "master", IntegratedSecurity = true };
using (var sqlConn = new SqlConnection(sqlConnectionStringBuilder.ConnectionString))
{
using (var sqlComm = new SqlCommand(sql,sqlConn))
{
sqlConn.Open();
using(var sqlReader = sqlComm.ExecuteReader())
{
while (sqlReader.Read())
{
Console.WriteLine("{0}", sqlReader[0]);
}
}
}
}
}
Enjoy!