AI智能
改变未来

理解ASP.NET Core – [01] Startup

注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

准备工作:一份ASP.NET Core Web API应用程序

当我们来到一个陌生的环境,第一件事就是找到厕所在哪。

当我们接触一份新框架时,第一件事就是找到程序入口,即Main方法

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>();});}

代码很简单,典型的建造者模式:通过

IHostBuilder

创建一个

通用主机(Generic Host)

,然后启动它(至于什么是通用主机,咱们后续的文章会说到)。咱们不要一上来就去研究

CreateDefaultBuilder

ConfigureWebHostDefaults

这些方法的源代码,应该去寻找能看的见、摸得着的,很明显,只有

Startup

Startup类

Startup

类承担应用的启动任务,所以按照约定,起名为

Startup

,不过你可以修改为任意类名(强烈建议类名为Startup)。

默认的

Startup

结构很简单,包含:

  • 构造函数
  • Configuration

    属性

  • ConfigureServices

    方法

  • Configure

    方法

public class Startup{public Startup(IConfiguration configuration){Configuration = configuration;}public IConfiguration Configuration { get; }// This method gets called by the runtime. Use this method to add services to the container.// 该方法由运行时调用,使用该方法向DI容器添加服务public void ConfigureServices(IServiceCollection services){}// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.// 该方法由运行时调用,使用该方法配置HTTP请求管道public void Configure(IApplicationBuilder app, IWebHostEnvironment env){}}

Startup构造函数

当使用通用主机(Generic Host)时,Startup构造函数支持注入以下三种服务类型:

  • IConfiguration
  • IWebHostEnvironment
  • IHostEnvironment
public Startup(IConfiguration configuration,IHostEnvironment hostEnvironment,IWebHostEnvironment webHostEnvironment){Configuration = configuration;HostEnvironment = hostEnvironment;WebHostEnvironment = webHostEnvironment;}public IConfiguration Configuration { get; }public IHostEnvironment HostEnvironment { get; set; }public IWebHostEnvironment WebHostEnvironment { get; set; }

这里你会发现

HostEnvironment

WebHostEnvironment

的实例是同一个。别着急,后续文章我们聊到Host的时候,你就明白了。

ConfigureServices

  • 该方法是可选
  • 该方法用于添加服务到DI容器中
  • 该方法
    Configure

    方法之前被调用

  • 该方法要么无参数,要么只能有一个参数且类型必须为
    IServiceCollection
  • 该方法内的代码大多是形如
    Add{Service}

    的扩展方法

常用的服务有(部分服务框架已默认注册):

  • AddControllers

    :注册Controller相关服务,内部调用了

    AddMvcCore

    AddApiExplorer

    AddAuthorization

    AddCors

    AddDataAnnotations

    AddFormatterMappings

    等多个扩展方法

  • AddOptions

    :注册Options相关服务,如

    IOptions<>

    IOptionsSnapshot<>

    IOptionsMonitor<>

    IOptionsFactory<>

    IOptionsMonitorCache<>

    等。很多服务都需要Options,所以很多服务注册的扩展方法会在内部调用

    AddOptions
  • AddRouting

    :注册路由相关服务,如

    IInlineConstraintResolver

    LinkGenerator

    IConfigureOptions<RouteOptions>

    RoutePatternTransformer

  • AddAddLogging

    :注册Logging相关服务,如

    ILoggerFactory

    ILogger<>

    IConfigureOptions<LoggerFilterOptions>>

  • AddAuthentication

    :注册身份认证相关服务,以方便后续注册JwtBearer、Cookie等服务

  • AddAuthorization

    :注册用户授权相关服务

  • AddMvc

    :注册Mvc相关服务,比如Controllers、Views、RazorPages等

  • AddHealthChecks

    :注册健康检查相关服务,如

    HealthCheckService

    IHostedService

Configure

  • 该方法是必须
  • 该方法用于配置HTTP请求管道,通过向管道添加中间件,应用不同的响应方式。
  • 该方法
    ConfigureServices

    方法之后被调用

  • 该方法中的参数可以接受任何已注入到DI容器中的服务
  • 该方法内的代码大多是形如
    Use{Middleware}

    的扩展方法

  • 该方法内中间件的注册顺序与代码的书写顺序是一致的,先注册的先执行,后注册的后执行

常用的中间件有

  • UseDeveloperExceptionPage

    :当发生异常时,展示开发人员异常信息页。如图

  • UseRouting

    :路由中间件,根据Url中的路径导航到对应的Endpoint。必须与

    UseEndpoints

    搭配使用。

  • UseEndpoints

    :执行路由所选择的Endpoint对应的委托。

  • UseAuthentication

    :身份认证中间件,用于对请求用户的身份进行认证。比如,早晨上班打卡时,管理员认出你是公司员工,那么才允许你进入公司。

