ASP.NET Core 中文文档 第三章 原理(13)管理应用程序状态
原文:Managing Application State
作者:Steve Smith
翻译:姚阿勇(Dr.Yao)
校对:高嵩
在 ASP.NET Core 中,有多种途径可以对应用程序的状态进行管理,取决于检索状态的时机和方式。本文简要介绍几种可选的方式,并着重介绍为 ASP.NET Core 应用程序安装并配置会话状态支持。
应用程序状态的可选方式
应用程序状态 指的是用于描述应用程序当前状况的任意数据。包括全局的和用户特有的数据。之前版本的ASP.NET(甚至ASP)都内建了对全局的 Application 和 State 以及其他很多种状态存储的支持。
Application储存和ASP.NET的Cache缓存的特性几乎一样,只是少了一些功能。在 ASP.NET Core 中,Application已经没有了;可以用Caching 的实现来代替Application的功能,从而把之前版本的 ASP.NET 应用程序升级到 ASP.NET Core 。
应用程序开发人员可以根据不同因素来选择不同的方式储存状态数据:
- 数据需要储存多久?
- 数据有多大?
- 数据的格式是什么?
- 数据是否可以序列化?
- 数据有多敏感?能不能保存在客户端?
根据这些问题的答案,可以选择不同的方式储存和管理 ASP.NET Core 应用程序状态。
HttpContext.Items
当数据仅用于一个请求之中时,用 Items 集合储存是最好的方式。数据将在每个请求结束之后被丢弃。它可以作为组件和中间件在一个请求期间的不同时间点进行互相通讯的最佳手段。
QueryString 和 Post
在查询字符串( QueryString )中添加数值、或利用 POST 发送数据,可以将一个请求的状态数据提供给另一个请求。这种技术不应该用于敏感数据,因为这需要将数据发送到客户端,然后再发送回服务器。这种方法也最好用于少量的数据。查询字符串对于持久地保留状态特别有用,可以将状态嵌入链接通过电子邮件或社交网络发出去,以备日后使用。然而,用户提交的请求是无法预期的,由于带有查询字符串的网址很容易被分享出去,所以必须小心以避免跨站请求伪装攻击( Cross-Site Request Forgery (CSRF))。(例如,即便设定了只有通过验证的用户才可以访问带有查询字符串的网址执行请求,攻击者还是可能会诱骗已经验证过的用户去访问这样的网址)。
Cookies
与状态有关的非常小量的数据可以储存在 Cookies 中。他们会随每次请求被发送,所以应该保持在最小的尺寸。理想情况下,应该只使用一个标识符,而真正的数据储存在服务器端的某处,键值与这个标识符关联。
Session
会话( Session )储存依靠一个基于 Cookie 的标识符来访问与给定浏览器(来自一个特定机器和特定浏览器的一系列访问请求)会话相关的数据。你不能假设一个会话只限定给了一个用户,因此要慎重考虑在会话中储存哪些信息。这是用来储存那种针对具体会话,但又不要求永久保持的(或者说,需要的时候可以再从持久储存中重新获取的)应用程序状态的好地方。详情请参考下文 安装和配置 Session。
Cache
缓存( Caching )提供了一种方法,用开发者自定义的键对应用程序数据进行储存和快速检索。它提供了一套基于时间和其他因素来使缓存项目过期的规则。详情请阅读 Caching 。
Configuration
配置( Configuration )可以被认为是应用程序状态储存的另外一种形式,不过通常它在程序运行的时候是只读的。详情请阅读 Configuration。
其他持久化
任何其他形式的持久化储存,无论是 Entity Framework 和数据库还是类似 Azure Table Storage 的东西,都可以被用来储存应用程序状态,不过这些都超出了 ASP.NET 直接支持的范围。
使用 HttpContext.Items
HttpContext 抽象提供了一个简单的 IDictionary<object, object> 类型的字典集合,叫作 Items。在每个请求中,这个集合从 HttpRequest 开始起就可以使用,直到请求结束后被丢弃。要存取集合,你可以直接给键控项赋值,或根据给定键查询值。
举个例子,一个简单的中间件 Middleware可以在 Items 集合中增加一些内容:
app.Use(async (context, next) =>
{
// perform some verification
context.Items["isVerified"] = true;
await next.Invoke();
});
而在之后的管道中,其他的中间件就可以访问到这些内容了:
app.Run(async (context) =>
{
await context.Response.WriteAsync("Verified request? "
+ context.Items["isVerified"]);
});
Items的键名是简单的字符串,所以如果你是在开发跨越多个应用程序工作的中间件,你可能要用一个唯一标识符作为前缀以避免键名冲突。(如:采用"MyComponent.isVerified",而非简单的"isVerified")。
安装和配置 Session
ASP.NET Core 发布了一个关于会话的程序包,里面提供了用于管理会话状态的中间件。你可以在 project.json 中加入对 Microsoft.AspNetCore.Session 的引用来安装这个程序包:
当安装好程序包后,必须在你的应用程序的 Startup 类中对 Session 进行配置。Session 是基于 IDistributedCache 构建的,因此你也必须把它配置好,否则会得到一个错误。
如果你一个
IDistributedCache的实现都没有配置,则会得到一个异常,说“在尝试激活 'Microsoft.AspNetCore.Session.DistributedSessionStore' 的时候,无法找到类型为 'Microsoft.Extensions.Caching.Distributed.IDistributedCache' 的服务。”
ASP.NET 提供了 IDistributedCache 的多种实现, in-memory 是其中之一(仅用于开发期间和测试)。要配置会话采用 in-memory ,需将 Microsoft.Extensions.Caching.Memory 依赖项加入你的 project.json 文件,然后再把以下代码添加到 ConfigureServices:
services.AddDistributedMemoryCache();
services.AddSession();
然后,将下面的代码添加到 Configure 中 app.UseMVC() 之前 ,你就可以在程序代码里使用会话了:
app.UseSession();
安装和配置好之后,你就可以从 HttpContext 引用Session了。
如果你在调用
UseSession之前尝试访问Session,则会得到一个InvalidOperationException异常,说“ Session 还没有在这个应用程序或请求中配置好。”
警告: 如果在开始向
Response响应流中写入内容之后再尝试创建一个新的Session(比如,还没有创建会话 cookie),你将会得到一个InvalidOperationException异常,说“不能在开始响应之后再建立会话。”

