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