【Asp.Net MVC】透過 Asp.Net MVC Filter 實作 Action Logging 機制

.Net MVC Filter 有四種類型:Authorization(驗證)、Action(動作)、Result(結果)、Exception(例外)
有關各類型的介紹可參考 MSDN 的 Filtering in ASP.NET MVC 介紹,
以下本文將簡單介紹如何使用 Asp.Net MVC 中的 Action filters 來實作 Action Logging 機制 ..

前言

簡單來說,
我們可以將 Asp.Net MVC Filter 視為每個指定目標的「Interceptor(攔截器)」, 
在各攔截器中可以去做很多我們想做的事情, 例如:

  1. 可以使用 Authorization Filter 攔截器進行 FormsAuthentication 或 Session 狀態的驗證
  2. 可以使用 Exception Filter 攔截器進行 Exception Catch 並進行 Logging
  3. ... 可以使用其他類型的攔截器實作其他事情

身為一個開發者,
常常會收到客戶丟過來的不知名錯誤或問題單,
為了能在最短的時間內, 
透過系統的 Log 了解客戶到底傳送了什麼資料到後端,
這時候 Action Filter 攔截器即大大發揮了它的效益 ..

※ 備註:本文搭配 NLog 套件實作 Logging 機制, 於此不針對 NLog 多做介紹

關於 Action filters

實作 Action Log 機制
Step 1. 首先, 我們先在 Controller 覆寫(Override)OnActionExecuting 事件
/// <summary>
/// 執行 Action 觸發事件
/// </summary>
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
    base.OnActionExecuting(filterContext);

    // TODO:準備開始實作
}
Step 2. 接下來, 透過 JSON.NET 解析 ActionExecutingContext 中的 ActionParameters(參數資訊)
// 參數資訊
string parametersInfo = JsonConvert.SerializeObject(filterContext.ActionParameters, new JsonSerializerSettings()
{
    ContractResolver = new ReadablePropertiesOnlyResolver()
});

可以看到, 上述程式針對物件進行 Json Convert 的時候, 有額外加上 Json 解析器的設定,
目的是為了保留或去除我們所要看到的資訊,
因為從前端往 Controller 傳送的資料類型可能會包含檔案資訊,
為了提升 Log 的易讀性, 故把 Stream 相關 Type 資訊過濾掉,
以下為該解析器(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 3. 再來, 進一步從 ActionExecutingContext  中解析出 Runtime 的 Controller / Action 資訊
string controllerName = filterContext.Controller.GetType().Name;
string actionName = filterContext.ActionDescriptor.ActionName;
Step 4. 最後, 我們將訊息打包後寫入 Log, 即完成 Action Logging 啦
// 訊息內容
string message = string.Format(
    "{0}.{1}() => {2}",
    controllerName,
    actionName,
    string.IsNullOrEmpty(parametersInfo) ? "(void)" : parametersInfo
);

// 寫入訊息(透過 NLog 套件)
logger.Info(message);
Final. 完整的程式碼
/// <summary>
/// 執行 Action 觸發事件
/// </summary>
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
	base.OnActionExecuting(filterContext);

	// 參數資訊
	string parametersInfo = JsonConvert.SerializeObject(filterContext.ActionParameters, new JsonSerializerSettings()
	{
		ContractResolver = new ReadablePropertiesOnlyResolver()
	});

	// 運行中的 Controller & Action 資訊
	string controllerName = filterContext.Controller.GetType().Name;
	string actionName = filterContext.ActionDescriptor.ActionName;

	// 訊息內容
	string message = string.Format(
		"{0}.{1}() => {2}",
		controllerName,
		actionName,
		string.IsNullOrEmpty(parametersInfo) ? "(void)" : parametersInfo
	);

	// 寫入訊息
	Logger logger = LogManager.GetCurrentClassLogger();
	logger.Info(message);
}
Demo. 結果展示(Log File)

進階實作 Action Log 機制:掛載自定義的 Attribute

上述的作法是直接在 Controller 中 Override OnActionExecuting 事件,
作者本身覺得這樣的寫法會讓程式碼看起來沒那麼簡潔,
於是整合了 C# 所提供的一個定義宣告式標記(Tag)的機制:屬性(Attribute),
透過自訂一個攔截器 Attribute , 並掛載至指定 Controller 層級的 Class 或 Action 上,
即可達成預期的 Logging 機制 ..

Step 1. 自訂攔截器 Attribute(InterceptorOfControllerAttribute), 設定允許掛載在 Class 或 Action 上
using Newtonsoft.Json;
using NLog;
using System;
using System.Web.Mvc;

namespace AopLogger.Filters
{
    /// <summary>
    /// Controller 攔截器擴增屬性
    /// </summary>
    [AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
    sealed class InterceptorOfControllerAttribute : ActionFilterAttribute
    {
        /// <summary>
        /// 執行 Action 觸發事件
        /// </summary>
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            // 訊息管理器
            Logger logger = LogManager.GetCurrentClassLogger();

            // 參數資訊
            string parametersInfo = JsonConvert.SerializeObject(filterContext.ActionParameters, new JsonSerializerSettings()
            {
                ContractResolver = new ReadablePropertiesOnlyResolver()
            });

            // 運行中的 Controller & Action 資訊
            string controllerName = filterContext.Controller.GetType().Name;
            string actionName = filterContext.ActionDescriptor.ActionName;

            // 訊息內容
            string message = string.Format(
                "{0}.{1}() => {2}",
                controllerName,
                actionName,
                string.IsNullOrEmpty(parametersInfo) ? "(void)" : parametersInfo
            );

            // 寫入訊息
            logger.Info(message);
        }
    }
}
Step 2. 將自訂的攔截器 Attribute 掛載至指定 Controller 層級的 Class 或 Action 上即完成
using AopLogger.Filters;
using AopLogger.Models;
using AopLogger.Services;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;

namespace AopLogger.Controllers
{
    [InterceptorOfController]
    public class LoggerController : Controller
    {
        /// <summary>
        /// 測試首頁
        /// </summary>
        public ActionResult Index(TestModel paramA, string paramB)
        {
            return View();
        }
    }
}
延伸閱讀

想必大家已經了解如何實作 Controller 層級的 Action Logging 機制了吧,
接下來將進一步分享,
如何透過掛載攔截器 Attribute 的方式,
實現 BLL 層(Service, 商業邏輯層)與 DAL 層(Repository, 資料訪問層)的 AOP Logging 機制!(連結