C# 6 與 .NET Core 1.0 高級編程 - 40 ASP.NET Core(下) Professional C# 6 and .NET Core 1.0 - 40 ASP.NET Core

 譯文,我的原創,轉載請註明出處(C# 6 與 .NET Core 1.0 高級編程 - 40 章  ASP.NET Core(下)),不對的地方歡迎指出與交流。  html

章節出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位閱讀時仔細分辨,惟望莫誤人子弟。  git

附英文版原文:Professional C# 6 and .NET Core 1.0 - 40 ASP.NET Core
github

本章節譯文分爲上下篇,上篇見:C# 6 與 .NET Core 1.0 高級編程 - 40 ASP.NET Core(上)web

---------------------------------------------數據庫

請求和應答

經過HTTP協議,客戶端向服務器發出請求。該請求經過響應回答。npm

該請求包括頭部,而且在許多狀況下,包括到服務器的主體信息。服務器根據客戶端的須要經過主體信息定義不一樣的結果。來看看能夠從客戶端讀取什麼信息。編程

要將HTML格式的輸出返回到客戶端,GetDiv方法建立一個div元素,其中包含傳遞的參數key和value(代碼文件WebSampleApp/RequestAndResponseSample.cs)的span元素:json

public static string GetDiv(string key, string value) => $"<div><span>{key}:</span><span>{value}</span></div>";

由於在如下示例中須要這些HTML div和span標籤來包圍字符串,因此建立擴展方法來覆蓋該功能(代碼文件WebSampleApp/HtmlExtensions.cs):設計模式

public static class HtmlExtensions { public static string Div(this string value) => $"<div>{value}</div>"; public static string Span(this string value) => $"<span>{value}</span>"; }

方法 GetRequestInformation 使用 HttpRequest 去對象訪問Scheme,Host,Path,QueryString,Method和Protocol屬性(代碼文件WebSampleApp/RequestAndResponseSample.cs):數組

public static string GetRequestInformation(HttpRequest request) { var sb = new StringBuilder(); sb.Append(GetDiv("scheme", request.Scheme)); sb.Append(GetDiv("host", request.Host.HasValue ? request.Host.Value : "no host")); sb.Append(GetDiv("path", request.Path)); sb.Append(GetDiv("query string", request.QueryString.HasValue ? request.QueryString.Value :"no query string")); sb.Append(GetDiv("method", request.Method)); sb.Append(GetDiv("protocol", request.Protocol)); return sb.ToString(); }

Startup類的Configure方法更改成調用GetRequestInformation方法,並經過HttpContext的Request屬性傳遞HttpRequest。 結果寫入Response對象(代碼文件WebSampleApp/Startup.cs):

app.Run(async (context) => { await context.Response.WriteAsync(RequestAndResponseSample.GetRequestInformation(context.Request)); });

從Visual Studio啓動程序將產生如下信息:

scheme:http
host:localhost:5000
path: /
query string: no query string
method: GET
protocol: HTTP/1.1

添加一個路徑到路徑值的請求結果,例如 http://localhost:5000/Index,設置以下:

scheme:http
host:localhost:5000
path: /Index
query string: no query string
method: GET
protocol: HTTP/1.1

添加查詢字符串,如 http://localhost:5000/Add?x=3&y=5, 查詢字符串訪問  QueryString,以下所示:

query string: ?x=3&y=5

下一個代碼片斷中,使用HttpRequest的Path屬性來建立輕量級自定義路由。 根據客戶端設置的路徑,調用不一樣的方法(代碼文件WebSampleApp/Startup.cs):

app.Run(async (context) => { string result = string.Empty; switch (context.Request.Path.Value.ToLower()) { case"/header": result = RequestAndResponseSample.GetHeaderInformation(context.Request); break; case"/add": result = RequestAndResponseSample.QueryString(context.Request); break; case"/content": result = RequestAndResponseSample.Content(context.Request); break; case"/encoded": result = RequestAndResponseSample.ContentEncoded(context.Request); break; case"/form": result = RequestAndResponseSample.GetForm(context.Request); break; case"/writecookie": result = RequestAndResponseSample.WriteCookie(context.Response); break; case"/readcookie": result = RequestAndResponseSample.ReadCookie(context.Request); break; case"/json": result = RequestAndResponseSample.GetJson(context.Response); break; default: result = RequestAndResponseSample.GetRequestInformation(context.Request); break; } await context.Response.WriteAsync(result); });

如下部分將實現不一樣的方法來顯示請求頭信息,查詢字符串等。

請求頭信息

來看看客戶端在HTTP頭信息中發送的信息。 爲了訪問HTTP頭信息,HttpRequest對象定義Headers屬性。 這是IHeaderDictionary類型,它包含一個頭的名稱和值的字符串數組的字典。 使用此信息,先前建立的GetDiv方法用於爲客戶端寫入div元素(代碼文件WebSampleApp/RequestAndResponseSample.cs):

public static string GetHeaderInformation(HttpRequest request) { var sb = new StringBuilder(); IHeaderDictionary headers = request.Headers; foreach (var header in request.Headers) { sb.Append(GetDiv(header.Key, string.Join(";", header.Value))); } return sb.ToString(); }

結果取決於所使用的瀏覽器。 咱們來比較一下他們中的幾個。 如下是來自Windows 10觸摸設備上的Internet Explorer 11:

Connection: Keep-Alive
Accept: text/html,application/xhtml+xml,image/jxr,*.*
Accept-Encoding: gzip, deflate
Accept-Language: en-Us,en;q=0.8,de-AT;q=0.6,de-DE;q=0.4,de;q=0.2
Host: localhost:5000
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; 
rv:11.0)
like Gecko 

