原文:Managing Application State

作者:Steve Smith

翻译:姚阿勇(Dr.Yao)

校对:高嵩

在 ASP.NET Core 中,有多种途径可以对应用程序的状态进行管理,取决于检索状态的时机和方式。本文简要介绍几种可选的方式,并着重介绍为 ASP.NET Core 应用程序安装并配置会话状态支持。

查看或下载示例代码

应用程序状态的可选方式

应用程序状态 指的是用于描述应用程序当前状况的任意数据。包括全局的和用户特有的数据。之前版本的ASP.NET(甚至ASP)都内建了对全局的 ApplicationState 以及其他很多种状态存储的支持。

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();

然后,将下面的代码添加到 Configureapp.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。不过,有扩展方法可以让我们在使用诸如 StringInt32 的简单类型时更加容易。

// 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[] 字节流的类型获取,然后利用 MemoryStreamBinaryFormatter 将它反序列化,如上所示。如果 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 演示了如何利用 MemoryStreamBinaryFormatter 将自定义类型对象序列化为一个 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)管理应用程序状态的更多相关文章

  1. ASP.NET Core 中文文档 第三章 原理(6)全球化与本地化

    原文:Globalization and localization 作者:Rick Anderson.Damien Bowden.Bart Calixto.Nadeem Afana 翻译:谢炀(Kil ...

  2. ASP.NET Core 中文文档 第三章 原理(1)应用程序启动

    原文:Application Startup 作者:Steve Smith 翻译:刘怡(AlexLEWIS) 校对:谢炀(kiler398).许登洋(Seay) ASP.NET Core 为你的应用程 ...

  3. ASP.NET Core 中文文档 第三章 原理(2)中间件

    原文:Middleware 作者:Steve Smith.Rick Anderson 翻译:刘怡(AlexLEWIS) 校对:许登洋(Seay) 章节: 什么是中间件 用 IApplicationBu ...

  4. ASP.NET Core 中文文档 第三章 原理(3)静态文件处理

    原文:Working with Static Files 作者:Rick Anderson 翻译:刘怡(AlexLEWIS) 校对:谢炀(kiler398).许登洋(Seay).孟帅洋(书缘) 静态文 ...

  5. ASP.NET Core 中文文档 第三章 原理(10)依赖注入

    原文:Dependency Injection 作者:Steve Smith 翻译:刘浩杨 校对:许登洋(Seay).高嵩 ASP.NET Core 的底层设计支持和使用依赖注入.ASP.NET Co ...

  6. ASP.NET Core 中文文档 第三章 原理(11)在多个环境中工作

    原文: Working with Multiple Environments 作者: Steve Smith 翻译: 刘浩杨 校对: 孟帅洋(书缘) ASP.NET Core 介绍了支持在多个环境中管 ...

  7. ASP.NET Core 中文文档 第三章 原理(17)为你的服务器选择合适版本的.NET框架

    原文:Choosing the Right .NET For You on the Server 作者:Daniel Roth 翻译:王健 校对:谢炀(Kiler).何镇汐.许登洋(Seay).孟帅洋 ...

  8. ASP.NET Core 中文文档 第三章 原理(7)配置

    原文:Configuration 作者:Steve Smith.Daniel Roth 翻译:刘怡(AlexLEWIS) 校对:孟帅洋(书缘) ASP.NET Core 支持多种配置选项.应用程序配置 ...

  9. ASP.NET Core 中文文档 第三章 原理(8)日志

    原文:Logging 作者:Steve Smith 翻译:刘怡(AlexLEWIS) 校对:何镇汐.许登洋(Seay) ASP.NET Core 内建支持日志,也允许开发人员轻松切换为他们想用的其他日 ...

随机推荐

  1. [版本控制之道] Git 常用的命令总结(欢迎收藏备用)

    坚持每天学习,坚持每天复习,技术永远学不完,自己永远要前进 总结日常开发生产中常用的Git版本控制命令 ------------------------------main-------------- ...

  2. 【NLP】蓦然回首:谈谈学习模型的评估系列文章(一)

    统计角度窥视模型概念 作者:白宁超 2016年7月18日17:18:43 摘要:写本文的初衷源于基于HMM模型序列标注的一个实验,实验完成之后,迫切想知道采用的序列标注模型的好坏,有哪些指标可以度量. ...

  3. 【微信小程序开发•系列文章六】生命周期和路由

    这篇文章理论的知识比较多一些,都是个人观点,描述有失妥当的地方希望读者指出. [微信小程序开发•系列文章一]入门 [微信小程序开发•系列文章二]视图层 [微信小程序开发•系列文章三]数据层 [微信小程 ...

  4. Android—基于GifView显示gif动态图片

    android中显示gif动态图片用到了开源框架GifView 1.拷GifView.jar到自己的项目中. 2.将自己的gif图片拷贝到drawable文件夹 3.在xml文件中设置基本属性: &l ...

  5. 简单Linux命令学习笔记

    1.查看进程 ps -ef | grep 关键字       /*关键字为服务名*/ netstat -unltp | grep 关键字        /*关键字为服务名或者是端口均可*/ 2.杀死进 ...

  6. 第10章 Shell编程(4)_流程控制

    5. 流程控制 5.1 if语句 (1)格式: 格式1 格式2 多分支if if [ 条件判断式 ];then #程序 else #程序 fi if [ 条件判断式 ] then #程序 else # ...

  7. 数据库 oracle数据库基本知识

    sqlplus登录 普通用户登录 c:\>sqlplus 请输入用户名:scott 请输入口令: sqlplus scott/ quit退出 管理员登录 sqlplus /nolog 连接数据库 ...

  8. JavaScript中undefined与null的区别

    通常情况下, 当我们试图访问某个不存在的或者没有赋值的变量时,就会得到一个undefined值.Javascript会自动将声明是没有进行初始化的变量设为undifined. 如果一个变量根本不存在会 ...

  9. Ubuntu(Linux) + mono + jexus +asp.net MVC3 部署

    感谢  张善友 的建议,我把 微信订餐  由nginx 改成 jexus,目前运行状况来说,确实稳定了很多,再次感谢. 部署步骤参考 jexus官网:http://www.jexus.org/ htt ...

  10. EQueue文件持久化消息关键点设计思路

    要持久化的关键数据有三种 消息: 队列,队列中存放的是消息索引信息,即消息在文件中的物理位置(messageOffset)和在队列中的逻辑位置(queueOffset)的映射信息: 队列消费进度,表示 ...