Skip to content

Latest commit

 

History

History
2344 lines (1845 loc) · 92.6 KB

File metadata and controls

2344 lines (1845 loc) · 92.6 KB

五、ASP.NET Core 2.0 的基本概念——第 2 部分

前一章让你了解了在使用 ASP 时可以使用的各种功能和特性.NET Core 2.0 用于构建高效和更易于维护的 web 应用。 我们已经解释了一些基本概念,并且您已经看到了如何将它们应用于一个名为Tic-Tac-Toe的实际应用的多个示例。

到目前为止,你已经很好地进步了,因为你已经吸收了 ASP.NET Core 2.0 应用具有内部结构,如何正确配置它们,以及如何使用自定义行为扩展它们,这是未来构建您自己的应用的关键。

但我们不要止步于此! 在本章的最后,你将发现如何最好地实现缺失的组件,进一步发展现有的组件,并添加客户端代码,以允许你拥有一个完整运行的端到端三连字应用。

在本章中,我们将涵盖以下主题:

  • 使用 JavaScript、捆绑和缩小优化客户端开发
  • 使用 WebSockets 进行实时通信
  • 利用会话和用户缓存管理
  • 为多语言用户界面应用全球化和本地化
  • 配置您的应用和服务
  • 使用测井和遥测技术进行监测和监督
  • 实现高级依赖注入概念
  • 一次构建并在多个环境中运行

使用 JavaScript 进行客户端开发

在前一章中,您使用 MVC 模式创建了一个主页和一个用户注册页面。 您实现了一个控制器(UserRegistrationController)和一个相应的视图,用于处理用户注册请求。 然后您添加了一个服务(UserService)和中间件(CommunicationMiddleware),但是我们刚刚开始,所以它们还没有完成。

当与Tic-Tac-Toe应用的初始工作流相比较时,我们可以看到仍有许多东西缺失,比如整个客户端部分,真正与通信中间件一起工作,以及我们仍需要实现的多个其他特性。

让我们从客户端部分开始,看看如何应用更高级的技术。 然后,您将学习如何尽可能地优化一切。

如果您还记得,上次我们是在用户向注册表单提交数据之后停止的,注册表单被发送到UserService。 然后,我们只显示了一条纯文本消息,如下所示:

但是,这里的处理还没有结束。 我们需要添加整个电子邮件确认过程使用客户端开发和 JavaScript,这是我们接下来要做的:

  1. 打开 Visual Studio 2017 并打开井字游戏项目。 在UserRegistrationController中添加一个名为EmailConfirmation的新方法:
        [HttpGet] 
        public IActionResult EmailConfirmation (string email) 
        { 
          ViewBag.Email = email; 
          return View(); 
        } 
  1. 右键单击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. 
  1. 转到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); 
          } 
        } 
  1. F5启动应用,注册一个新用户,并验证新的 EmailConfirmation 页面是否正确显示:

非常好,您已经实现了完成用户注册过程所需的第一组修改。 在接下来的部分中,我们需要检查用户是否确认了他的电子邮件地址。 让我们看看接下来怎么做:

  1. IUser接口中添加两个新方法GetUserByEmailUpdateUser。 这些将用于处理电子邮件确认更新:
        public interface IUserService 
        { 
          Task<bool> RegisterUser(UserModel userModel); 
          Task<UserModel> GetUserByEmail(string email); 
          Task UpdateUser(UserModel user); 
        } 
  1. 实现新方法,使用静态的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; 
          } 
        } 
  1. 添加一个名为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; } 
        } 
  1. 添加一个名为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); 
          } 
        } 
  1. 通过右键单击Index方法生成相应的视图,同时选择 Create 模板并使用前面的GameInvitationModel作为 Model 类:

  1. 修改自动生成的视图,删除所有不必要的输入控件,只留下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> 
  1. 现在,更新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(); 
      }
  1. F5启动应用,注册新用户,验证 Email 确认页面是否和之前一样。 在 Microsoft Edge 中,按F5重新加载页面,如果一切正常,你现在应该被重定向到游戏邀请页面:

太好了,又有进步了! 在游戏邀请之前,一切都很顺利,但不幸的是,仍然需要用户干预。 用户需要按F5手动刷新 Email 确认页面,直到 Email 确认完成; 只有这样,他才会被重定向到游戏邀请页面。

整个刷新过程必须在下一步中自动化并优化。 你的选择是:

  • 在页面的标题部分放置一个 HTML 元刷新标记
  • 使用简单的 JavaScript,它以编程方式进行刷新
  • 使用 jQuery 实现XMLHttpRequest(XHR

HTML3 引入了 meta refresh 标签,可以在一定时间后自动刷新页面。 然而,这种方法是不可取的,因为它创建了一个高服务器负载,并且在 Microsoft Edge 的安全设置可能完全禁用它,一些广告拦截器将停止它的工作。 所以,如果你使用它,你不能确定它是否能正常工作。

使用简单的 JavaScript 可以很好地以编程的方式自动化页面刷新,但它主要有相同的缺陷,因此不推荐使用。

XHR 是我们真正需要的,因为它提供了我们的井字策略应用所需要的东西。 它允许:

  • 更新网页而不重新加载他们
  • 即使在页面加载之后,也从服务器请求和接收数据
  • 发送数据到后台的服务器

现在,您将使用 XHR 来自动化和优化用户注册电子邮件确认处理的客户端实现。 这样做的步骤如下:

  1. wwwroot文件夹中创建名为app的新文件夹(此文件夹将包含以下步骤中的所有客户端代码),并在此文件夹中创建名为js的子文件夹。
  2. wwwroot/app/js文件夹中添加一个新的 JavaScript 文件scripts1.js,其内容如下:
        var interval; 
        function EmailConfirmation(email) { 
          interval = setInterval(() => { 
            CheckEmailConfirmationStatus(email); 
          }, 5000); 
        } 
  1. 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"); 
              } 
          }); 
        } 
  1. Views\Shared\_Layout.cshtml文件中打开布局页面,并在关闭body标签之前添加一个新的Development环境元素(最好将其放在那里):
        <environment include="Development"> 
          <script src="~/app/js/scripts1.js"></script> 
          <script src="~/app/js/scripts2.js"></script> 
        </environment> 
  1. 更新通信中间件中的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(); 

          } 
        } 
  1. 通过在页面底部添加对 JavaScriptEmailConfirmation函数的调用来更新EmailConfirmation视图:
        @section Scripts 
        { 
          <script> 
             $(document).ready(function () { 
               EmailConfirmation('@ViewBag.Email'); 
             }); 
          </script> 
        } 
  1. 更新UserRegistrationController中的EmailConfirmation方法。 由于通信中间件现在要模拟有效的电子邮件确认,删除以下行:
        user.IsEmailConfirmed = true; 
        user.EmailConfirmationDate = DateTime.Now; 
        await _userService.UpdateUser(user); 
  1. F5启动应用并注册一个新用户。 你会看到一个 JavaScript 警告框返回WaitingForEmailConfirmation,一段时间后,另一个提示 OK:

  1. 现在,您必须更新scripts2.js文件中的CheckEmailConfirmationStatus方法,以便在收到确认的电子邮件时重定向。 为此,删除alert("OK");指令,并在其位置添加以下指令:
        window.location.href = "/GameInvitation?email=" + email;
  1. 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.

优化您的 web 应用,并使用捆绑和缩小

正如你在第四章中看到的,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 文件:

  1. 在顶部菜单中选择“工具|扩展和更新”,单击“在线”,在搜索框中输入Bundler & Minifier,选择 Bundler&Minifier,最后单击“下载”:

  1. 关闭 Visual Studio; 安装将继续进行。 接下来,点击修改:

  1. 重新启动 Visual Studio。 您现在要通过捆绑和缩小来优化打开连接的数量和带宽使用。 为此,向项目中添加一个名为bundleconfig.json的新 JSON 文件。
  2. 更新bundleconfig.json文件,将两个 JavaScript 文件捆绑为一个名为site.js的文件,并缩小site.csssite.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 
          }   
        ] 
  1. 右键单击项目,选择 Bundler & Minifier |更新包:

  1. 在解决方案资源管理器中,您可以看到已经生成了两个名为site.min.csssite.min.js的新文件。
  2. 在任务运行器资源管理器中,你可以看到你为项目配置的捆绑和缩小过程:

  1. 右键单击 Update all files 并选择 Run。 现在你可以更详细地看到和理解这个过程在做什么:

  1. 通过右键单击 Update 所有文件并选择 Bindings | after build 来安排每次构建之后执行的过程。 将生成一个名为bundleconfig.json.bindings的新文件,如果您删除wwwroot/js文件夹并重新构建项目,那么这些文件将自动生成。

  2. 要查看新生成的文件的运行情况,转到项目设置中的 Debug 选项卡,将ASPNETCORE_ENVIRONMENT变量设置为Staging,然后保存:

  1. F5启动应用,在 Microsoft Edge 中按F12打开开发人员工具,重新注册过程。 您将看到只有捆绑的和缩小的site.min.csssite.min.js文件被加载,并且加载时间更快:

好了,现在我们知道了如何实现客户端,以及如何在现代 web 应用开发中受益于捆绑和缩小,让我们回到*《Tic-Tac-Toe*游戏,并进一步优化它并添加缺失的组件。

使用 WebSockets 进行实时通信

在上一节的末尾,一切都像预期的那样完全自动化地工作。 然而,仍有一些改进的空间。

实际上,客户端定期向服务器端发送请求,以查看电子邮件确认状态是否已更改。 这可能会导致大量请求查看状态是否发生了变化。

此外,一旦电子邮件被确认,服务器端不能立即通知客户端,因为它必须等待客户端请求的响应。

在本节中,您将了解 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应用的客户端实现:

  1. 转到Configure方法中的一字棋Startup类,在通信中间件和 MVC 中间件之前添加 WebSockets 中间件(记住中间件调用顺序对确保正确的行为很重要):
        app.UseWebSockets(); 
        app.UseCommunicationMiddleware(); 
        ... 
  1. 更新通信中间件,为 WebSockets 通信添加两个新方法SendStringAsyncReceiveStringAsync:
       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(); 
           } 
         } 
       } 
  1. 更新通讯中间件,添加一个名为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); 
          } 
        } 
  1. 更新通信中间件中的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); 
          } 
        }  
  1. 修改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); 
          } 
        } 
  1. 修改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) { 
         }; 
       };
  1. 当您启动应用并继续进行用户注册时,您将获得是否支持 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应用中激活内存会话提供程序来存储用户界面文化和语言:

  1. 打开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>
  1. 打开HomeController并添加一个名为SetCulture的新方法。 这将包含在一个会话变量中存储用户区域性设置的代码:
        public IActionResult SetCulture(string culture) 
        { 
          Request.HttpContext.Session.SetString("culture", culture); 
          return RedirectToAction("Index"); 
        } 
  1. 更新从区域性会话变量中检索区域性的HomeController方法的Index:
        public IActionResult Index() 
        { 
          var culture =
            Request.HttpContext.Session.GetString("culture"); 
          ViewBag.Language = culture; 
          return View(); 
        } 
  1. 进入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; 
        } 
  1. 添加 ASP 的内置会话中间件.NET Core 2.0 中的ConfigureServices方法Startup类:
        services.AddSession(o => 
        { 
          o.IdleTimeout = TimeSpan.FromMinutes(30); 
        });
  1. Startup类的Configure方法中激活会话中间件,将其添加到静态文件中间件之后:
        app.UseStaticFiles(); 
        app.UseSession(); 
  1. 更新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); 
        }
  1. 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:

  1. 转到Services文件夹并添加一个名为CultureProviderResolverService的新服务。 这将通过查看Culture查询字符串、Culturecookie 和Culture会话变量(在本章前一节中创建)来检索区域性设置。
  2. 通过从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); 
          } 
        } 
  1. Startup类的ConfigureServices方法的顶部添加本地化服务:
        public void ConfigureServices(IServiceCollection services) 
        { 
          services.AddLocalization(options => options.ResourcesPath = 
            "Localization"); 
          ... 
        } 
  1. 将本地化中间件添加到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)

  1. 在解决方案资源管理器中,添加一个名为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.

  1. 打开GameInvitationController.resx资源文件,添加一个新的GameInvitationConfirmationMessage英文:

  1. 在同一个Controllers文件夹中,为法语翻译添加一个新的资源文件GameInvitationController.fr.resx:

  1. 转到GameInvitationController,添加stringLocalizer,并更新构造函数实现:
        private IStringLocalizer<GameInvitationController>
         _stringLocalizer; 
        private IUserService _userService; 
        public GameInvitationController(IUserService userService,
         IStringLocalizer<GameInvitationController> stringLocalizer) 
        { 
          _userService = userService; 
          _stringLocalizer = stringLocalizer; 
        } 
  1. GameInvitationController中添加一个新的Index方法。 这将根据应用区域设置返回本地化消息:
        [HttpPost] 
        public IActionResult Index(
         GameInvitationModel gameInvitationModel) 
        {   
          return Content(_stringLocalizer[
          "GameInvitationConfirmationMessage",
           gameInvitationModel.EmailTo]); 
        } 
  1. 以英语(默认的区域性)启动应用,然后注册一个新用户,直到您收到以下文本消息,这应该是英语:

  1. 使用用户界面语言下拉菜单将应用语言更改为法语,然后注册一个新用户,直到你收到以下文本消息,现在应该是法语:

