[.NET] 在WebAPI中使用AOP的方式,在控制器中加入Attribute集中進行Cache的處理

在前篇 [.NET] 在WebAPI中使用AOP的方式,在控制器中加入Attribute集中進行Log的處理 文章中提到,透過在控制器中加入一行Attribute的屬性,就可以在每一個控制器中進行Log寫入的實作
而在這篇文章中,會依據寫入Log的方式,實作只要加上一行Attribute的設定,就自動將要傳出的內容放到快取之中,並直接回傳快取的內容而不進入控制器本身的Action

要實作控制器快取的方式很簡單,基於寫入Log的方式再進行一部份的強化就可以了

1.首先,加入一個新的類別庫,或是在現有的類別庫中,新增一個[Cache.cs]的類別,這個類別是用來實作MemoryCache的使用

2.在Cache.cs中,加入下方的程式碼

public class Cache
{
    ObjectCache cache;

    public Cache()
    { 
        cache = MemoryCache.Default;
    }

    /// <summary>
    /// 取得快取的動作
    /// </summary>
    /// <param name="query">進行快取取得的查詢物件</param>
    /// <returns></returns>
    public T GetCache<T>(string strCacheName)
    {
        CacheItem item = cache.GetCacheItem(strCacheName);
        return (item != null) ? (T)item.Value : default;
    }

    /// <summary>
    /// 寫入快取的動作
    /// </summary>
    /// <param name="value">寫入快取的物件</param>
    /// <returns></returns>
    public void SetCache<T>(string strCacheName, T objCacheValue, int intAbsoluteExpirationMinute = 0, int intSlidingExpirationMinute = 0)
    {
        CacheItemPolicy policy = new CacheItemPolicy();

        // 指定過期時間,如果都沒有指定,就預設7天
        if (intAbsoluteExpirationMinute > 0)
            policy.AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(intAbsoluteExpirationMinute);
        else if (intSlidingExpirationMinute > 0)
            policy.SlidingExpiration = TimeSpan.FromMinutes(intSlidingExpirationMinute);
        else
            policy.SlidingExpiration = TimeSpan.FromDays(7);

        cache.Set(strCacheName, objCacheValue, policy);
    }

    /// <summary>
    /// 清除快取的動作
    /// </summary>
    /// <param name="value">清除快取的物件</param>
    /// <returns></returns>
    public void ClearCache(string strCacheName) => cache.Remove(strCacheName);
}

GetCache以及SetCache分別是用來作為寫入快取以及取得快取用的方法

2.在API的站台中,加入[CacheHandle.cs],在這個類別中,我們會實作寫入與取得快取的Attribute

3.在CacheHandle.cs檔案中,先加入下面的程式碼

Cache objCache = new Cache();
/// <summary>
/// 指定分鐘數後回收快取,不指定的話預設值為0
/// </summary>
public int AbsoluteExpirationMinute { get; set; }
/// <summary>
/// 快取最後一次使用後重新指定快取的過期分鐘數,不指定的話預設值為0
/// </summary>
public int SlidingExpirationMinute { get; set; }
/// <summary>
/// 快取的物件類型
/// </summary>
public enumCacheDataType CacheDataType { get; set; }
/// <summary>
/// 是否啟用輸入與輸出資料對應的快取機制,當設定為false時,則不論輸入為何,輸出內容都會一模一樣
/// </summary>
public bool EnableKeyValueMapping { get; set; }

/// <summary>
/// 快取的物件
/// </summary>
private class CacheItem
{
    public string CacheName { get; set; }
    public string CacheValue { get; set; }
}

/// <summary>
/// 快取的物件類型
/// </summary>
public enum enumCacheDataType
{
    Int,
    String,
    Bool,
    Decimal,
    JObject,
}

這部份的程式碼,主要是在Attribute中加入幾個需要指定的設定值,像是快取的保留時間、回傳物件的類型,以及是否要啟用不同的傳入值,對應到不同的輸出內容等等。這些設定值都會在接下來的程式碼去實現它

4.接著,在CacheHandle.cs中,繼續加入下面的內容

