几乎每个 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 的外观提供了一种更健壮的方法来访问会话变量。这是一种实施起来非常简单的技术,但好处很大。