译文,我的原创,转载请注明出处(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 ∥ 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
要记住多个请求之间的用户数据,可使用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的全部信息。
服务器返回超过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章“本地化。
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应用程序。