管理应用程序状态

管理应用程序状态

原文: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秒钟。

时间: 2024-10-07 22:56:51

管理应用程序状态的相关文章

Angular vs React 最全面深入对比

如今,Angular和React这两个JavaScript框架可谓红的发紫,同时针对这两个框架的选择变成了当下最容易被问及或者被架构设计者考虑的问题,本文或许无法告诉你哪个框架更优秀,但尽量从更多的角度去比较两者,尽可能的为你在选择时提供更多的参考意见. 选择的方法 在选择之前,我们尝试带着一些问题去审视你将要选择的框架(或者是任何工具),尝试用这些问题的答案来帮助我们更加了解框架,也更加让选择变得更容易 框架本身的问题: 是否成熟?谁在背后支持呢? 具备的功能? 采用什么架构和模式? 生态系统

改善C#编程的50个建议(46-50)

-------------------------翻译 By Cryking----------------------------- -----------------------转载请注明出处,谢谢!------------------------ 46 创建完整的特定应用程序异常类 异常是一种报告错误的机制,它可以在远离错误发生的地方处理错误.所有关于错误发生的的信息必须包含在异常对象中.在错误发生的过程中,你可能想把底层的错误转化 成详细的应用程序错误,而且不丢失关于原始错误的任何信息.

WebService的使用

Web Service三种核心技术标准1.服务提供者(服务端)2.服务请求者(客户端)3.服务注册中心(服务代理者) 在构建和使用Web Service时,主要用到以下几个关键的技术和规则:1.XML:描述数据的标准方法2.SOAP:简单对象访问协议3.WSDL:Web服务描述语言4.UDDI(Universal Description, Discovery and Integration):通用描述.发现与集成,它是一种独立于 平台的,基于XML语言的用于在互联网上描述商务的协议 长项一:跨防

Apache Flink 是什么?

架构 Apache Flink 是一个框架和分布式处理引擎,用于在无边界和有边界数据流上进行有状态的计算.Flink 能在所有常见集群环境中运行,并能以内存速度和任意规模进行计算. 接下来,我们来介绍一下 Flink 架构中的重要方面. 处理无界和有界数据 任何类型的数据都可以形成一种事件流.信用卡交易.传感器测量.机器日志.网站或移动应用程序上的用户交互记录,所有这些数据都形成一种流. 数据可以被作为 无界 或者 有界 流来处理. 无界流 有定义流的开始,但没有定义流的结束.它们会无休止地产生

框架比较

三大框架比较 Angular:(大而全) Angular是一个框架,而不是一个库,他是一个完整的解决方案,功能齐全. 适合较为复杂的SPA应用,比如工具类.OA类的应用.它属于大型框架,各个模块比较完整,提供的工具和拓展也比较丰富,适合沉淀大型项目,但上手成本高.并且原生依赖于typescript. 适合有Java等面向对象语言开发经验的程序员使用. 缺点:变动较快,兼容问题,并且以后可能会抛弃typescript转为dart. React:(灵活) React是声明式,函数式的,适合对性能要求

web应用程序状态管理

一.Web应用程序状态形式1.表单隐藏字段2.cookie——把用户状态信息通过服务器发送到客户端浏览器中保存3.Session--会话跟踪,服务器为客户端创建并维护的用于存放客户状态数据的session对象4.URL地址重写.(一)cookie 1:Cookie原理: 服务器在响应请求时将一些数据以“键-值”对的形式通过响应信息保存在客户端,当浏览器再次访问相同的应用时,会将原先的Cookie通过请求信息带到服务器端. Cookie cookie = new Cookie("cookienam

应用程序状态管理①隐藏表单字段②cookie③session④URL重写

应用程序状态管理:服务器连接时无状态的,每次发送的请求对于服务器都是新的,而不知道两次是否是同一人发送的,为了解决应用程序状态,有4个常用方法:表单隐藏字段:动态生成的才有效cookie:记录在浏览器端,键值对,可见,不安全session:记录在服务器端,也是键值对,服务器维护需要大量资源,可以设定有效时限URL重写:在用户禁用cookie时,发送地址包含有会话ID Cookie是web服务器发送到客户端浏览器的简短文本信息,在第一次访问服务器后就由服务器向客户端浏览器发送,以后再访问同一个站点

JavaWeb chapeter 5 Web应用程序状态管理

1.  HTTP协议使用的是无状态连接,对容器而言,每一个请求都来自于一个新的客户. 2. html表单隐藏字段:对用户在网站上的访问进行会话跟踪.为服务器端程序提供预定义的输入.存储动态产生的页面上下文信息:不足:只支持动态页面. Cookie: Session: URL重写: 3.  cookie: Web服务器为客户端浏览器并保存的简短文本信息: 客户端浏览器向服务器发送请求:服务器发送cookie给客户端:客户端再次发送请求给服务器:客户端返回cookie服务器端. 缺点:数据保存在客户

2016.5.11(Web应用程序状态管理)

Web状态管理概述 HTTP协议使用的是无状态的连接 对容器而言,每一个请求都来自于一个新的客户 状态管理解决方案-隐藏字段 表单隐藏字段 <input type=”hidden” name=”session” value=”...”> 1:对用户在网站上的访问进行会话跟踪. 2:为服务器端程序提供预定义的输入. 3:存储动态产生的页面上下文信息. 状态管理解决方案-Cookie 把客户的状态信息保存在客户端浏览器 状态管理解决方案-Session 把客户的状态信息保存在服务器端 Sessio