幾乎每個 ASP.NET 應用程式都需要追蹤使用者會話的資料。 ASP.NET 提供 HttpSessionState 類別來儲存會話狀態值。可以使用靜態 HttpContext.Current.Session 屬性在整個應用程式中存取每個 HTTP 請求的 HttpSessionState 類別的實例。使用 Page 或 UserControl 的 Session 屬性,可以在每個 Page 和 UserControl 上更輕鬆地存取相同實例。
HttpSessionState 類別提供鍵/值對的集合,其中鍵的類型為 String,值的類型為 Object。這意味著 Session 非常靈活,您可以在 Session 中儲存幾乎任何類型的資料。
但是(總是有一個但是)這種彈性並不是沒有代價的。成本是很容易將錯誤引入到您的應用程式中。許多可能引入的錯誤不會透過單元測試發現,並且可能不會透過任何形式的結構化測試發現。這些錯誤通常僅在應用程式部署到生產環境時才會出現。當它們確實出現時,通常很難(如果不是不可能的話)確定它們是如何發生的並能夠重現錯誤。這意味著修復它們的成本非常昂貴。
本文提出了一種有助於防止此類錯誤的策略。它使用一種稱為外觀的設計模式,因為它將 HttpSessionState 類別提供的非常自由的介面(可以滿足任何應用程式的要求)與專為特定應用程式構建的精心設計和控制的介麵包裝在一起。如果您不熟悉設計模式或外觀模式,快速在網路上搜尋「外觀設計模式」將為您提供充足的背景知識。但是,您不必了解設計模式才能理解本文。
本文中顯示的範例程式碼是用 C# 編寫的,但這些概念適用於任何 .NET 語言。
問題是什麼?
在本文的這一部分中,我將描述在沒有外觀的情況下直接存取 HttpSessionState 類別的問題。我將描述可能引入的錯誤類型。
下面顯示了為存取會話狀態變數而編寫的典型程式碼。
// 保存會話變數
會話[“某個字串”] = anyOldObject;
// 讀取會話變數
DateTime 開始日期 = (DateTime)Session["StartDate"];
問題源自於 HttpSessionState 提供的靈活介面:鍵只是字串,值不是強型別的。
使用字串文字作為鍵
如果使用字串文字作為鍵,則編譯器不會檢查鍵的字串值。透過簡單的輸入錯誤可以輕鬆建立新的會話值。
會話[“已收到”] = 27;...
會話[“已收到”] = 32;
在上面的程式碼中,已儲存兩個單獨的會話值。
大多數像這樣的錯誤將透過單元測試來識別——但並非總是如此。該值未如預期發生變化可能並不總是顯而易見。
我們可以透過使用常數來避免這種錯誤:
private const string returned = "received";...
會話[已收到] = 27;...
會話[已收到] = 32;
沒有類型檢查
不對會話變數中儲存的值進行類型檢查。編譯器無法檢查所儲存內容的正確性。
考慮以下程式碼:
Session["maxValue"] = 27;...
int maxValue = (int)Session["maxValue"];
在其他地方,以下程式碼用於更新該值。
會話[“最大值”] = 56.7;
如果再次執行將「maxValue」會話變數讀入 maxValue int 變數的程式碼,則會拋出 InvalidCastException。
大多數像這樣的錯誤將透過單元測試來識別——但並非總是如此。
無意中重複使用密鑰
即使我們在每個頁面上為會話金鑰定義常數,也可能會無意中跨頁面使用相同的金鑰。考慮以下範例:
一頁上的程式碼:
private const string edit = "edit";...
會話[編輯] = true;
第二頁的程式碼,顯示在第一頁之後:
private const string edit = "edit";...
if ((bool)會話[編輯])
{
…
}
第三個不相關的頁面上的程式碼:
private const string edit = "edit";...
會話[編輯] = false;
如果由於某種原因在第二頁顯示之前顯示了第三頁,則該值可能不是預期的值。程式碼可能仍然可以運行,但結果將是錯誤的。
通常這個錯誤不會在測試中被發現。只有當使用者執行某些特定的頁面導航組合(或開啟新的瀏覽器視窗)時,該錯誤才會顯現。
最糟糕的是,沒有人知道錯誤已經出現,我們可能最終會將資料修改為意想不到的值。
無意中再次使用密鑰
在上面的範例中,相同的資料類型儲存在會話變數中。由於沒有對儲存的內容進行類型檢查,因此也可能會出現資料類型不相容的問題。
一頁上的程式碼:
Session["FollowUp"] = "true";
第二頁代碼:
Session["FollowUp"] = 1;
第三頁代碼:
Session["FollowUp"] = true;
當錯誤出現時,將拋出 InvalidCastException。
通常這個錯誤不會在測試中被發現。只有當使用者執行某些特定的頁面導覽組合(或開啟新的瀏覽器視窗)時,該錯誤才會顯現。
我們能做什麼?
第一個快速修復
我們能做的第一件也是最簡單的事情是確保我們永遠不會使用字串文字作為會話金鑰。始終使用常量,從而避免簡單的打字錯誤。
私有常數字串限制=“限制”;...
會話[限制] = 27;...
會話[限制] = 32;
然而,當常數在本地定義時(例如在頁面層級),我們仍然可能會無意中重複使用相同的鍵。
更好的快速修復
不要在每個頁面上定義常數,而是將所有會話金鑰常數分組到一個位置並提供將出現在 Intellisense 中的文件。文件應清楚地表明會話變數的用途。例如,僅為會話金鑰定義一個類別:
public static class SessionKeys
{
/// <摘要>
/// 最大...
/// 摘要>
公共常數字串限制=“限制”;
}
...
會話[SessionKeys.Limit] = 27;
當您需要新的會話變數時,如果您選擇已使用的名稱,那麼當您將常數新增至 SessionKeys 類別時您就會知道這一點。您可以查看目前的使用情況,並確定是否應該使用不同的金鑰。
但是,我們仍然無法確保資料類型的一致性。
更好的方法 - 使用外觀
僅從應用程式中的一個靜態類別(外觀)存取 HttpSessionState。不得從頁面或控制項內部直接存取 Session 屬性,且除了從外觀的內部之外,不得直接存取 HttpContext.Current.Session
所有會話變數都將作為外觀類別的屬性公開。
這與對所有會話鍵使用單一類別具有相同的優點,此外還有以下優點:
放入會話變數中的內容的強類型化。
無需在使用會話變數的程式碼中進行轉換。
屬性設定器的所有好處都可以驗證放入會話變數中的內容(而不僅僅是類型)。
存取會話變數時屬性取得器的所有好處。例如,第一次存取變數時對其進行初始化。
會話外觀類別範例
下面是一個範例類,用於為名為 MyApplication 的應用程式實作會話外觀。
坍塌
/// <摘要>
/// MyApplicationSession 為 ASP.NET Session 物件提供外觀。
/// 所有對Session變數的存取都必須通過此類。
/// 摘要>
公共靜態類別 MyApplicationSession
{
# 區域私有常數
//------------------------------------------------ --------------------
私人常數字串使用者授權=「使用者授權」;
私有常數字串 teamManagementState = "TeamManagementState";
私有常數字串startDate =“開始日期”;
私有 const string endDate = "EndDate";
//------------------------------------------------ --------------------
# endregion
# 區域公共屬性
//------------------------------------------------ --------------------
/// <摘要>
/// Username是目前使用者的網域名稱和使用者名稱。
/// 摘要>
公共靜態字串用戶名
{
取得{返回HttpContext.Current.User.Identity.Name; }
}
/// <摘要>
/// UserAuthorization 包含了授權訊息
/// 當前使用者。
/// 摘要>
公共靜態使用者授權使用者授權
{
得到
{
使用者授權 userAuth
= (使用者授權)HttpContext.Current.Session[使用者授權];
// 檢查UserAuthorization是否過期
如果 (
userAuth == null ||
(userAuth.Created.AddMinutes(
MyApplication.Settings.Caching.AuthorizationCache.CacheExpiryMinutes))
< 日期時間.Now
)
{
userAuth = UserAuthorization.GetUserAuthorization(使用者名稱);
用戶授權=用戶授權;
驗證
;
}
私人集合
{
HttpContext.Current.Session[使用者授權] = 值;
}
}
/// <摘要>
/// TeamManagementState用於儲存目前狀態
/// TeamManagement.aspx 頁面。
/// 摘要>
公共靜態團隊管理狀態團隊管理狀態
{
得到
{
返回 (TeamManagementState)HttpContext.Current.Session[teamManagementState];
}
放
{
HttpContext.Current.Session[teamManagementState] = 值;
}
}
/// <摘要>
/// StartDate 是用於過濾記錄的最早日期。
/// 摘要>
公共靜態日期時間開始日期
{
得到
{
if (HttpContext.Current.Session[startDate] == null)
返回日期時間.MinValue;
別的
return (DateTime)HttpContext.Current.Session[startDate];
}
放
{
HttpContext.Current.Session[開始日期] = 值;
}
}
/// <摘要>
/// EndDate 是用於篩選記錄的最新日期。
/// 摘要>
公共靜態日期時間結束日期
{
得到
{
if (HttpContext.Current.Session[endDate] == null)
返回日期時間.MaxValue;
別的
return (DateTime)HttpContext.Current.Session[endDate];
}
放
{
HttpContext.Current.Session[結束日期] = 值;
}
}
//------------------------------------------------ --------------------
# 結束區域
}
此類別示範了屬性 getter 的使用,如果未明確儲存值,則屬性 getter 可以提供預設值。例如,StartDate 屬性提供 DateTime.MinValue 作為預設值。
UserAuthorization 屬性的屬性 getter 提供了 UserAuthorization 類別實例的簡單緩存,確保會話變數中的實例保持最新。此屬性也顯示了私有設定器的使用,因此會話變數中的值只能在外觀類別的控制下設定。
Username 屬性示範了一個可能曾經儲存為會話變數但不再以這種方式儲存的值。
以下程式碼顯示如何透過外觀存取會話變數。請注意,在此程式碼中不需要進行任何轉換。
// 保存會話變數
MyApplicationSession.StartDate = DateTime.Today.AddDays(-1);
// 讀取會話變數
日期時間開始日期 = MyApplicationSession.StartDate;
額外好處
外觀設計模式的另一個好處是它對應用程式的其餘部分隱藏了內部實作。也許將來您可能會決定使用內建 ASP.NET HttpSessionState 類別之外的另一種機制來實現會話狀態。您只需要更改外觀的內部結構 - 無需更改應用程式其餘部分中的任何其他內容。
概括
使用 HttpSessionState 的外觀提供了一種更健全的方法來存取會話變數。這是一種實施起來非常簡單的技術,但好處很大。