/// <summary>
/// 當WebAPI的控制器剛被啟動的時候,會進入至這個覆寫的事件中
/// </summary>
/// <param name="actionContext"></param>
public override void OnActionExecuting(HttpActionContext actionContext)
{
    DateTime dtStart = DateTime.UtcNow;
    string strInput = "";

    // 因為傳入的參數為多數,所以ActionArguments必須用迴圈將之取出
    foreach (var item in actionContext.ActionArguments)
    {
        // 取出傳入的參數名稱
        string strParamName = item.Key;

        // 取出傳入的內容並作Json資料的處理
        strInput += strParamName + ":" + JsonConvert.SerializeObject(item.Value) + ".";
    }

    // 將資料存入Context中
    actionContext.Request.Properties.Add(new KeyValuePair<string, object>("__CacheInputData__", strInput));

    // 判斷是否有存在快取資料, 如果存在快取的話就直接回傳
    string strActionName = actionContext.ActionDescriptor.ControllerDescriptor.ControllerName + "-" + actionContext.ActionDescriptor.ActionName;
    List<CacheItem> objCacheItem = objCache.GetCache<List<CacheItem>>(strActionName);
    if (objCacheItem != null)
    {
        CacheItem objItem = null;

        // 如果啟用鍵值對應,就要過濾輸入值找出對應內容
        if (this.EnableKeyValueMapping)
            objItem = objCacheItem.Where(x => x.CacheName == strInput).FirstOrDefault();
        else
            objItem = objCacheItem.FirstOrDefault();

        if (objItem != null)
        {
            object objReturn = null;
            switch (this.CacheDataType)
            {
                case enumCacheDataType.Bool: objReturn = bool.Parse(objItem.CacheValue.Replace(@"""", "")); break;
                case enumCacheDataType.Decimal: objReturn = decimal.Parse(objItem.CacheValue.Replace(@"""", "")); break;
                case enumCacheDataType.Int: objReturn = int.Parse(objItem.CacheValue.Replace(@"""", "")); break;
                case enumCacheDataType.JObject: objReturn = JObject.Parse(objItem.CacheValue); break;
                case enumCacheDataType.String: objReturn = objItem.CacheValue.Replace(@"""", ""); break;
            }
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.OK, objReturn);
        }
    }
}

這段程式碼覆寫了OnActionExecuting的事件,當有Action被進行呼叫的時候,就會將傳入的輸入值與快取中的內容作查詢,如果有查到保留在快取中的內容,就會直接透過CreateResponse的方法回傳記憶體快取中的內容而不進入到API的Action之中
其中,如果[EnableKeyValueMapping]這個設定值設定為true,就會針對指定的輸入值取得指定的輸出,若是設定為false,那就不管輸入值是多少,輸出值都是一模一樣的內容

5.繼續在CacheHanlde.cs的類別中加入下面程式碼

/// <summary>
/// 當WebAPI的控制器結束動作,會進入這個覆寫的事件中
/// </summary>
/// <param name="actionExecutedContext"></param>
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
    DateTime dtEnd = DateTime.UtcNow;
    string strOutput = "";

    if (actionExecutedContext.Response != null)
    {
        // 將actionExecutedContext.Response.Content轉換成Json的字串
        if (actionExecutedContext.Response.Content != null && !actionExecutedContext.Response.Content.GetType().ToString().Contains("System.IO.Stream"))
        {
            string strResponseContent = JsonConvert.SerializeObject(actionExecutedContext.Response.Content);

            // 將Json字串轉換成我們自訂的ResponseContentModel物件
            ResponseContentModel objResponseContent = JsonConvert.DeserializeObject<ResponseContentModel>(strResponseContent);

            // 取出從WebAPI回傳的物件,並轉會成Json字串
            strOutput = JsonConvert.SerializeObject(objResponseContent.Value);
        }
    }

    // 取得Input資料
    object objInput;
    actionExecutedContext.Request.Properties.TryGetValue("__CacheInputData__", out objInput);
    string strInput = (string)objInput;

    // 寫入快取資料
    string strActionName = actionExecutedContext.ActionContext.ActionDescriptor.ControllerDescriptor.ControllerName + "-" + actionExecutedContext.ActionContext.ActionDescriptor.ActionName;
    List<CacheItem> objCacheItem = objCache.GetCache<List<CacheItem>>(strActionName);
    if (objCacheItem == null)
        objCacheItem = new List<CacheItem>();

    var objItem = objCacheItem.Where(x => x.CacheName == strInput).FirstOrDefault();
    if (objItem == null)
    {
        objCacheItem.Add(new CacheItem()
        {
            CacheName = strInput,
            CacheValue = strOutput,
        });
    }

    objCache.SetCache(strActionName, objCacheItem, this.AbsoluteExpirationMinute, this.SlidingExpirationMinute);
}

這段程式碼中,覆寫了OnActionExecuted的事件,也就是當API中的Action有真的被執行到且完成的情況,會將回傳的內容放入到記憶體快取中,以保留讓OnActionExecuting進行比對與快取的取得

到這邊Attribute所需要的內容都已經完成了,接下來我們就要開始實作Controller中Action加入CacheHandle的功能了

6.在Controller資料夾中,新增一個[CacheSampleController.cs]的控制器

 7.在CacheSampleController.cs中加入下面的程式碼

/// <summary>
/// 取得字串用的控制器,加入快取的AOP機制
/// </summary>
/// <param name="strName"></param>
/// <returns></returns>
[CacheHandle(AbsoluteExpirationMinute =1, SlidingExpirationMinute =1, CacheDataType = CacheHandle.enumCacheDataType.String, EnableKeyValueMapping = true)]
[HttpGet]
[ActionName("GetStringCache")]
public string GetStringCache([FromUri]string strName)
{
    string strResponse = "";

    switch (strName)
    {
        case "John": strResponse = "John Wick"; break;
        case "Mary": strResponse = "Mary Jane"; break;
        case "Amy": strResponse = "Amy Adams"; break;
    }
    System.Threading.Thread.Sleep(5000);

    return strResponse;
}

/// <summary>
/// 取得模型用的控制器,加入快取的AOP機制
/// </summary>
/// <param name="query"></param>
/// <returns></returns>
[CacheHandle(AbsoluteExpirationMinute = 1, SlidingExpirationMinute = 1, CacheDataType = CacheHandle.enumCacheDataType.JObject, EnableKeyValueMapping = true)]
[HttpPost]
[ActionName("GetModelCache")]
public ResultModel GetModelCache(QueryModel query)
{
    ResultModel objResponse = new ResultModel();

    switch (query.Name)
    {
        case "John": objResponse = new ResultModel {Age = 52, Phone = "1234567890" }; break;
        case "Mary": objResponse = new ResultModel { Age = 13, Phone = "1122334455" }; break;
        case "Amy": objResponse = new ResultModel { Age = 47, Phone = "0099887766" }; break;
    }
    System.Threading.Thread.Sleep(5000);

    return objResponse;
}

public class QueryModel
{
    public string Name { get; set; }
}

public class ResultModel
{
    public string Phone { get; set; }
    public int Age { get; set; }
}

在第一個GetStringCache的Action中,取得輸入的名字,然後就會回傳指定的完整名,並且在Action中加入CacheHandle的設定以及啟用KeyValueMapping。第二個Action也是一樣的作法,差異只是在於一個是HttpGet、另一個是HttpPost
而為了測試是否真的有進入到快取,我在Action中加入了Sleep(5000)的動作,讓Action停留5秒才進行資料的回傳。若是快取的機制真的有啟動的話,CacheHandle就會略過5秒的等待直接回傳結果給呼叫端

下圖是進行GetStringCache的實測,第一次的呼叫,從POSTMAN的內容可以看到,大約花費5.3秒左右

當使用POSTMAN進行第二次呼叫時,API只使用了30毫秒就回傳結果,代表並沒有進入到Action中,而是在CacheHandle就回傳結果到呼叫端了

使用POST的Action,第一次呼叫時,也花費了5秒左右,代表Action中的Sleep(5000)有真的等待到5秒

第二次呼叫時,一樣只花費約33毫秒就回傳訊息了,CacheHandle發揮了它的作用

AOP能作的事很多,除了前幾篇文章中提到,可以寫入LOG、進行例外狀態的集中管理,本篇文章還加上了指定Action可以直接進行記憶體快取的配置與使用,對於要重覆回傳相同資料的Action來說,不但可以大大減輕AP層每次都要連線資料庫的負擔外,還可以有效的降低AP主機需要進行資料運算的負擔

程式碼下載
https://github.com/madukapai/maduka-WebAPI