Google Chrome 47.0版顯示此信息,包括來自AppleWebKit,Chrome和Safari的版本號:

Connection: keep-alive
Accept: 
text/html,application/xhtml,application/xml;q=0.9,image/webp,*.*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-Us;en;q=0.8
Host: localhost:5000
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome 47.0.2526.80 Safari/537.36

Microsoft Edge提供了此信息,包括來自AppleWebKit,Chrome,Safari和Edge的版本號:

Connection: Keep-Alive
Accept: text/html,application/xhtml+xml,image/jxr,*.*
Accept-Encoding: gzip, deflate
Accept-Language: en-Us,en;q=0.8,de-AT;q=0.6,de-DE;q=0.4,de;q=0.2
Host: localhost:5000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML,

從這個頭信息中能夠得出什麼結論?

Connection頭是HTTP 1.1協議的加強。有了這個,客戶端能夠請求保持鏈接打開。一般使用HTML,客戶端發出多個請求,例如以獲取圖像,CSS和JavaScript文件。服務器可能會知足請求,也可能會忽略該請求以防負載太高,最好是關閉鏈接。

Accept頭定義了瀏覽器接受的mime格式。該列表按優先格式排序。根據該信息,可能會決定根據客戶端的須要以不一樣的格式返回數據。 IE更適應HTML格式,而後是XHTML和JXR。 Google Chrome則是不一樣的列表,它更喜歡這些格式:HTML,XHTML,XML和WEBP。利用這些信息中的一些,還定義了量詞。用於輸出的瀏覽器在此列表的末尾都有*。*,以接受返回的全部數據。

Accept-Language頭信息顯示用戶已配置的語言。該信息能夠返回本地化信息。本地化在第28章「本地化」中討論。

注意 好久之前,服務器保留了很長的瀏覽器功能列表。這些列表用於瞭解瀏覽器可使用的功能。要識別瀏覽器,可使用來自瀏覽器的用於映射功能的代理字符串。隨着時間的推移,瀏覽器提供錯誤的信息,甚至容許用戶配置想要的瀏覽器名稱,以便獲得一些更多的功能(由於瀏覽器列表一般沒有在服務器上更新)。過去Internet Explorer(IE)一般須要與全部其餘瀏覽器不一樣的編程。 Microsoft Edge與IE很是不一樣,而且有更多與其餘供應商的瀏覽器相同的功能。這就是爲何Microsoft Edge在User-Agent字符串中顯示Mozilla,AppleWebKit,Chrome,Safari和Edge。最好不要使用此User-Agent字符串來獲取可用的功能列表。相反,請檢查須要編程的特定功能。

目前爲止,經過瀏覽器發送的頭信息是發送到很是簡單的網站的信息。一般會有更多的細節,如Cookie,身份驗證信息,以及自定義信息。要查看發送到服務器和從服務器發送的全部信息(包括標題信息),可使用瀏覽器的開發人員工具並啓動網絡會話,不只能夠看到發送到服務器的全部請求,並且還會看到頭,主體,參數,Cookie和計時信息,如圖40.11所示。 

圖40.11

查詢字符串

可使用Add方法分析查詢字符串。該方法須要 x 和 y 參數,若是這些參數是數字則相加,並以div標記返回計算結果。上一節中顯示的方法 GetRequestInformation 演示瞭如何使用 HttpRequest 對象的 QueryString 屬性訪問完整的查詢字符串。要訪問查詢字符串的部分,可使用Query屬性。如下代碼片斷使用Get方法訪問 x 和 y 的值。若是在查詢字符串中找不到相應的鍵,此方法將返回null(代碼文件WebSampleApp/RequestAndResponseSample.cs):

public static string QueryString(HttpRequest request) { var sb = new StringBuilder(); string xtext = request.Query["x"]; string ytext = request.Query["y"]; if (xtext == null &DoubleVerticalBar; ytext == null) { return"x and y must be set"; } int x, y; if (!int.TryParse(xtext, out x)) { return $"Error parsing {xtext}"; } if (!int.TryParse(ytext, out y)) { return $"Error parsing {ytext}"; } return $"{x} + {y} = {x + y}".Div(); }

從Query字符串返回的 IQueryCollection 還容許使用Keys屬性訪問全部鍵,它還提供了一個 ContainsKey 方法來檢查指定的鍵是否可用。

使用URL  http://localhost:5000/add?x=39&y=3 在瀏覽器中顯示此結果:

39 + 3 = 42

編碼

返回用戶輸入的數據可能很危險。咱們能夠用Content方法作到這一點。如下方法直接返回經過查詢數據字符串傳遞的數據(代碼文件WebSampleApp/RequestAndResponseSample.cs):

public static string Content(HttpRequest request) => request.Query["data"];

使用URL  http://localhost:5000/content?data=sample 調用此方法,只返回字符串"sample"。使用相同的方法,用戶還能夠傳遞HTML內容,如 http://localhost:5000/content?data=<h1>Heading 1</h1> 是什麼結果?圖40.12顯示了h1元素由瀏覽器解釋,文本以標題格式顯示。在某些狀況下,用戶但願容許這樣作 - 例如,當用戶(可能不是匿名用戶)正在爲網站編寫文章時。

 

圖40.12

在不檢查用戶輸入的狀況下,用戶也能夠傳遞諸如  http://localhost:5000/content?data=<script>alert(「hacker」);</script> 。可使用JavaScript警報功能彈出消息框。將用戶重定向到其餘網站也很容易。當此用戶輸入存儲在站點中時,一個用戶能夠輸入這樣的腳本,而且打開該頁面的全部其餘用戶被相應地重定向。

返回用戶輸入的數據應始終編碼。要結果有沒有編碼,可使用 HtmlEncoder 類進行HTML編碼,如如下代碼段中所示(代碼文件WebSampleApp/RequestResponseSample.cs):

public static string ContentEncoded(HttpRequest request) => HtmlEncoder.Default.Encode(request.Query["data"]);

注意 使用 HtmlEncoder 須要NuGet包 System.Text.Encodings.Web。

運行應用程序,使用  http://localhost:5000/encoded?data=<script>alert(「hacker」);</script>  傳遞具備編碼的相同JavaScript代碼,客戶端只看到JavaScript代碼在瀏覽器中,它沒有被解釋(見圖40.13)。

圖40.13

發送的編碼字符串相似於如下示例 - 字符引用小於號(<),大於號(>)和引號(「):

<script>alert("hacker");</script>

表單數據

不要用查詢字符串將數據從用戶傳遞到服務器,而是使用表單HTML元素。示例使用HTTP POST請求,而不是GET。使用POST請求時用戶數據與請求的正文一塊兒傳遞,而不是以查詢字符串方式傳遞。

使用表單數據定義有兩個請求。首先,表單經過GET請求發送到客戶端,而後用戶填寫表單並使用POST請求提交數據。經過傳遞/ form路徑調用的方法依次調用GetForm或ShowForm方法,具體取決於HTTP方法類型(代碼文件WebSampleApp/RequestResponseSample.cs):

public static string GetForm(HttpRequest request) { string result = string.Empty; switch (request.Method) { case"GET": result = GetForm(); break; case"POST": result = ShowForm(request); break; default: break; } return result; }

該表單建立 text1的輸入元素和 Submit 按鈕建立。 單擊 Submit 按鈕使用方法參數定義的HTTP方法調用表單的 action 方法:

private static string GetForm() =>
  "<form method=\"post\" action=\"form\">" +
    "<input type=\"text\" name=\"text1\" />" +
    "<input type=\"submit\" value=\"Submit\" />" +
  "</form>";

爲了讀取表單數據,HttpRequest類定義了一個Form屬性。 該屬性返回一個IFormCollection對象,其中包含發送到服務器的表單中的全部數據:

private static string ShowForm(HttpRequest request) { var sb = new StringBuilder(); if (request.HasFormContentType) { IFormCollection coll = request.Form; foreach (var key in coll.Keys) { sb.Append(GetDiv(key, HtmlEncoder.Default.Encode(coll[key]))); } return sb.ToString(); } else return"no form".Div(); }

使用/form 連接,GET請求接收到表單(參見圖40.14)。單擊提交按鈕時,表單與POST請求一塊兒發送,能夠看到表單數據的text1 內容(參見圖40.15)。 

圖40.14

 

圖40.15

Cookies

要記住多個請求之間的用戶數據,可使用Cookie。將Cookie添加到 HttpResponse 對象將HTTP頭中的cookie從服務器發送到客戶端。默認狀況下,Cookie是臨時的(不存儲在客戶端上),若是URL是來自Cookie的同一個域,則瀏覽器將其發送回服務器。能夠設置路徑來限制瀏覽器返回Cookie的時間。在這種狀況下,只有當它來自同一個域而且使用路徑/cookies時才返回Cookie。設置Expires屬性時,cookie是一個持久性cookie,所以存儲在客戶端上。超時後cookie將被移除。然而也沒法保證Cookie不被提早刪除(代碼文件WebSampleApp/RequestResponseSample.cs):

public static string WriteCookie(HttpResponse response) { response.Cookies.Append("color","red", new CookieOptions { Path ="/cookies", Expires = DateTime.Now.AddDays(1) }); return"cookie written".Div(); }

經過讀取 HttpRequest 對象能夠再次讀取cookie。 Cookie屬性包含瀏覽器返回的全部Cookie:

public static string ReadCookie(HttpRequest request) { var sb = new StringBuilder(); IRequestCookieCollection cookies = request.Cookies; foreach (var key in cookies.Keys) { sb.Append(GetDiv(key, cookies[key])); } return sb.ToString(); }

測試Cookie,也可使用瀏覽器的開發人員工具。 這些工具顯示有關發送和接收的Cookie的全部信息。

發送JSON

服務器返回超過HTML代碼,也返回許多不一樣類型的數據格式,如CSS文件,圖像和視頻。 客戶端知道它在響應頭中的MIME類型的幫助下接收什麼類型的數據。

方法 GetJson 從具備 Title,Publisher和Author 屬性的匿名對象建立JSON字符串。 要使用JSON序列化對象,須要添加NuGet包NewtonSoft.Json,並導入命名空間NewtonSoft.Json。 JSON格式的MIME類型是application/json。 這是經過HttpResponse的ContentType屬性設置的(代碼文件WebSampleApp/RequestResponseSample.cs):

public static string GetJson(HttpResponse response) { var b = new { Title ="Professional C# 6", Publisher ="Wrox Press", Author ="Christian Nagel" }; string json = JsonConvert.SerializeObject(b); response.ContentType ="application/json"; return json; }

注意 要使用JsonConvert類,須要添加NuGet包Newtonsoft.Json。

如下是返回給客戶端的數據。

{"Title":"Professional C# 6","Publisher":"Wrox Press", "Author":"Christian Nagel"}

注意 第42章「ASP.NET Web API」中介紹了發送和接收JSON。

依賴注入

依賴注入深深集成在ASP.NET Core中。此設計模式提供鬆耦合,由於服務僅用於接口。實現接口的具體類型是注入的。使用ASP.NET內置依賴注入機制,注入經過具備註入接口類型的參數的構造函數進行。

依賴注入分離服務契約和服務實現。該服務能夠在不知道具體實現的狀況下使用 - 只須要一個合同。這容許在單個位置替換全部使用服務的服務(例如日誌記錄)。

讓咱們經過建立自定義服務來更詳細地瞭解依賴注入。

定義服務

首先,聲明示例服務的合同。經過接口定義合同能夠將服務實現與其使用分離 - 例如,使用不一樣的實現進行單元測試(代碼文件WebSampleApp/Services/ISampleService.cs):

public interface ISampleService { IEnumerable<string> GetSampleStrings(); }

類DefaultSampleService實現接口ISampleService(代碼文件WebSampleApp/Services/DefaultSampleService.cs):

public class DefaultSampleService : ISampleService { private List<string> _strings = new List<string> {"one","two","three" }; public IEnumerable<string> GetSampleStrings() => _strings; }

註冊服務

使用 AddTransient 方法(這是程序集 Microsoft.Extensions.DependencyInjection.Abstractions 在命名空間Microsoft.Extensions.DependencyInjection 中定義的 IServiceCollection 的擴展方法),DefaultSampleService 類型映射到ISampleService。 使用ISampleService接口時,DefaultSampleService類型將被實例化(代碼文件WebSampleApp/Startup.cs):

public void ConfigureServices(IServiceCollection services) { services.AddTransient<ISampleService, DefaultSampleService>(); // etc.
} 

內置依賴注入服務定義了幾個生存期類型。AddTransient 方法每次注入服務時都會從新實例化服務。

使用AddSingleton方法,服務只被實例化一次。每次注入都使用相同的實例:

services.AddSingleton <ISampleService,DefaultSampleService>();

AddInstance 方法須要實例化一個服務並將實例傳遞給此方法。這樣就定義了服務的生命週期:

var sampleService = new DefaultSampleService(); services.AddInstance<ISampleService>(sampleService);

