【Asp.Net MVC】使用 ContextBoundObject 搭配 Attribute 實現 AOP Logging 機制

前幾天,
作者我有發表一篇「透過 Asp.Net MVC Filter 實作 Controller 層級的 Action Logging 機制」文章,
想必大家也跟我一樣好奇,
如果想更進一步得在 Controller 以外的 BLL 層(Service, 商業邏輯層)或 DAL 層(Repository, 資料訪問層),
掛載能 Logging 傳入 Action 參數值的攔截器到底該如何實作?
接下來, 就分享給大家我如何在 BLL 層與 DAL 層實現 AOP Logging 機制的方法吧!

前言

先前我們是透過 Asp.Net MVC Filter 來實作 Controller 層級的 AOP Logging 機制,
而本篇文章,
將使用「繼承 ContextBoundObject 基底類別」搭配「客製 Attribute 攔截器」的方式,
來實現 BLL 與 DAL 層級的 AOP Logging 機制!

實作 AOP Logging 機制 

Step 1. 首先, 我們針對 BLL 層與 DAL 層各建立一個範例類別: Service & Dac

(BLL 層)Service - TestService:

/// <summary>
/// BLL Layer
/// </summary>
public class TestService
{
	public void Test(TestModel paramA, string paramB, int paramC)
	{
		TestDac dac = new TestDac();
		dac.Test(paramA, paramB, 10);
	}
}

(DAL 層)Dac - TestDac:

/// <summary>
/// DAL Layer
/// </summary>
public class TestDac
{
	public void Test(TestModel paramA, string paramB, int paramC)
	{
		// TODO
	}
}

Step 2. 接下來, 在掛載於上述兩者的攔截器 Attribute 實作前, 為了能順利從 Remoting IMessageSink 擷取出上下文內容, 我們必須將兩者的類別繼承 ContextBoundObject

(BLL 層)Service - TestService:

/// <summary>
/// BLL Layer
/// </summary>
public class TestService : ContextBoundObject
{
	public void Test(TestModel paramA, string paramB, int paramC)
	{
		TestDac dac = new TestDac();
		dac.Test(paramA, paramB, 10);
	}
}

 (DAL 層)Dac - TestDac:

/// <summary>
/// DAL Layer
/// </summary>
public class TestDac : ContextBoundObject
{
	public void Test(TestModel paramA, string paramB, int paramC)
	{
		// TODO
	}
}

Step 3. 再來, 為了讓兩者訊息的呈現更加地彈性, 特地各為兩者製作不同的 Attribute, 而此 Attribute 需分別繼承 ContextAttribute 與實作 IContributeObjectSink 的 GetObjectSink 方法(方法的實作下一步驟說明)

(BLL 層)Service 的攔截器 Attribute:

/// <summary>
/// Service 攔截器擴增屬性
/// </summary>
public class InterceptorOfServiceAttribute : ContextAttribute, IContributeObjectSink
{
	/// <summary>
	/// 調用層
	/// </summary>
	private readonly string layer = "SERVICE";

	/// <summary>
	/// 建構子
	/// </summary>
	public InterceptorOfServiceAttribute() : base("InterceptorOfServiceAttribute") { }

	/// <summary>
	/// 接收指定鏈結的訊息
	/// </summary>
	/// <param name="obj">提供的訊息接收是鏈結前面指定的鏈結</param>
	/// <param name="nextSink">目前為止所撰寫之接收鏈結</param>
	/// <returns>複合接收鏈結</returns>
	public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink nextSink)
	{
		return new InterceptorHandler(this.layer, nextSink);
	}
}

(DAL 層)Dac 的攔截器 Attribute :

/// <summary>
/// Dac 攔截器擴增屬性
/// </summary>
public class InterceptorOfDacAttribute : ContextAttribute, IContributeObjectSink
{
	/// <summary>
	/// 調用層
	/// </summary>
	private readonly string layer = "DAC";

	/// <summary>
	/// 建構子
	/// </summary>
	public InterceptorOfDacAttribute() : base("InterceptorOfDacAttribute") { }

	/// <summary>
	/// 接收指定鏈結的訊息
	/// </summary>
	/// <param name="obj">提供的訊息接收是鏈結前面指定的鏈結</param>
	/// <param name="nextSink">目前為止所撰寫之接收鏈結</param>
	/// <returns>複合接收鏈結</returns>
	public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink nextSink)
	{
		return new InterceptorHandler(this.layer, nextSink);
	}
}

Step 4. 接下來的重頭戲, 即 GetObjectSink 就是我們在接收上下文訊息後要加入行為的接口, 可以看到我們在方法中回傳一個自定義的 InterceptorHandler 類別, 透過該類別來協助我們進行 Logging 的動作

