[廚餘回收] 關於 ASP.NET MVC 中 OutputCache 的 VaryByParam 屬性有一個不盡善盡美的地方

這天在追查為何 A 客戶的訂單會出貨到 B 客戶哪裡去? 於是我看到了下面這段程式碼:

在 Login 的 Action 上面被加了 OutputCacheAttribute,Duration 屬性被設成 3,其中還用到了 VaryByParam 屬性,我猜寫這段 Code 的工程師應該是想要為每個登入的帳號做 OutputCache,這樣設定沒有問題,問題出在 HTTP Request 的發送內容。

首先我們得了解 VaryByParam 的實際作用是什麼? 在官方文件的說明是這樣的:

在使用 OutputCache 的時候,我們可以指定 VaryByParam 屬性,來更準確地控制 OutputCache 的內容,它可以是以下三種值:

  • *:QueryString 或 POST 的參數不同,就會建立不同版本的 Cache 內容。
  • none:永遠不會建立不同版本的 Cache 內容。
  • 以逗號(,)隔開的參數清單:根據參數清單內的參數,建立不同版本的 Cache 內容。

ContentType 的差異

我用一個範例來說明,我有一個 Login Action,有兩個參數分別是 memberAccountmemberPassword,我想要讓 Login Action 的回傳內容快取 10 分鐘,於是我就加上 OutputCacheAttribute,Duration 指定為 600,還附加 VaryByParam 指定為 memberAccount;memberPassword,防止快取的內容受到其他參數的干擾。

一般我們用 jQuery.ajax() POST HTTP Request 大都這樣寫,這沒毛病。

function login() {
    $.ajax({
        url: "/Test/Login",
        type: "POST",
        data: { memberAccount: $("#memberAccount").val(), memberPassword: $("#memberPassword").val() }
    }).done(function (data, textStatus, jqXHR) {
        console.log(data);
    });
}

OutputCache 也完全作用,沒有問題。

另一位工程師說,他 Content 想改 JSON 格式,於是他就直接動手了。

function login() {
    $.ajax({
        url: "/Test/Login",
        type: "POST",
        contentType: "application/json; charset=utf-8",
        data: JSON.stringify({ memberAccount: $("#memberAccount").val(), memberPassword: $("#memberPassword").val() })
    }).done(function (data, textStatus, jqXHR) {
        console.log(data);
    });
}

這時候就會發現,無論 memberAccount 及 memberPassword 的值怎麼改,回應的內容停在第一次 Cache 的結果。

這邊就是我覺得 OutputCache 不盡善盡美的地方,我們在使用 VaryByParam 指定參數清單的時候,OutputCache 需要根據參數清單去取得參數的值拿來產生 Cache Key,那 OutputCache 會從哪裡去取得參數的值? 答案是 Request.QueryStringRequest.Form,可是我們如果把 ContentType 改用 application/json,Content 改用 JSON 格式的話,Request.QueryString 及 Request.Form 會是空集合,這當然就取不到參數值來產生 Cache Key 了,Cache Key 就會都是一樣的,Cache Key 既然一樣,那麼 Cache 的內容當然也一樣了。

其實我認為 OutputCache 是可以根據 ContentType 去解析 Content 取得參數值的,但是它沒有做,不過我們也不必失望,我們可以用 VaryByCustom 來解決我們的問題,我附加 VaryByCustom 並且將值指定為 JsonParam:memberAccount;memberPassword

接著到 Global.asax.cs 覆寫 GetVaryByCustomString() 方法

這樣我們發送 JSON 格式的 Content,OutputCache 照樣可以 Work。

最後,我認為像 Login 這類型做登入檢查、安全驗證的 Action,最好還是不要用 OutputCache。

參考資料

C# 指南 ASP.NET 教學 ASP.NET MVC 指引
Azure SQL Database 教學 SQL Server 教學 Xamarin.Forms 教學