1. 程式人生 > >ASP.NET Core 中文文件 第三章 原理(13)管理應用程式狀態

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

在 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管理應用程式狀態

在 ASP.NET Core 中,有多種途徑可以對應用程式的狀態進行管理,取決於檢索狀態的時機和方式。本文簡要介紹幾種可選的方式,並著重介紹為 ASP.NET Core 應用程式安裝並配置會話狀態支援。 應用程式狀態的可選方式 應用程式狀態 指的是用於描述應用程式當前狀況的任意資料。包括全域性的和使用者特

ASP.NET Core 中文 原理2中介軟體

章節: 什麼是中介軟體 中介軟體是用於組成應用程式管道來處理請求和響應的元件。管道內的每一個元件都可以選擇是否將請求交給下一個元件、並在管道中呼叫下一個元件之前和之後執行某些操作。請求委託被用來建立請求管道,請求委託處理每一個 HTTP 請求。 請求委託通過使用 IApplicationBuilder

ASP.NET Core 中文 原理6全球化與本地化

使用 ASP.NET Core 建立一個多語言版本的網站有助於你吸引到更多的使用者,ASP.NET Core 提供服務和中介軟體來支援本地化語言和文化。 國際化涉及 全球化 和 本地化。全球化是為了應用程式支援不同文化而設計的。全球化增加了對特定地理區域的語言文字的輸入、顯示和輸出的支援。 本地化是針對一個

ASP.NET Core 中文 原理11在多個環境中工作

ASP.NET Core 介紹了支援在多個環境中管理應用程式行為的改進,如開發(development),預演(staging)和生產(production)。環境變數用來指示應用程式正在執行的環境,允許應用程式適當地配置。 章節: 開發,預演,生產 ASP.NET Core 引用了一個特定的環境變數

ASP.NET Core 中文 原理4路由

路由是用來把請求對映到路由處理程式。應用程式一啟動就配置了路由,並且可以從URL中提取值用於處理請求。它還負責使用 ASP.NET 應用程式中定義的路由來生成連結。 這份文件涵蓋了初級的ASP.NET核心路由。對於 ASP.NET 核心 MVC 路由, 請檢視 Routing to Controller A

ASP.NET Core 中文 原理1應用程式啟動

ASP.NET Core 為你的應用程式提供了處理每個請求的完整控制。Startup 類是應用程式的入口(entry point),這個類可以設定配置(configuration)並且將應用程式將要使用的服務連線起來。開發人員可以在 Startup 類中配置請求管道,該管道將用於處理應用程式的所有請求。 章

ASP.NET Core 中文 原理16.NET開放Web介面OWIN

ASP.NET Core 支援 OWIN(即 Open Web Server Interface for .NET 的首字母縮寫),OWIN的目標是用於解耦Web Server和Web Application。此外, OWIN為中介軟體定義了一個標準方法用來處理單個請求以及相關聯的響應。ASP.NET Co

ASP.NET Core 中文 原理17為你的伺服器選擇合適版本的.NET框架

