比较 WebApplicationBuilder 和 Generic Host
这是该系列的第二篇文章:探索 .NET 6:
- Part 1 - 探索 .NET 6 中的 ConfigurationManager
- Part 2 - 比较 WebApplicationBuilder 和 Generic Host(当前文章)
- Part 3 - 探索 WebApplicationBuilder 背后的代码
- Part 4 - 使用 WebApplication 构建中间件管道
- Part 5 - 使用 WebApplicationBuilder 支持 EF Core 迁移
- Part 6 - 在 .NET 6 中使用 WebApplicationFactory 支持集成测试
- Part 7 - 用于 .NET 6 中 ASP.NET Core的分析器
- Part 8 - 使用源代码生成器提高日志记录性能
- Part 9 - 源代码生成器更新:增量生成器
- Part 10 - .NET 6 中新的依赖关系注入功能
- Part 11 - [CallerArgumentExpression] and throw helpers
- Part 12 - 将基于 .NET 5 启动版本的应用升级到 .NET 6
有一种新的"默认"方式可以在.NET 中构建应用程序,使用 WebApplication.CreateBuilder()
。在这篇文章中,我将这种方法与以前的方法进行比较,讨论进行更改的原因,并查看其影响。在下一篇文章中,我将介绍 WebApplication
和 WebApplicationBuilder
背后的代码,以了解它们的工作原理。
构建 ASP.NET 核心应用程序:历史课
在我们看.NET 6 之前,我认为值得看看 ASP.NET Core 应用程序的"引导"过程在过去几年中是如何演变的,因为最初的设计对我们今天所处的位置产生了巨大的影响。当我们在下一篇文章中查看WebApplicationBuilder
背后的代码时,这将变得更加明显!
即使我们忽略 .NET Core 1.x(此时完全不受支持),我们也有三种不同的范例来配置 ASP.NET Core 应用程序:
WebHost.CreateDefaultBuilder()
:从 ASP.NET Core 2.x 开始配置 ASP.NET Core 应用程序的"原始"方法。Host.CreateDefaultBuilder()
:在通用主机之上重新构建 ASP.NET Core,支持其他工作负载,如 Worker 服务。.NET Core 3.x 和 .NET 5 中的默认方法。WebApplication.CreateBuilder()
:.NET 6 中的新热点。
为了更好地了解这些差异,我在以下各节中重现了典型的"启动"代码,这应该使 .NET 6 中的更改更加明显。
ASP.NET Core 2.x: WebHost.CreateDefaultBuilder()
在 ASP.NET Core 1.x 的第一个版本中(如果我没记错的话),没有"默认"主机的概念。ASP.NET Core 的一个意识形态是,一切都应该"为游戏付费",也就是说,如果你不需要使用它,你就不应该为那里的功能付出代价。
在实践中,这意味着"入门"模板包含大量样板和大量 NuGet 包。为了抵消看到所有这些代码只是为了入门而感到震惊,ASP.NET Core 引入了WebHost.CreateDefaultBuilder()
。这将为您设置一大堆默认值,创建一个 IWebHostBuilder
,并构建一个 IWebHost
。
早在 2017 年,我就研究了
WebHost.CreateDefaultBuilder()
的代码,并将其与 ASP.NET Core 1.x 进行了比较,以防万一你觉得像一个记忆之旅。
从一开始,ASP.NET Core 就将"主机"引导与"应用程序"引导分开。从历史上看,这表现为在两个文件(传统上称为 Program.cs 和 Startup.cs)之间拆分启动代码。
在 ASP.NET Core 2.1中,Program.cs调用WebHost.CreateDefaultBuilder()
,它设置应用程序配置(例如从appsettings.json加载),日志记录并配置Kestrel和/或IIS集成。
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
}
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.Build();
}
默认模板还引用Startup
类。此类不显式实现接口。相反,IWebHostBuilder
实现知道要查找 ConfigureServices
() 和 Configure
() 方法来分别设置依赖关系注入容器和中间件管道。
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
// 此方法由运行时调用。使用此方法配置 HTTP 请求管道。
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
在上面的 Startup
类中,我们将 MVC 服务添加到容器,添加异常处理和静态文件中间件,然后添加 MVC 中间件。MVC 中间件是最初构建应用程序的唯一真正实用的方法,它既适用于服务器呈现的视图,也适用于 RESTful API 端点。
ASP.NET Core 3.x/5:通用主机构建器
ASP.NET Core 3.x为 ASP.NET Core的启动代码带来了一些重大变化。以前,ASP.NET Core只能真正用于Web / HTTP工作负载,但在.NET Core 3.x中,已经进行了一项迁移以支持其他方法:长时间运行的"工作线程服务"(例如,用于使用消息队列),gRPC服务,Windows服务等。目标是与这些其他应用类型共享专门为构建 Web 应用(配置、日志记录、DI)而构建的基本框架。
结果是创建了一个"通用主机"(而不是Web主机),并在其上对 ASP.NET 核心堆栈进行了"重新平台化"。没有IWebHostBuilder
,而是IHostBuilder
。
同样,如果您有兴趣,我有一系列关于此迁移的同期文章!
此更改导致了一些不可避免的重大更改,但 ASP.NET 团队尽最大努力为针对IWebHostBuilder
而不是IHostBuilder
编写的所有代码提供路由。其中一种解决方法是在Program.cs模板中默认使用的 ConfigureWebHostDefaults
() 方法:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
};
}
}
ConfigureWebHostDefaults
需要注册 ASP.NET Core 应用的 Startup
类,这表明 .NET 团队在提供从 IWebHostBuilder
到 IHostBuilder
的迁移路径方面面临一个挑战。Startup
与 Web 应用密不可分,因为 Configure
() 方法与配置中间件有关。但是Worker服务和许多其他应用没有中间件,因此 Startup 类成为"通用主机"级别概念是没有意义的。
这就是IHostBuilder
上的ConservationWebHostDefaults
()扩展方法的用武之地。此方法将 IHostBuilder
包装在一个内部类 GenericWebHostBuilder
中,并设置 WebHost.CreateDefaultBuilder
() 在 ASP.NET Core 2.1 中执行的所有默认值。GenericWebHostBuilder
充当旧的 IWebHostBuilder
和新的 IHostBuilder
之间的适配器。
ASP.NET Core 3.x的另一个重大变化是引入了终结点路由。终结点路由是使概念可用的第一次尝试之一,这些概念以前仅限于 ASP.NET Core的MVC部分,在本例中为路由概念。这需要对中间件管道进行一些重新思考,但在许多情况下,必要的更改是最小的。
尽管有这些变化,ASP.NET Core 3.x中的Startup
类看起来与2.x版本非常相似。下面的示例几乎等同于 2.x 版本(不过我已切换到 Razor Pages 而不是 MVC)。
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
}
// 此方法由运行时调用。使用此方法配置 HTTP 请求管道。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
}
ASP.NET Core 5给现有应用程序带来的大改动相对较少,因此从3.x升级到5通常就像更改目标框架和更新一些NuGet包一样简单。🎉
对于 .NET 6,如果要升级现有应用程序,希望仍然如此。但对于新应用,默认的引导体验已完全改变...
ASP.NET 核心6:Web应用程序构建器
以前所有版本的 ASP.NET Core 已将配置拆分为 2 个文件。在 .NET 6 中,对 C#、BCL 和 ASP.NET Core 的大量更改意味着现在所有内容都可以在单个文件中。
请注意,没有什么能强制您使用此样式。我在 ASP.NET Core 3.x/5 代码中显示的所有代码在 .NET 6 中仍然有效!
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.MapGet("/", () => "Hello World!");
app.MapRazorPages();
app.Run();
这里有很多变化,但一些最明显的是:
- 顶级语句意味着可以没有
Program.Main()
样板。 - 隐式 using 指令意味着不需要 using 语句。我没有将它们包含在以前版本的代码段中,但是.NET 6不需要它们!
- 无
Startup
类 - 所有内容都在一个文件中。
这显然是减少了很多代码,但这是必要的吗?它只是为了减少而减少吗?它是如何工作的?
所有代码都去哪儿了?
.NET 6的一大焦点是"新手"的观点。作为 ASP.NET Core的初学者,您必须快速掌握大量概念。看看我的书的目录;有很多事情要让你头疼!
.NET 6 中的更改主要集中在删除与入门相关的"仪式"上,并隐藏了新手可能感到困惑的概念。例如:
- 开始时不需要
using
语句。尽管工具在实践中通常不会使这些成为问题,但在开始时,它们显然是一个不必要的概念。 - 与此类似,
namespace
在开始时是一个不必要的概念。 Program.Main()
...为什么叫这个名字?为什么我需要它?因为你有。除了现在你没有。- 配置不会在两个文件(Program.cs和Startup.cs之间拆分。虽然我喜欢这种"关注点的分离",但我不会错过解释为什么这种分离对新人来说是这样的。
- 当我们谈论
Startup
时,我们不再需要解释"魔术"方法,即使它们没有显式实现接口,也可以调用它们。
此外,我们还有新的 WebApplication
和 WebApplicationBuilder
类型。这些类型并不是实现上述目标所必需的,但它们确实提供了某种"更干净"的配置体验。
我们真的需要一种新的类型吗?
好吧,不,我们不需要它。我们可以编写一个 .NET 6 应用,该应用与使用通用主机的上述示例非常相似:
var hostBuilder = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddRazorPages();
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.Configure((ctx, app) =>
{
if (ctx.HostingEnvironment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", () => "Hello World!");
endpoints.MapRazorPages();
});
});
});
hostBuilder.Build().Run();
我认为你必须同意,这看起来比.NET 6 WebApplication
版本复杂得多。我们有一大堆嵌套的lambda,你必须确保你得到正确的重载,以便你可以访问配置(例如),一般来说,它将(主要是)过程引导脚本变成更复杂的东西。
WebApplicationBuilder
的另一个好处是,启动期间的异步代码要简单得多。您可以随时调用异步方法。这应该有望使我在 ASP.NET Core 3.x/5中写的这个系列过时!
关于WebApplicationBuilder
和WebApplication
的巧妙之处在于,它们本质上等同于上面的通用主机设置,但它们使用可以说是更简单的API来完成的。
大多数配置发生在WebApplicationBuilder中
让我们从WebApplicationBuilder开始。
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
WebApplicationBuilder负责4个主要的事情:
- 使用
builder.Configuration
添加配置 - 使用
builder.Services
添加服务 - 使用
builder.Logging
配置日志 - 生成 IHostBuilder 和 IWebHostBuilder 配置
依次来分析每一个...
WebApplicationBuilder
公开了 ConfigurationManager
类型,用于添加新的配置源以及访问配置值,如我在上一篇文章中所述。
它还公开一个 IServiceCollection
,用于将服务添加到 DI 容器。因此,对于通用主机,您必须执行以下操作
var hostBuilder = Host.CreateDefaultBuilder(args);
hostBuilder.ConfigureServices(services =>
{
services.AddRazorPages();
services.AddSingleton<MyThingy>();
})
使用WebApplicationBuilder
,你可以这样做
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSingleton<MyThingy>();
同样,对于日志记录,而不是这样做
var hostBuilder = Host.CreateDefaultBuilder(args);
hostBuilder.ConfigureLogging(builder =>
{
builder.AddFile();
})
你可以这样做:
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddFile();
这具有完全相同的行为,只是在更易于使用的API中。对于那些直接依赖 IHostBuilder
或 IWebHostBuilder
的扩展点,WebApplicationBuilder
分别公开属性 Host
和 WebHost
。
例如,Serilog的 ASP.NET Core集成与IHostBuilder
挂钩,因此在 ASP.NET Core 3.x/5中,您将使用以下命令添加它:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseSerilog() // <-- 添加此行
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
使用 WebApplicationBuilder
,您将在 Host 属性上调用 UseSerilog()
,而不是在生成器本身上进行调用:
builder.Host.UseSerilog();
实际上,WebApplicationBuilder
是您执行除中间件管道之外的所有配置的地方。
WebApplication 担任多项任务
在 WebApplicationBuilder
上配置完所有需要完成的内容后,您可以调用 Build()
来创建 WebApplication
的实例:
var app = builder.Build();
WebApplication
很有趣,因为它实现了多个不同的接口:
后两点非常相关。在 ASP.NET Core 3.x 和 5 中,IEndpointRouteBuilder
用于通过调用 UseEndpoints()
并向其传递 lambda 来添加终结点,例如:
public void Configure(IApplicationBuilder app)
{
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
对于刚接触 ASP.NET Core 的人来说,此 .NET 3.x/5 模式有一些复杂性:
- 中间件管道构建发生在
Startup
中的Configure()
函数中(你必须知道要看那里) - 您必须确保调用
app.UseEndpoints()
之前先调用app.UseRouting()
(以及将其他中间件放在正确的位置) - 您必须使用 lambda 来配置终节点(对于熟悉 C# 的用户来说并不复杂,但新手可能会感到困惑)
WebApplication
显著简化了此模式:
app.UseStaticFiles();
app.MapRazorPages();
这显然要简单得多,尽管我发现它有点令人困惑,因为中间件和终结点之间的区别远不如.NET 5.x等中那么清晰。这可能只是一个味道问题,但我认为它混淆了"订单很重要"的信息(这适用于中间件,但通常不适用于终结点)。
我还没有展示的是WebApplication
和WebApplicationBuilder如何构建的具体细节。在下一篇文章中,我将揭开帷幕,以便我们可以看到幕后到底发生了什么。
总结
在这篇文章中,我描述了 ASP.NET Core应用程序的引导如何从2.x版本一直更改为.NET 6。我展示了 .NET 6 中引入的新 WebApplication
和 WebApplicationBuilder
类型,讨论了引入它们的原因,以及它们带来的一些优势。最后,我讨论了这两个类所扮演的不同角色,以及它们的 API 如何简化启动体验。在下一篇文章中,我将查看类型背后的一些代码,以了解它们的工作原理。
Any fool can write code that a computer can understand. Good programmers write code that humans can understand. –Martin Fowler