程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#用Attribute實現AOP事務

C#用Attribute實現AOP事務

編輯:關於C#

C# 用Attribute實現AOP事務 [C# | AOP | Attribute | ContextAttribute | IContributeObjectSink | IMessageSink ]

閱前注意

1.整篇文章的核心和突破點在於上下文Context的使用,務必注意CallContext在整個程序中起到的作用

2.本文中看到的SqlHelper使用的是微軟SqlHelper.cs。

3.本文重點在於如何實現,並且已經測試通過,只貼關鍵性代碼,所以請認真閱讀,部分代碼直接拷貝下來運行是會出錯的!

正文

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

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);
        }
        #endregion

代碼說明:

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

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

UserInfoAction.cs

public class UserInfoAction : SqlDAL
    {
        /// <summary>
        /// 添加用戶
        /// </summary>
        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;
        /// <summary>
        /// 僅支持有事務時操作
        /// </summary>
        public SqlTransaction SqlTrans
        {
            get
            {
                if (_SqlTrans == null)
                {
                    //從上下文中試圖取得事務
                    object obj = CallContext.GetData(TransactionAop.ContextName);
                    if (obj != null && obj is SqlTransaction)
                        _SqlTrans = obj as SqlTransaction;
                }
                return _SqlTrans;
            }
            set { _SqlTrans = value; }
        }
        #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)
        {
            if (SqlTrans == null)
                return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, CommandType.Text, cmdText);
            else
                return SqlHelper.ExecuteNonQuery(SqlTrans, CommandType.Text, cmdText);
        }
        public int ExecuteNonQuery(string cmdText, CommandType type)
        {
            if (SqlTrans == null)
                return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText);
            else
                return SqlHelper.ExecuteNonQuery(SqlTrans, type, cmdText);
        }
        public int ExecuteNonQuery(string cmdText, CommandType type, params SqlParameter[] cmdParameters)
        {
            if (SqlTrans == null)
                return SqlHelper.ExecuteNonQuery(ConnectionString.ConnectionString, type, cmdText, cmdParameters);
            else
                return SqlHelper.ExecuteNonQuery(SqlTrans, type, cmdText, cmdParameters);
        }
        #endregion
    }

代碼說明:

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

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

3.TransactionAop將在後面給出。

UserInfoAction.cs

[Transaction]
    public class UserInfoAction : SqlDAL
    {
        [TransactionMethod]
        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);
        }
    }

代碼說明:

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

2.兩個Attribute後面將給出。

/// <summary>
    /// 標注類某方法內所有數據庫操作加入事務控制
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public sealed class TransactionAttribute : ContextAttribute, IContributeObjectSink
    {
        /// <summary>
        /// 標注類某方法內所有數據庫操作加入事務控制,請使用TransactionMethodAttribute同時標注
        /// </summary>
        public TransactionAttribute()
            : base("Transaction")
        { }
        public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink next)
        {
            return new TransactionAop(next);
        }
    }
    /// <summary>
    /// 標示方法內所有數據庫操作加入事務控制
    /// </summary>
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public sealed class TransactionMethodAttribute : Attribute
    {
        /// <summary>
        /// 標示方法內所有數據庫操作加入事務控制
        /// </summary>
        public TransactionMethodAttribute()
        {
        }
    }

代碼說明:

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

2.TransactionAop將在後面給出。

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

TransactionAop.cs

public sealed class TransactionAop : IMessageSink
    {
        private IMessageSink nextSink; //保存下一個接收器
        /// <summary>
        /// 構造函數
        /// </summary>
        /// <param name="next">接收器</param>
        public TransactionAop(IMessageSink nextSink)
        {
            this.nextSink = nextSink;
        }
        /// <summary>
        ///  IMessageSink接口方法,用於異步處理,我們不實現異步處理,所以簡單返回null,
        ///  不管是同步還是異步,這個方法都需要定義
        /// </summary>
        /// <param name="msg"></param>
        /// <param name="replySink"></param>
        /// <returns></returns>
        public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
        {
            return null;
        }
        /// <summary>
        /// 下一個接收器
        /// </summary>
        public IMessageSink NextSink
        {
            get { return nextSink; }
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        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);
            else
            {
                //此處換成自己的數據庫連接
                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;
        }
        /// <summary>
        /// 用於提取、存儲SqlTransaction
        /// </summary>
        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
        {
            get
            {
                if (_sqlDao == null)
                {
                    _sqlDao = new SqlDAL();
                    object obj = CallContext.GetData(TransactionAop.ContextName);
                    if (obj != null && obj is SqlTransaction)
                        _sqlDao.SqlTrans = obj as SqlTransaction;
                }
                return _sqlDao;
            }
        }

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

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved