失踪人口回归。去年六月份开始,我开始翻译一千多页的《CSharp 7 in a Nutshell》到现在为止终于告一段落。我又回归了表世界。从这次开始我希望展开一个全新的主题。我叫它 ASP.NET Core 沉思录(多么高大上的名字,自我陶醉~)。今天是第一个主题。CreateWebHostBuilder 是一个 Convension。

太长不读

对于 WebApplicationFactory<T> 而言,默认情况下会采取如下假定:

  • Startup 所在的程序集应当就是应用程序入口(Main)所在的程序集;(官方工程模板的坑)

  • 应用程序入口所在的类(Program),里面会包含整个创建和配置 IWebHostBuilder 的过程;

  • 创建和配置 IWebHostBuilder 的过程是由应用程序入口所在类的 CreateWebHostBuilder 方法完成的。

在满足上述假定的情况下,无需额外代码,Web 应用的执行和测试将共享相同的逻辑。如若不然,则测试失败。如果无法满足上述三种条件还可以通过集成 WebApplicationFactory<T> 并重写 CreateWebHostBuilder 方法来解决。

以上约束仅仅限定于 WebApplicationFactory<T>,若直接在测试中使用 TestServer 则没有这种限制。

WebApplicationFactory<T>T 并不是 TStartup,而是应用程序入口所在的程序集中的任意类型。

娓娓道来

如果我们使用 dotnet 命令行创建一个 ASP.NET Core MVC/WebAPI 的工程。那么它的启动代码大概是这样的:

public static class Program{    public static void Main(string[] args)    {        CreateWebHostBuilder(args).Build().Run();    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args)    {        // Modified a little bit for the sake of illustration        return new WebHostBuilder()            .UseKestrel()            .ConfigureLogging(lb =>            {                lb.SetMinimumLevel(LogLevel.Debug).AddConsole();            })            .UseStartup<Startup>();    }}

有没有小伙伴好奇,为什么需要一个 CreateWebHostBuilder 方法?从直观上看,它是创建并完成基本的 IWebHostBuilder 配置的方法。这个方法应在测试中进行复用以确保测试和应用程序中的 IWebHostBuilder 配置几乎相同,例如:

[Fact]public async Task should_get_response_text(){    IWebHostBuilder webHostBuilder = Program.CreateWebHostBuilder(Array.Empty<string>());

    using (var testServer = new TestServer(webHostBuilder))    using (HttpClient client = testServer.CreateClient())    {        HttpResponseMessage response = await client.GetAsync("/message");

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);        Assert.Equal("Hello", await response.Content.ReadAsStringAsync());    }}

这个测试是可以顺利通过的。但是我们认为将 Program.CreateWebHostBuilder 暴露并不是一个好的感觉。我们更希望把这个配置过程分离。例如分离到一个类中:

public class WebHostBuilderConfigurator{    public IWebHostBuilder Configure(IWebHostBuilder webHostBuilder)    {        return webHostBuilder            .UseKestrel()            .ConfigureLogging(lb =>            {                lb.SetMinimumLevel(LogLevel.Debug).AddConsole();            })            .UseStartup<Startup>();    }}

这样,Program 仅仅包含整个应用程序的入口,CreateWebHostBuilder 方法就被删掉了:

public static void Main(string[] args){    var webHostBuilder = new WebHostBuilder();    new WebHostBuilderConfigurator().Configure(webHostBuilder).Build().Run();}

测试也就变成了:

[Fact]public async Task should_get_response_text(){    IWebHostBuilder webHostBuilder = new WebHostBuilderConfigurator().Configure(new WebHostBuilder());

    using (var testServer = new TestServer(webHostBuilder))    using (HttpClient client = testServer.CreateClient())    {        HttpResponseMessage response = await client.GetAsync("/message");

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);        Assert.Equal("Hello", await response.Content.ReadAsStringAsync());    }}

看起来不错,测试也通过了真是可喜可贺。现在我们准备使用更加完善的 WebApplicationFactory<T> 代替 TestServer 进行测试:

[Fact]public async Task should_get_response_text_using_web_app_factory(){    using (var factory = new WebApplicationFactory<Startup>().WithWebHostBuilder(        wb => new WebHostBuilderConfigurator().Configure(wb)))    using (HttpClient client = factory.CreateClient())    {        HttpResponseMessage response = await client.GetAsync("/message");

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);        Assert.Equal("Hello", await response.Content.ReadAsStringAsync());    }}

看起来不错,但是发现测试运行的时候却失败了。并伴有诡异的异常信息:

System.InvalidOperationException : No method 'public static IWebHostBuilder CreateWebHostBuilder(string[] args)' found on 'WebApp.Program'. Alternatively, WebApplicationFactory`1 can be extended and 'protected virtual IWebHostBuilder CreateWebHostBuilder()' can be overridden to provide your own IWebHostBuilder instance.

哦,真神奇,它怎么找到 WebApp.Program 的?我只告诉了它 Startup 而并没有提供任何 Program 类型的信息啊?而这个时候,如果我们老老实实的恢复 WebApp.Program 类中的 CreateWebHostBuilder 方法,那么测试就顺利通过了。

这是为什么呢?原来让测试环境尽可能的 Match 执行环境是我们共同的心愿,WebApplicationFactory 希望能够自动的帮我们解决这个问题,于是它做了如下的假定:

  • Startup 所在的程序集应当就是应用程序入口(Main)所在的程序集;

  • 应用程序入口所在的类(Program),里面会包含整个创建和配置 IWebHostBuilder 的过程;

  • 创建和配置 IWebHostBuilder 的过程是由应用程序入口所在类的 CreateWebHostBuilder 方法完成的。

只要符合这三个假定,那么你尽可不费吹灰之力就达到了产品测试配置一致的目的。而如果不符合这个假定将让测试在默认状态下执行失败。具体的代码请参考 这里 和 这里。从 WebHostFactoryResolver 里面可以看出,除了 CreateWebHostBuilder 方法之外,BuildWebHost 也是一个 Convension,只不过主要是为了向前兼容的目的。

在真实的项目中,很可能是不满足这三个条件的,那么怎么办呢?还好我们可以通过集成 WebApplicationFactory<T> 并重写 CreateWebHostBuilder 方法来解决这个问题:

public class MyWebApplicationFactory : WebApplicationFactory<Startup>{    protected override IWebHostBuilder CreateWebHostBuilder()    {        var webHostBuilder = new WebHostBuilder();        new WebHostBuilderConfigurator().Configure(webHostBuilder);        return webHostBuilder;    }}

并相应的将测试更改为:

[Fact]public async Task should_get_response_text_using_web_app_factory(){    using (var factory = new MyWebApplicationFactory())    using (HttpClient client = factory.CreateClient())    {        HttpResponseMessage response = await client.GetAsync("/message");

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);        Assert.Equal("Hello", await response.Content.ReadAsStringAsync());    }}

就可以了。

最后,需要提醒的是 WebApplicationFactory<T>TTEntryPoint ,是入口所在的程序集的类型。虽然平常大家都喜欢写 Startup

总结

请飞到文章开头~ :-D

【ASP.NET Core 沉思录】CreateWebHostBuilder 是一个 Convension相关推荐

  1. ASP.NET Core 沉思录 - 结构化日志

    在 <ASP.NET Core 沉思录 - Logging 的两种介入方法>中我们介绍了 ASP.NET Core 中日志的基本设计结构.这一次我们来观察日志记录的格式,并进一步考虑如何在 ...

  2. ASP.NET Core 沉思录 - Logging 的两种介入方法

    ASP.NET Core 中依赖注入是一个很重要的环节.因为几乎所有的对象都是由它创建的(相关文章请参见<ASP.NET Core 沉思录 - ServiceProvider 的二度出生> ...

  3. ASP.NET Core 沉思录 - ServiceProvider 的二度出生

    ASP.NET Core 终于将几乎所有的对象创建工作都和依赖注入框架集成了起来.并对大部分的日常工作进行了抽象.使得整个框架扩展更加方便.各个部分的集成也更加容易.今天我们要思考的部分仍然是从一段每 ...

  4. ASP.NET Core 沉思录 - 环境的思考

    我的博客换新家啦,新的地址为:https://clrdaily.com :-D 今天我们来一起思考一下如何在不同的环境应用不同的配置.这里的配置不仅仅指 IConfiguration 还包含 IWeb ...

  5. ASP.NET Core ActionFilter引发的一个EF异常

    最近在使用ASP.NET Core的时候出现了一个奇怪的问题.在一个Controller上使用了一个ActionFilter之后经常出现EF报错. InvalidOperationException: ...

  6. ASP.NET Core Web API下事件驱动型架构的实现(一):一个简单的实现

    很长一段时间以来,我都在思考如何在ASP.NET Core的框架下,实现一套完整的事件驱动型架构.这个问题看上去有点大,其实主要目标是为了实现一个基于ASP.NET Core的微服务,它能够非常简单地 ...

  7. asp编程工具_使用ASP.NET Core构建RESTful API的技术指南

    译者荐语:利用周末的时间,本人拜读了长沙.NET技术社区翻译的技术文章<微软RESTFul API指南>,打算按照步骤写一个完整的教程,后来无意中看到了这篇文章,与我要写的主题有不少相似之 ...

  8. 在 ASP.NET Core 中使用 Serilog 使用 Fluentd 将日志写入 Elasticsearch

    在 ASP.NET Core 中使用 Serilog 使用 Fluentd 将日志写入 Elasticsearch 原文来自:https://andrewlock.net/writing-logs-t ...

  9. 《从零开始学ASP.NET CORE MVC》:ASP.NET Core 中的 Main方法(5)

    本文出自<从零开始学ASP.NET CORE MVC> 推荐文章:ASP.NET Core Web 项目文件 ASP.NET Core 中的 Main方法 一个开始专心写字的人 在ASP. ...

最新文章

  1. Android java.lang.UnsatisfiedLinkError
  2. 11个三相异步电动机常见故障与维修方法。
  3. 蓝桥杯 - 翻硬币(贪心)
  4. 误差error,偏置bias,方差variance的见解
  5. python整数加法计算器_Python应用实例赏析2.1简单计算
  6. Hi3520d uImage制作 uboot制作 rootfs制作
  7. linux文件句柄数
  8. HDU 2553 N皇后问题 DFS 简单题
  9. 集合之HashSet
  10. 【扩频通信】基于matlab直接序列扩频通信【含Matlab源码 1004期】
  11. linux下安装共享软件,Linux 系统下各种包的安装方法分享
  12. 递归的经典例子 java_java递归算法经典实例
  13. c语言按键长按双击,51单片机实现单按键单击、双击、长按功能
  14. 【数字IC验证快速入门】7、验证岗位中必备的数字电路基础知识(含常见面试题)
  15. python numpy安装教程_python 环境下安装 numpy
  16. 57个你没有听过的Google产品
  17. 智慧社区中的物联网产品应用
  18. aven 项目 pom.xml 第一行报错 Unknow。。。。。。。解决办法
  19. int类型转成Long类型
  20. SIMD<SIMT<SMT: NVIDIA GPU的并行机制

热门文章

  1. 白色裤子为什么会沾上蓝色_什么是蓝色的,为什么它可以在Mac上运行?
  2. chrome怎么隐藏浏览器_如何使用Google Chrome的隐藏阅读器模式
  3. requests保存图片
  4. WPF 获取鼠标屏幕位置、窗口位置、控件位置
  5. eclipse创建maven多模块项目(单个类似)
  6. 带有帐号密码验证的apche服务器文件下载
  7. 存储设备分区,格式化,挂载
  8. WebView 访问 url asset sd 网页
  9. Helpdesk 流程
  10. 里程碑 .Net7再更新,从此彻底碾压Java!