就这样,您已经了解了如何在应用中本地化任何类型的字符串,这对于某些特定的应用用例可能很有用。 但是,在处理视图时,这不是推荐的方法。

ASP.NET Core 2.0 框架为本地化视图提供了一些强大的特性。 在下一个例子中,你将使用视图本地化方法:

  1. 更新Startup类中的ConfigureServices方法,并将视图本地化服务添加到 MVC 服务声明中:
        services.AddMvc().AddViewLocalization(
          LanguageViewLocationExpanderFormat.Suffix,
          options => options.ResourcesPath = "Localization"); 
  1. 修改Views/ViewImports.cshtml文件,添加 View Localizer 功能,使其适用于所有视图:
        @using Microsoft.AspNetCore.Mvc.Localization 
        @inject IViewLocalizer Localizer 
  1. 打开主页视图并添加一个新标题,它将进一步本地化,如下所示:
        <h2>@Localizer["Title"]</h2> 
  1. 在解决方案资源管理器中,转到Localization文件夹并创建一个名为Views的子文件夹,然后在该文件夹中添加两个名为Home.Index.resxHome.Index.fr.resx的新资源文件:

  1. 打开Home.Index.resx文件,为英文标题添加一个条目:

  1. 打开Home.Index.fr.resx文件,为法语标题添加一个条目:

  1. 启动应用,将用户界面语言设置为 English:

  1. 通过“用户界面e Lalanguage”下拉菜单,将应用语言修改为法语。 标题现在应该显示在法语:

您已经了解了如何轻松地本地化视图,但是如何本地化视图中使用数据注释的表单呢? 让我们更详细地看一下; 你会惊讶于 ASP.NET Core 2.0 框架必须提供在这种情况下!

我们将在下面的例子中完全本地化用户注册表单:

  1. 在解决方案资源管理器中,转到Localization/Views文件夹,添加两个新的资源文件UserRegistration.Index.resxUserRegistration.Index.fr.resx

  2. 打开UserRegistration.Index.resx文件,添加一个TitleSubTitle元素的英文翻译:

  1. 打开UserRegistration.Index.fr.resx文件,添加一个TitleSubTitle元素,并使用法语翻译:

  1. 更新用户注册索引视图以使用视图定位器:
        @model TicTacToe.Models.UserModel 
        @{ 
           ViewData["Title"] = Localizer["Title"]; 
        } 
        <h2>@ViewData["Title"]</h2> 
        <h4>@Localizer["SubTitle"]</h4> 
        <hr /> 
        <div class="row"> 
        ... 
  1. 启动应用,通过“用户界面语言”下拉菜单设置语言为法语,然后进入“用户注册”界面。 标题应该用法语显示。 点击 Create 而不输入任何内容,看看会发生什么:

这里缺了点什么。 您已经为页面标题和用户注册页面的副标题添加了本地化,但是我们仍然缺少表单的一些本地化。 但我们遗漏了什么?

您肯定已经看到错误消息还没有本地化和翻译。 我们正在使用 Data Annotation 框架进行错误处理和表单验证,那么如何本地化 Data Annotation 验证错误消息呢? 这就是你现在将要看到的:

  1. Startup类的ConfigureServices方法中将数据注释本地化服务添加到 MVC 服务声明中:
        services.AddMvc().AddViewLocalization(
          LanguageViewLocationExpanderFormat.Suffix, options =>
          options.ResourcesPath = "Localization") 
          .AddDataAnnotationsLocalization();
  1. 转到Localization文件夹并创建名为Models的子文件夹,然后添加两个名为UserModel.resxUserModel.fr.resx的新资源文件。
  2. 用英文更新UserModel.resx文件:

  1. 用法语翻译更新UserModel.fr.resx文件:

  1. 更新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; } 
        }
  1. 重新构建解决方案并启动应用。 你会看到整个用户注册页面,包括错误消息,现在完全翻译时,更改用户界面语言为法语:

您已经了解了如何使用数据注释本地化字符串、视图甚至错误消息。 为此,您使用了 ASP 的内置特性.NET Core 2.0,因为它们包含了开发多语言本地化 web 应用的一切。 下一节将介绍如何配置应用和服务。

配置您的应用和服务

在前面的部分中,通过向用户注册过程中添加缺失的组件,甚至本地化Tic-Tac-Toe应用的部分,您进行了进一步的改进。 但是,您总是通过在代码中以编程方式设置用户确认来模拟电子邮件确认。 在本节中,我们将修改此部分,以便真正向新注册用户发送电子邮件,并使所有内容完全可配置。

首先,你要添加一个新的电子邮件服务,它将被用来发送电子邮件给刚刚在网站上注册的用户:

  1. Services文件夹中,添加一个名为EmailService的新服务,并实现一个默认的SendEmail方法(我们稍后将更新它):
        public class EmailService 
        { 
          public Task SendEmail(string emailTo, string subject,
           string message) 
          { 
            return Task.CompletedTask; 
          } 
        } 
  1. 提取IEmailService接口:

  1. 将新的 Email 服务添加到Startup类的ConfigureServices方法中(我们想要一个单一的应用实例,所以将其添加为 Singleton):
        services.AddSingleton<IEmailService, EmailService>(); 
  1. 更新UserRegistrationController以访问在上一步中创建的EmailService:
        readonly IUserService _userService; 
        readonly IEmailService _emailService; 
        public UserRegistrationController(IUserService userService,
         IEmailService emailService) 
        { 
          _userService = userService; 
          _emailService = emailService; 
        } 
  1. 更新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 配置文件:

  1. 向项目添加一个新的appsettings.json配置文件,并添加以下自定义部分。 这将用于配置电子邮件服务:
        "Email": { 
          "MailType": "SMTP", 
          "MailServer": "localhost", 
          "MailPort": 25, 
          "UseSSL": false, 
          "UserId": "", 
          "Password": "", 
          "RemoteServerAPI": "", 
          "RemoteServerKey": "" 
        } 
  1. 在解决方案资源管理器中,在项目的根目录下创建一个名为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; 
          } 
        }  
  1. 更新EmailService实现,添加EmailServiceOptions,并向类添加一个参数化构造函数:
        private EmailServiceOptions _emailServiceOptions; 
        public EmailService(IOptions<EmailServiceOptions> 
         emailServiceOptions) 
        { 
          _emailServiceOptions = emailServiceOptions.Value; 
        } 
  1. 添加一个新的构造函数到Startup类,以允许您配置您的电子邮件服务:
        public IConfiguration _configuration { get; } 
        public Startup(IConfiguration configuration) 
        { 
          _configuration = configuration; 
        } 
  1. 更新Startup类的ConfigureServices方法:
        services.Configure<EmailServiceOptions>
         (_configuration.GetSection("Email")); 
        services.AddSingleton<IEmailService, EmailService>(); 
  1. 更新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; 
        } 
  1. 将一个断点放入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接口轻松创建你自己的提供商,这就是你将在这里看到的:

  1. 添加一个新的类库(.NET Core)项目到解决方案,并将其命名为TicTacToe.Logging(删除自动生成的Class1.cs文件):

  1. 通过 NuGet 包管理器添加 NuGet 包Microsoft.Extensions.LoggingMicrosoft.Extensions.Logging.Configuration:

  1. 在 TicTacToe Web 应用项目中添加一个项目引用,以便能够使用TicTacToe.Logging类库中的资产:

  1. 添加一个名为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; } 
        } 
  1. 添加一个名为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(); 
            } 
          } 

        }
  1. 添加一个名为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)); 
          } 
        } 
  1. 添加一个名为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() 
          { 
          } 
        }
  1. 为了简化从 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; 
          } 
        } 
  1. 在 TicTacToe Web 项目中,在Options文件夹中添加两个新选项LoggingProviderOptionLoggingOptions:
        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; } 
        }
  1. 在 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; 
          } 
        }
  1. 转到 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;

  1. appsettings.json文件中添加一个名为Logging的新部分:
        "Logging": { 
          "Providers": [ 
            { 
              "Name": "Console", 
              "LogLevel": "1" 
            }, 
            { 
              "Name": "File", 
              "LogLevel": "2" 
            } 
          ], 
          "MinimumLevel": 1 
        }
  1. 启动应用,并验证是否在应用文件夹中名为logs的文件夹中创建了一个新的日志文件:

这是第一步,简单而快速地完成。 现在,您有了一个日志文件,可以将日志写入其中。 您将看到,使用集成的日志功能从 ASP 的任何地方创建日志都是同样容易的.NET Core 2.0 应用(ControllersServices等)。

让我们快速添加一些日志到井字游戏应用:

  1. 更新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; 
       } 
  1. 更新UserRegistrationController中的EmailConfirmation方法,并在方法开始处添加日志:
        _logger.LogInformation($"##Start## Email confirmation
         process for {email}");
  1. 更新 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; 
          } 
        }
  1. 打开生成的日志文件,分析其内容:

实现高级依赖注入概念

在前一章中,您看到了依赖注入(DI)的工作方式以及如何使用构造函数注入方法。 但是,如果您需要在运行时注入许多实例,那么这个方法可能会相当麻烦,并且会使代码的理解和维护变得复杂。

因此,您可以使用一种更先进的 DI 技术,称为方法注射。 这允许直接从代码中访问实例。

在下面的示例中,您将添加一个处理游戏邀请的新服务,并更新Tic-Tac-Toe应用,在使用方法注入的同时,可以发送电子邮件联系其他用户加入游戏:

  1. 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)); 
          } 
        }
  1. 提取IGameInvitationService接口:

  1. Startup类的ConfigureServices方法中添加新的游戏邀请服务(我们想要一个单一的应用实例,所以将其添加为 Singleton):
        services.AddSingleton<IGameInvitationService,
         GameInvitationService>(); 
  1. 更新GameInvitationController中的Index方法,使用RequestServicesprovider 通过方法注入一个游戏邀请服务实例:
 [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.

  1. GameInvitationController中添加一个名为GameInvitationConfirmation的新方法:
        [HttpGet] 
        public IActionResult GameInvitationConfirmation(Guid id,
         [FromServices]IGameInvitationService gameInvitationService) 
        { 
          var gameInvitation = gameInvitationService.Get(id).Result; 
          return View(gameInvitation); 
        } 
  1. 为前面添加的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> 
        } 
  1. 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); 
          } 
        }
  1. 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; 
            } 
          }); 
        } 
  1. 更新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) { 
          }; 
        };  
  1. 在通信中间件中添加一个名为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;

  1. 添加一个名为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); 
          } 
        } 
  1. 更新通信中间件中的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)时才需要使用它:

  1. 将 SendGrid NuGet 包添加到项目中。 这将用于未来 Azure 生产部署的Tic-Tac-Toe应用:

  1. 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; 
          } 
        } 
  1. 添加一个新的扩展方法,以便能够更容易地针对特定环境声明特定的电子邮件服务。 为此,转到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; 
          } 
        } 
  1. 更新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.

  1. 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 类,例如StartupDevelopmentStartupStagingStartupProduction。 与之前的方法一样,它们将被自动使用; 你这边什么都不用做了。 只需更新Program类,实现特定于环境的 Startup 类,它就可以工作了。 ASP.NET Core 2.0 提供了这些有用的特性,让我们的生活变得轻松多了。

总结

在本章中,你已经学习了 ASP 的一些更高级的概念.NET Core 2.0,并实现了Tic-Tac-Toe应用中缺少的一些组件。

首先,您使用 JavaScript 创建了Tic-Tac-Toeweb 应用的客户端部分。 我们已经探索了如何通过使用捆绑和缩小来优化你的 web 应用,以及如何使用 WebSockets 来实现实时通信场景。

此外,您还看到了如何从集成的用户和会话处理中获益,这在一个易于理解的示例中显示。

然后,我们介绍了多语言用户界面、应用和服务配置的全球化和本地化,以及日志记录,以便更好地理解应用在运行时发生的事情。

最后,我们通过一个实际示例演示了如何一次性构建应用,然后根据部署目标使用多个ConfigureServicesConfigure方法以及多个Startup类的概念,使其适应不同的环境。

在下一章中,我们将讨论 ASP.NET Core MVC、MVC 中的 Razor(区域、布局、局部视图等等)、Razor 页面和视图引擎。