第四種服務的生存期基於當前上下文。ASP.NET MVC 當前上下文基於HTTP請求。只要調用相同請求的操做,不一樣注入使用相同的實例。使用新請求,將建立一個新實例。爲了定義基於上下文的生命週期,AddScoped 方法將服務契約映射到服務:

services.AddScoped<ISampleService>();

注入服務

服務註冊後,能夠注入它。在目錄Controllers中建立名爲HomeController的控制器類型。內置依賴注入框架會使用構造函數注入,所以定義了接收 ISampleService 接口的構造函數。方法Index接收 HttpContext 而且可使用它來讀取請求信息,並返回一個 HTTP 狀態值。在實現中,ISampleService 用於從服務獲取字符串。控制器添加一些HTML元素將字符串放入列表(代碼文件WebSampleApp/Controllers/HomeController.cs):

public class HomeController { private readonly ISampleService _service; public HomeController(ISampleService service) { _service = service; } public async Task<int> Index(HttpContext context) { var sb = new StringBuilder(); sb.Append("<ul>"); sb.Append(string.Join("", _service.GetSampleStrings().Select( s => $"<li>{s}</li>").ToArray())); sb.Append("</ul>"); await context.Response.WriteAsync(sb.ToString()); return 200; } }

注意 此示例控制器直接返回HTML代碼。 實際上最好將功能與用戶界面分開,並從不一樣的 類 - 視圖 建立HTML代碼。 這種分離最好使用一個框架:ASP.NET MVC。 這個框架在第41章中解釋。

調用控制器

要經過依賴注入來實例化控制器,HomeController 類是用 IServiceCollection 服務註冊的。 這一次不使用接口,所以只須要使用 AddTransient 方法調用具體實現服務類型(代碼文件WebSampleApp/Startup.cs):

public void ConfigureServices(IServiceCollection services) { services.AddTransient<ISampleService, DefaultSampleService>(); services.AddTransient<HomeController>(); // etc.
}

包含路由信息的 Configure 方法如今已更改以檢查 /home 路徑。 若是表達式返回 true,HomeController 經過依賴注入經過調用註冊的應用程序服務上的 GetService 方法來實例化。 IApplicationBuilder 接口定義了一個ApplicationServices 屬性,返回實現 IServiceProvider 的對象。 這裏能夠訪問已註冊的全部服務。 使用這個控制器,經過傳遞 HttpContext 來調用Index方法。 狀態代碼將寫入應答對象:

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { app.Run(async (context) => { // etc.
    if (context.Request.Path.Value.ToLower() =="/home") { HomeController controller = app.ApplicationServices.GetService<HomeController>(); int statusCode = await controller.Index(context); context.Response.StatusCode = statusCode; return; } }); // etc.
}

圖40.16顯示了運行 home 地址URL的應用程序時無序列表的輸出

 

圖40.16

路由使用映射

前面的代碼片斷中,當URL的路徑是 「/home」時,調用HomeController類。 沒有去留意查詢字符串或子文件夾。 固然,能夠經過只檢查字符串的一個子集來作到這一點。 可是,有一個更好的方法。 ASP.NET支持使用IApplicationBuilder 的擴展的子應用程序:Map方法。如下代碼片斷定義了到 /home2 路徑的映射,並運行HomeController的Invoke方法(代碼文件WebSampleApp/Startup.cs):

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { // etc.
  app.Map("/home2", homeApp => { homeApp.Run(async context => { HomeController controller = app.ApplicationServices.GetService<HomeController>(); int statusCode = await controller.Index(context); context.Response.StatusCode = statusCode; }); }); // etc.
}

不只可使用Map方法,也可使用MapWhen。 使用如下代碼段,MapWhen 管理的映射在路徑以 /configuration 開頭時適用。 剩餘的路徑寫入到剩餘的變量,能夠用於方法調用的不一樣:

