• Patterns for application development with ASP.NET Core


    此文章翻译自 NDC { London } 16-20 January 2017 上, Damian Edwards和David Fowler的演讲,如果翻译不周,请大家指出错误。

    Logging

    1. 生产环境总是配置一个Logger(比如: Serilog, Application Insights)

    2. 日志作为诊断应用程序问题的入口

    3. 不需要重启应用程序,就能改变日志的级别

      在开发环境应该记录尽可能多的日志,但是生产环境出于性能考虑,应该只记录Warning以上的日志

    4. 如果不想显示太多的信息,可以选择特定的Category

      比如只想调试SQL语句,可以只记录Category为Microsoft.EntityFrameworkCore.*的日志。

    5. ASP.NET按照如下方式记录日志:

      • ANCM(IIS): 将不能启动进程的错误记录到EventLog

        ANCM是指ASP.NET Core Module。按照Damian的说法,当IIS进程无法启动的时候是很崩溃的,此时可以从Windows Event Log中查看具体的错误。

      • ANCM also uses regular IIS failed request tracing

      • 其他未处理的异常将由为logger providers提供的logger记录

    Configuration & Options

    1. 随着配置文件的改变而自动重新加载配置

    2. 当配置内容的值更改时,使用IOptionsSnapshot重新加载IOptions对象

      IOptionsSnapshot是ASP.NET Core 1.1提供的新接口,这个接口在配置文件改变时会重新绑定配置对象的值,这样可以在应用程序运行时动态修改配置内容。

    3. 使用User Secrets, 环境变量,Azure KeyVault等方式存储和环境有关的配置内容

      比如每个环境的连接字符串,启动的端口不一致,并且这些配置是很少发生改变的,可以配置在环境变量中。

    4. 应用程序部署在IIS时,可以通过web.config配置环境变量

    File Upload

    1. 如果上传的文件需要提供给浏览器访问,将它们放在web root下

    2. 最简单的处理文件上传的方法,是使用MVC里的IFormFile或者IFormFileCollection

      这2个接口可以直接定义为Action的参数,也可以定义在Model里。

      和传统的ASP.NET相比,少了Request.Files。

    3. 也可以使用低级别的API,比如HttpRequest.ReadFormAsync和HttpRequest.Form

    4. 阻止用户上传任何可以在服务器执行的文件

      比较容易疏忽的是.cshtml文件。ASP.NET Core 2.0提供了Razor Page,那么.cshtml文件很可能被制作成一个可以直接访问的Razor Page上传上来。

    5. 大文件会提供buffer,防止服务器内存暴增

    6. 在保存文件之前,检查客户端传递的文件路径

      保证文件上传功能的安全性一直是个斗智斗勇的过程,David演示了一个简单的Hack场景。如果直接使用如下的方式保存文件:

      var path = Path.Combine(env.ContentRoot, file.FileName);
      

      当客户端传递来的FileName是"ViewsHomeIndex.cshtml"时,你的网站就杯具了。

    7. 处理很大的multipart uploads时,不要使用model binding。手动的调用HttpRequest.GetMultipartBoundary()方法。

      这个方法仅在ASP.NET CORE 1.1.0提供

    8. IIS的限制仍然生效

      这里应该说的是maxRequestSize之类的限制

    Dependency Injection

    1. 保持你的DI工厂快速响应,不要在构造服务时使用异步方法

      如下代码会导致死锁

      class Program
      {
          static void Main(string[] args)
          {
              DeadlockWithFactory();
      
              Console.WriteLine("hello");
          }
      
          private static void DeadlockWithFactory()
          {
              var services = new ServiceCollection();
              services.AddSingleton(s =>
              {
                  var b = GetBAsync(s).Result; 
      
                  return new A(b);
              });
      
              services.AddSingleton<B>();
      
              var serviceProvider = services.BuildServiceProvider();
              var a = serviceProvider.GetService<A>(); //1. 此处lock了Container
          }
      
          private static async Task<B> GetBAsync(IServiceProvider s)
          {
              await Task.Delay(1000); //2. 此处由于使用了await关键字,离开了主线程,进入了后台线程
      
              return s.GetRequiredService<B>(); //3. 此处后台线程尝试lock Container,导致死锁
          }
      }
      
      public class A
      {
          public A(B b)
          {
      
          }
      }
      
      public class B
      {
      
      }
      
    2. 避免手动的调用GetService

      这里的大概意思是,由于手动调用GetService依赖了IServiceProvider,会使得单元测试变复杂。

      有些场景下,我们不需要在构造函数注入服务,而是想在执行到特定方法的时候才注入。Controller层面有个FromServices特性可以标记在Action的参数上。如果是下层代码,则可以自己定义一个XXXProvider,提供一个ResolveXXX方法,那么只需要在构造函数注入XXXProvider即可。这种方式适用于构造数据库连接,打开文件等。

    3. 实现了IDisposable接口的服务会由Container控制调用它们的Dispose方法,如果在顶层Container中构造出该服务的实例,会导致内存泄漏。

      Container构造的服务,会在Container释放的时候自动释放。如果在ApplicationServices上构造出的实例,在应用停止后才会释放。

    4. Turn on scope validation to make sure you don't have scoped services capturing singletons.

      想像一下这个场景

      var services = new ServiceCollection();
      services.AddSingleton<A>();
      services.AddScoped<B>();
      
      var sp = services.BuildServiceProvider();
      var a = sp.GetRequiredService<A>();
      

      如果A依赖了B,那么本应该是Scoped的B类型被捕获成了单例。

      在ASP.NET Core 1.1中, BuildServiceProvider方法提供了一个新的参数 validateScopes,上面的代码可以修改成

      var sp = services.BuildServiceProvider(validateScopes: true); 
      var a = sp.GetRequiredService<A>(); //如果A依赖B,那么此处会抛出System.InvalidOperationException: Cannot consume scoped service 'B' from singleton 'A'.
      
    5. 优化依赖注入

      构造函数注入

      public class A(B b) { }
      public class B(C c) { }
      public class C() { }
      

      等同于

      new A(new B(new C()));
      

      ServiceProvider注入

      public class A(IServiceProvider sp) { _b = sp.GetService<B>(); }
      public class B(IServiceProvider sp) { _c = sp.GetService<C>(); }
      public class C() { }
      

      等同于

      new C();
      new B(GetService<C>());
      new A(GetService<B>());
      

    MVC

    1. Protect against mass assignment with separate input/output models

      在现有MVC模式中,我们一般定义一个ViewModel来展示整个页面需要显示的内容,再定义一些Model存储客户端传递的参数。我们要尽量避免这个过程中产生的重复编码。

      public class AdminViewModel
      {
          [Display(Name = "Live Show Embed URL", Description = "URL for embedding the live show")]
          [DataType(DataType.Url)]
          public string LiveShowEmbedUrl { get; set; }
          
          [Display(Name = "Live Show HTML", Description = "HTML content for the live show")]
          [DataType(DataType.MultilineText)]
          public string LiveShowHtml { get; set; }
      
          [Display(Name = "Next Show Date/time", Description = "Exact date and time of the next live show in Pacific Time")]
          [DateAfterNow(TimeZoneId = "Pacific Standard Time")]
          public DateTime? NextShowDatePst { get; set; }
      
          [Display(Name = "Standby Message", Description = "Message to show on home page during show standby")]
          public string AdminMessage { get; set; }
      
          public string NextShowDateSuggestionPstAM { get; set; }
      
          public string NextShowDateSuggestionPstPM { get; set; }
      
          public string SuccessMessage { get; set; }
      
          public bool ShowSuccessMessage => !string.IsNullOrEmpty(SuccessMessage);
      
          public AppSettings AppSettings { get; set; }
      
          public string EnvironmentName { get; set; }
      }
      
      [ModelMetadataType(typeof(AdminViewModel))]
      public class AdminInputModel
      {
          public string LiveShowEmbedUrl { get; set; }
      
          public string LiveShowHtml { get; set; }
      
          public DateTime? NextShowDatePst { get; set; }
      
          public string AdminMessage { get; set; }
      }
      

      在这个例子中,在Get请求时需要返回一个包含了所有展示内容的AdminViewModel,在Post请求时只需要客户端传递部分参数,因此可以定义一个AdminInputModel,同时指定ModelMetadataType(typeof(AdminViewModel)),这样验证的Attribute可以从AdminViewModel中自动复制过来,避免重复编码。

      同时,推荐大家采用类似AutoMapper之类的第三方库解决对象复制、克隆过程中产生的重复编码。

      在上述代码中,我更倾向于以如下的方式定义Model

      public class AdminViewModel
      {
          public AdminInputModel Input { get; set; }
      
          public string NextShowDateSuggestionPstAM { get; set; }
      
          public string NextShowDateSuggestionPstPM { get; set; }
      
          public string SuccessMessage { get; set; }
      
          public bool ShowSuccessMessage => !string.IsNullOrEmpty(SuccessMessage);
      
          public AppSettings AppSettings { get; set; }
      
          public string EnvironmentName { get; set; }
      }
      
      [ModelMetadataType(typeof(AdminViewModel))]
      public class AdminInputModel
      {
          [Display(Name = "Live Show Embed URL", Description = "URL for embedding the live show")]
          [DataType(DataType.Url)]
          public string LiveShowEmbedUrl { get; set; }
          
          [Display(Name = "Live Show HTML", Description = "HTML content for the live show")]
          [DataType(DataType.MultilineText)]
          public string LiveShowHtml { get; set; }
      
          [Display(Name = "Next Show Date/time", Description = "Exact date and time of the next live show in Pacific Time")]
          [DateAfterNow(TimeZoneId = "Pacific Standard Time")]
          public DateTime? NextShowDatePst { get; set; }
      
          [Display(Name = "Standby Message", Description = "Message to show on home page during show standby")]
          public string AdminMessage { get; set; }
      }
      
    2. TagHelpers

      • 正确的处理属性值
      • 属性中带有文件路径时,确保在执行前验证文件是否存在
    3. 在Post-Redirect-Get场景中使用TempData

      当你在一个Post的请求里完成操作,并且需要重定向到另外一个页面展示操作结果时,可以通过URL传参,Cookies, Sessions等技术来实现,也可以使用TempData来实现。

      ASP.NET Core的TempData是基于Cookies的。

    Testing your pipeline

    1. 使用TestHost在内存中测试整个管道,而不用访问网络

      [Fact]
      public async Task VerifyResponse()
      {
          var builder = new WebHostBuilder()
                  .UseStartup<Startup>();
      
          var server = new TestServer(builder);
          var client = server.CreateClient();
      
          var response = await client.GetAsync("http://something");
      
          Assert.Equal("Hello World", await response.Content.ReadAsStringAsync());
      }
      
    2. 单元测试中也可以使用enviroments来对不同的环境进行测试

      [Fact]
      public async Task VerifyTestEnvironmentResponse()
      {
          var builder = new WebHostBuilder()
                  .UseEnvironment("Test")
                  .UseStartup<StartupWithEnvironment>();
      
          var server = new TestServer(builder);
          var client = server.CreateClient();
      
          var response = await client.GetAsync("http://something");
      
          Assert.Equal("Test", await response.Content.ReadAsStringAsync());
      }
      

    Per request state

    1. HttpContext.Items

      和以往一样,可以将与会话相关的数据存储在HttpContext.Items

    2. HttpContext.Get/SetFeature

      通过调用HttpContext.Features.Set方法添加自定义的行为

    3. Scoped services

      每个会话开始会构造一个Scope,会话结束会释放该Scope。因此和会话相关的服务最好注册为Scoped。

    4. 在Request Context以外要访问HttpContext时,可以使用IHttpContextAcessor接口。

      public class A
      {
          private HttpContext HttpContext { get; set; }
      
          public A(IHttpContextAcessor httpContextAcessor)
          {
              this.HttpContext = httpContextAcessor.HttpContext;
          }
      }
      

    备注

    1. 原文视频:Youtube

    2. 相关源码

  • 相关阅读:
    Fault-Tolerant Virtual Machine 论文笔记
    Google File System 论文笔记
    Amazon Aurora 论文笔记
    MATLAB入门学习(二):分支语句和编程设计
    MATLAB入门学习(一):基础准备
    矩阵连乘问题
    合并排序 java
    生产者与消费者 代码实现 java
    图的封装(C++)
    二叉树的封装
  • 原文地址:https://www.cnblogs.com/scheshan/p/6752113.html
Copyright © 2020-2023  润新知