ASP.NET Core基於 .NET Core 專案模型,它支援構建能夠執行在 Windows、Mac和 Linux 上的跨平臺應用程式。當您構建一個 .Net Core 專案的時候,您可以選擇一種 .NET框架來構建您的應用程式,.NET Framework (CLR)、 .NET Core (Core

ASP.NET Core 中文 MVC01ASP.NET Core MVC 概覽

ASP.NET Core MVC 是使用模型-檢視-控制器(Model-View-Controller)設計模式構建網頁應用與 API 的豐富的框架。 什麼是 MVC 模式? 模型-檢視-控制器(MVC)架構模式將一個應用區分為三部分主要元件:模型、檢視、與控制器。這種模式有助實現關注分離。使用這種模式,使

ASP.NET Core 中文 MVC4.6Areas區域

Areas 是 ASP.NET MVC 用來將相關功能組織成一組單獨名稱空間(路由)和資料夾結構(檢視)的功能。使用 Areas 建立層次結構的路由,是通過新增另一個路由引數 area 到 Controller 和 action。 Areas 提供了一種把大型 ASP.NET Core MVC Web 應用

ASP.NET Core 中文 測試5.2整合測試

整合測試確保應用程式的元件組裝在一起時正常工作。 ASP.NET Core支援使用單元測試框架和可用於處理沒有網路開銷請求的內建測試的網路主機整合測試。 章節: 整合測試介紹 整合測試驗證應用程式不同的部位是否正確地組裝在一起。不像單元測試,整合測試經常涉及到應用基礎設施,如資料庫,檔案系統,網路資源

ASP.NET Core 中文 MVC4.2控制器操作的路由

ASP.NET Core MVC 使用路由 中介軟體 來匹配傳入請求的 URL 並對映到具體的操作。路由通過啟動程式碼或者特性定義。路由描述 URL 路徑應該如何匹配到操作。路由也同樣用於生成響應中返回的 URL(用於連結)。 這篇文章將解釋 MVC 和路由之間的相互作用,以及典型的 MVC 應用程式如何使

ASP.NET Core 中文 MVC4.1Controllers, Actions 和 Action Results

Action 和 action result 是開發者使用 ASP.NET MVC 構建應用程式的基礎部分。 什麼是 Controller 在 ASP.NET MVC 中, 控制器( Controller  )用於定義和聚合操作(Action)的一個集合。操作( 或操作方法 )是控制器中處理入站請求的一個方

ASP.NET Core 中文 MVC2.3格式化響應資料

ASP.NET Core MVC 內建支援對相應資料(response data)的格式化,用來修正格式或生成客戶端指定的格式。 特定格式的操作結果 某些操作結果(Action result)的型別是指定的特定格式,比如 JsonResult 或 ContentResult。Action 可以返回格式化為

ASP.NET Core 中文 MVC4.4依賴注入和控制器

ASP.NET Core MVC 控制器應通過它們的構造器明確的請求它們的依賴關係。在某些情況下,單個控制器的操作可能需要一個服務,在控制器級別上的請求可能沒有意義。在這種情況下,你也可以選擇將服務作為 action 方法的引數。 章節: 依賴注入 依賴注入(Dependency injection,

ASP.NET Core 中文 MVC4.3過濾器

ASP.NET MVC 過濾器 可在執行管道的前後特定階段執行程式碼。過濾器可以配置為全域性有效、僅對控制器有效或是僅對 Action 有效。 過濾器如何工作? 不同的過濾器型別會在執行管道的不同階段執行,因此它們各自有一套適用場景。根據你實際要解決的問題以及在請求管道中執行的位置來選擇建立不同的過濾器。

ASP.NET Core 中文 MVC3.8檢視中的依賴注入

ASP.NET Core 支援在檢視中使用 依賴注入 。這將有助於提供檢視專用的服務,比如本地化或者僅用於填充檢視元素的資料。你應該儘量保持控制器和檢視間的關注點分離(separation of concerns)。你的檢視所顯示的大部分資料應該從控制器傳入。 章節: 一個簡單的示例 你可以使用 @i

ASP.NET Core 中文 MVC3.7 區域性檢視partial

ASP.NET Core MVC 支援區域性檢視,當你需要在多個不同檢視間重用同一個頁面部件時會顯得特別有用。 什麼是區域性檢視? 區域性檢視是在其它檢視中被渲染的檢視。區域性檢視執行後生成的 HTML 結果會被渲染到呼叫方檢視或父檢視中。跟檢視檔案一樣,區域性檢視檔案也使用 .cshtml 作為副檔名。

ASP.NET Core 中文 MVC4.5測試控制器邏輯

ASP.NET MVC 應用程式的控制器應當小巧並專注於使用者介面。涉及了非 UI 事務的大控制器更難於測試和維護。 章節: 為什麼要測試控制器 控制器是所有 ASP.NET Core MVC 應用程式的核心部分。因此,你應當確保它們的行為符合應用的預期。 自動化測試可以為你提供這樣的保障並能夠在進入生

ASP.NET Core 中文 MVC3.9檢視元件

章節: 介紹檢視元件 檢視元件是 ASP.NET Core MVC 中的新特性,與區域性檢視相似,但是它們更加的強大。檢視元件不使用模型繫結,只取決於呼叫它時所提供的資料。檢視元件有以下特點: 渲染一個塊,而不是整個響應 在控制器和檢視之間同樣包含了關注點分離和可測試性帶來的好處 可以擁有引數和業務邏