  • UseAuthorization

    :用户授权中间件,用于对请求用户进行授权。比如,虽然你是公司员工,但是你是一名.NET开发工程师,那么你只允许坐在.NET开发工程师区域的工位上,而不能坐在老总的办公室里。

  • UseMvc

    :Mvc中间件。

  • UseHealthChecks

    :健康检查中间件。

  • UseMiddleware

    :用来添加匿名中间件的,通过该方法,可以方便的添加自定义中间件。

省略Startup类

另外,

Startup

类也可以省略,直接进行如下配置即可(虽然可以这样做,但是不推荐):

public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>{// ConfigureServices 可以调用多次,最终会将结果聚合webBuilder.ConfigureServices(services =>{})// Configure 如果调用多次,则只有最后一次生效.Configure(app =>{var env = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();});});

IStartupFilter

public interface IStartupFilter{Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);}

有时,我们想要将一系列相关中间件的注册封装到一起,那么我们只需要通过实现

IStartupFilter

,并在

Startup.ConfigureServices

中配置

IStartupFilter

的依赖注入即可。

  • IStartupFilter

    中配置的中间件,总是比

    Startup

    类中

    Configure

    方法中的中间件先注册;对于多个

    IStartupFilter

    实现,执行顺序与服务注册时的顺序一致

我们可以通过一个例子来验证一下中间件的注册顺序。

首先是三个

IStartupFilter

的实现类:

public class FirstStartupFilter : IStartupFilter{public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)=> app =>{app.Use((context, next) =>{Console.WriteLine("First");return next();});next(app);};}public class SecondStartupFilter : IStartupFilter{public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)=> app =>{app.Use((context, next) =>{Console.WriteLine("Second");return next();});next(app);};}public class ThirdStartupFilter : IStartupFilter{public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)=> app =>{app.Use((context, next) =>{Console.WriteLine("Third");return next();});next(app);};}

接下来进行注册:

