天天看點

C# 用Attribute實作AOP事務

正文

     首先我們來看一段未加事務的代碼:

     SqlDAL.cs     

public abstract class SqlDAL

    {

        #region ConnectionString

        private SqlConnectionStringBuilder _ConnectionString = null;

        /// <summary>

        /// 字元串連接配接

        /// </summary>

        public virtual SqlConnectionStringBuilder ConnectionString

        {

            get

            {

                if (_ConnectionString == null || string.IsNullOrEmpty(_ConnectionString.ConnectionString))

                {

                    _ConnectionString = new SqlConnectionStringBuilder(Configurations.SQLSERVER_CONNECTION_STRING);

                }

                return _ConnectionString;

            }

            set { _ConnectionString = value; }

        }

        #endregion

        #region ExecuteNonQuery

        public int ExecuteNonQuery(string cmdText)

            return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, CommandType.Text, cmdText);

        public int ExecuteNonQuery(string cmdText, CommandType type)

            return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText);

        public int ExecuteNonQuery(string cmdText, CommandType type, params SqlParameter[] cmdParameters)

            return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText, cmdParameters);

     代碼說明:

          1.     本類對SqlHelper.cs 進一步封裝。

          2.     Configurations.SQLSERVER_CONNECTION_STRING 替換成自己的連接配接字元串就行了。

     UserInfoAction.cs

    public class UserInfoAction : SqlDAL

        /// 添加使用者

        public void Add(UserInfo user)

            StringBuilder sb = new StringBuilder();

            sb.Append("UPDATE [UserInfo] SET Password='");

            sb.Append(user.Password);

            sb.Append("' WHERE UID=");

            sb.Append(user.UID);

            ExecuteNonQuery(sql);

    }

     如果我們要加入事務,通常的辦法就是在方法内try、catch然後Commit、Rollback,缺點就不說了,下面我會邊貼代碼邊講解,力圖大家也能掌握這種方法: )

     先貼前面兩個被我修改的類

     SqlDAL.cs

public abstract class SqlDAL : ContextBoundObject

        private SqlTransaction _SqlTrans;

        /// 僅支援有事務時操作

        public SqlTransaction SqlTrans

                if (_SqlTrans == null)

                    //從上下文中試圖取得事務

                    object obj = CallContext.GetData(TransactionAop.ContextName);

                    if (obj != null && obj is SqlTransaction)

                        _SqlTrans = obj as SqlTransaction;

                return _SqlTrans;

            set { _SqlTrans = value; }

            if (SqlTrans == null)

                return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, CommandType.Text, cmdText);

            else

                return SqlHelper.ExecuteNonQuery(SqlTrans, CommandType.Text, cmdText);

                return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText);

                return SqlHelper.ExecuteNonQuery(SqlTrans, type, cmdText);

                return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText, cmdParameters);

                return SqlHelper.ExecuteNonQuery(SqlTrans, type, cmdText, cmdParameters);

     代碼說明:

          1.     加了一個屬性(Property)SqlTrans,并且每個ExecuteNonQuery執行前都加了判斷是否以事務方式執行。這樣做是為後面從上下文中取事務做準備。

          2.     類繼承了ContextBoundObject,注意,是必須的,MSDN是這樣描述的:定義所有上下文綁定類的基類。

          3.     TransactionAop将在後面給出。

    [Transaction]

        [TransactionMethod]

          1.     很簡潔、非侵入式、很少改動、非常友善(想要事務就加2個标記,不想要就去掉)。

          2.     兩個Attribute後面将給出。

    /// <summary>

    /// 标注類某方法内所有資料庫操作加入事務控制

    /// </summary>

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]

    public sealed class TransactionAttribute : ContextAttribute, IContributeObjectSink

        /// 标注類某方法内所有資料庫操作加入事務控制,請使用TransactionMethodAttribute同時标注

        public TransactionAttribute()

            : base("Transaction")

        { }

        public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink next)

            return new TransactionAop(next);

    /// 标示方法内所有資料庫操作加入事務控制

    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]

    public sealed class TransactionMethodAttribute : Attribute

        /// 标示方法内所有資料庫操作加入事務控制

        public TransactionMethodAttribute()

          1.     在上面兩篇文章中都是把IContextProperty, IContributeObjectSink單獨繼承并實作的,其實我們發現ContextAttribute已經繼承了IContextProperty,所有這裡我僅僅隻需要再繼承一下IContributeObjectSink就行了。關于這兩個接口的說明,上面文章中都有詳細的說明。

          2.     TransactionAop将在後面給出。

          3.     需要注意的是兩個Attribute需要一起用,并且我發現Attribute如果标記在類上他會被顯示的執行個體化,但是放在方法上就不會,打斷點可以跟蹤到這一過程,要不然我也不會費力氣弄兩個來标注了。

     TransactionAop.cs

