ASP.NET 的数据存储对象






4.58/5 (45投票s)
2004年10月22日
17分钟阅读

185152
了解 ASP.NET 的五种强大的存储机制,以及如何快速确定在特定情况下应使用哪种机制。
目录
引言
在本简短教程中,我们将探讨 ASP.NET 中可用于存储数据的五种不同对象。其中两种对象,`Application` 和 `Session`,对于任何来自 ASP 的人来说应该都很熟悉。另外三种,Context、Cache 和 ViewState,对 ASP.NET 来说是全新的。每种对象在特定条件下都非常理想,它们之间的差异仅在于作用域(即数据存在的时长和数据的可见性),而我们最终要达到的就是对这种差异的全面理解。此外,这些对象中的大多数都有替代方案,我们将简要介绍一下。
作用域
由于这些对象之间唯一的区别就是它们的作用域,因此清晰地理解其确切含义非常重要。在此讨论的上下文中,作用域指的是这些对象内部的数据(或对象本身)的生存时长。例如,用户的用户 ID 应该在用户注销之前一直存在,而用户输入的新密码应该只在单个请求的生命周期内存在。作用域还指数据的可见性。只有两种可见性类型:数据要么在整个应用程序中可用,要么仅对特定用户可用。例如,发送电子邮件时使用的 SMTP 服务器名称应该是全局可访问的,而电子邮件地址是特定于单个用户的。
通用性
上述对象都通过类似 `HashTable` 的东西来暴露其数据存储功能。换句话说,从它们中的任何一个获取或设置信息的方式都非常相似。它们都可以保存任何对象作为值,虽然其中一些可以保存对象作为键,但另一些只能保存字符串 - 而您将 98% 的时间使用字符串。例如:
//C#
//setting
Application.Add("smtpServer", "127.0.0.1");
Context.Items.Add("culture", new CultureInfo("en-CA"));
Session.Add("userId", 3);
//getting
string smtpServer = (string)Application["smtpServer"];
CultureInfo c = (CultureInfo)Context.Items["culture"];
int userId = (int)Session["userId"];
'VB
'setting
Application.Add("smtpServer", "127.0.0.1")
Context.Items.Add("culture", new CultureInfo("en-CA"))
Session.Add("userId", 3)
'getting
Dim smtpServer as string = cstr(Applciation("smtpServer"))
Dim c as CultureInfo = ctype(Context.Items("culture"), CultureInfo)
Dim userId as integer = cint(Session("userId"))
HTTPApplication
`Application` 对象是 `System.Web.HTTPApplication` 类的实例。通常,您会在 `Global.Asax` 的 `Application_Start` 事件或 `HttpModule` 的 `BeginRequest` 事件中设置 `Application` 对象的值。逻辑上要存储的值包括发送电子邮件时要使用的 SMTP 服务器、管理员的联系电子邮件地址以及您可能需要在所有用户/请求中全局使用的任何其他值。
工作进程回收与 Web 场
虽然说存储在 `Application` 中的数据在网站运行期间一直存在是正确的,但如果仅仅停留在这一点上,那就错了。从技术上讲,`Application` 中的数据一直存在,直到工作进程(实际上就是 `aspnet.exe`)消失。**这可能产生严重的后果**,这不仅仅是技术上的细节。ASP.NET 工作进程会因为多种原因进行自我回收,例如修改 `web.config`、空闲时间过长或消耗过多内存。如果您通过在 `Application_Start` 事件中设置值并在类/页面中只读取它们来使用 `Application` 对象,那么就没有问题。当工作进程自我回收时,`Application_Start` 会触发,您的值也会被正确设置。但是,如果您在 `Application_Start` 事件中设置了一个值,并在之后更新了这个值,那么当工作进程自我回收时,它将恢复到 `Application_Start` 的值。
另一件需要记住的事情是,存储在 `Application` 对象中的数据是特定于计算机的,并且(不容易)无法在 Web 场之间共享。
替代方案
虽然 `Application` 对象在经典 ASP 中可能非常有用,但现在(在我看来)已经有了许多更好的替代方案。
Web.Config
如果您需要只读/常量值(例如我们的 SMTP 服务器示例),请考虑使用 `web.config`。与 `Application` 中的值不同,`web.config` 可以轻松快速地更改。您可以在 `web.config` 中执行相当高级的操作,请查看我关于创建自定义配置的教程。
常量
您可以利用 ASP.NET 的面向对象特性,创建一个带有公共常量的实用程序类。说实话,除非您只是在模拟一些东西,**我不确定为什么您会选择这种方法而不是使用 web.config**。它实际上除了将来可能带来的麻烦之外,什么也提供不了。
HttpCache + (XML | DB)
虽然 `web.config` 中的自定义节对于只读值无疑是最佳选择,但对于读/写值并避免工作进程回收,我们能做什么呢?答案是将值存储在 XML 文件或数据库中。虽然您可以在经典 ASP 中执行同样的操作,但现在您可以利用一个新的存储对象 `HttpCache`(我们将在下面介绍)来避免您可能面临的任何主要的性能损失。这还可以避免与 `HttpApplication` 类相关的任何 Web 场问题。
结论
在我看来,从数据存储的角度来看,`HttpApplication` 类的有用性在 ASP.NET 中大大降低了。对于只读值,强大的自定义 `web.config` 配置节是一种更优雅、更灵活的解决方案。使用 XML 文件或数据库对于读/写值和 Web 场是理想的选择,并且与 `HttpCache` 对象结合使用时,可以轻松超越可怜的 `Application`。
HttpCache
我们将要看的第一个新的存储类是 `HttpCache`(缓存)类。它也是最独特的。这种独特性有几个原因。首先,`HttpCache` 并不是一个真正的存储机制,它主要用作数据库或文件的代理以提高性能。其次,虽然您通过指定键来读取缓存中的值,但在插入时您拥有更多的控制权,例如数据存储的时长、数据移除时触发的事件,等等。读取缓存中的值时,不能保证数据一定存在(ASP.NET 会因为多种原因移除数据),因此,使用缓存的典型方式如下:
private DataTable GetStates() {
string cacheKey = "GetStates"; //the key to get/set our cache'd data
DataTable dt = Cache[cacheKey] as DataTable;
if(dt == null){
dt = DataBaseProvider.GetStates();
Cache.Insert(cacheKey, dt, null, DateTime.Now.AddHours(6), TimeSpan.Zero);
}
return dt;
}
Private Function GetStates() As DataTable
Dim cacheKey As String = "GetStates" 'the key to get/set our cache'd data
Dim dt As DataTable = CType(Cache(cacheKey), DataTable)
If dt Is Nothing Then
dt = DataBaseProvider.GetStates()
Cache.Insert(cacheKey, dt, Nothing, DateTime.Now.AddHours(6), TimeSpan.Zero)
End If
Return dt
End Function
我们首先要声明一个 `cacheKey` **[行: 2]**,我们将用它来从缓存中检索和存储信息。接下来,我们使用该键并尝试从缓存中获取值 **[行: 3]**。如果这是我们第一次调用此方法,或者数据因任何原因已被删除,我们将得到 `null` / `Nothing` **[行: 4]**。如果我们确实得到了 `null` / `Nothing`,我们将通过虚构的 `DataBaseProvider.GetStates()` 调用命中数据库 **[行: 5]**,并使用我们的 `cacheKey` 将值插入 `Cache` 对象 **[行: 6]**。插入时,我们不指定文件依赖项,并希望缓存能在六小时后过期。
上段代码中需要注意的重要一点是,当我们在缓存中找到数据时,处理密集型数据访问代码 `DataBaseProvider.GetStates()` 会被跳过。在此示例中,真正的存储机制是一个虚构的数据库;`HttpCache` 仅充当代理。您更可能需要根据参数检索信息,例如特定国家/地区的所有州/省,这可以通过下面的 VB.NET 代码轻松实现。
Private Function GetStates(ByVal countryId As Integer) As DataTable
Dim cacheKey As String = "GetStates:" & countryId.ToString()
'the key to get/set our cache'd data
Dim dt As DataTable = CType(Cache(cacheKey), DataTable)
If dt Is Nothing Then
dt = DataBaseProvider.GetStates(countyId)
Cache.Insert(cacheKey, dt, Nothing, _
DateTime.Now.AddHours(6), TimeSpan.Zero)
End If
Return dt
End Function
我真正需要做的就是将参数添加到我的 `cacheKey` 中,这意味着如果我首先尝试获取国家/地区 3 的州,我的 `cacheKey` 将看起来像 `GetStates:3`,我需要访问数据库。后续的 `GetStates:3` 请求将避免数据库调用。但是,当我请求国家/地区 2 的州时,我的 `cacheKey` 看起来像 `GetStates:2`,当首次调用时会命中数据库,然后检索正确的值。
SessionState
Session 在特定用户访问的整个生命周期内存在,或者直到 Session 超时,或者您将其移除。您在 ASP 时代可能听过很多次“不要使用 Session”或“Session 是邪恶的”。Session 的问题在于它易于使用,但可能对性能产生严重影响,因为它们存储在内存中。在 ASP.NET 中,您可以选择将 Session 存储在内存中、存储在 ASP.NET 的一个特殊服务中,或者存储在 SQL 数据库中。拥有这种选择并明智地使用它,**使得在 ASP.NET 中使用 Session 成为一件好事**。
您可以通过 `web.config` 的 `sessionState` 元素来控制 Session 的存储位置,特别是 `mode` 属性。
<system.web>
<!-- can use a mode of "Off", "InProc", "StateServer" or "SQLServer".
These are CaSe-SeNsItIvE -->
<sessionState mode="InProc" />
...
</system.web>
InProc
`InProc` 意味着 Session 存储在 ASP.NET 工作进程内部 - 这几乎就是经典 ASP 中 Session 的工作方式。以这种方式存储数据可能导致性能问题(因为它使用了 Web 服务器的 RAM),并且还存在与工作进程回收相关的与 `Application` 对象读/写使用相关的全部问题。但是,对于合适的网站明智地使用,例如在中小规模的网站上跟踪用户 ID,它们性能极佳,是理想的解决方案。您不需要做任何特别的事情,只需将 `sessionState` 的 `mode` 设置为“`InProc`”。
StateServer
`StateServer` 是一个默认关闭的服务。您可以通过转到“管理”->“服务”并右键单击“ASP.NET State 服务”(转到属性并选择“自动”,如果您希望它在 Windows 启动时启动)来启用它。当您的 `sessionState` 设置为 `StateServer` 时,Session 不会存储在 ASP.NET 工作进程中,从而避免了工作进程回收问题。此外,两个或更多独立的 Web 服务器可以访问单个 `StateServer`,**这意味着 Session 状态将自动在 Web 场之间共享**。
在使用 `StateServer` 时,有两件非常重要的事情需要牢记。首先,它不像将数据直接存储在 ASP.NET 工作进程中那样快。这可以通过智能地使用 `HttpCache` 类来轻松解决。其次,存储在 `StateServer` 中的数据必须是可序列化的。`String`、`int` 和大多数内置类型通常都是自动可序列化的。自定义类通常可以通过 `System.SerializableAttribute` 属性进行标记。
[Serializable()]
public class User {
private int userId;
private string userName;
private UserStatus status;
public int UserId {get { return userId; } set { userId = value; }}
public string UserName {get { return userName; }set { userName = value; }}
public UserStatus Status {get { return status; }set { status = value; }}
public enum UserStatus {
Invalid = 0,
Valid = 1
}
}
要使用 `StateServer`,您必须同时指定 `SessionState` 模式以及 `StateServer` 的地址,通过 `stateConnectionString`。`StateServer` 默认运行在端口 42424 上,因此下面的示例连接到本地计算机上的状态服务器。
<sessionState mode="StateServer"
stateConnectionString="tcpip=127.0.0.1:42424" />
SQL Server
使用 SQL Server 类似于使用 `StateServer`。两者都需要数据是可序列化的,两者总体上都较慢(但不会导致整个应用程序出现性能问题),并且两者都可以被多个 Web 服务器访问。两者之间的区别在于,显然,一个使用 `StateServer` 服务,而另一个使用 SQL Server。这具有相当广泛的影响。通过将您的 Session 存储在 SQL Server 中,您可以利用多数据库、负载均衡和容错能力。您也必须为此支付高昂的费用。
启用 SQL Server 类似于 `StateServer`:将 `mode` 设置为 `SQLServer`,并通过 `sqlConnectionString` 属性指定连接。
<sessionState mode="SQLServer"
stateConnectionString="Initial Catalog=Session;server=localhsot;uid=sa;pwd=;" />
此外,您必须运行一个脚本来创建数据库。该脚本位于 `system drive\WINNT\Microsoft.NET\Framework\version\InstallSqlState.sql`,例如:我的计算机上是 `C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\InstallSqlState.sql`(因为我使用的是 XP,所以是 `Windows` 而不是 `WinNT`)。只需运行它即可。
替代方案
考虑到 Session 现在实现得多么灵活和强大,没有太多好的替代方案,但这里还是列出了一些:
Cookies
Cookies 的行为与 Session 差不多,但存储在用户的计算机上。这使得它们不适合存储大量数据或敏感信息。此外,并非所有人都启用了 Cookies(随着隐私意识的增强,这一点越来越真实)。另一方面,与 Session 不同,您可以轻松使用 Cookies 来存储跨多个访问的信息。Cookies 曾经是存储您不太关心是否保留的信息的好地方,例如用户名(嘿,如果存在就太好了,用户少输入了一个东西。如果不存在,也没关系),但大多数浏览器在这方面做得比您使用 Cookies 更好。
Querystring
Querystring 曾经被认为是 Session 的一个可能替代方案。但同样,您无法在其中存储大量/复杂数据或敏感信息。此外,它是一个真正的维护噩梦。
URL 重写
我不会详细介绍 URL 重写(请查看我本地化教程,其中我简要解释了它)。我喜欢使用 URL 重写来处理简单的事情,例如本地化,因为它比使用 Querystring 所需的维护少得多,而且看起来非常专业。但与 Cookie 或 Querystring 替代方案一样,这在大多数情况下是行不通的。
结论
正如您可能已经猜到的,ASP.NET 中的 Session 比经典 ASP 中的 Session 要有用得多。主要原因是您有了一个很好的替代方案来存储它们,而不是仅仅在内存中。但随之而来的是一系列其他替代方案:跨 Web 场共享(State Server/SQL Server)、负载均衡(SQL Server)、容错(SQL Server)以及其他一些不错的功能。ASP.NET 中 Session 的另一个优点是**即使用户的浏览器不支持 Session Cookies 也能正常工作**。这是通过自动将 Session ID 放入 Querystring 来实现的,而不是使用 Session Cookie。当然,我刚才说过 QueryString 是一个糟糕的替代方案,但这会自动完成,对我们来说是免维护的!
ViewState
您可能听说了许多关于 ViewState 和 ASP.NET 的信息。它是 ASP.NET 的许多重要功能之一。幸运的是(或者不幸的是,取决于您如何看待它),我计划不详细介绍它,除了如何将其用作存储机制。无论它多么光荣,ViewState 都是一个隐藏的表单字段。您可能在经典 ASP 中经常使用隐藏表单字段来跟踪从一个页面到发布页面的值……我们使用 ViewState 的方式也一样。它的作用域,您可以想象,是针对单个用户,从一个页面到另一个页面。
有些人可能不知道 ViewState 的一点是,它并不是什么神秘的东西,您不能触碰它。您可以轻松地从中存储和检索值。
'VB
Dim pollId As Integer
If Not Page.IsPostBack Then
pollId = getPollId()
viewstate.Add("pollId", pollId)
Else
pollId = CInt(viewstate("pollId"))
End If
//C#
int pollId;
if(!Page.IsPostBack){
pollId = getPollId();
ViewState.Add("pollId", pollId);
}else{
pollId = (int)ViewState["pollId"];
}
在上面的示例中,我们调用了一个昂贵的函数 `getPollId()` **[行: 4]** 并将结果存储在 ViewState 中。当页面回发时,我们可以避免相同的调用,只需从 ViewState 中检索值。
结论
当涉及到执行与上面示例类似的操作时,使用 ViewState 是一个很好的解决方案。您总是可以简单地使用一个隐藏的表单字段,或者 `Page.RegisterHiddenField()`(它为您创建一个隐藏字段),然后使用 `Request.Form`,但我不会称之为替代方案,因为它们几乎是相同的。当然,如果您需要存储对象、大量数据或敏感信息,Session 可能是您想要的。
HttpContext
我个人认为我把最好的留到了最后。我认为 `HttpContext` 可能是 ASP.NET 数据存储对象中最不为人知的一个,虽然它不能解决您所有的问题,但在许多情况下它确实非常有用。在所有对象中,它的作用域最有限——它存在于单个请求的生命周期内(请记住 ViewState 存在于两个请求的生命周期内,即原始请求和回发请求)。`HTTPContext` 最常见的用法是在 `Application_BeginRequest` 中将数据存储到其中,并在您的页面、用户控件、服务器控件和业务逻辑中根据需要进行访问。例如,假设我构建了一个门户网站基础设施,在每个请求开始时,都会创建一个门户对象,该对象识别请求所在的门户以及门户内的哪个部分。我们将跳过所有门户代码,但您的 `Application_BeginRequest` 可能如下所示:
protected void Application_BeginRequest(Object sender, EventArgs e) {
Context.Items.Add("portal", new Portal(Request.Url));
}
然后,您可以通过以下方式在整个请求过程中访问该门户的实例:
'VB
Dim portal As Portal= CType(Context.Items("portal"), Portal)
您不应该使用 `HttpContext` 来传递 `Page` 和 `UserControl` 之间的信息,而应使用强类型控件、接口和基类。您应该使用 `HttpContext` 来存储特定于请求的信息,您希望使其在应用程序的所有层中都可用。
使用 `HttpContext` 的另一个例子是简单的性能监视器。
'VB
'Fires when the request is first made
Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
Context.Items.Add("StartTime", DateTime.Now)
End Sub
'fires at the end of the request
Sub Application_EndRequest(ByVal sender As Object, ByVal e As EventArgs)
Dim startTime As DateTime = CDate(Context.Items("StartTime"))
If DateTime.Now.Subtract(startTime).TotalSeconds > 4 Then
'Log this slow request
End If
End Sub
应用程序启动时,我们将当前时间存储在上下文中 **[行: 4]**。应用程序结束时,我们检索该值 **[行: 9]**,并将其与当前时间进行比较 **[行: 10]**,如果它大于某个阈值(在本例中为 4 秒),我们就可以记录一些可能有助于我们识别瓶颈的信息。
结论
我喜欢 `HttpContext` 的原因有两个。首先,因为它相对不为人知且使用不普遍。即使是微软的文档也没有充分说明它有多么有用。其次,因为它确实非常有用且方便。有替代方案可以使用它,`Session` 可能是最明显的。如果您不确定使用哪个,请问问自己这些信息是特定于用户的(`Session`)还是特定于请求的(`HttpContext`)。
通过强类型化来更进一步
如果说我有一个不喜欢的关于上述所有机制的地方,那就是它们返回对象。最坏的情况下,这会导致运行时错误;在最好的情况下,它也会给您带来维护问题。解决方案是强类型化对象。看下面的代码:
Dim currentUser as User = ctype(Session("currentUser"), User)
If currentUser is Nothing Then
'do something, what?
End if
那么,上面代码有什么问题?首先,我们需要将 `Session` 对象强制转换为 `CurrentUser`,我们需要指定键“`currentUser`”,并且我们需要添加错误处理。问题是**我们每次访问值时都需要这样做**。我们可能在 `Page` 中做一次,在我们的每个 `UserControl` 中做一次,并在某些业务逻辑中做一次。这不是很封装,对吧?即使将键从“`currentUser`”更改为“`User`”也需要一次潜在危险的搜索和替换。
解决方案是将此代码封装到 `Shared
` / `static
` 属性中。
Public Shared ReadOnly Property CurrentUser() As User
Get
Dim _currentUser As User = _
CType(HttpContext.Current.Session("CurrentUser"), User)
If _currentUser Is Nothing Then
_currentUser = New User
HttpContext.Current.Session.Add("CurrentUser", _currentUser)
End If
Return _currentUser
End Get
End Property
我将在下一节中解释 `HttpContext.Current` 的所有内容,目前,只假设它在那里。代码变化不大。基本上,我们添加了一些错误处理……但这段代码是封装的,看看访问该值有多干净:
Dim currentUser as User = User.CurrentUser
只有一个硬编码的“`currentUser`”键,那就是在 `User` 类中。我们不再需要强制转换,**并且我们拥有完整的 IntelliSense 支持**。
强类型化仅对于作用域广泛的对象才有意义。上面的示例是一个很好的例子。但是,如果您发现自己经常从上述某个存储对象访问值,并反复进行强制转换和错误检查,请考虑上述解决方案。
从业务层访问这些对象
在代码隐藏文件中编程时,上述所有对象都可供您使用。您可以简单地键入“`ViewState.Add("xxx", "yyy")`”,然后就可以正常工作了。但这实际上是因为它们是 `Page` 类的属性(尝试输入 `Page.V`,您会看到 IntelliSense 会列出 ViewState)。
并非您所有的代码都将用 C# 编写。您很可能会有一堆类(用户、组、门户、部分、角色……)、验证对象和实用程序类,它们不继承自 `System.web.UI.Page`。这是否意味着所有这些对象都对您来说是丢失的?完全不是。在 Web 请求内,所有这些对象都可以通过 `System.Web.HttpContext.Current` 访问。上面的示例显示了如何使用它。请记住,如果您的代码不是在 Web 请求的上下文中执行的,`HttpContext.Current` 可能会返回 `null`。
可以通过上述提到的方法或 `HttpRuntime.Cache` 来访问 `HttpCache` 对象,从而使您能够在非 Web 请求中使用 `HTTPCache` 的强大功能。
结论
希望本教程为您提供了这些存储对象的概述,以及一些帮助您选择正确对象的见解。请注意,我们只看了这些对象的数据存储功能,其中一些实际上做了更多事情(ViewState 和 `HttpContext`)。然而,您可以利用它们的数据存储功能,而无需了解更多关于它们的信息。请始终问问自己数据将如何使用。它是为所有用户(`Application`、`HttpCache`)还是特定用户(`Session`、ViewState、Context)提供的?它的生命周期长(`Application`、`HttpCache`、`Session`)还是短(ViewState、Context)?
请记住考虑替代方案,尤其是在查看 `Application` 对象时。在某些情况下,特定的替代方案可能是完成这项工作的最佳工具,即使总体而言它并不是一个好东西。让您的生活更轻松,并在合适的时候封装对这些存储对象的访问。