前一章让你了解了在使用 ASP 时可以使用的各种功能和特性.NET Core 2.0 用于构建高效和更易于维护的 web 应用。 我们已经解释了一些基本概念,并且您已经看到了如何将它们应用于一个名为Tic-Tac-Toe的实际应用的多个示例。
到目前为止,你已经很好地进步了,因为你已经吸收了 ASP.NET Core 2.0 应用具有内部结构,如何正确配置它们,以及如何使用自定义行为扩展它们,这是未来构建您自己的应用的关键。
但我们不要止步于此! 在本章的最后,你将发现如何最好地实现缺失的组件,进一步发展现有的组件,并添加客户端代码,以允许你拥有一个完整运行的端到端三连字应用。
在本章中,我们将涵盖以下主题:
- 使用 JavaScript、捆绑和缩小优化客户端开发
- 使用 WebSockets 进行实时通信
- 利用会话和用户缓存管理
- 为多语言用户界面应用全球化和本地化
- 配置您的应用和服务
- 使用测井和遥测技术进行监测和监督
- 实现高级依赖注入概念
- 一次构建并在多个环境中运行
在前一章中,您使用 MVC 模式创建了一个主页和一个用户注册页面。 您实现了一个控制器(UserRegistrationController
)和一个相应的视图,用于处理用户注册请求。 然后您添加了一个服务(UserService
)和中间件(CommunicationMiddleware
),但是我们刚刚开始,所以它们还没有完成。
当与Tic-Tac-Toe应用的初始工作流相比较时,我们可以看到仍有许多东西缺失,比如整个客户端部分,真正与通信中间件一起工作,以及我们仍需要实现的多个其他特性。
让我们从客户端部分开始,看看如何应用更高级的技术。 然后,您将学习如何尽可能地优化一切。
如果您还记得,上次我们是在用户向注册表单提交数据之后停止的,注册表单被发送到UserService
。 然后,我们只显示了一条纯文本消息,如下所示:
但是,这里的处理还没有结束。 我们需要添加整个电子邮件确认过程使用客户端开发和 JavaScript,这是我们接下来要做的:
- 打开 Visual Studio 2017 并打开井字游戏项目。 在
UserRegistrationController
中添加一个名为EmailConfirmation
的新方法:
[HttpGet]
public IActionResult EmailConfirmation (string email)
{
ViewBag.Email = email;
return View();
}
- 右键单击
EmailConfirmation
方法,生成相应的视图,并更新一些有意义的信息:
@{
ViewData["Title"] = "EmailConfirmation";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>EmailConfirmation</h2>
An email has been sent to @ViewBag.Email, please confirm your
email address by clicking on the provided link.
- 转到
UserRegistrationController
并修改Index
方法,从上一步重定向到EmailConfirmation
方法,而不是返回文本消息:
[HttpPost]
public async Task<IActionResult> Index(UserModel userModel)
{
if (ModelState.IsValid)
{
await _userService.RegisterUser(userModel);
return RedirectToAction(nameof(EmailConfirmation),
new { userModel.Email });
}
else
{
return View(userModel);
}
}
- 按F5启动应用,注册一个新用户,并验证新的 EmailConfirmation 页面是否正确显示:
非常好,您已经实现了完成用户注册过程所需的第一组修改。 在接下来的部分中,我们需要检查用户是否确认了他的电子邮件地址。 让我们看看接下来怎么做:
- 在
IUser
接口中添加两个新方法GetUserByEmail
和UpdateUser
。 这些将用于处理电子邮件确认更新:
public interface IUserService
{
Task<bool> RegisterUser(UserModel userModel);
Task<UserModel> GetUserByEmail(string email);
Task UpdateUser(UserModel user);
}
- 实现新方法,使用静态的
ConcurrentBag
来保存UserModel
,并修改UserService
中的RegisterUser
方法,如下所示:
public class UserService : IUserService
{
private static ConcurrentBag<UserModel> _userStore;
static UserService()
{
_userStore = new ConcurrentBag<UserModel>();
}
public Task<bool> RegisterUser(UserModel userModel)
{
_userStore.Add(userModel);
return Task.FromResult(true);
}
public Task<UserModel> GetUserByEmail(string email)
{
return Task.FromResult(_userStore.FirstOrDefault(
u => u.Email == email));
}
public Task UpdateUser(UserModel userModel)
{
_userStore = new ConcurrentBag<UserModel>
(_userStore.Where(u => u.Email != userModel.Email))
{
userModel
};
return Task.CompletedTask;
}
}
- 添加一个名为
GameInvitationModel
的新模型。 这将用于用户注册成功后的游戏邀请:
public class GameInvitationModel
{
public Guid Id { get; set; }
public string EmailTo { get; set; }
public string InvitedBy { get; set; }
public bool IsConfirmed { get; set; }
public DateTime ConfirmationDate { get; set; }
}
- 添加一个名为
GameInvitationController
的新控制器,并更新其Index
方法来自动设置InvitedBy
属性:
public class GameInvitationController : Controller
{
private IUserService _userService;
public GameInvitationController(IUserService userService)
{
_userService = userService;
}
[HttpGet]
public async Task<IActionResult> Index(string email)
{
var gameInvitationModel = new GameInvitationModel {
InvitedBy = email };
return View(gameInvitationModel);
}
}
- 通过右键单击
Index
方法生成相应的视图,同时选择 Create 模板并使用前面的GameInvitationModel
作为 Model 类:
- 修改自动生成的视图,删除所有不必要的输入控件,只留下
EmailTo
输入控件:
@model TicTacToe.Models.GameInvitationModel
@{
ViewData["Title"] = "Index";
}
<h4>GameInvitationModel</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Index">
<input type="hidden" asp-for="Id" />
<input type="hidden" asp-for="InvitedBy" />
<div asp-validation-summary="ModelOnly"
class="text-danger"></div>
<div class="form-group">
<label asp-for="EmailTo" class="control-label"></label>
<input asp-for="EmailTo" class="form-control" />
<span asp-validation-for="EmailTo"
class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create"
class="btn btn-default" />
</div>
</form>
</div>
</div>
- 现在,更新
UserRegistrationController
中的EmailConfirmation
方法。 用户的电子邮件被确认后,必须被重定向到GameInvitationController
,正如你所看到的,我们现在将在代码中模拟有效的确认:
[HttpGet]
public async Task<IActionResult> EmailConfirmation(string email)
{
var user = await _userService.GetUserByEmail(email);
if (user?.IsEmailConfirmed == true)
return RedirectToAction("Index", "GameInvitation",
new { email = email });
ViewBag.Email = email;
user.IsEmailConfirmed = true;
user.EmailConfirmationDate = DateTime.Now;
await _userService.UpdateUser(user);
return View();
}
- 按F5启动应用,注册新用户,验证 Email 确认页面是否和之前一样。 在 Microsoft Edge 中,按F5重新加载页面,如果一切正常,你现在应该被重定向到游戏邀请页面:
太好了,又有进步了! 在游戏邀请之前,一切都很顺利,但不幸的是,仍然需要用户干预。 用户需要按F5手动刷新 Email 确认页面,直到 Email 确认完成; 只有这样,他才会被重定向到游戏邀请页面。
整个刷新过程必须在下一步中自动化并优化。 你的选择是:
- 在页面的标题部分放置一个 HTML 元刷新标记
- 使用简单的 JavaScript,它以编程方式进行刷新
- 使用 jQuery 实现XMLHttpRequest(XHR
HTML3 引入了 meta refresh 标签,可以在一定时间后自动刷新页面。 然而,这种方法是不可取的,因为它创建了一个高服务器负载,并且在 Microsoft Edge 的安全设置可能完全禁用它,一些广告拦截器将停止它的工作。 所以,如果你使用它,你不能确定它是否能正常工作。
使用简单的 JavaScript 可以很好地以编程的方式自动化页面刷新,但它主要有相同的缺陷,因此不推荐使用。
XHR 是我们真正需要的,因为它提供了我们的井字策略应用所需要的东西。 它允许:
- 更新网页而不重新加载他们
- 即使在页面加载之后,也从服务器请求和接收数据
- 发送数据到后台的服务器
现在,您将使用 XHR 来自动化和优化用户注册电子邮件确认处理的客户端实现。 这样做的步骤如下:
- 在
wwwroot
文件夹中创建名为app
的新文件夹(此文件夹将包含以下步骤中的所有客户端代码),并在此文件夹中创建名为js
的子文件夹。 - 在
wwwroot/app/js
文件夹中添加一个新的 JavaScript 文件scripts1.js
,其内容如下:
var interval;
function EmailConfirmation(email) {
interval = setInterval(() => {
CheckEmailConfirmationStatus(email);
}, 5000);
}
- 在
wwwroot/app/js
文件夹中添加一个新的 JavaScript 文件scripts2.js
,其内容如下:
function CheckEmailConfirmationStatus(email) {
$.get("/CheckEmailConfirmationStatus?email=" + email,
function (data) {
if (data === "OK") {
if (interval !== null)
clearInterval(interval);
alert("ok");
}
});
}
- 在
Views\Shared\_Layout.cshtml
文件中打开布局页面,并在关闭body
标签之前添加一个新的Development
环境元素(最好将其放在那里):
<environment include="Development">
<script src="~/app/js/scripts1.js"></script>
<script src="~/app/js/scripts2.js"></script>
</environment>
- 更新通信中间件中的
Invoke
方法,并添加一个名为ProcessEmailConfirmation
的新方法,它将模拟电子邮件确认:
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.Equals(
"/CheckEmailConfirmationStatus"))
{
await ProcessEmailConfirmation(context);
}
else
{
await _next?.Invoke(context);
}
}
private async Task ProcessEmailConfirmation(
HttpContext context)
{
var email = context.Request.Query["email"];
var user = await _userService.GetUserByEmail(email);
if (string.IsNullOrEmpty(email))
{
await context.Response.WriteAsync("BadRequest:Email is
required");
}
else if (
(await _userService.GetUserByEmail(email)).IsEmailConfirmed)
{
await context.Response.WriteAsync("OK");
}
else
{
await context.Response.WriteAsync(
"WaitingForEmailConfirmation");
user.IsEmailConfirmed = true;
user.EmailConfirmationDate = DateTime.Now;
_userService.UpdateUser(user).Wait();
}
}
- 通过在页面底部添加对 JavaScript
EmailConfirmation
函数的调用来更新EmailConfirmation
视图:
@section Scripts
{
<script>
$(document).ready(function () {
EmailConfirmation('@ViewBag.Email');
});
</script>
}
- 更新
UserRegistrationController
中的EmailConfirmation
方法。 由于通信中间件现在要模拟有效的电子邮件确认,删除以下行:
user.IsEmailConfirmed = true;
user.EmailConfirmationDate = DateTime.Now;
await _userService.UpdateUser(user);
- 按F5启动应用并注册一个新用户。 你会看到一个 JavaScript 警告框返回
WaitingForEmailConfirmation
,一段时间后,另一个提示 OK:
- 现在,您必须更新
scripts2.js
文件中的CheckEmailConfirmationStatus
方法,以便在收到确认的电子邮件时重定向。 为此,删除alert("OK");
指令,并在其位置添加以下指令:
window.location.href = "/GameInvitation?email=" + email;
- 按F5启动应用并注册一个新用户。 一切都应该自动化,你应该自动重定向到游戏邀请页面的最后:
Note that if you still see the alert box even though you have updated the project in Visual Studio, you might have to delete the cached data in your browser to have the JavaScript refreshed correctly in your browser and see the new behavior.
正如你在第四章,中看到的,ASP 的基本概念.NET Core 2.0 -第 1 部分,我们选择了经过社区验证的 Bower 作为客户端包管理器。 我们保持了bower.json
文件不变,这意味着我们恢复了四个默认包,并在 ASP 中添加了一些引用.NET Core 2.0 布局页面使用它们:
在当今现代 web 应用开发的世界中,在开发过程中最好将客户端 JavaScript 代码和 CSS 样式表分离到多个文件中。 但是,在生产环境中,拥有如此多的文件可能会在运行时导致性能和带宽问题。
这就是为什么在构建过程中,在生成最终发布包之前,必须对所有内容进行优化,这意味着 JavaScript 和 CSS 文件必须捆绑和缩小。 TypeScript 和 CoffeeScript 文件必须被编译成 JavaScript。
捆绑和缩小是两种可以用来提高 web 应用整体页面加载性能的技术。 捆绑允许将多个文件组合成一个文件,而最小化则优化了 JavaScript 和 CSS 文件的代码,以实现更小的有效负载。 它们一起工作可以减少服务器请求的数量以及总体请求的大小。
ASP.NET Core 2.0 支持不同的捆绑和精简解决方案:
- Visual Studio 扩展捆绑和 Minifier
- 狼吞虎咽地吃
- Grunt
让我们看看如何使用 Visual Studio 扩展名 Bundler&Minifier 和bundleconfig.json
文件在tic - tick - toe项目中捆绑和缩小多个 JavaScript 文件:
- 在顶部菜单中选择“工具|扩展和更新”,单击“在线”,在搜索框中输入
Bundler & Minifier
,选择 Bundler&Minifier,最后单击“下载”:
- 关闭 Visual Studio; 安装将继续进行。 接下来,点击修改:
- 重新启动 Visual Studio。 您现在要通过捆绑和缩小来优化打开连接的数量和带宽使用。 为此,向项目中添加一个名为
bundleconfig.json
的新 JSON 文件。 - 更新
bundleconfig.json
文件,将两个 JavaScript 文件捆绑为一个名为site.js
的文件,并缩小site.css
和site.js
文件:
[
{
"outputFileName": "wwwroot/css/site.min.css",
"inputFiles": [
"wwwroot/css/site.css"
]
},
{
"outputFileName": "wwwroot/js/site.js",
"inputFiles": [
"wwwroot/app/js/scripts1.js",
"wwwroot/app/js/scripts2.js"
],
"sourceMap": true,
"includeInProject": true
},
{
"outputFileName": "wwwroot/js/site.min.js",
"inputFiles": [
"wwwroot/js/site.js"
],
"minify": {
"enabled": true,
"renameLocals": true
},
"sourceMap": false
}
]
- 右键单击项目,选择 Bundler & Minifier |更新包:
- 在解决方案资源管理器中,您可以看到已经生成了两个名为
site.min.css
和site.min.js
的新文件。 - 在任务运行器资源管理器中,你可以看到你为项目配置的捆绑和缩小过程:
- 右键单击 Update all files 并选择 Run。 现在你可以更详细地看到和理解这个过程在做什么:
-
通过右键单击 Update 所有文件并选择 Bindings | after build 来安排每次构建之后执行的过程。 将生成一个名为
bundleconfig.json.bindings
的新文件,如果您删除wwwroot/js
文件夹并重新构建项目,那么这些文件将自动生成。 -
要查看新生成的文件的运行情况,转到项目设置中的 Debug 选项卡,将
ASPNETCORE_ENVIRONMENT
变量设置为Staging
,然后保存:
- 按F5启动应用,在 Microsoft Edge 中按F12打开开发人员工具,重新注册过程。 您将看到只有捆绑的和缩小的
site.min.css
和site.min.js
文件被加载,并且加载时间更快:
好了,现在我们知道了如何实现客户端,以及如何在现代 web 应用开发中受益于捆绑和缩小,让我们回到*《Tic-Tac-Toe*游戏,并进一步优化它并添加缺失的组件。
在上一节的末尾,一切都像预期的那样完全自动化地工作。 然而,仍有一些改进的空间。
实际上,客户端定期向服务器端发送请求,以查看电子邮件确认状态是否已更改。 这可能会导致大量请求查看状态是否发生了变化。
此外,一旦电子邮件被确认,服务器端不能立即通知客户端,因为它必须等待客户端请求的响应。
在本节中,您将了解 WebSockets(https://docs.microsoft.com/en-us/aspnet/core/fundamentals/websockets)的概念,以及它们将如何允许您进一步优化客户端实现。
WebSockets 支持基于 TCP 的持久双向通信通道,这对于需要运行实时通信场景(聊天、股票行情、游戏等)的应用尤其有趣。 碰巧,我们的应用是一个游戏,它是主要的应用类型之一,很大程度上受益于直接使用套接字连接。
Note that you could also consider SignalR as an alternative. At the time of writing this book, the SignalR Core version was not yet available. However, it could be available after publication, so you should look it up and use it instead if it is available. It will provide a better solution for real-time communication scenarios and encapsulate some of the functionalities missing from WebSockets you might have implemented for yourself manually.
You can look it up at https://github.com/aspnet/SignalR.
让我们通过使用 WebSockets 来优化Tic-Tac-Toe应用的客户端实现:
- 转到
Configure
方法中的一字棋Startup
类,在通信中间件和 MVC 中间件之前添加 WebSockets 中间件(记住中间件调用顺序对确保正确的行为很重要):
app.UseWebSockets();
app.UseCommunicationMiddleware();
...
- 更新通信中间件,为 WebSockets 通信添加两个新方法
SendStringAsync
和ReceiveStringAsync
:
private static Task SendStringAsync(WebSocket socket,
string data, CancellationToken ct = default(CancellationToken))
{
var buffer = Encoding.UTF8.GetBytes(data);
var segment = new ArraySegment<byte>(buffer);
return socket.SendAsync(segment, WebSocketMessageType.Text,
true, ct);
}
private static async Task<string> ReceiveStringAsync(
WebSocket socket, CancellationToken ct =
default(CancellationToken))
{
var buffer = new ArraySegment<byte>(new byte[8192]);
using (var ms = new MemoryStream())
{
WebSocketReceiveResult result;
do
{
ct.ThrowIfCancellationRequested();
result = await socket.ReceiveAsync(buffer, ct);
ms.Write(buffer.Array, buffer.Offset, result.Count);
}
while (!result.EndOfMessage);
ms.Seek(0, SeekOrigin.Begin);
if (result.MessageType != WebSocketMessageType.Text)
throw new Exception("Unexpected message");
using (var reader = new StreamReader(ms, Encoding.UTF8))
{
return await reader.ReadToEndAsync();
}
}
}
- 更新通讯中间件,添加一个名为
ProcessEmailConfirmation
的新方法,通过 WebSockets 进行邮件确认处理:
public async Task ProcessEmailConfirmation(HttpContext context,
WebSocket currentSocket, CancellationToken ct, string email)
{
UserModel user = await _userService.GetUserByEmail(email);
while (!ct.IsCancellationRequested &&
!currentSocket.CloseStatus.HasValue &&
user?.IsEmailConfirmed == false)
{
if (user.IsEmailConfirmed)
{
await SendStringAsync(currentSocket, "OK", ct);
}
else
{
user.IsEmailConfirmed = true;
user.EmailConfirmationDate = DateTime.Now;
await _userService.UpdateUser(user);
await SendStringAsync(currentSocket, "OK", ct);
}
Task.Delay(500).Wait();
user = await _userService.GetUserByEmail(email);
}
}
- 更新通信中间件中的
Invoke
方法,并从上一步中添加对 WebSockets 特定方法的调用,同时仍然保留不支持 WebSockets 的浏览器的标准实现:
public async Task Invoke(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest)
{
var webSocket =
await context.WebSockets.AcceptWebSocketAsync();
var ct = context.RequestAborted;
var json = await ReceiveStringAsync(webSocket, ct);
var command = JsonConvert.DeserializeObject<dynamic>(json);
switch (command.Operation.ToString())
{
case "CheckEmailConfirmationStatus":
{
await ProcessEmailConfirmation(context, webSocket,
ct, command.Parameters.ToString());
break;
}
}
}
else if (context.Request.Path.Equals(
"/CheckEmailConfirmationStatus"))
{
await ProcessEmailConfirmation(context);
}
else
{
await _next?.Invoke(context);
}
}
- 修改
scripts1.js
文件,添加一些特定于 websocket 的代码来打开和使用 socket:
var interval;
function EmailConfirmation(email) {
if (window.WebSocket) {
alert("Websockets are enabled");
openSocket(email, "Email");
}
else {
alert("Websockets are not enabled");
interval = setInterval(() => {
CheckEmailConfirmationStatus(email);
}, 5000);
}
}
- 修改
scripts2.js
文件,添加一些特定于 websockets 的代码,用于打开和使用 sockets,并在邮件确认后重定向到游戏邀请页面:
function CheckEmailConfirmationStatus(email) {
$.get("/CheckEmailConfirmationStatus?email=" + email,
function (data) {
if (data === "OK") {
if (interval !== null)
clearInterval(interval);
window.location.href = "/GameInvitation?email=" + email;
}
});
}
var openSocket = function (parameter, strAction) {
if (interval !== null)
clearInterval(interval);
var protocol = location.protocol === "https:" ?
"wss:" : "ws:";
var operation = "";
var wsUri = "";
if (strAction == "Email") {
wsUri = protocol + "//" + window.location.host +
"/CheckEmailConfirmationStatus";
operation = "CheckEmailConfirmationStatus";
}
var socket = new WebSocket(wsUri);
socket.onmessage = function (response) {
console.log(response);
if (strAction == "Email" && response.data == "OK") {
window.location.href = "/GameInvitation?email=" +
parameter;
}
};
socket.onopen = function () {
var json = JSON.stringify({
"Operation": operation,
"Parameters": parameter
});
socket.send(json);
};
socket.onclose = function (event) {
};
};
- 当您启动应用并继续进行用户注册时,您将获得是否支持 WebSockets 的信息。 如果是,你会被重定向到游戏邀请页面,像以前一样,但好处是更快的处理时间:
这就是我们在 ASP 下进行客户端开发和优化的过程.NET Core 2.0。 现在,您将看到如何进一步扩展和完成井字策略应用。 这些核心概念将帮助您在日常工作中构建多语言的、可用于生产的 web 应用。
作为一名 web 开发人员,您可能知道 HTTP 是一种无状态协议,这意味着默认情况下不存在会话这样的概念。 每个请求都是独立处理的,不同请求之间不保留任何值。
尽管如此,处理数据有不同的方法。 您可以使用查询字符串、提交表单数据,也可以使用 cookie 在客户机上存储数据。 然而,所有这些机制或多或少都是手动的,需要自己管理。
如果你是一个有经验的 ASP.NET 开发人员,您将熟悉会话状态和会话变量的概念。 这些变量存储在 web 服务器上,您可以在不同的用户请求有一个中心位置来存储和接收数据时访问它们。 会话状态非常适合存储特定于会话的用户数据,而不需要永久持久性。
Note that it is best practice to not store any sensitive data in session variables due to security reasons. Users might not close their browsers; thus, session cookies might not be cleared (also, some browsers keep session cookies alive).
Also, a session might not be restricted to a single user, other users might continue with the same session, which could provide security risks.
ASP.NET Core 2.0 通过使用专用的会话中间件来提供会话状态和会话变量。 基本上,有两种不同类型的会话提供程序:
- 内存中的会话提供程序(本地到单个服务器)
- 分布式会话提供程序(在多个服务器之间共享)
让我们看看如何在Tic-Tac-Toe应用中激活内存会话提供程序来存储用户界面文化和语言:
- 打开
Views\Shared\_Layout.cshtml
文件中的布局页面,在其他菜单项之后添加一个新的用户界面语言下拉菜单。 这将允许用户在英语和法语之间进行选择:
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown"
href="#">Settings<span class="caret"></span></a>
<ul class="dropdown-menu multi-level">
<li class="dropdown-submenu">
<a class="dropdown-toggle" data-toggle="dropdown"
href="#">Select your language (@ViewBag.Language)
<span class="caret"></span></a>
<ul class="dropdown-menu">
<li @(ViewBag.Language == "EN" ? "active" : "")>
<a asp-controller="Home" asp-action="SetCulture"
asp-route-culture="EN">English</a></li>
<li @(ViewBag.Language == "FR" ? "active" : "")>
<a asp-controller="Home" asp-action="SetCulture"
asp-route-culture="FR">French</a></li>
</ul>
</li>
</ul>
</li>
- 打开
HomeController
并添加一个名为SetCulture
的新方法。 这将包含在一个会话变量中存储用户区域性设置的代码:
public IActionResult SetCulture(string culture)
{
Request.HttpContext.Session.SetString("culture", culture);
return RedirectToAction("Index");
}
- 更新从区域性会话变量中检索区域性的
HomeController
方法的Index
:
public IActionResult Index()
{
var culture =
Request.HttpContext.Session.GetString("culture");
ViewBag.Language = culture;
return View();
}
- 进入
wwwroot/css/site.css
文件,为用户界面语言下拉菜单添加一些新的 CSS 类,使其看起来更现代:
.dropdown-submenu {
position: relative;
}
.dropdown-submenu > .dropdown-menu {
top: 0;
left: 100%;
margin-top: -6px;
margin-left: -1px;
-webkit-border-radius: 0 6px 6px 6px;
-moz-border-radius: 0 6px 6px;
border-radius: 0 6px 6px 6px;
}
.dropdown-submenu:hover > .dropdown-menu {
display: block;
}
.dropdown-submenu > a:after {
display: block;
content: " ";
float: right;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 5px 0 5px 5px;
border-left-color: #ccc;
margin-top: 5px;
margin-right: -10px;
}
.dropdown-submenu:hover > a:after {
border-left-color: #fff;
}
.dropdown-submenu.pull-left {
float: none;
}
.dropdown-submenu.pull-left > .dropdown-menu {
left: -100%;
margin-left: 10px;
-webkit-border-radius: 6px 0 6px 6px;
-moz-border-radius: 6px 0 6px 6px;
border-radius: 6px 0 6px 6px;
}
- 添加 ASP 的内置会话中间件.NET Core 2.0 中的
ConfigureServices
方法Startup
类:
services.AddSession(o =>
{
o.IdleTimeout = TimeSpan.FromMinutes(30);
});
- 在
Startup
类的Configure
方法中激活会话中间件,将其添加到静态文件中间件之后:
app.UseStaticFiles();
app.UseSession();
- 更新
GameInvitationController
中的Index
方法,设置 email 会话变量:
[HttpGet]
public async Task<IActionResult> Index(string email)
{
var gameInvitationModel = new GameInvitationModel {
InvitedBy = email };
HttpContext.Session.SetString("email", email);
return View(gameInvitationModel);
}
- 按F5 启动应用。 你应该看到新的用户界面语言下拉菜单,有英语和法语之间的选择:
很好,您已经了解了如何激活和使用会话状态。 然而,大多数时候你会有多个 web 服务器,而不是一个,特别是在今天的云环境中。 那么,如何在分布式缓存中从内存中存储会话状态呢?
这很简单,您只需要在 Startup 类中注册额外的服务。 这些附加服务将提供此功能。 下面是一些例子:
- 分布式内存缓存:
services.AddDistributedMemoryCache();
- 分布式 SQL Server 缓存:
services.AddDistributedSqlServerCache(o =>
{
o.ConnectionString = _configuration["DatabaseConnection"];
o.SchemaName = "dbo";
o.TableName = "sessions";
});
- 分布式复述,缓存:
services.AddDistributedRedisCache(o =>
{
o.Configuration = _configuration["CacheRedis:Connection"];
o.InstanceName = _configuration["CacheRedis:InstanceName"];
});
我们在本节中添加了一个新的用户界面语言下拉菜单,但是您还没有看到如何在您的应用中处理多种语言。 不能再浪费时间了; 让我们看看如何做到这一点,并在下一节中使用下拉菜单和会话变量来动态地更改用户界面语言。
有时您的应用取得了成功,有时甚至取得了相当大的成功,因此您希望在国际上向更广泛的受众提供它们,并在更大的规模上部署它们。 但糟糕的是,您无法轻松实现这一点,因为您从一开始就没有想到要对应用进行本地化,而现在您必须修改已经运行的应用,并冒着回归和不稳定的风险。
不要掉进这个陷阱! 从一开始就要考虑您的目标用户和未来的部署策略!
在项目开始的时候就应该考虑本地化应用,特别是在使用 ASP 的时候,这是非常容易和直接的.NET Core 2.0 框架。 它提供了用于此目的的现有服务和中间件。
为显示、输入和输出构建支持不同语言和文化的应用称为全球化,而使全球化应用适应特定的文化称为本地化。
本地化 ASP 有三种不同的方法.NET Core 2.0 web 应用:
- 字符串定位器
- 视图定位器
- 本地化数据注释
在本节中,您将了解全球化和本地化的概念,以及它们如何让您进一步优化您的网站以实现国际化。
For additional information on globalization and localization, please visit https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization.
那么,你如何开始呢? 首先,让我们看看如何让Tic-Tac-Toe应用可本地化,通过使用 String Localizer:
- 转到
Services
文件夹并添加一个名为CultureProviderResolverService
的新服务。 这将通过查看Culture
查询字符串、Culture
cookie 和Culture
会话变量(在本章前一节中创建)来检索区域性设置。 - 通过从
RequestCultureProvider
继承CultureProviderResolverService
来实现CultureProviderResolverService
,并覆盖其特定的方法:
public class CultureProviderResolverService :
RequestCultureProvider
{
private static readonly char[] _cookieSeparator = new[] {'|' };
private static readonly string _culturePrefix = "c=";
private static readonly string _uiCulturePrefix = "uic=";
public override async Task<ProviderCultureResult>
DetermineProviderCultureResult(HttpContext httpContext)
{
if (GetCultureFromQueryString(httpContext,
out string culture))
return new ProviderCultureResult(culture, culture);
else if (GetCultureFromCookie(httpContext, out culture))
return new ProviderCultureResult(culture, culture);
else if (GetCultureFromSession(httpContext, out culture))
return new ProviderCultureResult(culture, culture);
return await NullProviderCultureResult;
}
private bool GetCultureFromQueryString(
HttpContext httpContext, out string culture)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var request = httpContext.Request;
if (!request.QueryString.HasValue)
{
culture = null;
return false;
}
culture = request.Query["culture"];
return true;
}
private bool GetCultureFromCookie(HttpContext httpContext,
out string culture)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var cookie = httpContext.Request.Cookies["culture"];
if (string.IsNullOrEmpty(cookie))
{
culture = null;
return false;
}
culture = ParseCookieValue(cookie);
return !string.IsNullOrEmpty(culture);
}
public static string ParseCookieValue(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var parts = value.Split(_cookieSeparator,
StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2)
{
return null;
}
var potentialCultureName = parts[0];
var potentialUICultureName = parts[1];
if (!potentialCultureName.StartsWith(_culturePrefix) ||
!potentialUICultureName.StartsWith(_uiCulturePrefix))
{
return null;
}
var cultureName =
potentialCultureName.Substring(_culturePrefix.Length);
var uiCultureName =
potentialUICultureName.Substring(_uiCulturePrefix.Length);
if (cultureName == null && uiCultureName == null)
{
return null;
}
if (cultureName != null && uiCultureName == null)
{
uiCultureName = cultureName;
}
if (cultureName == null && uiCultureName != null)
{
cultureName = uiCultureName;
}
return cultureName;
}
private bool GetCultureFromSession(HttpContext httpContext,
out string culture)
{
culture = httpContext.Session.GetString("culture");
return !string.IsNullOrEmpty(culture);
}
}
- 在
Startup
类的ConfigureServices
方法的顶部添加本地化服务:
public void ConfigureServices(IServiceCollection services)
{
services.AddLocalization(options => options.ResourcesPath =
"Localization");
...
}
- 将本地化中间件添加到
Startup
类中的Configure
方法中,并定义所支持的区域性:
Note that the order of adding middlewares is important, as you have already seen. You have to add the Localization Middleware just before the MVC Middleware.
...
var supportedCultures =
CultureInfo.GetCultures(CultureTypes.AllCultures);
var localizationOptions = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
};
localizationOptions.RequestCultureProviders.Clear();
localizationOptions.RequestCultureProviders.Add(new
CultureProviderResolverService());
app.UseRequestLocalization(localizationOptions);
app.UseMvc(...);
Note that you can use different methods to change the culture of your applications during runtime:
**Query strings: **Provide the culture in the URI
Cookies: Store the culture in a cookie
Browser: Browser page language settings
Custom: Implement your own provider (shown in this example)
- 在解决方案资源管理器中,添加一个名为
Localization
的新文件夹(它将用于存储资源文件),创建一个名为Controllers
的子文件夹,然后在该文件夹中添加一个名为GameInvitationController.resx
的新资源文件。
Note that you can put your resource files either into subfolders (for example, Controllers
, Views
, and more) or directly name your files accordingly (for example, Controllers.GameInvitationController.resx
, Views.Home.Index.resx
, and more). However, we advise you to use the folder approach for clarity, readability, and better organization of your files.
If you have errors when using your resource files with .NET Core, right-click on each file and select Properties. Then, check in each file that the Build Action is set to Content instead of Embedded Resource. There are bugs that should have been fixed by the final release, but if they are not, you can use this handy work-around to make everything work as expected.
- 打开
GameInvitationController.resx
资源文件,添加一个新的GameInvitationConfirmationMessage
英文:
- 在同一个
Controllers
文件夹中,为法语翻译添加一个新的资源文件GameInvitationController.fr.resx
:
- 转到
GameInvitationController
,添加stringLocalizer
,并更新构造函数实现:
private IStringLocalizer<GameInvitationController>
_stringLocalizer;
private IUserService _userService;
public GameInvitationController(IUserService userService,
IStringLocalizer<GameInvitationController> stringLocalizer)
{
_userService = userService;
_stringLocalizer = stringLocalizer;
}
- 在
GameInvitationController
中添加一个新的Index
方法。 这将根据应用区域设置返回本地化消息:
[HttpPost]
public IActionResult Index(
GameInvitationModel gameInvitationModel)
{
return Content(_stringLocalizer[
"GameInvitationConfirmationMessage",
gameInvitationModel.EmailTo]);
}
- 以英语(默认的区域性)启动应用,然后注册一个新用户,直到您收到以下文本消息,这应该是英语:
- 使用用户界面语言下拉菜单将应用语言更改为法语,然后注册一个新用户,直到你收到以下文本消息,现在应该是法语:
就这样,您已经了解了如何在应用中本地化任何类型的字符串,这对于某些特定的应用用例可能很有用。 但是,在处理视图时,这不是推荐的方法。
ASP.NET Core 2.0 框架为本地化视图提供了一些强大的特性。 在下一个例子中,你将使用视图本地化方法:
- 更新
Startup
类中的ConfigureServices
方法,并将视图本地化服务添加到 MVC 服务声明中:
services.AddMvc().AddViewLocalization(
LanguageViewLocationExpanderFormat.Suffix,
options => options.ResourcesPath = "Localization");
- 修改
Views/ViewImports.cshtml
文件,添加 View Localizer 功能,使其适用于所有视图:
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
- 打开主页视图并添加一个新标题,它将进一步本地化,如下所示:
<h2>@Localizer["Title"]</h2>
- 在解决方案资源管理器中,转到
Localization
文件夹并创建一个名为Views
的子文件夹,然后在该文件夹中添加两个名为Home.Index.resx
和Home.Index.fr.resx
的新资源文件:
- 打开
Home.Index.resx
文件,为英文标题添加一个条目:
- 打开
Home.Index.fr.resx
文件,为法语标题添加一个条目:
- 启动应用,将用户界面语言设置为 English:
- 通过“用户界面e Lalanguage”下拉菜单,将应用语言修改为法语。 标题现在应该显示在法语:
您已经了解了如何轻松地本地化视图,但是如何本地化视图中使用数据注释的表单呢? 让我们更详细地看一下; 你会惊讶于 ASP.NET Core 2.0 框架必须提供在这种情况下!
我们将在下面的例子中完全本地化用户注册表单:
-
在解决方案资源管理器中,转到
Localization/Views
文件夹,添加两个新的资源文件UserRegistration.Index.resx
和UserRegistration.Index.fr.resx
。 -
打开
UserRegistration.Index.resx
文件,添加一个Title
和SubTitle
元素的英文翻译:
- 打开
UserRegistration.Index.fr.resx
文件,添加一个Title
和SubTitle
元素,并使用法语翻译:
- 更新用户注册索引视图以使用视图定位器:
@model TicTacToe.Models.UserModel
@{
ViewData["Title"] = Localizer["Title"];
}
<h2>@ViewData["Title"]</h2>
<h4>@Localizer["SubTitle"]</h4>
<hr />
<div class="row">
...
- 启动应用,通过“用户界面语言”下拉菜单设置语言为法语,然后进入“用户注册”界面。 标题应该用法语显示。 点击 Create 而不输入任何内容,看看会发生什么:
这里缺了点什么。 您已经为页面标题和用户注册页面的副标题添加了本地化,但是我们仍然缺少表单的一些本地化。 但我们遗漏了什么?
您肯定已经看到错误消息还没有本地化和翻译。 我们正在使用 Data Annotation 框架进行错误处理和表单验证,那么如何本地化 Data Annotation 验证错误消息呢? 这就是你现在将要看到的:
- 在
Startup
类的ConfigureServices
方法中将数据注释本地化服务添加到 MVC 服务声明中:
services.AddMvc().AddViewLocalization(
LanguageViewLocationExpanderFormat.Suffix, options =>
options.ResourcesPath = "Localization")
.AddDataAnnotationsLocalization();
- 转到
Localization
文件夹并创建名为Models
的子文件夹,然后添加两个名为UserModel.resx
和UserModel.fr.resx
的新资源文件。 - 用英文更新
UserModel.resx
文件:
- 用法语翻译更新
UserModel.fr.resx
文件:
- 更新
UserModel
实现,使其能够使用上面的资源文件:
public class UserModel
{
public Guid Id { get; set; }
[Display(Name = "FirstName")]
[Required(ErrorMessage = "FirstNameRequired")]
public string FirstName { get; set; }
[Display(Name = "LastName")]
[Required(ErrorMessage = "LastNameRequired")]
public string LastName { get; set; }
[Display(Name = "Email")]
[Required(ErrorMessage = "EmailRequired"),
DataType(DataType.EmailAddress)]
[EmailAddress]
public string Email { get; set; }
[Display(Name = "Password")]
[Required(ErrorMessage = "PasswordRequired"),
DataType(DataType.Password)]
public string Password { get; set; }
public bool IsEmailConfirmed { get; set; }
public System.DateTime? EmailConfirmationDate { get; set; }
public int Score { get; set; }
}
- 重新构建解决方案并启动应用。 你会看到整个用户注册页面,包括错误消息,现在完全翻译时,更改用户界面语言为法语:
您已经了解了如何使用数据注释本地化字符串、视图甚至错误消息。 为此,您使用了 ASP 的内置特性.NET Core 2.0,因为它们包含了开发多语言本地化 web 应用的一切。 下一节将介绍如何配置应用和服务。
在前面的部分中,通过向用户注册过程中添加缺失的组件,甚至本地化Tic-Tac-Toe应用的部分,您进行了进一步的改进。 但是,您总是通过在代码中以编程方式设置用户确认来模拟电子邮件确认。 在本节中,我们将修改此部分,以便真正向新注册用户发送电子邮件,并使所有内容完全可配置。
首先,你要添加一个新的电子邮件服务,它将被用来发送电子邮件给刚刚在网站上注册的用户:
- 在
Services
文件夹中,添加一个名为EmailService
的新服务,并实现一个默认的SendEmail
方法(我们稍后将更新它):
public class EmailService
{
public Task SendEmail(string emailTo, string subject,
string message)
{
return Task.CompletedTask;
}
}
- 提取
IEmailService
接口:
- 将新的 Email 服务添加到
Startup
类的ConfigureServices
方法中(我们想要一个单一的应用实例,所以将其添加为 Singleton):
services.AddSingleton<IEmailService, EmailService>();
- 更新
UserRegistrationController
以访问在上一步中创建的EmailService
:
readonly IUserService _userService;
readonly IEmailService _emailService;
public UserRegistrationController(IUserService userService,
IEmailService emailService)
{
_userService = userService;
_emailService = emailService;
}
- 更新
UserRegistrationController
中的EmailConfirmation
方法来调用EmailService
中的SendEmail
方法:
[HttpGet]
public async Task<IActionResult> EmailConfirmation(string email)
{
var user = await _userService.GetUserByEmail(email);
var urlAction = new UrlActionContext
{
Action = "ConfirmEmail",
Controller = "UserRegistration",
Values = new { email },
Protocol = Request.Scheme,
Host = Request.Host.ToString()
};
var message = $"Thank you for your registration on our web
site, please click here to confirm your email " +
$"{Url.Action(urlAction)}";
try
{
_emailService.SendEmail(email,
"Tic-Tac-Toe Email Confirmation", message).Wait();
}
catch (Exception e)
{
}
if (user?.IsEmailConfirmed == true)
return RedirectToAction("Index", "GameInvitation",
new { email = email });
ViewBag.Email = email;
return View();
}
很好,你现在有了电子邮件服务,但你的工作还没有完成。 您需要能够配置服务,以设置与环境相关的参数(SMTP 服务器名称、端口、SSL 等),然后发送电子邮件。 将来几乎所有您创建的服务都将具有某种类型的配置,这些配置应该可以在您的代码外部进行配置。
ASP.NET Core 2.0 有一个内置的配置 API。 它提供了在应用运行时从多个源读取配置数据的各种功能。 Name-value对用于配置数据持久化,可以划分为多级结构。 此外,配置数据可以自动反序列化为普通的旧 c#对象(POCO),其中只包含私有成员和属性。
支持以下配置源:
- 配置文件(JSON、XML,甚至是经典的 INI 文件)
- 环境变量
- 命令行参数
- 内存中的。net 对象
- 加密用户商店
- Azure 关键库
- 定制的供应商
For more information on the Configuration API, please visit https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration?tabs=basicconfiguration.
让我们看看如何通过使用 ASP 来快速配置电子邮件服务.NET Core 2.0 配置 API 和 JSON 配置文件:
- 向项目添加一个新的
appsettings.json
配置文件,并添加以下自定义部分。 这将用于配置电子邮件服务:
"Email": {
"MailType": "SMTP",
"MailServer": "localhost",
"MailPort": 25,
"UseSSL": false,
"UserId": "",
"Password": "",
"RemoteServerAPI": "",
"RemoteServerKey": ""
}
- 在解决方案资源管理器中,在项目的根目录下创建一个名为
Options
的新文件夹。 在这个文件夹中添加一个名为EmailServiceOptions
的新 POCO,并为前面看到的选项实现私有成员和公共属性:
public class EmailServiceOptions
{
public string MailType { get; set; }
public string MailServer { get; set; }
public string MailPort { get; set; }
public string UseSSL { get; set; }
public string UserId { get; set; }
public string Password { get; set; }
public string RemoteServerAPI { get; set; }
public string RemoteServerKey { get; set; }
public EmailServiceOptions()
{
}
public EmailServiceOptions(string mailType,
string mailServer, string mailPort, string useSSL,
string userId, string password, string remoteServerAPI,
string remoteServerKey)
{
MailType = mailType;
MailServer = mailServer;
MailPort = mailPort;
UseSSL = useSSL;
UserId = userId;
Password = password;
RemoteServerAPI = remoteServerAPI;
RemoteServerKey = remoteServerKey;
}
}
- 更新
EmailService
实现,添加EmailServiceOptions
,并向类添加一个参数化构造函数:
private EmailServiceOptions _emailServiceOptions;
public EmailService(IOptions<EmailServiceOptions>
emailServiceOptions)
{
_emailServiceOptions = emailServiceOptions.Value;
}
- 添加一个新的构造函数到
Startup
类,以允许您配置您的电子邮件服务:
public IConfiguration _configuration { get; }
public Startup(IConfiguration configuration)
{
_configuration = configuration;
}
- 更新
Startup
类的ConfigureServices
方法:
services.Configure<EmailServiceOptions>
(_configuration.GetSection("Email"));
services.AddSingleton<IEmailService, EmailService>();
- 更新
EmailService
中的SendEmail
方法。 使用电子邮件服务选项从配置文件检索设置:
public Task SendEmail(string emailTo, string subject,
string message)
{
using (var client =
new SmtpClient(_emailServiceOptions.MailServer,
int.Parse(_emailServiceOptions.MailPort)))
{
if (bool.Parse(_emailServiceOptions.UseSSL) == true)
client.EnableSsl = true;
if (!string.IsNullOrEmpty(_emailServiceOptions.UserId))
client.Credentials =
new NetworkCredential(_emailServiceOptions.UserId,
_emailServiceOptions.Password);
client.Send(new MailMessage("example@example.com",
emailTo, subject, message));
}
return Task.CompletedTask;
}
- 将一个断点放入
EmailService
构造函数中,并按F5以 Debug 模式启动应用,验证是否已从配置文件中正确检索到电子邮件服务选项值。 如果你有一个 SMTP 服务器,你也可以验证邮件是否真的被发送:
您已经看到了如何通过使用 ASP 的内置配置 API 来配置您的应用和服务.NET Core 2.0,它允许您编写更少的代码,提高效率,同时最终提供一个更优雅、更可维护的解决方案。
当你在开发你的应用时,你会使用一个著名的集成开发环境,比如 Visual Studio 2017 或 Visual Studio Code,如本书开篇章节所述。 你每天都这样做,你做的大多数事情都是反射性的,一段时间后你会自动地去做。
通过使用 Visual Studio 2017 的高级调试特性,您可以很自然地调试应用并理解运行时发生的事情。 查找变量值、查看以何种顺序调用哪些方法、了解注入了哪些实例以及捕获异常,是构建健壮并响应业务需求的应用的关键。
然后,在将应用部署到生产环境时,您突然错过了所有这些特性。 您很少会发现安装了 Visual Studio 的生产环境,但是,错误和意外行为会发生,您需要能够尽快地理解和修复它们。
这就是测井和遥测技术发挥作用的地方。 检测应用和日志记录时进入和离开的方法,以及重要的变量值或任何你认为重要的信息在运行时,你将能够去应用日志,看看发生在生产环境的问题。
在上一节中,我们添加了用于发送电子邮件的 Email Service,并使用外部配置文件对其进行了配置。 如果配置的 SMTP 服务器没有响应怎么办? 如果我们忘记将服务器设置从开发更新到生产呢? 现在,我们只会在浏览器中显示一个异常消息:
在本节中,我们将向您展示如何使用日志记录和异常处理来为这类问题提供更好、更工业化的解决方案。
ASP.NET Core 2.0 提供了内置的对以下目标日志的支持:
- Azure AppServices 乳制品
- 控制台
- Windows 事件源
- 跟踪
- 调试器输出
- 应用的见解
但是默认情况下不支持文件、数据库和日志服务。 如果要将日志发送到这些目标,则需要使用第三方日志记录器解决方案,如 log4net、Serilog、NLog、Apache、ELMAH 或 logger。
你也可以通过实现ILoggerProvider
接口轻松创建你自己的提供商,这就是你将在这里看到的:
- 添加一个新的类库(.NET Core)项目到解决方案,并将其命名为
TicTacToe.Logging
(删除自动生成的Class1.cs
文件):
- 通过 NuGet 包管理器添加 NuGet 包
Microsoft.Extensions.Logging
和Microsoft.Extensions.Logging.Configuration
:
- 在 TicTacToe Web 应用项目中添加一个项目引用,以便能够使用
TicTacToe.Logging
类库中的资产:
- 添加一个名为
LogEntry
的新类。 这将包含日志数据:
internal class LogEntry
{
public int EventId { get; internal set; }
public string Message { get; internal set; }
public string LogLevel { get; internal set; }
public DateTime CreatedTime { get; internal set; }
}
- 添加一个名为
FileLoggerHelper
的新类。 这将用于文件操作:
internal class FileLoggerHelper
{
private string fileName;
public FileLoggerHelper(string fileName)
{
this.fileName = fileName;
}
static ReaderWriterLock locker = new ReaderWriterLock();
internal void InsertLog(LogEntry logEntry)
{
var directory = System.IO.Path.GetDirectoryName(fileName);
if (!System.IO.Directory.Exists(directory))
System.IO.Directory.CreateDirectory(directory);
try
{
locker.AcquireWriterLock(int.MaxValue);
System.IO.File.AppendAllText(fileName,
$"{logEntry.CreatedTime} {logEntry.EventId}
{logEntry.LogLevel} {logEntry.Message}" +
Environment.NewLine);
}
finally
{
locker.ReleaseWriterLock();
}
}
}
- 添加一个名为
FileLogger
的新类并实现ILogger
接口:
public sealed class FileLogger : ILogger
{
private string _categoryName;
private Func<string, LogLevel, bool> _filter;
private string _fileName;
private FileLoggerHelper _helper;
public FileLogger(string categoryName, Func<string, LogLevel,
bool> filter, string fileName)
{
_categoryName = categoryName;
_filter = filter;
_fileName = fileName;
_helper = new FileLoggerHelper(fileName);
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public void Log<TState>(LogLevel logLevel, EventId eventId,
TState state, Exception exception, Func<TState, Exception,
string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
if (formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}
var message = formatter(state, exception);
if (string.IsNullOrEmpty(message))
{
return;
}
if (exception != null)
{
message += "\n" + exception.ToString();
}
var logEntry = new LogEntry
{
Message = message,
EventId = eventId.Id,
LogLevel = logLevel.ToString(),
CreatedTime = DateTime.UtcNow
};
_helper.InsertLog(logEntry);
}
public bool IsEnabled(LogLevel logLevel)
{
return (_filter == null || _filter(_categoryName, logLevel));
}
}
- 添加一个名为
FileLoggerProvider
的新类,并实现ILoggerProvider
接口。 这将在稍后注入:
public class FileLoggerProvider : ILoggerProvider
{
private readonly Func<string, LogLevel, bool> _filter;
private string _fileName;
public FileLoggerProvider(Func<string, LogLevel,
bool> filter, string fileName)
{
_filter = filter;
_fileName = fileName;
}
public ILogger CreateLogger(string categoryName)
{
return new FileLogger(categoryName, _filter, _fileName);
}
public void Dispose()
{
}
}
- 为了简化从 web 应用调用文件日志提供程序的过程,我们需要添加一个名为
FileLoggerExtensions
的静态类(以配置节、文件名和日志详细级别作为参数):
public static class FileLoggerExtensions
{
const long DefaultFileSizeLimitBytes = 1024 * 1024 * 1024;
const int DefaultRetainedFileCountLimit = 31;
public static ILoggingBuilder AddFile(this ILoggingBuilder
loggerBuilder, IConfigurationSection configuration)
{
if (loggerBuilder == null)
{
throw new ArgumentNullException(nameof(loggerBuilder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
var minimumLevel = LogLevel.Information;
var levelSection = configuration["Logging:LogLevel"];
if (!string.IsNullOrWhiteSpace(levelSection))
{
if (!Enum.TryParse(levelSection, out minimumLevel))
{
System.Diagnostics.Debug.WriteLine("The minimum level
setting `{0}` is invalid", levelSection);
minimumLevel = LogLevel.Information;
}
}
return loggerBuilder.AddFile(configuration[
"Logging:FilePath"], (category, logLevel) =>
(logLevel >= minimumLevel), minimumLevel);
}
public static ILoggingBuilder AddFile(this ILoggingBuilder
loggerBuilder, string filePath, Func<string, LogLevel,
bool> filter, LogLevel minimumLevel = LogLevel.Information)
{
if (String.IsNullOrEmpty(filePath)) throw
new ArgumentNullException(nameof(filePath));
var fileInfo = new System.IO.FileInfo(filePath);
if (!fileInfo.Directory.Exists)
fileInfo.Directory.Create();
loggerBuilder.AddProvider(new FileLoggerProvider(filter,
filePath));
return loggerBuilder;
}
public static ILoggingBuilder AddFile(this ILoggingBuilder
loggerBuilder, string filePath,
LogLevel minimumLevel = LogLevel.Information)
{
if (String.IsNullOrEmpty(filePath)) throw
new ArgumentNullException(nameof(filePath));
var fileInfo = new System.IO.FileInfo(filePath);
if (!fileInfo.Directory.Exists)
fileInfo.Directory.Create();
loggerBuilder.AddProvider(new FileLoggerProvider((category,
logLevel) => (logLevel >= minimumLevel), filePath));
return loggerBuilder;
}
}
- 在 TicTacToe Web 项目中,在
Options
文件夹中添加两个新选项LoggingProviderOption
和LoggingOptions
:
public class LoggingProviderOption
{
public string Name { get; set; }
public string Parameters { get; set; }
public int LogLevel { get; set; }
}
public class LoggingOptions
{
public LoggingProviderOption[] Providers { get; set; }
}
- 在 TicTacToe Web 项目中,添加一个名为
ConfigureLoggingExtension
的新扩展到Extensions
文件夹:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using TicTacToe.Logging;
...
public static class ConfigureLoggingExtension
{
public static ILoggingBuilder AddLoggingConfiguration(this
ILoggingBuilder loggingBuilder, IConfiguration configuration)
{
var loggingOptions = new LoggingOptions();
configuration.GetSection("Logging").Bind(loggingOptions);
foreach (var provider in loggingOptions.Providers)
{
switch (provider.Name.ToLower())
{
case "console":
{
loggingBuilder.AddConsole();
break;
}
case "file":
{
string filePath = System.IO.Path.Combine(
System.IO.Directory.GetCurrentDirectory(), "logs",
$"TicTacToe_{System.DateTime.Now.ToString(
"ddMMyyHHmm")}.log");
loggingBuilder.AddFile(filePath,
(LogLevel)provider.LogLevel);
break;
}
default:
{
break;
}
}
}
return loggingBuilder;
}
}
- 转到 TicTacToe Web 应用项目的
Program
类,更新BuildWebHost
方法,并调用之前的扩展:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
.UseStartup<Startup>()
.PreferHostingUrls(true)
.UseUrls("http://localhost:5000")
.UseApplicationInsights()
.ConfigureLogging((hostingcontext, logging) =>
{
logging.AddLoggingConfiguration(
hostingcontext.Configuration);
})
.Build();
Don't forget to add the following using statement at the beginning of the class:
using TicTacToe.Extensions;
- 在
appsettings.json
文件中添加一个名为Logging
的新部分:
"Logging": {
"Providers": [
{
"Name": "Console",
"LogLevel": "1"
},
{
"Name": "File",
"LogLevel": "2"
}
],
"MinimumLevel": 1
}
- 启动应用,并验证是否在应用文件夹中名为
logs
的文件夹中创建了一个新的日志文件:
这是第一步,简单而快速地完成。 现在,您有了一个日志文件,可以将日志写入其中。 您将看到,使用集成的日志功能从 ASP 的任何地方创建日志都是同样容易的.NET Core 2.0 应用(Controllers
,Services
等)。
让我们快速添加一些日志到井字游戏应用:
- 更新
UserRegistrationController
构造函数实现:
readonly IUserService _userService;
readonly IEmailService _emailService;
readonly ILogger<UserRegistrationController> _logger;
public UserRegistrationController(IUserService userService,
IEmailService emailService, ILogger<UserRegistrationController>
logger)
{
_userService = userService;
_emailService = emailService;
_logger = logger;
}
- 更新
UserRegistrationController
中的EmailConfirmation
方法,并在方法开始处添加日志:
_logger.LogInformation($"##Start## Email confirmation
process for {email}");
- 更新 Email Service 实现,在其构造函数中添加一个记录器,并添加一个新的
SendMail
方法:
public class EmailService : IEmailService
{
private EmailServiceOptions _emailServiceOptions;
readonly ILogger<EmailService> _logger;
public EmailService(IOptions<EmailServiceOptions>
emailServiceOptions, ILogger<EmailService> logger)
{
_emailServiceOptions = emailServiceOptions.Value;
_logger = logger;
}
public Task SendEmail(string emailTo, string subject,
string message)
{
try
{
_logger.LogInformation($"##Start sendEmail## Start
sending Email to {emailTo}");
using (var client =
new SmtpClient(_emailServiceOptions.MailServer,
int.Parse(_emailServiceOptions.MailPort)))
{
if (bool.Parse(_emailServiceOptions.UseSSL) == true)
client.EnableSsl = true;
if (!string.IsNullOrEmpty(_emailServiceOptions.UserId))
client.Credentials =
new NetworkCredential(_emailServiceOptions.UserId,
_emailServiceOptions.Password);
client.Send(new MailMessage("example@example.com",
emailTo, subject, message));
}
}
catch (Exception ex)
{
_logger.LogError($"Cannot send email {ex}");
}
return Task.CompletedTask;
}
}
- 打开生成的日志文件,分析其内容:
在前一章中,您看到了依赖注入(DI)的工作方式以及如何使用构造函数注入方法。 但是,如果您需要在运行时注入许多实例,那么这个方法可能会相当麻烦,并且会使代码的理解和维护变得复杂。
因此,您可以使用一种更先进的 DI 技术,称为方法注射。 这允许直接从代码中访问实例。
在下面的示例中,您将添加一个处理游戏邀请的新服务,并更新Tic-Tac-Toe应用,在使用方法注入的同时,可以发送电子邮件联系其他用户加入游戏:
- 在
Services
文件夹中添加一个名为GameInvitationService
的新服务,用于管理游戏邀请(添加、更新、删除等):
public class GameInvitationService
{
private static ConcurrentBag<GameInvitationModel>
_gameInvitations;
public GameInvitationService()
{
_gameInvitations = new ConcurrentBag<GameInvitationModel>();
}
public Task<GameInvitationModel> Add(GameInvitationModel
gameInvitationModel)
{
gameInvitationModel.Id = Guid.NewGuid();
_gameInvitations.Add(gameInvitationModel);
return Task.FromResult(gameInvitationModel);
}
public Task Update(GameInvitationModel gameInvitationModel)
{
_gameInvitations = new ConcurrentBag<GameInvitationModel>
(_gameInvitations.Where(x => x.Id != gameInvitationModel.Id))
{
gameInvitationModel
};
return Task.CompletedTask;
}
public Task<GameInvitationModel> Get(Guid id)
{
return Task.FromResult(_gameInvitations.FirstOrDefault(
x => x.Id == id));
}
}
- 提取
IGameInvitationService
接口:
- 在
Startup
类的ConfigureServices
方法中添加新的游戏邀请服务(我们想要一个单一的应用实例,所以将其添加为 Singleton):
services.AddSingleton<IGameInvitationService,
GameInvitationService>();
- 更新
GameInvitationController
中的Index
方法,使用RequestServices
provider 通过方法注入一个游戏邀请服务实例:
[HttpPost]
public IActionResult Index(GameInvitationModel
gameInvitationModel, [FromServices]IEmailService emailService)
{
var gameInvitationService =
Request.HttpContext.RequestServices.GetService
<IGameInvitationService>();
if (ModelState.IsValid)
{
emailService.SendEmail(gameInvitationModel.EmailTo,
_stringLocalizer["Invitation for playing a Tic-Tac-Toe game"],
_stringLocalizer[$"Hello, you have been invited to play
the Tic-Tac-Toe game by {0}. For joining the game,
please click here {1}", gameInvitationModel.InvitedBy,
Url.Action("GameInvitationConfirmation",
"GameInvitation", new { gameInvitationModel.InvitedBy,
gameInvitationModel.EmailTo }, Request.Scheme,
Request.Host.ToString())]);
var invitation =
gameInvitationService.Add(gameInvitationModel).Result;
return RedirectToAction("GameInvitationConfirmation",
new { id = invitation.Id });
}
return View(gameInvitationModel);
}
Don't forget to add the following using statement at the beginning of the class: using Microsoft.Extensions.DependencyInjection;
, otherwise the .GetService<IGameInvitationService>();
method cannot be used and you will get build errors.
- 在
GameInvitationController
中添加一个名为GameInvitationConfirmation
的新方法:
[HttpGet]
public IActionResult GameInvitationConfirmation(Guid id,
[FromServices]IGameInvitationService gameInvitationService)
{
var gameInvitation = gameInvitationService.Get(id).Result;
return View(gameInvitation);
}
- 为前面添加的
GameInvitationConfirmation
方法创建一个新视图。 这将向用户显示一个等待消息:
@model TicTacToe.Models.GameInvitationModel
@{
ViewData["Title"] = "GameInvitationConfirmation";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>@Localizer["You have invited {0} to play a Tic-Tac-Toe game
with you, please wait until the user is connected",
Model.EmailTo]</h1>
@section Scripts{
<script>
$(document).ready(function () {
GameInvitationConfirmation('@Model.Id');
});
</script>
}
- 在
scripts1.js
文件中添加一个名为GameInvitationConfirmation
的新方法。 您可以使用与现有EmailConfirmation
方法相同的基本结构:
function GameInvitationConfirmation(id) {
if (window.WebSocket) {
alert("Websockets are enabled");
openSocket(id, "GameInvitation");
}
else {
alert("Websockets are not enabled");
interval = setInterval(() => {
CheckGameInvitationConfirmationStatus(id);
}, 5000);
}
}
- 在
scripts2.js
文件中添加一个名为CheckGameInvitationConfirmationStatus
的方法。 您可以使用与现有CheckEmailConfirmationStatus
方法相同的基本结构:
function CheckGameInvitationConfirmationStatus(id) {
$.get("/GameInvitationConfirmation?id=" + id, function (data) {
if (data.result === "OK") {
if (interval !== null)
clearInterval(interval);
window.location.href = "/GameSession/Index/" + id;
}
});
}
- 更新
scripts2.js
文件中的openSocket
方法,添加具体的游戏邀请案例:
var openSocket = function (parameter, strAction) {
if (interval !== null)
clearInterval(interval);
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
var operation = "";
var wsUri = "";
if (strAction == "Email") {
wsUri = protocol + "//" + window.location.host +
"/CheckEmailConfirmationStatus";
operation = "CheckEmailConfirmationStatus";
}
else if (strAction == "GameInvitation") {
wsUri = protocol + "//" + window.location.host +
"/GameInvitationConfirmation";
operation = "CheckGameInvitationConfirmationStatus";
}
var socket = new WebSocket(wsUri);
socket.onmessage = function (response) {
console.log(response);
if (strAction == "Email" && response.data == "OK") {
window.location.href = "/GameInvitation?email=" + parameter;
}
else if (strAction == "GameInvitation") {
var data = $.parseJSON(response.data);
if (data.Result == "OK")
window.location.href = "/GameSession/Index/" + data.Id;
}
};
socket.onopen = function () {
var json = JSON.stringify({
"Operation": operation,
"Parameters": parameter
});
socket.send(json);
};
socket.onclose = function (event) {
};
};
- 在通信中间件中添加一个名为
ProcessGameInvitationConfirmation
的新方法。 这将在不支持 WebSockets 的浏览器下处理游戏邀请请求:
private async Task ProcessGameInvitationConfirmation(HttpContext
context)
{
var id = context.Request.Query["id"];
if (string.IsNullOrEmpty(id))
await context.Response.WriteAsync("BadRequest:Id is required");
var gameInvitationService =
context.RequestServices.GetService<IGameInvitationService>();
var gameInvitationModel =
await gameInvitationService.Get(Guid.Parse(id));
if (gameInvitationModel.IsConfirmed)
await context.Response.WriteAsync(
JsonConvert.SerializeObject(new
{
Result = "OK",
Email = gameInvitationModel.InvitedBy,
gameInvitationModel.EmailTo
}));
else
{
await context.Response.WriteAsync(
"WaitGameInvitationConfirmation");
}
}
Don't forget to add the following using statement at the beginning of the class:
using Microsoft.Extensions.DependencyInjection;
- 添加一个名为
ProcessGameInvitationConfirmation
的新方法,并向通信中间件添加附加参数。 这将处理游戏邀请请求,同时使用支持它的浏览器的 WebSockets:
private async Task
ProcessGameInvitationConfirmation(HttpContext context,
WebSocket webSocket, CancellationToken ct, string parameters)
{
var gameInvitationService =
context.RequestServices.GetService<IGameInvitationService>();
var id = Guid.Parse(parameters);
var gameInvitationModel = await gameInvitationService.Get(id);
while (!ct.IsCancellationRequested &&
!webSocket.CloseStatus.HasValue &&
gameInvitationModel?.IsConfirmed == false)
{
await SendStringAsync(webSocket,
JsonConvert.SerializeObject(new
{
Result = "OK",
Email = gameInvitationModel.InvitedBy,
gameInvitationModel.EmailTo,
gameInvitationModel.Id
}), ct);
Task.Delay(500).Wait();
gameInvitationModel = await gameInvitationService.Get(id);
}
}
- 更新通信中间件中的
Invoke
方法。 从现在开始,这必须适用于电子邮件确认和游戏邀请确认,无论是否使用 WebSockets:
public async Task Invoke(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest)
{
...
switch (command.Operation.ToString())
{
...
case "CheckGameInvitationConfirmationStatus":
{
await ProcessGameInvitationConfirmation(context,
webSocket, ct, command.Parameters.ToString());
break;
}
}
}
else if (context.Request.Path.Equals(
"/CheckEmailConfirmationStatus"))
{
await ProcessEmailConfirmation(context);
}
else if (context.Request.Path.Equals(
"/CheckGameInvitationConfirmationStatus"))
{
await ProcessGameInvitationConfirmation(context);
}
else
{
await _next?.Invoke(context);
}
}
在本节中,您已经看到了如何在 ASP 中使用方法注入.NET Core 2.0 web 应用。 这是注入服务的首选方法,您应该在适当的时候使用它。
此外,你已经很好地实现了井字游戏。 几乎所有关于用户注册、电子邮件确认、游戏邀请和游戏邀请确认的内容现在都已经实现了。
在构建应用之后,您必须考虑将它们部署到不同的环境中。 正如您在前面关于配置的部分中已经看到的,您可以使用配置文件来更改服务甚至应用的配置。
在多个环境的情况下,您必须为每个环境复制appsettings.json
文件并相应地将其命名为appsettings.{EnvironmentName}.json
。
ASP.NET Core 2.0 将自动按层次顺序检索配置设置,首先从公共appsettings.json
文件中检索,然后从相应的appsettings.{EnvironmentName}.json
文件中检索,同时在必要时添加或替换值。
然而,根据不同的部署环境和配置开发使用不同组件的条件代码一开始似乎很复杂。 在传统应用中,您必须创建大量代码来自己处理所有不同的情况,然后对其进行维护。
在 ASP.NET Core 2.0,您可以使用大量的内部功能来实现这个目标。 然后,您可以简单地使用环境变量(开发、登台、生产等)来指示特定的运行时环境,从而为该环境配置应用。
正如您将在本节中看到的,您可以使用特定的方法名甚至类名来使用 ASP 提供的现有注入和覆盖机制.NET Core 2.0 开箱即用,用于配置您的应用。
在下面的例子中,我们正在向应用(SendGrid
)添加一个特定于环境的组件,只有当应用部署到特定的生产环境(Azure)时才需要使用它:
- 将 SendGrid NuGet 包添加到项目中。 这将用于未来 Azure 生产部署的Tic-Tac-Toe应用:
- 在
Services
文件夹中添加一个名为SendGridEmailService
的新服务。 这将用于通过SendGrid
发送电子邮件。 让它继承IEmailService
接口并实现特定的SendEmail
方法:
public class SendGridEmailService : IEmailService
{
private EmailServiceOptions _emailServiceOptions;
private ILogger<EmailService> _logger;
public SendGridEmailService(IOptions<EmailServiceOptions>
emailServiceOptions, ILogger<EmailService> logger)
{
_emailServiceOptions = emailServiceOptions.Value;
_logger = logger;
}
public Task SendEmail(string emailTo, string subject,
string message)
{
_logger.LogInformation($"##Start## Sending email via
SendGrid to :{emailTo} subject:{subject} message:{message}");
var client =
new SendGrid.SendGridClient(
_emailServiceOptions.RemoteServerAPI);
var sendGridMessage =
new SendGrid.Helpers.Mail.SendGridMessage
{
From = new SendGrid.Helpers.Mail.EmailAddress(
_emailServiceOptions.UserId)
};
sendGridMessage.AddTo(emailTo);
sendGridMessage.Subject = subject;
sendGridMessage.HtmlContent = message;
client.SendEmailAsync(sendGridMessage);
return Task.CompletedTask;
}
}
- 添加一个新的扩展方法,以便能够更容易地针对特定环境声明特定的电子邮件服务。 为此,转到
Extensions
文件夹并添加一个新的EmailServiceExtension
:
public static class EmailServiceExtension
{
public static IServiceCollection AddEmailService(
this IServiceCollection services, IHostingEnvironment
hostingEnvironment, IConfiguration configuration)
{
services.Configure<EmailServiceOptions>
(configuration.GetSection("Email"));
if (hostingEnvironment.IsDevelopment() ||
hostingEnvironment.IsStaging())
{
services.AddSingleton<IEmailService, EmailService>();
}
else
{
services.AddSingleton<IEmailService, SendGridEmailService>();
}
return services;
}
}
- 更新
Startup
类以使用之前创建的资产。 为了更好的可读性和可维护性,我们将进一步为我们必须支持的每个环境创建一个专用的ConfigureServices
方法,删除现有的ConfigureServices
方法,并添加以下特定于环境的ConfigureServices
方法:
public IConfiguration _configuration { get; }
public IHostingEnvironment _hostingEnvironment { get; }
public Startup(IConfiguration configuration,
IHostingEnvironment hostingEnvironment)
{
_configuration = configuration;
_hostingEnvironment = hostingEnvironment;
}
public void ConfigureCommonServices(IServiceCollection services)
{
services.AddLocalization(options =>
options.ResourcesPath = "Localization");
services.AddMvc().AddViewLocalization(
LanguageViewLocationExpanderFormat.Suffix, options =>
options.ResourcesPath =
"Localization").AddDataAnnotationsLocalization();
services.AddSingleton<IUserService, UserService>();
services.AddSingleton<IGameInvitationService,
GameInvitationService>();
services.Configure<EmailServiceOptions>
(_configuration.GetSection("Email"));
services.AddEmailService(_hostingEnvironment, _configuration);
services.AddRouting();
services.AddSession(o =>
{
o.IdleTimeout = TimeSpan.FromMinutes(30);
});
}
public void ConfigureDevelopmentServices(
IServiceCollection services)
{
ConfigureCommonServices(services);
}
public void ConfigureStagingServices(
IServiceCollection services)
{
ConfigureCommonServices(services);
}
public void ConfigureProductionServices(
IServiceCollection services)
{
ConfigureCommonServices(services);
}
Note that you could also apply the same approach to the Configure
method in the Startup
class. For that, you just remove the existing Configure
method and add new methods for the environments you would like to support, such as ConfigureDevelopment
, ConfigureStaging
, and ConfigureProduction
. The best practice would be to combine all of the common code into a ConfigureCommon
method and call it from the other methods, as shown below for the specific ConfigureServices
methods.
- 按F5启动应用,并验证一切仍然正常运行。 您应该看到添加的方法将被自动使用,并且应用功能齐全。
这很简单也很直接! 环境没有特定的条件代码,没有复杂的发展和维护,只是非常清晰和易于理解的方法,这些方法包含它们为之开发的环境名称。 对于一次性构建并在多个环境中运行的问题,这是一个非常干净的解决方案。
但是,这还不是全部! 如果我们告诉你,你不需要有一个单独的启动类? 如果每个环境都有一个专用的 Startup 类,其中只有适用于其上下文的代码,那会怎么样呢? 那太好了,对吧? 这正是 ASP.NET Core 2.0 提供。
为了能够在每个环境中使用专用的 Startup 类,你只需要更新 Program 类,它是 ASP 的主要入口点.NET Core 2.0 应用。 您只需更改BuildWebHost
方法中传递程序集名称.UseStartup("TicTacToe")
的一行代码,而不是.UseStartup<Startup>()
,然后您就可以使用这个神奇的特性:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
.UseStartup("TicTacToe")
.PreferHostingUrls(true)
.UseUrls("http://localhost:5000")
.UseApplicationInsights()
.Build();
}
}
现在,您可以为不同的环境添加专用的 Startup 类,例如StartupDevelopment
、StartupStaging
和StartupProduction
。 与之前的方法一样,它们将被自动使用; 你这边什么都不用做了。 只需更新Program
类,实现特定于环境的 Startup 类,它就可以工作了。 ASP.NET Core 2.0 提供了这些有用的特性,让我们的生活变得轻松多了。
在本章中,你已经学习了 ASP 的一些更高级的概念.NET Core 2.0,并实现了Tic-Tac-Toe应用中缺少的一些组件。
首先,您使用 JavaScript 创建了Tic-Tac-Toeweb 应用的客户端部分。 我们已经探索了如何通过使用捆绑和缩小来优化你的 web 应用,以及如何使用 WebSockets 来实现实时通信场景。
此外,您还看到了如何从集成的用户和会话处理中获益,这在一个易于理解的示例中显示。
然后,我们介绍了多语言用户界面、应用和服务配置的全球化和本地化,以及日志记录,以便更好地理解应用在运行时发生的事情。
最后,我们通过一个实际示例演示了如何一次性构建应用,然后根据部署目标使用多个ConfigureServices
和Configure
方法以及多个Startup
类的概念,使其适应不同的环境。
在下一章中,我们将讨论 ASP.NET Core MVC、MVC 中的 Razor(区域、布局、局部视图等等)、Razor 页面和视图引擎。