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的辦法了,感謝歡迎大家提建議 :)