在前篇 [.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主機需要進行資料運算的負擔