Loading

比较 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()。在这篇文章中,我将这种方法与以前的方法进行比较,讨论进行更改的原因,并查看其影响。在下一篇文章中,我将介绍 WebApplicationWebApplicationBuilder 背后的代码,以了解它们的工作原理。

构建 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.csStartup.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 团队在提供从 IWebHostBuilderIHostBuilder 的迁移路径方面面临一个挑战。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时,我们不再需要解释"魔术"方法,即使它们没有显式实现接口,也可以调用它们。

此外,我们还有新的 WebApplicationWebApplicationBuilder 类型。这些类型并不是实现上述目标所必需的,但它们确实提供了某种"更干净"的配置体验。

我们真的需要一种新的类型吗?

好吧,不,我们不需要它。我们可以编写一个 .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中写的这个系列过时!

关于WebApplicationBuilderWebApplication的巧妙之处在于,它们本质上等同于上面的通用主机设置,但它们使用可以说是更简单的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中。对于那些直接依赖 IHostBuilderIWebHostBuilder 的扩展点,WebApplicationBuilder 分别公开属性 HostWebHost

例如,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 中引入的新 WebApplicationWebApplicationBuilder 类型,讨论了引入它们的原因,以及它们带来的一些优势。最后,我讨论了这两个类所扮演的不同角色,以及它们的 API 如何简化启动体验。在下一篇文章中,我将查看类型背后的一些代码,以了解它们的工作原理。




Comparing WebApplicationBuilder to the Generic Host

posted @ 2022-03-06 11:39  GerryGe  阅读(496)  评论(1编辑  收藏  举报