PathString remaining; app.MapWhen(context => context.Request.Path.StartsWithSegments("/configuration", out remaining), configApp => { configApp.Run(async context => { // etc.
 } });

能夠訪問 HttpContext 的任何其餘信息,例如客戶端的主機信息,而不只僅使用該路徑(context.Request.Host)或已認證的用戶(context.User.Identity.IsAuthenticated)。

使用中間件

ASP.NET Core能夠輕鬆建立在調用控制器以前調用的模塊。它能夠用於添加頭信息,驗證令牌,構建緩存,建立日誌跟蹤等。一箇中間件模塊在另外一個以後被連接,直到全部鏈接的中間件類型被調用。

可使用Visual Studio項目模板中間件類建立中間件類。使用此中間件類型,能夠建立接收對下一個中間件類型的引用的構造函數。 RequestDelegate是一個委託,它接收一個HttpContext做爲參數並返回一個Task。這正是Invoke方法的簽名。在此方法中,您能夠訪問請求和響應信息。類型HeaderMiddleware向HttpContext的響應添加一個樣本頭。做爲最後一個操做,Invoke方法調用下一個中間件模塊(代碼文件WebSampleApp/Middleware/HeaderMiddleware.cs):

public class HeaderMiddleware { private readonly RequestDelegate _next; public HeaderMiddleware(RequestDelegate next) { _next = next; } public Task Invoke(HttpContext httpContext) { httpContext.Response.Headers.Add("sampleheader", new string[] {"addheadermiddleware"}); return _next(httpContext); } }

爲了方便配置中間件類型,擴展方法 UseHeaderMiddleware 擴展了接口 IApplicationBuilder,它調用方法UseMiddleware :

public static class HeaderMiddlewareExtensions { public static IApplicationBuilder UseHeaderMiddleware( this IApplicationBuilder builder) => builder.UseMiddleware<HeaderMiddleware>(); }

另外一種中間件類型是 Heading1Middleware。 這種類型相似於之前的中間件類型,它只將 heading 1 寫入響應(代碼文件WebSampleApp/Middleware/Heading1Middleware.cs):

public class Heading1Middleware { private readonly RequestDelegate _next; public Heading1Middleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext httpContext) { await httpContext.Response.WriteAsync("<h1>From Middleware</h1>"); await _next(httpContext); } } public static class Heading1MiddlewareExtensions { public static IApplicationBuilder UseHeading1Middleware( this IApplicationBuilder builder) => builder.UseMiddleware<Heading1Middleware>(); }

如今輪到Startup類和Cofigure 方法工做,配置全部中間件類型。 擴展方法已經準備好調用(代碼文件WebSampleApp/Startup.cs):

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { // etc.
 app.UseHeaderMiddleware(); app.UseHeading1Middleware(); // etc.
}

運行應用程序時,將看到返回到客戶端的標題(使用瀏覽器的開發人員工具),而且標題會顯示在每一個頁面中,不管先前建立的連接是什麼(參見圖40.17)。 

圖40.17

會話狀態

使用中間件實現的服務是會話狀態。會話狀態容許服務器臨時記住來自客戶端的數據。會話狀態自己被實現爲中間件。

會話狀態在用戶首次從服務器請求頁面時啓動。當用戶在服務器上保持打開頁面時,會話繼續保持直到超時(一般爲10分鐘)發生。爲了在用戶導航到新頁面時保持服務器上的狀態,能夠將狀態寫入會話。當達到超時時,會話數據將被移除。

爲了識別會話,第一次請求會建立有會話標識符的臨時cookie。每次請求服務器時 cookie 從客戶端返回,直到瀏覽器關閉,cookie會被刪除。會話標識符也能夠在URL字符串中發送,做爲使用Cookie的替代方法。

在服務器端,會話信息能夠存儲在內存中。Web中存儲在內存中的會話狀態不會在不一樣系統之間傳播。使用粘性會話(譯者注:sticky session ,有翻譯爲 粘滯會話) 配置,用戶始終返回到同一物理服務器,即便在其餘系統上相同的狀態無效也不要緊(除非一個服務器出現故障)。沒有粘性會話,而且還處理故障服務器,選項存在於SQL服務器數據庫的分佈式存儲器內來存儲會話狀態(譯者注:沒徹底理解這句話,僅是按字面翻譯,讀者可查看原文校驗)。在分佈式存儲器中存儲會話狀態還有助於服務器進程的回收;若是隻使用一個服務器進程,回收殺死會話狀態。

爲了在ASP.NET中使用會話狀態,須要添加NuGet包Microsoft.AspNet.Session。此包提供了 AddSession 擴展方法,能夠在 Startup 類的 ConfigureServices 方法中調用。該參數可以配置空閒超時和 cookie 選項。 cookie用於標識會話。會話還使用實現接口 IDistributedCache 的服務。一個簡單的實現是用於進程內會話狀態的緩存。方法AddCaching添加如下緩存服務(代碼文件WebSampleApp/Startup.cs):

public void ConfigureServices(IServiceCollection services) { services.AddTransient<ISampleService, DefaultSampleService>(); services.AddTransient<HomeController>(); services.AddCaching(); services.AddSession(options => options.IdleTimeout = TimeSpan.FromMinutes(10)); } 

注意 IDistributedCache 的其餘實現是在 NuGet包 Microsoft.Extensions.Caching.Redis 的 RedisCache 和 Microsoft.Extensions.Caching.SqlServer 的 SqlServerCache 。

