Article .Net inline transactionsThe problemSooner or later an application that deals with business logic will need transaction support. The reason should be really obvious: some pieces of work must be treat as a one as the ACID rules already stated. In .Net, if you need transactions you will usually need COM+, and if you need COM+ you must use ServicedComponent. A ServicedComponent is a ContextBoundObject derived class with his own Proxy. That allows you to create COM+ components without the boring COM interfaces. The only requirement is that you register your assembly in the Component services using the 'regsvcs' tool. If using a ServicedComponent is enough and joyfully for you then please stop reading this right now ;-) IMHO this type of use is very intrusive and breaks your business logic with a non-OOP approach. For instance you could have a Bank class that deals with Bank business logic. Some methods needs transactions, others do not, but the Transaction option can be applied only in classes, not methods... Then you must break down your Bank class into others classes and group by transaction requirement. Awful! SuggestionWe don't need anything but the natural way of programming. No ServicedComponents, no break down our logic classes to satisfy the transaction requirement... So imagine a business logic class implemented like the following:
public class Bank
{
public BalanceInfo GetCustomerBalance(Customer customer)
{
}
public void DebitCustomerAccount(Customer customer, float amount)
{
}
public void CreditCustomerAccount(Customer customer, float amount)
{
}
}
While this is very simplist, it should give you an idea. The GetCustomerBalance should supports transaction but should not create a transaction by itself. The Debit and Credit method are different, they need a transaction. But we don't like to break ours methods apart. What you think about the following use?
public class Bank
{
[RequiresTransaction(TrasactionOption.Supports)]
public BalanceInfo GetCustomerBalance(Customer customer)
{
}
[RequiresTransaction(TrasactionOption.Required)]
public void DebitCustomerAccount(Customer customer, float amount)
{
}
[RequiresTransaction(TrasactionOption.Required)]
public void CreditCustomerAccount(Customer customer, float amount)
{
}
}
Better? I hope so. How to accomplish?Using the Remoting infrastructure should enable us to accomplish this use. It's very easy, I must say. We only need to create enviroments from that our code will execute. Now, pay attention: when someone calls one of our methods marked with a RequiresTransactionAttribute attribute we must intercept this call, create the appropriated environment and execute the call inside the environment. Easy or not? Follows the IEnviroment interface definition:
/// <summary>
/// Summary description for IEnvironment.
/// </summary>
public interface IEnvironment : IDisposable
{
/// <summary>
/// Executes the message in the target environment.
/// </summary>
/// <param name="target">The object instance that owns the message.</param>
/// <param name="message">The message (method and arguments) to be executed.</param>
/// <returns>Returns the method result.</returns>
IMethodReturnMessage Execute(MarshalByRefObject target,
IMethodCallMessage message,
bool autoComplete);
}
Now we need implementations for differents types of transaction, such as:
These environments are exactly what COM+ supports. ContextBoundObjectTo enable our magic we need to do two things:
Why?! When our class gets instantiated, .Net will ask the ContextAttribute if the current context is ok. If our answer is yes, then .Net will smile and go on. If our answer is no then .Net will create a new context and give us a change to change it's sink through IContextProperties... Here is the key: we add a new MessageSink to intercept the call. We don't use proxies directly, instead we left this to .Net. Follows the updated Bank class:
[TransactionAware]
public class Bank : ContextBoundObject
{
[RequiresTransaction(TrasactionOption.Supports)]
public BalanceInfo GetCustomerBalance(Customer customer)
{
}
[RequiresTransaction(TrasactionOption.Required)]
public void DebitCustomerAccount(Customer customer, float amount)
{
}
[RequiresTransaction(TrasactionOption.Required)]
public void CreditCustomerAccount(Customer customer, float amount)
{
}
}
TransactionAwareAttributeNow follows our implementation of TransactionAwareAttribute. Notice how we answer the IsContextOK question:
[AttributeUsage(AttributeTargets.Class, Inherited=true)]
public class TransactionAwareAttribute : ContextAttribute, IContributeObjectSink
{
public TransactionAwareAttribute() : base("Transaction.Layer")
{
}
public override bool IsContextOK(Context context,
IConstructionCallMessage ctor)
{
bool contextValid = false;
foreach(object contextProperty in context.ContextProperties)
{
if (contextProperty == this)
{
contextValid = true;
break;
}
}
return contextValid;
}
public override void GetPropertiesForNewContext(
IConstructionCallMessage ctor)
{
ctor.ContextProperties.Add(this);
}
public IMessageSink GetObjectSink(
MarshalByRefObject target, IMessageSink next)
{
return new TransactionMessageSink(target, next);
}
}
TransactionMessageSinkNow follows our implementation of TransactionMessageSink. This sink participates in the sink chain and is responsible for checking if the called method has the RequiresTransactionAttribute. If so, it instantiates the correct environment and delegates the execution. The code became a little messy to get around a little problem when using interfaces... Well, let's forget about that for a while ;-)
public class TransactionMessageSink : IMessageSink
{
private IMessageSink _nextSink;
private MarshalByRefObject _target;
private Type _targetType;
public TransactionMessageSink(MarshalByRefObject target, IMessageSink nextSink)
{
_target = target;
if (RemotingServices.IsTransparentProxy(target))
{
_targetType = RemotingServices.GetRealProxy(target).GetProxiedType();
}
_nextSink = nextSink;
}
public IMessage SyncProcessMessage(IMessage message)
{
if (message is IMethodCallMessage)
{
IMethodCallMessage methodMessage = message as IMethodCallMessage;
MethodBase method =
RemotingServices.GetMethodBaseFromMethodMessage(methodMessage);
RequiresTransactionAttribute requiresTransaction =
FindTransactionAttribute(methodMessage);
if (!method.IsSpecialName && requiresTransaction != null)
{
bool isAutoComplete = IsAutoComplete(methodMessage);
IEnvironment environment = null;
switch(requiresTransaction.Option)
{
case TransactionOption.Required:
environment = new TransactionRequiredEnvironment();
break;
case TransactionOption.RequiresNew:
environment = new NewTransactionRequiredEnvironment();
break;
case TransactionOption.Supported:
environment = new SupportsTransactionEnvironment();
break;
case TransactionOption.NotSupported:
environment = new NotSupportedTransactionEnvironment();
break;
case TransactionOption.Disabled:
environment = new DisabledTransactionEnvironment();
break;
}
IMessage returnMessage = null;
try
{
returnMessage = environment.Execute(_target,
methodMessage, isAutoComplete);
}
catch(Exception e)
{
throw e;
}
finally
{
try
{
environment.Dispose();
}
catch(Exception e)
{
Console.Write("\nDispose Error. Reason " + e.Message );
}
}
return returnMessage;
}
}
return _nextSink.SyncProcessMessage(message);
}
public IMessageSink NextSink
{
get
{
return _nextSink;
}
}
public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
{
throw new NotSupportedException("AsyncProcessMessage not supported.");
}
private RequiresTransactionAttribute FindTransactionAttribute(
IMethodCallMessage message)
{
object[] attributes = FindAttributes(message,
typeof(RequiresTransactionAttribute));
if (attributes != null && attributes.Length != 0)
{
return (RequiresTransactionAttribute) attributes[0];
}
return null;
}
private bool IsAutoComplete(IMethodCallMessage message)
{
object[] attributes = FindAttributes(message, typeof(AutoCompleteAttribute));
if (attributes != null && attributes.Length != 0)
{
return ((AutoCompleteAttribute) attributes[0]).Value;
}
return false;
}
private object[] FindAttributes(IMethodCallMessage message, Type attributeType)
{
return FindAttributes(message, message.MethodBase, attributeType);
}
private object[] FindAttributes(IMethodCallMessage message,
MethodBase method, Type attributeType)
{
object[] attributes = method.GetCustomAttributes(attributeType, true);
if (attributes.Length != 0)
{
return attributes;
}
else
{
if (method.IsAbstract && _targetType != null)
{
MethodInfo info = _targetType.GetMethod(method.Name,
(Type[]) message.MethodSignature);
if (info != null)
{
return FindAttributes(message, info, attributeType);
}
}
}
return null;
}
}
Done!The .Net Remoting infrastructure enable us to achieve such nice things and get rid of imposed limitations. Feel free to download the complete source code. Keep in mind that this code was made in a few hours. It haven't been optimized for production environment, so I'm providing the source just as a sample the usage of ContextBoundObjects and COM+ transactions. You have been warned. Best regards |