我們將 InterceptorHandler 視為處理上下文接口的主體,
並將所要實作的行為寫在 SyncProcessMessage 方法中,
由於我們要實現的是 BLL 層與 DAL 層的 AOP Logging 機制,
所以我們要將 SyncProcessMessage 方法中所接收的訊息(IMessage)轉為方法調用的接口(IMethodCallMessage
再從方法調用接口訊息中解析出我們所需要紀錄的方法調用端、方法簽章(含參數型態)、參數名稱及參數值 ... 等資訊,
以下為 InterceptorHandler 的程式:

/// <summary>
/// 攔截器處理
/// </summary>
public class InterceptorHandler : IMessageSink
{
	/// <summary>
	/// 攔截器調用層
	/// </summary>
	private string _callLayer;

	// 下一個接收器
	private IMessageSink _nextSink;

	/// <summary>
	/// 建構子
	/// </summary>
	/// <param name="callLayer">攔截器調用層</param>
	/// <param name="nextSink">下一個接收器</param>
	public InterceptorHandler(string callLayer, IMessageSink nextSink)
	{
		this._callLayer = callLayer;
		this._nextSink = nextSink;
	}

	/// <summary>
	/// 取得下一個接收器
	/// </summary>
	public IMessageSink NextSink
	{
		get { return _nextSink; }
	}

	/// <summary>
	/// 同步處理訊息
	/// </summary>
	/// <param name="msg">訊息接收</param>
	/// <returns>複合接收鏈結</returns>
	public IMessage SyncProcessMessage(IMessage msg)
	{
		// 紀錄器
		Logger logger = LogManager.GetCurrentClassLogger();

		// 攔截的訊息
		IMethodCallMessage interceptMsg = msg as IMethodCallMessage;

		#region 參數資訊

		// 收集參數資訊(參數名稱、值)
		IDictionary parameters = new Dictionary<string, object>();
		IList<string> parameterSignatures = new List<string>();
		for (int i = 0; i < interceptMsg.Args.Length; i++)
		{
			parameters.Add(interceptMsg.GetInArgName(i), interceptMsg.GetArg(i));
		}

		// 解析方法簽章(變數型態)
		foreach (Type signature in (Array)interceptMsg.MethodSignature)
		{
			parameterSignatures.Add(signature.FullName);
		}

		// 參數資訊轉成 Json 字串呈現
		string parametersInfo = JsonConvert.SerializeObject(parameters, new JsonSerializerSettings()
		{
			ContractResolver = new ReadablePropertiesOnlyResolver()
		});

		#endregion

		// 訊息內容
		string message = string.Format(
			"[{0}] {1}.{2}({3}) => {4}",
			this._callLayer,
			interceptMsg.TypeName.Split(',')[0],
			interceptMsg.MethodName,
			parameterSignatures.Count > 0 ? string.Join(",", parameterSignatures) : string.Empty,
			string.IsNullOrEmpty(parametersInfo) ? "(void)" : parametersInfo
		);

		// 寫入紀錄
		logger.Info(message);

		return this.NextSink.SyncProcessMessage(msg);
	}

	/// <summary>
	/// 非同步處理訊息(不需要)
	/// </summary>
	public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink) { return null; }
}

同前傳「透過 Asp.Net MVC Filter 實作 Controller 層級的 Action Logging 機制」文章中所提到,
此處的參數資訊也是透過 JSON.NET 來解析,
而上述程式針對物件進行 Json Convert 的時候, 也有額外加上 Json 解析器的設定,
目的即為了保留(去除)我們所想(不想)看到的資訊, 進而提升 Log 的易讀性, 
以下為該解析器(ReadablePropertiesOnlyResolver.cs)的程式:

/// <summary>
/// JsonSerializer 讀取屬性的解析器設定
/// </summary>
class ReadablePropertiesOnlyResolver : DefaultContractResolver
{
    /// <summary>
    /// 建立可呈現(解析)的屬性
    /// </summary>
    /// <returns>呈現的屬性</returns>
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        JsonProperty property = base.CreateProperty(member, memberSerialization);
        if (typeof(Stream).IsAssignableFrom(property.PropertyType))
        {
            property.Ignored = true;
        }
        return property;
    }
}

Step 5. 最後, 只要將我們為 BLL 層與 DAL 層所客製的 Attribute 掛載上去即完成

(BLL 層)Service - TestService:

/// <summary>
/// BLL Layer
/// </summary>
[InterceptorOfService]
public class TestService : ContextBoundObject
{
	public void Test(TestModel paramA, string paramB, int paramC)
	{
		TestDac dac = new TestDac();
		dac.Test(paramA, paramB, 10);
	}
}

(DAL 層)Dac - TestDac:

/// <summary>
/// DAL Layer
/// </summary>
[InterceptorOfDac]
public class TestDac : ContextBoundObject
{
	public void Test(TestModel paramA, string paramB, int paramC)
	{
		// TODO
	}
}
結果展示(DEMO)

貼心小提醒
  1. 如果系統每層、支程式都需要加上 AOP Logging 機制, 可以逐層各抽出一個底層類別, 並將攔截器的屬性掛載在底層類別, 每支功能實作時皆繼承底層類別即擁有 AOP Logging 機制
  2. 類別在繼承使用 ContextBoundObject 時, 如果過於濫用可能造成系統處理效能上的損失, 請斟酌使用!(在便利性與效能間取得平衡)