爲了使用會話,須要經過調用UseSession擴展方法來配置會話。 須要在任何應答寫入應答以前調用此方法(例如使用UseHeaderMiddleware 和 UseHeading1Middleware完成),所以 UseSession 在其餘方法以前調用。 使用會話信息的代碼映射到 /session 路徑(代碼文件WebSampleApp/Startup.cs):

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { // etc.
 app.UseSession(); app.UseHeaderMiddleware(); app.UseHeading1Middleware(); app.Map("/session", sessionApp => { sessionApp.Run(async context => { await SessionSample.SessionAsync(context); }); }); // etc.
}

可使用Setxxx方法(如 SetString 和 SetInt32)寫入會話狀態。 這些方法是使用從HttpContext的Session屬性返回的 ISession 接口定義的。 會話數據使用Getxxx方法檢索(代碼文件WebSampleApp/SessionSample.cs):

public static class SessionSample { private const string SessionVisits = nameof(SessionVisits); private const string SessionTimeCreated = nameof(SessionTimeCreated); public static async Task SessionAsync(HttpContext context) { int visits = context.Session.GetInt32(SessionVisits) ?? 0; string timeCreated = context.Session.GetString(SessionTimeCreated) ??
      string.Empty; if (string.IsNullOrEmpty(timeCreated)) { timeCreated = DateTime.Now.ToString("t", CultureInfo.InvariantCulture); context.Session.SetString(SessionTimeCreated, timeCreated); } DateTime timeCreated2 = DateTime.Parse(timeCreated); context.Session.SetInt32(SessionVisits, ++visits); await context.Response.WriteAsync( $"Number of visits within this session: {visits}" + $"that was created at {timeCreated2:T};" + $"current time: {DateTime.Now:T}"); } }

注意 示例代碼使用固定時區來存儲會話建立時的時間。顯示給用戶的時間是使用特定的時區。使用固定時區在服務器上存儲特定時區數據是一個很好的作法。關於固定時區和如何設置時區的信息在第28章「本地化。

配置ASP.NET

Web應用程序須要存儲系統管理員能夠更改的配置信息,例如鏈接字符串。在下一章中,將建立一個須要鏈接字符串的數據驅動應用程序。

ASP.NET Core 1.0的配置再也不像之前版本的ASP.NET那樣基於XML的配置文件web.config和machine.config。舊的配置文件中程序集引用和程序集重定向與數據庫鏈接字符串和應用程序設置混合。如今再也不是這樣的格式。你已經看到了 project.json 文件來定義程序集引用。該文件沒有定義鏈接字符串和應用程序設置。應用程序設置一般存儲在appsettings.json中,配置更加靈活,能夠選擇使用多個 JSON 或 XML 文件以及環境變量進行配置。

項目模板 ASP.NET 配置文件添加默認的 ASP.NET 配置文件-appsettings.json。項目模板自動建立DefaultConnection 設置,以後添加了AppSettings(代碼文件WebSampleApp/appsettings.json):

{
  "AppSettings": {
    "SiteName":"Professional C# Sample"
  },
  "Data": {
    "DefaultConnection": {
      "ConnectionString":
  "Server= (localdb)\\MSSQLLocalDB;Database=_CHANGE_ME;Trusted_Connection=True;"
    }
  }
}

須要配置使用的配置文件。在 Startup 類的構造函數中這樣作: ConfigurationBuilder類用於從配置文件建立配置。 能夠有多個配置文件。示例代碼使用擴展方法 AddJsonFile 將 appsettings.json 添加到 ConfigurationBuilder。 配置完成後,使用Build方法讀取配置文件。 返回的 IConfigurationRoot 結果被分配給只讀屬性 Configuration,這使得之後很容易讀取配置信息(代碼文件WebSampleApp/Startup.cs):

public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .AddJsonFile("appsettings.json"); // etc.
  Configuration = builder.Build(); } public IConfigurationRoot Configuration { get; } // etc.

可使用 AddXmlFile 方法添加 XML 配置文件,AddEnvironmentVariables 方法添加環境變量,AddCommandLine 方法向配置添加命令行參數。

配置文件默認狀況下使用Web應用程序的當前目錄。 若是須要更改目錄,能夠在調用方法 AddJsonFile 以前調用SetBasePath方法。 要檢索Web應用程序的目錄,能夠在構造函數中插入 IApplicationEnvironment 接口,並使用ApplicationBasePath 屬性。

讀取配置

經過映射/configuration/appsettings,/ configuration/database和/ configuration/secret連接來讀取不一樣的配置值(代碼文件WebSampleApp/Startup.cs):

PathString remaining; app.MapWhen(context => context.Request.Path.StartsWithSegments("/configuration", out remaining), configApp => { configApp.Run(async context => { if (remaining.StartsWithSegments("/appsettings")) { await ConfigSample.AppSettings(context, Configuration); } else if (remaining.StartsWithSegments("/database")) { await ConfigSample.ReadDatabaseConnection(context, Configuration); } else if (remaining.StartsWithSegments("/secret")) { await ConfigSample.UserSecret(context, Configuration); } }); });

