|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Summary and MotivationI am developing a simple in-house database application that doesn’t have the requirements of a full-fledged enterprise-wide, high security, multi-threaded, Object pooled, JIT Activated, heterogeneous distributed transactional application (phew). It uses SQL Server only, as the backend. It doesn’t have to scale to a gajillion users. It is a rather typical in-house simple database development project. I wanted some of the goodies of COM+ such as declarative transactions, in other words I wanted to write simple readable code like: [DbConnection] [Transaction(TransactionOption.Required)] sealed public class Employees : ContextBoundObject { [AutoComplete] public EmployeeDataSet GetEmployeeById(intnID) // Returns an EmployeeDataSet { return (EmployeeDataSet)Sql.RunSP("Employee_GetById", "Employees", new EmployeeDataSet(), Sql.CreateParameterWithValue("@EmployeeID", nID)); } } But I didn’t want the “hassles” of generating Strong Names, COM+ Registration, Configuration, all the “Extra Steps” that go along with the COM+ enterprise model. I know the COM+ (enterprise) support under the .NET environment is incredibly easy to use and configure… But it still seemed like a bit of extra deployment work for such a simple project. I also was interested in learning more about the .NET environment and in this light, my motivation makes more sense as clearly I did not save any time. *laughing*. So enough babbling, what I came up with was “Automatic Transactional” support and addition “Connection” support for the SQL server managed provider (though the source provided can easily be tweaked to provide support for the other managed providers). So you can write code as above instead of something like: sealed public class Employees { public EmployeeDataSet GetEmployeeById(intnID) // Returns an EmployeeDataSet { SqlConnection Connection = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["dsn"]); Connection.Open(); try { IDataParameter[] parameters = { SQL.CreateParameterWithValue("@EmployeeID", nID) }; return (EmployeeDataSet)SQL.RunSP("Employee_GetById", parameters, "Employees", new EmployeeDataSet(), Connection); } finally { Connection.Dispose(); Connection = null; } } } This code is a little longer, a little harder to read, and is still missing the manual transactional support. The real benefits to declarative automatic transactions are the causal effects. I.e. A calls B which calls C and if C fails you want to abort the entire transaction. Or in the case where you don’t know in advance how your objects are going to be composed into transactional units. How does it work?The first step was to somehow partition the objects into transactions based
upon declared attributes. So we could write the following annotation Contexts and Class AttributesI guess before we get to deep here, we must have a short interlude and talk about .NET “Contexts”. The .NET framework documentation defines a context as follows: A context is an ordered sequence of properties that define an environment for the objects resident inside it. Contexts get created during the activation process for objects that are configured to require certain automatic services such synchronization, transactions, just-in-time activation, security, and so on. Multiple objects can live inside a context. EEEEK! It sounds scary but in actual fact it is a real cool and an incredibly useful concept. Basically it is a way of grouping objects that share certain run-time properties together (not sure if that is any better *laughing*). Anyway, Contexts allow us to say all of these objects are going to share this transaction, or this class of objects always require a separate transaction. Contexts allow us to differentiate and therefore partition objects. It is beyond the scope of this article to go into all the details about how
to write custom attributes but essentially it involves deriving from one or the
public enum TransactionOption // The transaction options. { Disabled = 0, NotSupported, Required, RequiresNew, Supported } [AttributeUsage(AttributeTargets.Class)] public class TransactionAttribute : ContextAttribute { private TransactionOption transactionOption; public TransactionAttribute(TransactionOption transactionOption) : base("Transaction") { this.transactionOption = transactionOption; // Store the TransactionOption for later. } ... and implement the method public override boolIsContextOK(Context ctx, IConstructionCallMessage ctor) { if(transactionOption == TransactionOption.RequiresNew) // This class always requires a new Context return false; TransactionProperty transactionProperty = ctx.GetProperty(Transaction.PropertyName) as TransactionProperty; if(transactionOption == TransactionOption.Required) { // If there is no existing transaction context then create a new one if(transactionProperty == null) return false; } return true; // The current context is fine!! } ...
Context PropertiesContexts, like most other objects, have properties which define and hold their
state. And the good news is that these properties can be user defined. For
example I want to store a property on the context so that I can tell whether
there is already a transaction associated with this context. In many ways it is
equivalent to “
public override void GetPropertiesForNewContext(IConstructionCallMessage ctor) { ctor.ContextProperties.Add(this); } The only requirement is that the class implements the Interception (Finally)Well... there is a small part I left out. In order to make your classes
participate in this “Context” business, you must mark your class as
being a context bound class (which means it runs in a context). To do this you
must derive your class from
Some code for an example, the implementation of ... public IMessageSink GetObjectSink(MarshalByRefObject o, IMessageSinkm_Next) { TransactionAttribute transactionProperty = Transaction.ContextProperty; if(transactionProperty != null) { return new DbConnectionMessageSink(this, new TransactionMessageSink(transactionProperty, m_Next)); } return new DbConnectionMessageSink(this, m_Next); } One thing of interest is that the implementation always chains the Connection attribute first so that its method hook gets called before the transaction hook. There is no guarantee by putting the Connection attribute first before the Transaction attribute that it will get called first when you create a new instance. We need someway of controlling the order of things, otherwise we would try to create a transaction without a corresponding open connection. You always want (with blue being the interjected code): Connection.OpenConnection() Transaction.Begin() MethodCallGoesHere() Transaction.Commit() Connection.CloseConnection() To complete the Interception coup de gras
is the actual implementation of the public class TransactionMessageSink : IMessageSink { private IMessageSink m_Next; private TransactionAttribute m_TransactionAttribute; internal TransactionMessageSink(TransactionAttribute transactionProperty, IMessageSinkims) { m_Next = ims; m_TransactionAttribute = transactionProperty; } public IMessageSink NextSink { get { returnm_Next; } } public IMessageSync ProcessMessage(IMessageimCall) { // Perform whatever preprocessing is needed on the message if (!(imCallisIMethodMessage)) returnm_Next.SyncProcessMessage(imCall); IMethodMessage imm = imCall as IMethodMessage; bool bAutoComplete = (Attribute.GetCustomAttribute(imm.MethodBase, typeof(AutoCompleteAttribute)) != null); m_TransactionAttribute.DisableCommit(); SqlConnection Connection = (SqlConnection)DbConnectionAttribute.Connection; if(Connection == null) return m_Next.SyncProcessMessage(imCall); #if DEBUGGING_TRXS Console.WriteLine("[" + Thread.CurrentContext.ContextID + "]" + " Beginning Transaction..."); #endif SqlTransaction dbTransaction = m_TransactionAttribute.DbTransaction = Connection.BeginTransaction(System.Data.IsolationLevel.ReadUncommitted); // Dispatch the call on the object IMessage imReturn = m_Next.SyncProcessMessage(imCall); if(dbTransaction != null) { IMethodReturnMessage methodReturn = imReturn as IMethodReturnMessage; Exception exc = methodReturn.Exception; if (exc != null) { m_TransactionAttribute.SetAbort(); } else { if(bAutoComplete) m_TransactionAttribute.SetComplete(); } if(!m_TransactionAttribute.Done) m_TransactionAttribute.SetAbort(); if(m_TransactionAttribute.ContextConsistent) { #if DEBUGGING_TRXS Console.WriteLine("[" + Thread.CurrentContext.ContextID + "]" + " Commiting Transaction..."); #endif dbTransaction.Commit(); } else { #if DEBUGGING_TRXS Console.WriteLine("[" + Thread.CurrentContext.ContextID + "]" + " Aborting Transaction..."); #endif dbTransaction.Rollback(); } dbTransaction.Dispose(); dbTransaction = null; } returnimReturn; } public IMessageCtrlAsync ProcessMessage(IMessage im, IMessageSink ims) { // TODO: Find some way to also allow AsyncMessages to work (and ideally, be tracked) return m_Next.AsyncProcessMessage(im, ims); } } Jeez louise that is a lot
of code. A key part is the Putting it all TogetherThe title of the article said simple and we are yet to see anything that resembles simplicity. This is because the simple refers to the “Client” side (as it should be). So lets revisit the example at the top of the article and go over what we get for free: [DbConnection] [Transaction(TransactionOption.Required)] sealed public class Employees : ContextBoundObject { [AutoComplete] public EmployeeDataSet GetEmployeeById(intnID) // Returns an EmployeeDataSet { return (EmployeeDataSet)Sql.RunSP("Employee_GetById", "Employees", new EmployeeDataSet(), Sql.CreateParameterWithValue("@EmployeeID", nID)); } } We have a [DbConnection] attribute this provides our connection to and from the database. We have automatic transaction support with the [Transaction] attribute. We have the [AutoComplete] attribute that if all goes well during the method call, I.E, no exceptions, the transaction can be automatically committed. Lets look at the Sql.RunSP() method to see how the Connection and Transaction are accessed from within managed Sql Server. static public DataSetRunSP(string procName, string tableName, DataSet dataSet, params IDataParameter[] parameters) { SqlConnection dbConnection = (SqlConnection)DbConnectionUtil.Connection; SqlTransaction dbTransaction = (SqlTransaction)ContextUtil.DbTransaction; if((dbConnection == null) || (dbTransaction == null)) throw new System.Exception("No Connection!"); SqlDataAdapter DSCommand = new SqlDataAdapter(); DSCommand.SelectCommand = newSqlCommand(procName, dbConnection, dbTransaction); DSCommand.SelectCommand.CommandType = CommandType.StoredProcedure; if(parameters != null) { foreach ( SqlParameter parameter in parameters ) DSCommand.SelectCommand.Parameters.Add( parameter ); } DSCommand.Fill(dataSet, tableName); return dataSet; } As you can see there are two helper classes for accessing the current
connection and the current transaction, Implementation Notes
I hope this helps somebody out there, I would be interested in receiving any bug fixes, enhancements etc. | ||||||||||||||||||||