public sealed class TransactionAop : IMessageSink

        private IMessageSink nextSink; //儲存下一個接收器

        /// 構造函數

        /// <param name="next">接收器</param>

        public TransactionAop(IMessageSink nextSink)

            this.nextSink = nextSink;

        ///  IMessageSink接口方法,用于異步處理,我們不實作異步處理,是以簡單傳回null,

        ///  不管是同步還是異步,這個方法都需要定義

        /// <param name="msg"></param>

        /// <param name="replySink"></param>

        /// <returns></returns>

        public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)

            return null;

        /// 下一個接收器

        public IMessageSink NextSink

            get { return nextSink; }

        /// 

        public IMessage SyncProcessMessage(IMessage msg)

            IMessage retMsg = null;

            IMethodCallMessage call = msg as IMethodCallMessage;

            if (call == null ||  (Attribute.GetCustomAttribute(call.MethodBase, typeof(TransactionMethodAttribute))) == null)

                retMsg = nextSink.SyncProcessMessage(msg);

                //此處換成自己的資料庫連接配接

                using (SqlConnection Connect = new SqlConnection(Configurations.SQLSERVER_CONNECTION_STRING))

                    Connect.Open();

                    SqlTransaction SqlTrans = Connect.BeginTransaction();

                    //講存儲存儲在上下文

                    CallContext.SetData(TransactionAop.ContextName, SqlTrans);

                    //傳遞消息給下一個接收器 - > 就是指執行你自己的方法

                    retMsg = nextSink.SyncProcessMessage(msg);

                    if (SqlTrans != null)

                    {

                        IMethodReturnMessage methodReturn = retMsg as IMethodReturnMessage;

                        Exception except = methodReturn.Exception;

                        if (except != null)

                        {

                            SqlTrans.Rollback();

                            //可以做日志及其他處理

                        }

                        else

                            SqlTrans.Commit();

                        SqlTrans.Dispose();

                        SqlTrans = null;

                    }

            return retMsg;

        /// 用于提取、存儲SqlTransaction

        public static string ContextName

            get { return "TransactionAop"; }

          1.     IMessageSink     MSDN:定義消息接收器的接口。

          2.     主要關注SyncProcessMessage方法内的代碼,在這裡建立事務,并存儲在上下文中間,還記得上面SqlDAL的SqlTrans屬性麼,裡面就是從上下文中取得的。

          3.     請注意了,這裡能捕捉到錯誤,但是沒有辦法處理錯誤,是以錯誤會繼續往外抛,但是事務的完整性我們實作了。你可以在Global.asax可以做全局處理,也可以手動的try一下,但是我們不需要管理事務了,僅僅當普通的錯誤來處理了。

結束

     大家可以看到,在被标注的方法裡面所有的資料庫操作都會被事務管理起來,也算是了了我心願,貌似我的Attribute做權限又看到了一絲希望了,歡迎大家多提意見:)

補充(2009-1-8)

     關于在評論中提到的性能的問題,如果要使用AOP的方式來實作事務肯定比直接try catch 然後Commit 和 Rollback效率要低的,但是很明顯可維護性、使用友善性要高得多的,是以看個人需求了。這裡補充的是關于SqlDAL繼承ContextBoundObject的問題,以下是想到的解決辦法:

          1.     最簡單、修改UserInfoAction最少的辦法:把SqlDAL複制一份改下類名,繼承一下ContextBoundObject,然後把繼承類改一下。很不推薦:  (

          2.     從一開始就不使用繼承方法來通路資料層的方法,而是将SqlDAL改成一個普通類,通過聲明一個SqlDAL方式來通路資料層:

        private SqlDAL _sqlDao;

        public SqlDAL SqlDao

                if (_sqlDao == null)

                    _sqlDao = new SqlDAL();

                        _sqlDao.SqlTrans = obj as SqlTransaction;

                return _sqlDao;

          這樣相對于沒有加事務類僅僅多一個取值過程和判斷過程,效率應該還是比繼承SqlDAL直接繼承ContextBoundObject好很多。

          個人感覺還是不是很好,繼續探索,已經想到了減少一個Attribute的辦法了,感謝歡迎大家提建議 :)

本文轉自over140 51CTO部落格,原文連結:http://blog.51cto.com/over140/586466,如需轉載請自行聯系原作者