如今可使用 IconfigurationRoot 對象的索引器讀取配置。 可使用冒號訪問JSON樹的層次元素(代碼文件WebSampleApp/ConfigSample.cs):

public static async Task AppSettings(HttpContext context, IConfigurationRoot config) { string settings = config["AppSettings:SiteName"]; await context.Response.WriteAsync(settings.Div()); }

訪問數據庫鏈接字符串一樣相似:

public static async Task ReadDatabaseConnection(HttpContext context, IConfigurationRoot config) { string connectionString = config["Data:DefaultConnection:ConnectionString"]; await context.Response.WriteAsync(connectionString.Div()); }

運行Web應用程序訪問相應的 /configuration URL將返回配置文件中的值。

基於環境的不一樣配置

當使用不一樣的環境運行Web應用程序時(例如,在開發,測試和生產期間),可能還使用測試服務器,由於可能要使用不一樣的配置。而且不想將測試數據添加到生產數據庫。

ASP.NET 4爲XML文件建立了轉換,以定義不一樣配置的差別。這在 ASP.NET Core 1.0 可使用更簡單的方式完成。不一樣的配置值可使用不一樣的配置文件。

如下代碼段將使用環境名稱添加JSON配置文件,例如 appsettings.development.json 或appsettings.production.json(代碼文件WebSampleApp/Startup.cs):

var builder = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

能夠經過在項目屬性中設置環境變量或應用程序參數來配置環境,如圖40.18所示。

圖40.18

爲了以編程方式驗證託管環境,爲 IHostingEnvironment 定義擴展方法,例如 IsDevelopment,IsStaging和IsProduction。要測試任何環境名稱,能夠將驗證字符串傳遞給IsEnvironment:

if (env.IsDevelopment()) { // etc.
}

用戶機密

只要使用Windows身份驗證,在配置文件中有鏈接字符串不是大問題。使用鏈接字符串存儲用戶名和密碼時,將鏈接字符串添加到配置文件並將配置文件與源代碼存儲庫一塊兒存儲多是一個大問題。擁有公共存儲庫並使用配置存儲Amazon密鑰可能會致使很是快速地丟失數千美圓。黑客的後臺做業經過公共GitHub存儲庫梳理Amazon的密鑰以劫持賬戶和建立虛擬機用於製做比特幣。能夠閱讀http://readwrite.com/2014/04/15/amazon-web-services-hack-bitcoin-miners-github 瞭解有關此狀況的更多信息。

ASP.NET Core 1.0 對此有一些緩解措施:用戶機密。有了用戶機密,配置不會存儲在項目的配置文件中,它存儲在與賬戶相關聯的配置文件中。

隨着Visual Studio的安裝,SecretManager 已經安裝在系統上。在其餘系統上,則須要安裝NuGet包Microsoft.Extensions.SecretManager。在安裝了 SecretManager 而且使用應用程序定義了機密以後,可使用命令行工具user-secret來設置、刪除和從應用程序中列出用戶機密。機密存儲在用戶特定位置:

%AppData%\Microsoft\UserSecrets

一個簡單的方法來管理用戶機密是Visual Studio中的解決方案資源管理器。選擇 項目 節點並打開上下文菜單以選擇「管理用戶機密」。當在項目中第一次選擇此項時,它會在project.json中添加一個機密標識符(代碼文件WebSampleApp/project.json):

"userSecretsId":"aspnet5-WebSampleApp-20151215011720"

該標識符表示將在用戶特定的 UserSecrets 文件夾中找到的相同子目錄。 「管理用戶密碼」命令還會打開文件secrets.json,能夠在其中添加JSON配置信息:

{
  "secret1":  "this is a user secret"
}

僅當託管環境爲Development時才添加用戶機密(代碼文件WebSampleApp/Startup.cs):

if (env.IsDevelopment()) { builder.AddUserSecrets(); }

這樣機密不會存儲在代碼存儲庫中,黑客只有經過攻擊用戶系統才能被竊取。

總結

本章中探討了ASP.NET和Web應用程序的基礎。 看到了諸如 npm,Gulp和Bower等工具,以及它們如何集成 到Visual Studio中。 本章討論了處理客戶端的請求並響應。 看到了 ASP.NET 的依賴注入和服務的基礎,一個使用依賴注入的具體實現,如會話狀態。 還了解了如何以不一樣的方式存儲配置信息,針對不一樣環境(如開發和生產)的JSON配置以及如何存儲諸如雲服務密鑰之類的機密。

下一章展現如何使用本章中討論的基礎的 ASP.NET MVC 6 來建立Web應用程序。

相關文章
相關標籤/搜索