C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(下)

时间:2022-03-05 19:37:49

译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 40 章  ASP.NET Core(下)),不对的地方欢迎指出与交流。

章节出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位阅读时仔细分辨,唯望莫误人子弟。

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

本章节译文分为上下篇,上篇见:C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(上)

---------------------------------------------

请求和应答

通过HTTP协议,客户端向服务器发出请求。该请求通过响应回答。

该请求包括头部,并且在许多情况下,包括到服务器的主体信息。服务器根据客户端的需要通过主体信息定义不同的结果。来看看可以从客户端读取什么信息。

要将HTML格式的输出返回到客户端,GetDiv方法创建一个div元素,其中包含传递的参数key和value(代码文件WebSampleApp/RequestAndResponseSample.cs)的span元素:

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所示。

C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(下)

图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元素由浏览器解释,文本以标题格式显示。在某些情况下,用户希望允许这样做 - 例如,当用户(可能不是匿名用户)正在为网站编写文章时。

C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(下)

图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)。

C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(下)

图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)。

C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(下)

图40.14

C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(下)

图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()
});
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 ;
}
}

注意 此示例控制器直接返回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的应用程序时无序列表的输出

C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(下)

图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)。

C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(下)

图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());
}

注意 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) ?? ;
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所示。

C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(下)

图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应用程序。