ASP.NET Core 中文文檔 第三章 原理(13)管理應用程式狀態

来源:http://www.cnblogs.com/dotNETCoreSG/archive/2016/08/18/aspnetcore-3_13-managing-application-state.html
-Advertisement-
Play Games

在 ASP.NET Core 中,有多種途徑可以對應用程式狀態進行管理,取決於檢索狀態的時機和方式。本文簡要介紹幾種可選的方式,並著重介紹為 ASP.NET Core 應用程式安裝並配置會話狀態支持。 ...


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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. 下載Linux iso文件,官方下載鏈接中有Mirror,選擇一個合適的鏈接 http://mirrors.163.com/centos/7/isos/x86_64/CentOS-7-x86_64-Minimal-1511.iso 2. 燒錄成光碟,使用USBWriter寫入到U盤,或者使用虛 ...
  • 最近在看u-boot、osekOS的啟動代碼,其中涉及到lds文件,通過參考其他網友的文章,希望對lds文件有個明晰的認識,為了鞏固及加深影響,特將相關博客內容重寫一遍。 原始文章: http://linux.chinaunix.net/techdoc/beginner/2009/08/12/112 ...
  • 編程環境搭建: 因為ubuntu 12.04的內核版本已經是3.x,而目前一些講解內核驅動的書都是2.6.x。 嵌入式開發的版本一般都是基於3.14移植的,因為嵌入式是跑在開發板上的,所以開發驅動沒有問題。但是教材的例子一般都是基於PC機的2.6.x版本,雖然內核內部介面相對穩定,但是我也不太清楚。... ...
  • 源代碼如下: typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; typedef IMAGE_... ...
  • 查看文件內容 1.cat 命令 作用:查看文件內容 語法:cat 文件名 2. more 命令 作用:分頁查看文件內容 語法:more 文件名 例:more /etc/passwd 按下回車刷新一行,按下空格刷新一屏 退出:按q健 3.less 命令 作用:分頁查看文件內容 語法:less 文件名 ...
  • 1、LINQ是什麼? LINQ是Language Integrated Query的縮寫,即“語言集成查詢”的意思。LINQ的提出就是為了提供一種跨越各種數據源的統一的查詢方式,它主要包含4個組件--Linq to Objects、Linq to XML、Linq to DataSet和Linq t ...
  • 說明: 原文作者賢新 原文地址:http://www.cnblogs.com/chenxinblogs/p/4852813.html ViewData和ViewBag主要用於將數據從控制器中傳遞到視圖中去,ViewData本身就是一個字典。以KeyValue的形式存取值。ViewData的Value ...
  • 本文版權,歸博客園和作者吳雙共同所有。轉載和爬蟲請註明博客園蝸牛Redis系列文章地址 http://www.cnblogs.com/tdws/tag/NoSql/ Redis數據類型之集合(Set)。 單個集合中最多允許存儲2的三十二次方減1個元素。內部使用hash table散列表實現。 SAD ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...