Quantcast
Channel: Darkthread
Viewing all articles
Browse latest Browse all 428

ActionFilter Attribute 共用特性與狀態保存

$
0
0

同事報案,某個 Web API 會不定期出錯。進一步調查是近期啟動的一個新排程同步發出多個 API 呼叫,當 Web API 同時被多方呼叫,Web API 加掛用來寫 Log 的 ActionFilter Attribute 偶爾會發生 Dictionary.Add 重複加入相同鍵值的錯誤。因 Dictionary 被設成 ActionFilter Attribute Instance 的私有欄位,依我原先的理解,ActionFilter Attribute 每次呼叫時都應建立新 Instance,不致因共用打架,但觀察結果顯然與假設不符。進一步檢查 Log 軌跡,確實找到兩次 Request 共用 ActionFilterAttribute 覆寫變數內容的證據!由此,為什麼只在 Instance 執行一次 Dictionary.Add 卻發生鍵值重複便有了合理解釋。

爬文才發現,自 ASP.NET MVC3 起,Action Filter 更積極藉由 Cache 機制重複使用,不再每次 Request 重新建立:參考

In previous versions of ASP.NET MVC, action filters are create per request except in a few cases. This behavior was never a guaranteed behavior but merely an implementation detail and the contract for filters was to consider them stateless. In ASP.NET MVC 3, filters are cached more aggressively. Therefore, any custom action filters which improperly store instance state might be broken.

用以下實例重現問題。用 ActionFilterAttribute 做一個超簡單的執行耗時顯示,在 OnActionExecuting() 以物件欄位記錄 p 參數及開始時間、OnResultExecuted() 計算並顯示耗時及 p 參數:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace MVCLab.Models
{
publicclass ExecTimeAttribute : ActionFilterAttribute
    {
string id;
        DateTime start;
publicoverridevoid OnActionExecuting(ActionExecutingContext filterContext)
        {
            id = filterContext.HttpContext.Request["p"] ?? "NULL";
            start = DateTime.Now;
        }
publicoverridevoid OnResultExecuted(ResultExecutedContext filterContext)
        {
            var dura = DateTime.Now - start;
            filterContext.HttpContext.Response.Write($"<span>{id}: {dura.Milliseconds}ms</span>");
        }
    }
}

在 HomeController.cs Index Action 加上 [ExecTime] Attribute:

using MVCLab.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Mvc;
 
namespace MVCLab.Controllers
{
publicclass HomeController : Controller
    {
        [ExecTime]
public ActionResult Index()
        {
            var rnd = new Random();
            Thread.Sleep(rnd.Next(500));
return View();
        }
    }
}

Index.cshtml 長這様:

@{ Layout = null; }
<!DOCTYPEhtml>
<html>
<head>
<metaname="viewport"content="width=device-width"/>
<title>Index</title>
</head>
<body>
<div>測試(@Request["p"])</div>
</body>
</html>

寫個 Test1.html 做測試:

<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8"/>
<style>
        iframe { width: 150px; height: 60px; margin: 3px; float: left; }
</style>
</head>
<body>
<inputid="txtUrl"value="/Home/Index?p=001"/>
<buttononclick="test()">Test</button>
<br/>
<iframeid="frmTest"src="about:blank"></iframe>
<script>
function test() {
            document.getElementById("frmTest").src = document.getElementById("txtUrl").value;
        }
</script>
</body>
</html>

測試結果符合預期:

問題發生在多個 Request 並行時。用三個 IFrame 一次發出 3 個 Request,參數分別傳入 001、002、003:

<!DOCTYPEhtml>
<html>
<head>
<title></title>
<metacharset="utf-8"/>
<style>
        iframe { width: 150px; height: 60px; margin: 3px; float: left; }
</style>
</head>
<body>
<iframesrc="/Home/Index?p=001"></iframe>
<iframesrc="/Home/Index?p=002"></iframe>
<iframesrc="/Home/Index?p=003"></iframe>
</body>
</html>

然後它就壞掉了!下方顯示的 p 參數全部被覆寫成 003…

由此可知,使用 Attribute 物件屬性或欄位保存狀態可能因物件重複使用導致資料覆寫,故較好的做法是將狀態改存入專屬每個 Request 的 HttpContext,TempData 是不錯的選擇。修改後程式如下:

using System;
using System.Web.Mvc;
 
namespace MVCLab.Models
{
publicstaticclass MyContextExt
    {
publicstatic T GetVar<T>(this ResultExecutedContext ctx, string varName)
        {
return (T)ctx.HttpContext.Items[varName];
        }
publicstaticvoid StoreVar<T>(this ActionExecutingContext ctx, string varName, T data)
        {
            ctx.HttpContext.Items[varName] = data;
        }
    }
 
publicclass ExecTimeAttribute : ActionFilterAttribute
    {
//REF: http://stackoverflow.com/a/8937793/4335757
publicoverridevoid OnActionExecuting(ActionExecutingContext filterContext)
        {
            filterContext.StoreVar<string>("id", filterContext.HttpContext.Request["p"] ?? "NULL");
            filterContext.StoreVar<DateTime>("start", DateTime.Now);
        }
publicoverridevoid OnResultExecuted(ResultExecutedContext filterContext)
        {
            var dura = DateTime.Now - filterContext.GetVar<DateTime>("start");
            var id = filterContext.GetVar<string>("id");
            filterContext.HttpContext.Response.Write($"<span>{id}: {dura.Milliseconds}ms</span>");
        }
    }
}

改用 TempData 保存狀態後,程式運作正常,問題排除。


Viewing all articles
Browse latest Browse all 428

Trending Articles