public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).ConfigureServices(services =>{// 第一个被注册services.AddTransient<IStartupFilter, FirstStartupFilter>();}).ConfigureWebHostDefaults(webBuilder =>{webBuilder.UseStartup<Startup>();}).ConfigureServices(services =>{// 第三个被注册services.AddTransient<IStartupFilter, ThirdStartupFilter>();});public class Startup{public void ConfigureServices(IServiceCollection services){// 第二个被注册services.AddTransient<IStartupFilter, SecondStartupFilter>();}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){// 第四个被注册app.Use((context, next) =>{Console.WriteLine("Forth");return next();});}}

最后通过输出可以看到,执行顺序的确是这样子的。

FirstSecondThirdForth

IHostingStartup

IStartupFilter

不同的是,

IHostingStartup

可以在启动时通过外部程序集向应用增加更多功能。不过这要求必须调用

ConfigureWebHost

ConfigureWebHostDefaults

等类似用来配置Web主机的扩展方法

我们经常使用的Nuget包

SkyApm.Agent.AspNetCore

就使用了该特性。

下面我们就来看一下该如何使用它。

HostingStartup 程序集

要创建HostingStartup程序集,可以通过创建类库项目或无入口点的控制台应用来实现。

接下来咱们还是看一下上面提到过的

SkyApm.Agent.AspNetCore

using SkyApm.Agent.AspNetCore;[assembly: HostingStartup(typeof(SkyApmHostingStartup))]namespace SkyApm.Agent.AspNetCore{internal class SkyApmHostingStartup : IHostingStartup{public void Configure(IWebHostBuilder builder){builder.ConfigureServices(services => services.AddSkyAPM(ext => ext.AddAspNetCoreHosting()));}}}

该HostingStartup类:

  • 实现了
    IHostingStartup

    接口

  • Configure

    方法中使用

    IWebHostBuilder

    来添加增强功能

  • 配置了
    HostingStartup

    特性

HostingStartup 特性

HostingStartup

特性用于标识哪个类是HostingStartup类,HostingStartup类需要实现

IHostingStartup

接口。

当程序启动时,会自动扫描入口程序集和配置的待激活的的程序集列表(参见下方:激活HostingStarup程序集),来找到所有的

HostingStartup

特性,并通过反射的方式创建

Startup

并调用

Configure

方法。

SkyApm.Agent.AspNetCore

为例

using SkyApm.Agent.AspNetCore;[assembly: HostingStartup(typeof(SkyApmHostingStartup))]namespace SkyApm.Agent.AspNetCore{internal class SkyApmHostingStartup : IHostingStartup{public void Configure(IWebHostBuilder builder){builder.ConfigureServices(services => services.AddSkyAPM(ext => ext.AddAspNetCoreHosting()));}}}

激活HostingStarup程序集

要激活HostingStarup程序集,我们有两种配置方式:

1.使用环境变量(推荐)

使用环境变量,无需侵入程序代码,所以我更推荐大家使用这种方式。

配置环境变量

ASPNETCORE_HOSTINGSTARTUPASSEMBLIES

,多个程序集使用分号(;)进行分隔,用于添加要激活的程序集。变量

WebHostDefaults.HostingStartupAssembliesKey

就是指代这个环境变量的Key。

另外,还有一个环境变量,叫做

ASPNETCORE_HOSTINGSTARTUPEXCLUDEASSEMBLIES

,多个程序集使用分号(;)进行分隔,用于排除要激活的程序集。变量

WebHostDefaults.HostingStartupExcludeAssembliesKey

就是指代这个环境变量的Key。

我们在 launchSettings.json 中添加两个程序集:

"environmentVariables": {"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "SkyAPM.Agent.AspNetCore;HostingStartupLibrary"}

2.在程序中配置

public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>{webBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey,"SkyAPM.Agent.AspNetCore;HostingStartupLibrary").UseStartup<Startup>();});

这样就配置完成了,很??的一个功能点吧!

需要注意的是,无论使用哪种配置方式,当存在多个HostingStartup程序集时,将按配置这些程序集时的书写顺序执行

Configure

方法。

多环境配置

一款软件,一般要经过需求分析、设计编码,单元测试、集成测试以及系统测试等一系列测试流程,验收,最终上线。那么,就至少需要4套环境来保证系统运行:

  • Development

    :开发环境,用于开发人员在本地对应用进行调试运行

  • Test

    :测试环境,用于测试人员对应用进行测试

  • Staging

    :预发布环境,用于在正式上线之前,对应用进行集成、测试和预览,或用于验收

  • Production

    :生产环境,应用的正式线上环境

环境配置方式

通过环境变量

ASPNETCORE_ENVIRONMENT

指定运行环境

注意:如果未指定环境,默认情况下,为 Production

在项目的Properties文件夹里面,有一个“launchSettings.json”文件,该文件是用于配置VS中项目启动的。接下来我们就在

launchSettings.json

中配置一下。
先解释一下该文件中出现的几个参数:

  • commandName

    :指定要启动的Web服务器,有三个可选值:Project:启动 Kestrel

  • IISExpress:启动IIS Express
  • IIS:不启用任何Web服务器,使用IIS
  • dotnetRunMessages

    :bool字符串,指示当使用 dotnet run 命令时,终端能够及时响应并输出消息,具体参考stackoverflow和github issue

  • launchBrowser

    :bool值,指示当程序启动后,是否打开浏览器

  • launchUrl

    :默认启动路径

  • applicationUrl

    :应用程序Url列表,多个URL之间使用分号(

    ;

    )进行分隔。当launchBrowser为true时,将/作为浏览器默认访问的Url

  • environmentVariables

    :环境变量集合,在该集合内配置环境变量

  • {"$schema": "http://json.schemastore.org/launchsettings.json","profiles": {// 如果不指定profile,则默认选择第一个// Development"ASP.NET.WebAPI": {"commandName": "Project","dotnetRunMessages": "true","launchBrowser": true,"launchUrl": "weatherforecast","applicationUrl": "http://localhost:5000","environmentVariables": {"ASPNETCORE_ENVIRONMENT": "Development"}},// Test"ASP.NET.WebAPI.Test": {"commandName": "Project","dotnetRunMessages": "true","launchBrowser": true,"launchUrl": "weatherforecast","applicationUrl": "http://localhost:5000","environmentVariables": {"ASPNETCORE_ENVIRONMENT": "Test"}},// Staging"ASP.NET.WebAPI.Staging": {"commandName": "Project","dotnetRunMessages": "true","launchBrowser": true,"launchUrl": "weatherforecast","applicationUrl": "http://localhost:5000","environmentVariables": {"ASPNETCORE_ENVIRONMENT": "Staging"}},// Production"ASP.NET.WebAPI.Production": {"commandName": "Project","dotnetRunMessages": "true","launchBrowser": true,"launchUrl": "weatherforecast","applicationUrl": "http://localhost:5000","environmentVariables": {"ASPNETCORE_ENVIRONMENT": "Production"}},// 用于测试在未指定环境时,默认是否为Production"ASP.NET.WebAPI.Default": {"commandName": "Project","dotnetRunMessages": "true","launchBrowser": true,"launchUrl": "weatherforecast","applicationUrl": "http://localhost:5000"}}}

    配置完成后,就可以在VS上方工具栏中的项目启动处选择启动项了

    基于环境的 Startup

    Startup类支持针对不同环境进行个性化配置,有三种方式:

    1. IWebHostEnvironment

      注入 Startup 类

    2. Startup 方法约定
    3. Startup 类约定

    1.将

    IWebHostEnvironment

    注入 Startup 类

    通过将

    IWebHostEnvironment

    注入 Startup 类,然后在方法中使用条件判断书写不同环境下的代码。该方式适用于多环境下,代码差异较少的情况。

    public class Startup{public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment){Configuration = configuration;WebHostEnvironment = webHostEnvironment;}public IConfiguration Configuration { get; }public IWebHostEnvironment WebHostEnvironment { get; }public void ConfigureServices(IServiceCollection services){if (WebHostEnvironment.IsDevelopment()){Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");}else if (WebHostEnvironment.IsTest()){Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");}else if (WebHostEnvironment.IsStaging()){Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");}else if (WebHostEnvironment.IsProduction()){Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");}}public void Configure(IApplicationBuilder app){if (WebHostEnvironment.IsDevelopment()){Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");}else if (WebHostEnvironment.IsTest()){Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");}else if (WebHostEnvironment.IsStaging()){Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");}else if (WebHostEnvironment.IsProduction()){Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");}}}public static class AppHostEnvironmentEnvExtensions{public static bool IsTest(this IHostEnvironment hostEnvironment){if (hostEnvironment == null){throw new ArgumentNullException(nameof(hostEnvironment));}return hostEnvironment.IsEnvironment(AppEnvironments.Test);}}public static class AppEnvironments{public static readonly string Test = nameof(Test);}

    2.Startup 方法约定

    上面的方式把不同环境的代码放在了同一个方法中,看起来比较混乱也不容易区分。因此我们希望

    ConfigureServices

    Configure

    能够根据不同的环境进行代码拆分。

    我们可以通过方法命名约定来解决,约定

    Configure{EnvironmentName}Services

    Configure{EnvironmentName}Services

    来装载不同环境的代码。如果当前环境没有对应的方法,则使用原来的

    ConfigureServices

    Configure

    方法。

    我就只拿 Development 和 Production 举例了

    public class Startup{// 我这里注入 IWebHostEnvironment,仅仅是为了打印出来当前环境信息public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment){Configuration = configuration;WebHostEnvironment = webHostEnvironment;}public IConfiguration Configuration { get; }public IWebHostEnvironment WebHostEnvironment { get; }#region ConfigureServicesprivate void StartupConfigureServices(IServiceCollection services){Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");}public void ConfigureDevelopmentServices(IServiceCollection services){StartupConfigureServices(services);}public void ConfigureProductionServices(IServiceCollection services){StartupConfigureServices(services);}public void ConfigureServices(IServiceCollection services){StartupConfigureServices(services);}#endregion#region Configureprivate void StartupConfigure(IApplicationBuilder app){Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");}public void ConfigureDevelopment(IApplicationBuilder app){StartupConfigure(app);}public void ConfigureProduction(IApplicationBuilder app){StartupConfigure(app);}public void Configure(IApplicationBuilder app){StartupConfigure(app);}#endregion}

    3.Startup 类约定

    该方式适用于多环境下,代码差异较大的情况。

    程序启动时,会优先寻找当前环境命名符合

    Startup{EnvironmentName}

    的 Startup 类,如果找不到,则使用名称为

    Startup

    的类

    首先,

    CreateHostBuilder

    方法需要做一处修改

    public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>{//webBuilder.UseStartup<Startup>();webBuilder.UseStartup(typeof(Startup).GetTypeInfo().Assembly.FullName);});

    接下来,就是为各个环境定义 Startup 类了(我就只拿 Development 和 Production 举例了)

    public class StartupDevelopment{// 我这里注入 IWebHostEnvironment,仅仅是为了打印出来当前环境信息public StartupDevelopment(IConfiguration configuration, IWebHostEnvironment webHostEnvironment){Configuration = configuration;WebHostEnvironment = webHostEnvironment;}public IConfiguration Configuration { get; }public IWebHostEnvironment WebHostEnvironment { get; }public void ConfigureServices(IServiceCollection services){Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");}public void Configure(IApplicationBuilder app){Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");}}public class StartupProduction{public StartupProduction(IConfiguration configuration, IWebHostEnvironment webHostEnvironment){Configuration = configuration;WebHostEnvironment = webHostEnvironment;}public IConfiguration Configuration { get; }public IWebHostEnvironment WebHostEnvironment { get; }public void ConfigureServices(IServiceCollection services){Console.WriteLine($"{nameof(ConfigureServices)}: {WebHostEnvironment.EnvironmentName}");}public void Configure(IApplicationBuilder app){Console.WriteLine($"{nameof(Configure)}: {WebHostEnvironment.EnvironmentName}");}}
    赞(0) 打赏
    未经允许不得转载:爱站程序员基地 » 理解ASP.NET Core – [01] Startup