傳統交易管理需在每個方法手寫 try-catch、Commit/Rollback,侵入性強且易遺漏。本文展示以 ContextBoundObject、CallContext 與自訂 Attribute 實作非侵入式 AOP 交易。
public abstract class SqlDAL : ContextBoundObject {
private SqlTransaction _sqlTrans;
public SqlTransaction SqlTrans {
get {
if (_sqlTrans == null) {
object obj = CallContext.GetData(TransactionAop.ContextName);
if (obj != null && obj is SqlTransaction) _sqlTrans = (SqlTransaction)obj;
}
return _sqlTrans;
}
set { _sqlTrans = value; }
}
// ExecuteNonQuery 多載:若 SqlTrans 不為 null 則使用交易執行
public int ExecuteNonQuery(string cmdText) {
return SqlTrans == null
? SqlHelper.ExecuteNonQuery(ConnStr, CommandType.Text, cmdText)
: SqlHelper.ExecuteNonQuery(SqlTrans, CommandType.Text, cmdText);
}
// ... 其他多載省略
}
關鍵:繼承 ContextBoundObject,透過 CallContext 取得當前交易。
[Transaction]
public class UserInfoAction : SqlDAL {
[TransactionMethod]
public void Add(UserInfo user) {
// 僅撰寫業務 SQL,無交易程式碼
ExecuteNonQuery(sql);
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class TransactionAttribute : ContextAttribute, IContributeObjectSink {
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 { }
兩個 Attribute 必須搭配使用:類別標記啟用攔截器,方法標記指定哪些方法納入交易。
public sealed class TransactionAop : IMessageSink {
private IMessageSink _nextSink;
public TransactionAop(IMessageSink next) { _nextSink = next; }
public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink) { return null; }
public IMessageSink NextSink { get { return _nextSink; } }
public IMessage SyncProcessMessage(IMessage msg) {
IMethodCallMessage call = msg as IMethodCallMessage;
// 無標記或非目標方法直接傳遞
if (call == null || Attribute.GetCustomAttribute(call.MethodBase, typeof(TransactionMethodAttribute)) == null)
return _nextSink.SyncProcessMessage(msg);
// 建立交易並存入 CallContext
using (SqlConnection conn = new SqlConnection(ConnStr)) {
conn.Open();
SqlTransaction trans = conn.BeginTransaction();
CallContext.SetData(ContextName, trans);
IMessage retMsg = _nextSink.SyncProcessMessage(msg); // 執行實際業務方法
IMethodReturnMessage ret = retMsg as IMethodReturnMessage;
if (ret.Exception != null) trans.Rollback();
else trans.Commit();
}
return retMsg;
}
public static string ContextName { get { return "TransactionAop"; } }
}
SqlDAL 直接繼承 ContextBoundObject 造成效能損耗,可改為組合模式:在業務類別中持有 SqlDAL 實例,於屬性存取時從 CallContext 取得交易指派。此模式實現「想要交易加兩個標記,不想要就移除」的非侵入式管理,同樣原理可延伸至權限控制、日誌記錄等横切關注點。