实现细节
Session 利用一个 cookie 来跟踪和区分不同浏览器发出的请求。默认情况下,这个 cookie 命名为 ".AspNet.Session"并使用路径 "/"。此外,在默认情况下这个 cookie 不指定域,而且对于页面的客户端脚本是不可使用的(因为 CookieHttpOnly 的默认值是 True)。
这些默认值,包括 IdleTimeout (独立于 cookie 在服务端使用),都可以在通过 SessionOptions 配置 Session 的时候覆盖重写,如下所示:
services.AddSession(options =>
{
options.CookieName = ".AdventureWorks.Session";
options.IdleTimeout = TimeSpan.FromSeconds(10);
});
IdleTimeout 在服务端用来决定在会话被抛弃之前可以闲置多久。任何来到网站的请求通过 Session 中间件(无论这中间件对 Session 是读取还是写入)都会重置会话的超时时间。
Session是 无锁 的,因此如果两个请求都尝试修改会话的内容,最后一个会成功。此外,Session被实现为一个内容连贯的会话,就是说所有的内容都是一起储存的。这就意味着,如果两个请求是在修改会话中不同的部分(不同的键),他们还是会互相造成影响。
ISession
一旦 Session 安装和配置完成,你就可以通过 HttpContext 的一个名为 Session,类型为 ISession 的属性来引用会话了。
public interface ISession
{
bool IsAvailable { get; }
string Id { get; }
IEnumerable<string> Keys { get; }
Task LoadAsync();
Task CommitAsync();
bool TryGetValue(string key, out byte[] value);
void Set(string key, byte[] value);
void Remove(string key);
void Clear();
IEnumerable<string> Keys { get; }
}
因为 Session 是建立在 IDistributedCache 之上的,所以总是需要序列化被储存的对象实例。因此,这个接口使用 byte[] 而不是直接使用 object。不过,有扩展方法可以让我们在使用诸如 String 和 Int32 的简单类型时更加容易。
// session extension usage examples
context.Session.SetInt32("key1", 123);
int? val = context.Session.GetInt32("key1");
context.Session.SetString("key2", "value");
string stringVal = context.Session.GetString("key2");
byte[] result = context.Session.Get("key3");
如果要储存更复杂的对象,你需要把对象序列化为一个 byte[] 字节流以便储存,而后在获取对象的时候,还要将它们从 byte[] 字节流进行反序列化。
使用 Session 的示例
这个示例程序演示了如何使用 Session ,包括储存和获取简单类型以及自定义对象。为了便于观察会话过期后会发生什么,示例中将会话的超时时间配置为短短的10秒:
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
});
}
当你首次访问这个网页,它会在屏幕上显示说还没有会话被建立:

这个默认的行为是由下面这些 Startup.cs 里的中间件产生的,当有尚未建立会话的请求来访的时候,这些中间件就会执行(注意高亮部分):
// 主要功能中间件
app.Run(async context =>
{
RequestEntryCollection collection = GetOrCreateEntries(context);
if (collection.TotalCount() == 0)
{
await context.Response.WriteAsync("<html><body>");
await context.Response.WriteAsync("你的会话尚未建立。<br>");
await context.Response.WriteAsync(DateTime.Now.ToString() + "<br>");
await context.Response.WriteAsync("<a href=\"/session\">建立会话</a>。<br>");
}
else
{
collection.RecordRequest(context.Request.PathBase + context.Request.Path);
SaveEntries(context, collection);
// 注意:最好始终如一地在往响应流中写入内容之前执行完所有对会话的存取。
await context.Response.WriteAsync("<html><body>");
await context.Response.WriteAsync("会话建立于: " + context.Session.GetString("StartTime") + "<br>");
foreach (var entry in collection.Entries)
{
await context.Response.WriteAsync("路径: " + entry.Path + " 被访问了 " + entry.Count + " 次。<br />");
}
await context.Response.WriteAsync("你访问本站的次数是:" + collection.TotalCount() + "<br />");
}
await context.Response.WriteAsync("<a href=\"/untracked\">访问不计入统计的页面</a>.<br>");
await context.Response.WriteAsync("</body></html>");
});
GetOrCreateEntries 是一个辅助方法,它会从 Session 获取一个 RequestEntryCollection 集合,如果没有则创建一个空的,然后将其返回。这个集合保存 RequestEntry 对象实例,用来跟踪当前会话期间,用户发出的不同请求,以及他们对每个路径发出了多少请求。
public class RequestEntry
{
public string Path { get; set; }
public int Count { get; set; }
}
public class RequestEntryCollection
{
public List<RequestEntry> Entries { get; set; } = new List<RequestEntry>();
public void RecordRequest(string requestPath)
{
var existingEntry = Entries.FirstOrDefault(e => e.Path == requestPath);
if (existingEntry != null) { existingEntry.Count++; return; }
var newEntry = new RequestEntry()
{
Path = requestPath,
Count = 1
};
Entries.Add(newEntry);
}
public int TotalCount()
{
return Entries.Sum(e => e.Count);
}
}
储存在会话中的类型必须用
[Serializable]标记为可序列化的。
获取当前的 RequestEntryCollection 实例是由辅助方法 GetOrCreateEntries 来完成的:
private RequestEntryCollection GetOrCreateEntries(HttpContext context)
{
RequestEntryCollection collection = null;
byte[] requestEntriesBytes;
context.Session.TryGetValue("RequestEntries",out requestEntriesBytes);
if (requestEntriesBytes != null && requestEntriesBytes.Length > 0)
{
string json = System.Text.Encoding.UTF8.GetString(requestEntriesBytes);
return JsonConvert.DeserializeObject<RequestEntryCollection>(json);
}
if (collection == null)
{
collection = new RequestEntryCollection();
}
return collection;
}
如果对象实体存在于 Session 中,则会以 byte[] 字节流的类型获取,然后利用 MemoryStream 和 BinaryFormatter 将它反序列化,如上所示。如果 Session 中没有这个对象,这个方法则返回一个新的 RequestEntryCollection 实例。
在浏览器中,点击"建立会话"链接发起一个对路径"/session"的访问请求,然后得到如下结果:

刷新页面会使计数增加;再刷新几次之后,回到网站的根路径,如下显示,统计了当前会话期间所发起的所有请求:

建立会话是由一个中间件通过处理 "/session" 请求来完成的。
// 建立会话
app.Map("/session", subApp =>
{
subApp.Run(async context =>
{
// 把下面这行取消注释,并且清除 cookie ,在响应开始之后再存取会话时,就会产生错误
// await context.Response.WriteAsync("some content");
RequestEntryCollection collection = GetOrCreateEntries(context);
collection.RecordRequest(context.Request.PathBase + context.Request.Path);
SaveEntries(context, collection);
if (context.Session.GetString("StartTime") == null)
{
context.Session.SetString("StartTime", DateTime.Now.ToString());
}
await context.Response.WriteAsync("<html><body>");
await context.Response.WriteAsync("统计: 你已经对本程序发起了"+ collection.TotalCount() +"次请求.<br><a href=\"/\">返回</a>");
await context.Response.WriteAsync("</body></html>");
});
});
对该路径的请求会获取或创建一个 RequestEntryCollection 集合,再把当前路径添加到集合里,最后用辅助方法 SaveEntries 把集合储存到会话中去,如下所示:
private void SaveEntries(HttpContext context, RequestEntryCollection collection)
{
string json = JsonConvert.SerializeObject(collection);
byte[] serializedResult = System.Text.Encoding.UTF8.GetBytes(json);
context.Session.Set("RequestEntries", serializedResult);
}
SaveEntries 演示了如何利用 MemoryStream 和 BinaryFormatter 将自定义类型对象序列化为一个 byte[] 字节流,以便储存到 Session 中。
这个示例中还有一段中间件的代码值得注意,就是映射 "/untracked" 路径的代码。可以在下面看看它的配置:
// 一个配置于 app.UseSession() 之前,完全不使用 session 的中间件的例子
app.Map("/untracked", subApp =>
{
subApp.Run(async context =>
{
await context.Response.WriteAsync("<html><body>");
await context.Response.WriteAsync("请求时间: " + DateTime.Now.ToString() + "<br>");
await context.Response.WriteAsync("应用程序的这个目录没有使用 Session ...<br><a href=\"/\">返回</a>");
await context.Response.WriteAsync("</body></html>");
});
});
app.UseSession();
注意这个中间件是在 app.UseSession 被调用(第13行)之前 就配置好的。因此, Session 的功能在中间件中还不能用,那么访问到这个中间件的请求将不会重置会话的 IdleTimeout 。为了证实这一点,你可以在 /untracked 页面上反复刷新10秒钟,再回到首页查看。你会发现会话已经超时了,即使你最后一次刷新到现在根本没有超过10秒钟。
ASP.NET Core 中文文档 第三章 原理(13)管理应用程序状态的更多相关文章
- ASP.NET Core 中文文档 第三章 原理(6)全球化与本地化
原文:Globalization and localization 作者:Rick Anderson.Damien Bowden.Bart Calixto.Nadeem Afana 翻译:谢炀(Kil ...
- ASP.NET Core 中文文档 第三章 原理(1)应用程序启动
原文:Application Startup 作者:Steve Smith 翻译:刘怡(AlexLEWIS) 校对:谢炀(kiler398).许登洋(Seay) ASP.NET Core 为你的应用程 ...
- ASP.NET Core 中文文档 第三章 原理(2)中间件
原文:Middleware 作者:Steve Smith.Rick Anderson 翻译:刘怡(AlexLEWIS) 校对:许登洋(Seay) 章节: 什么是中间件 用 IApplicationBu ...
- ASP.NET Core 中文文档 第三章 原理(3)静态文件处理
原文:Working with Static Files 作者:Rick Anderson 翻译:刘怡(AlexLEWIS) 校对:谢炀(kiler398).许登洋(Seay).孟帅洋(书缘) 静态文 ...
- ASP.NET Core 中文文档 第三章 原理(10)依赖注入
原文:Dependency Injection 作者:Steve Smith 翻译:刘浩杨 校对:许登洋(Seay).高嵩 ASP.NET Core 的底层设计支持和使用依赖注入.ASP.NET Co ...
- ASP.NET Core 中文文档 第三章 原理(11)在多个环境中工作
原文: Working with Multiple Environments 作者: Steve Smith 翻译: 刘浩杨 校对: 孟帅洋(书缘) ASP.NET Core 介绍了支持在多个环境中管 ...
- ASP.NET Core 中文文档 第三章 原理(17)为你的服务器选择合适版本的.NET框架
原文:Choosing the Right .NET For You on the Server 作者:Daniel Roth 翻译:王健 校对:谢炀(Kiler).何镇汐.许登洋(Seay).孟帅洋 ...
- ASP.NET Core 中文文档 第三章 原理(7)配置
原文:Configuration 作者:Steve Smith.Daniel Roth 翻译:刘怡(AlexLEWIS) 校对:孟帅洋(书缘) ASP.NET Core 支持多种配置选项.应用程序配置 ...
- ASP.NET Core 中文文档 第三章 原理(8)日志
原文:Logging 作者:Steve Smith 翻译:刘怡(AlexLEWIS) 校对:何镇汐.许登洋(Seay) ASP.NET Core 内建支持日志,也允许开发人员轻松切换为他们想用的其他日 ...
随机推荐
- 浅谈WEB页面提速(前端向)
记得面试现在这份工作的时候,一位领导语重心长地谈道——当今的世界是互联网的世界,IT企业之间的竞争是很激烈的,如果一个网页的加载和显示速度,相比别人的站点页面有那么0.1秒的提升,那也是很大的一个成就 ...
- 在docker中运行ASP.NET Core Web API应用程序(附AWS Windows Server 2016 widt Container实战案例)
环境准备 1.亚马逊EC2 Windows Server 2016 with Container 2.Visual Studio 2015 Enterprise(Profresianal要装Updat ...
- C#中如何创建PDF网格并插入图片
这篇文章我将向大家演示如何以编程的方式在PDF文档中创建一个网格,并将图片插入特定的网格中. 网上有一些类似的解决方法,在这里我选择了一个免费版的PDF组件.安装控件后,创建新项目,添加安装目录下的d ...
- IOS FMDB 获取数据库表和表中的数据
ios开发中,经常会用到数据库sqlite的知识,除了增,删,改,查之外,我们说说如何获取数据库中有多少表和表相关的内容. 前言 跟数据库使用相关的一般的增删改查的语句,这里就不做解释了.在网上有很多 ...
- VS15 preview 5打开文件夹自动生成slnx.VC.db SQLite库疑惑?求解答
用VS15 preview 5打开文件夹(详情查看博客http://www.cnblogs.com/zsy/p/5962242.html中配置),文件夹下多一个slnx.VC.db文件,如下图: 本文 ...
- Android—基于微信开放平台v3SDK,开发微信支付填坑。
接触微信支付之前听说过这是一个坑,,,心里已经有了准备...我以为我没准跳坑出不来了,没有想到我填上了,调用成功之后我感觉公司所有的同事都是漂亮的,隔着北京的大雾霾我仿佛看见了太阳~~~好了,装逼结束 ...
- 完美解决CodeSmith无法获取MySQL表及列Description说明注释的方案
问题描述: CodeSmith是现在比较实用的代码生成器,但是我们发现一个问题: 使用CodeSmith编写MySQL模板的时候,会发现一个问题:MySQL数据表中的列说明获取不到,也就是column ...
- mysql 写入优化
1 主从分离 从表读取,主表可以去掉索引 2 先写入到文件或redis,定时刷新到库 3 用nginx 4 分库 分表 每个库表的数据总量少了 插入会快一点 5 最大限度减少查库的次数 6 一条sql ...
- ASP.NET 5 和Entity Framework 7公告仓库
ASP.NET 5 有一个公告仓库来介绍ASP.NET 5和 Entity Framework 7的主要变更说明,这个对于日夜更新的项目来说,很多人经常会遇到问题但是不知道去哪里寻找帮助,很多同学在做 ...
- 关于领域驱动设计(DDD)中聚合设计的一些思考
关于DDD的理论知识总结,可参考这篇文章. DDD社区官网上一篇关于聚合设计的几个原则的简单讨论: 文章地址:http://dddcommunity.org/library/vernon_2011/, ...