ASP.NET Core 运行原理解剖[2]:Hosting补充之配置介绍
在上一章中,我们介绍了 ASP.NET Core 的启动过程,主要是对 WebHost 源码的探索。而本文则是对上文的一个补充,更加偏向于实战,详细的介绍一下我们在实际开发中需要对 Hosting 做一些配置时经常用到的几种方式。
目录
本系列文章从源码分析的角度来探索 ASP.NET Core 的运行原理,分为以下几个章节:
ASP.NET Core 运行原理解剖[1]:Hosting
ASP.NET Core 运行原理解剖[2]:Hosting补充之配置介绍(Current)
ASP.NET Core 运行原理解剖[3]:Middleware-请求管道的构成
ASP.NET Core 运行原理解剖[4]:进入HttpContext的世界
ASP.NET Core 运行原理解剖[5]:Authentication
WebHostBuild
WebHostBuild 用来构建 WebHost ,也是我们最先接触的一个类,它提供了如下方法:
ConfigureAppConfiguration
Configuration 在 ASP.NET Core 进行了全新的设计,使其更加灵活简洁,可以支持多种数据源。在 ASP.NET Core 1.x 中,我们是在Startup
的构造函数中配置各种数据源的,而在 ASP.NET Core 2.0 中则移动了到Program
中,这样能与控制台应用程序保持一致:
public static class WebHostBuilderExtensions
{
public static IWebHostBuilder ConfigureAppConfiguration(this IWebHostBuilder hostBuilder, Action<IConfigurationBuilder> configureDelegate)
{
return hostBuilder.ConfigureAppConfiguration((context, builder) => configureDelegate(builder));
}
}
public class WebHostBuilder : IWebHostBuilder
{
private List<Action<WebHostBuilderContext, IConfigurationBuilder>> _configureAppConfigurationBuilderDelegates;
public IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate)
{
if (configureDelegate == null)
{
throw new ArgumentNullException(nameof(configureDelegate));
}
_configureAppConfigurationBuilderDelegates.Add(configureDelegate);
return this;
}
}
而_configureAppConfigurationBuilderDelegates
委托会在 WebHostBuilder 的BuildCommonServices
方法中执行,最后生成 IConfiguration 对象并以单例的形式注册到 DI 系统中, 我们可以在Startup
以及应用程序的任何地方,通过 DI 系统来获取到:
foreach (var configureAppConfiguration in _configureAppConfigurationBuilderDelegates)
{
configureAppConfiguration(_context, builder);
}
var configuration = builder.Build();
services.AddSingleton<IConfiguration>(configuration);
而在 上一章 中也介绍过,在CreateDefaultBuilder
中会通过该方法来添加appsettinggs.json
等基本配置的配置源。
UseSetting
UseSetting 是一个非常重要的方法,它用来配置 WebHost 中的 IConfiguration 对象。需要注意与上面ConfigureAppConfiguration
的区别, WebHost 中的 Configuration 只限于在 WebHost 使用,并且我们不能配置它的数据源,它只会读取ASPNETCORE_
开头的环境变量:
private IConfiguration _config;
public WebHostBuilder()
{
_config = new ConfigurationBuilder()
.AddEnvironmentVariables(prefix: "ASPNETCORE_")
.Build();
}
而我们比较熟悉的当前执行环境,也是通过该_config
来读取的,虽然我们不能配置它的数据源,但是它为我们提供了一个UseSetting
方法,为我们提供了一个设置_config
的机会:
public string GetSetting(string key)
{
return _config[key];
}
而我们通过UseSetting
设置的变量最终也会以MemoryConfigurationProvider
的形式添加到上面介绍的ConfigureAppConfiguration
所配置的IConfiguration
对象中。
UseStartup
UseStartup 这个我们都比较熟悉,它用来显式注册我们的Startup
类,可以使用泛性,Type , 和程序集名称三种方式来注册:
// 常用的方法
public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder) where TStartup : class
{
return hostBuilder.UseStartup(typeof(TStartup));
}
// 通过指定的程序集来注册 Startup 类
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, string startupAssemblyName)
{
if (startupAssemblyName == null)
{
throw new ArgumentNullException(nameof(startupAssemblyName));
}
return hostBuilder
.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName)
.UseSetting(WebHostDefaults.StartupAssemblyKey, startupAssemblyName);
}
// 最终的 Startup 类注册方法,上面两种只是一种简写形式
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType)
{
....
}
具体的注册方式,在 上一章 也介绍过,就是通过反射创建实例,然后注入到 DI 系统中。
ConfigureLogging
ConfigureLogging 用来配置日志系统,在 ASP.NET Core 1.x 中是在Startup
类的Configure
方法中,通过ILoggerFactory
扩展来注册的,在 ASP.NET Core 中也变得更加简洁,并且统一通过 WebHostBuild 来配置:
public static class WebHostBuilderExtensions
{
public static IWebHostBuilder ConfigureLogging(this IWebHostBuilder hostBuilder, Action<ILoggingBuilder> configureLogging)
{
return hostBuilder.ConfigureServices(collection => collection.AddLogging(configureLogging));
}
public static IWebHostBuilder ConfigureLogging(this IWebHostBuilder hostBuilder, Action<WebHostBuilderContext, ILoggingBuilder> configureLogging)
{
return hostBuilder.ConfigureServices((context, collection) => collection.AddLogging(builder => configureLogging(context, builder)));
}
}
AddLogging 是Microsoft.Extensions.Logging
提供的扩展方法,更具体的可以看我之前介绍的 ASP.NET Core 源码学习之 Logging 系列。
ConfigureServices
在上面的几个方法中,多次用到 ConfigureServices,而 ConfigureServices 与 Starup 中的 ConfigureServices 类似,都是用来注册服务的:
private readonly List<Action<WebHostBuilderContext, IServiceCollection>> _configureServicesDelegates;
public IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices)
{
if (configureServices == null)
{
throw new ArgumentNullException(nameof(configureServices));
}
_configureServicesDelegates.Add(configureServices);
return this;
}
但不同的是_configureServicesDelegates
的执行时机较早,是在WebHostBuilder的Build
方法中执行的,所以会参与 WebHost 中hostingServiceProvider
的构建。
其它
WebHostBuild 中还有很多配置的方法,就不再一一细说,在这里简单介绍一下:
-
UseContentRoot 使用
UseSetting
方法配置IConfiguration["contentRoot"]
,表示应用程序所在的默认文件夹地址,如 MVC 中视图的查询根目录。 -
UseWebRoot 使用
UseSetting
方法配置IConfiguration["webroot"]
,用来指定可让外部可访问的静态资源路径,默认为wwwroot
,并且是以contentRoot
为根目录。 -
CaptureStartupErrors 使用
UseSetting
方法配置IConfiguration["captureStartupErrors"]
,表示是否捕捉启动时的异常,如果为ture
,则在启动时发生异常也会启动 Http Server,并显示错误页面,否则,不会启动 Http Server。 -
UseEnvironment 使用
UseSetting
方法配置IConfiguration["environment"]
,用来指定执行环境。 -
UseServer 用来配置 Http Server 服务,
UseKestrel
便是此方法的简写形式。 -
UseUrls 使用
UseSetting
方法配置IConfiguration["urls"]
,用来配置 Http 服务器地址,多个使用;
分割。 -
UseShutdownTimeout 使用
UseSetting
方法配置IConfiguration["shutdownTimeoutSeconds"]
,用来设置 ASP.NET Core 停止时等待的时间。 -
DetailedErrors 表示是否显示详细的错误信息,可为
true/false
或1/0
,默认为 false,但它没有提供直接配置的方法,可以通过UseSetting
来指定IConfiguration["detailedErrors"]
。
ISartup
ISartup 是我们比较熟悉的,因为在我们创建一个默认的 ASP.NET Core 项目时,都会有一个Startup.cs
文件,包含三个约定的方法,按执行顺序排列如下:
1. ConfigureServices
ASP.NET Core 框架本身提供了一个 DI(依赖注入)系统,并且可以非常灵活的去扩展,很容易的切换成其它的 DI 框架(如 Autofac,Ninject 等)。在 ASP.NET Core 中,所有的实例都是通过这个 DI 系统来获取的,并要求我们的应用程序也使用 DI 系统,以便我们能够开发出更具弹性,更易维护,测试的应用程序。总之在 ASP.NET Core 中,一切皆注入。关于 “依赖注入” 这里就不再多说。
在 DI 系统中,想要获取服务,首先要进行注册,而ConfigureServices
方法便是用来注册服务的。
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IUserService, UserService>();
}
如上,我们为IUserService
接口注册了一个UserService
类型的实例。
2. ConfigureContainer(不常用)
ConfigureContainer 是用来替换 DI 框架的,如下,我们将 ASP.NET Core 内置的 DI 框架替换为 Autofac :
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterModule(new AutofacModule());
}
虽然 ASP.NET Core 自带的 DI 系统只提供了构造函数注入,以及不支持命名实例等,但我喜欢它的简洁,并且不太喜欢依赖太多第三库,一直也只使用了内置的DI框架,因此对这个方法也不太了解,就不再多说。
3. Configure
Configure 接收一个IApplicationBuilder
类型参数,而IApplicationBuilder
在 上一章 中介绍过,它是用来构建请求管道的,因此,也可以说 Configure 方法是用来配置请求管道的,通常会在这里会注册一些中间件。
public void Configure(IApplicationBuilder app)
{
app.Use(next =>
{
return async (context) =>
{
await context.Response.WriteAsync("Hello ASP.NET Core!");
};
});
}
所谓中间件,也就是对 HttpContext 进行处理的一种便捷方式,下文会详细来介绍。而如上代码,我们注册了一个最简单的中间件,通过浏览器访问,便可以看到 “Hello ASP.NET Core!” 。
通常,我们的 Startup 类并没有去实现IStartup
接口,这是因为我们在Configure
方法中,大多时候可能需要获取一些其它的服务,如我刚才注册的IUserService
,我们可以直接添加到 Configure 方法的参数列表当中:
public void Configure(IApplicationBuilder app, IUserService userService) { }
ASP.NET Core 会通过 DI 系统来解析到 userService 实例,但是 ASP.NET Core 中的 DI 系统是不支持普通方法的参数注入的,而是手动通过反射的方式来实现的:
services.AddSingleton(typeof(IStartup), sp =>
{
var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>();
var methods = StartupLoader.LoadMethods(sp, startupType, hostingEnvironment.EnvironmentName);
return new ConventionBasedStartup(methods);
});
而通过反射也可以为我们带来更大的灵活性,上面的LoadMethods
方法会根据当前的执行环境名称来查找适当的方法名:
public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName)
{
var configureMethod = FindConfigureDelegate(startupType, environmentName);
}
private static ConfigureBuilder FindConfigureDelegate(Type startupType, string environmentName)
{
var configureMethod = FindMethod(startupType, "Configure{0}", environmentName, typeof(void), required: true);
return new ConfigureBuilder(configureMethod);
}
更具体的可以查看 StartupLoader,ASP.NET Core 会根据当前环境的不同,而执行不同的方法:
public void ConfigureServices(IServiceCollection services) { }
public void ConfigureDevelopmentServices(IServiceCollection services) { }
public void ConfigureContainer(ContainerBuilder builder) {}
public void ConfigureDevelopmentContainer(ContainerBuilder builder) { }
public void Configure(IApplicationBuilder app) { }
public void ConfigureDevelopment(IApplicationBuilder app) { }
如上,当在Development
环境上执行时,会选择带Development
的方法来执行。
而在默认模版中是通过UseStartup<Startup>
的方式来注册 Startup 类的,我们也可以使用上面介绍的指定程序集名称的方式来注册:
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup("EmptyWebDemo")
.Build();
如上,我们指定在 EmptyWebDemo 中查找Startup
类,这样还有一个额外的好处,WebHost 同样会根据当前的执行环境来选择不同的Startup
类(如StartupDevelopment
),与上面介绍的Startup中方法的查询方式一样。
IHostingStartup
上面,我们介绍了Sartup
,而一个项目中只能一个Sartup
,因为如果配置多个,则最后一个会覆盖之前的。而在一个多层项目中,Sartup类一般是放在展现层中,我们在其它层也需要注册一些服务或者配置请求管道时,通常会写一个扩展方法:
public static class EfRepositoryExtensions
{
public static void AddEF(this IServiceCollection services,string connectionStringName)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionStringName), opt => opt.EnableRetryOnFailure())
);
services.TryAddScoped<IDbContext, AppDbContext>();
services.TryAddScoped(typeof(IRepository<,>), typeof(EfRepository<,>));
...
}
public static void UseEF(IApplicationBuilder app)
{
app.UseIdentity();
}
}
然后在 Startup 中调用这些扩展方法:
public void ConfigureDevelopmentServices(IServiceCollection services)
{
services.AddEF(Configuration.GetConnectionString("DefaultConnection");
}
public void ConfigureDevelopment(IApplicationBuilder app)
{
services.UseEF();
}
感觉这种方式非常丑陋,而在上一章中,我们知道 WebHost 会在 Starup 这前调用 IHostingStartup,于是我们便以如下方式来实现:
[assembly: HostingStartup(typeof(Zero.EntityFramework.EFRepositoryStartup))]
namespace Zero.EntityFramework
{
public class EFRepositoryStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionStringName), opt => opt.EnableRetryOnFailure())
);
services.TryAddScoped<IDbContext, AppDbContext>();
services.TryAddScoped(typeof(IRepository<,>), typeof(EfRepository<,>));
...
});
builder.Configure(app => {
app.UseIdentity();
});
}
}
}
如上,只需实现 IHostingStartup 接口,要清爽简单的多,怎一个爽字了得!不过,还需要进行注册才会被WebHost执行,首先要指定HostingStartupAttribute
程序集特性,其次需要配置 WebHost 中的 IConfiguration[hostingStartupAssemblies]
,以便 WebHost 能找到我们的程序集,可以使用如下方式配置:
WebHost.CreateDefaultBuilder(args)
// 如需指定多个程序集时,使用 ; 分割
.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "Zero.Application;Zero.EntityFramework")
这样便完成了 IHostingStartup 注册,不过还需要将包含IHostingStartup
的程序集放到 Bin 目录下,否则根本无法加载。不过 ASP.NET Core 也提供了类似插件的方式来指定IHostingStartup
程序集的查找位置,可通过设置DOTNET_ADDITIONAL_DEPS
和ASPNETCORE_HOSTINGSTARTUPASSEMBLIES
来实现,而这里就不再多说。
IHostingStartup 是由 WebHostBuilder 来调用的,执行时机较早,在创建 WebHost 之前执行,因此可以替换一些在 WebHost 中需要使用的服务。
IStartupFilter
IStartupFilter 是除Startup
和HostingStartup
之处另一种配置IApplicationBuilder
的方式:
public interface IStartupFilter
{
Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
}
它只有一个Configure
方法,是对 Starup 类中Configure
方法的拦截器,给我们一个在Configure
方法执行之前进行一些配置的机会。
让我们实践一把,先定义2个 StartupFilter:
public class A : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
Console.WriteLine("This is A1!");
return app =>
{
Console.WriteLine("This is A2!");
next(app);
};
}
}
public class B : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
Console.WriteLine("This is B1!");
return app =>
{
Console.WriteLine("This is B2!");
next(app);
};
}
}
然后让他们注册到DI系统中,WebHost 在执行 Starup 类中Configure
方法之前,会从 DI 系统中获取所有的IStartupFilter
来执行:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IStartupFilter, A>();
services.AddSingleton<IStartupFilter, B>();
}
public void Configure(IApplicationBuilder app)
{
Console.WriteLine("This is Configure!");
app.Use(next =>
{
return async (context) =>
{
await context.Response.WriteAsync("Hello ASP.NET Core!");
};
});
}
最终,它他的执行顺序为:B1 -> A1 -> A2 -> B2 -> Configure 。
IHostedService
当我们希望随着 ASP.NET Core 的启动,来执行一些后台任务(如:定期的刷新缓存等)时,并在 ASP.NET Core 停止时,可以优雅的关闭,则可以使用IHostedService
,它有如下定义:
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
很简单,只有开始和停止两个方法,它的用法大概是这个样子的:
public class CacheHostService : IHostedService
{
private readonly ICacheService _cacheService;
private CancellationTokenSource _cts;
private Task _executingTask;
public CacheHostService(ICacheService cacheService)
{
_cacheService = cacheService;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_executingTask = Task.Run(async () =>
{
while (!_cts.IsCancellationRequested)
{
Console.WriteLine("cancellationToken:" + _cts.IsCancellationRequested);
await _cacheService.Refresh();
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
}
});
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
// 发送停止信号,以通知我们的后台服务结束执行。
_cts.Cancel();
// 等待后台服务的停止,而 ASP.NET Core 大约会等待5秒钟(可在上面介绍的UseShutdownTimeout方法中配置),如果还没有执行完会发送取消信号,以防止无限的等待下去。
await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));
cancellationToken.ThrowIfCancellationRequested();
}
}
如上,我们定义了一个在台后每5秒刷新一次缓存的服务,并在 ASP.NET Core 程序停止时,优雅的关闭。最后,将它注册到 DI 系统中即可:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ICacheService, CacheService>();
services.AddSingleton<IHostedService, CacheHostService>();
}
WebHost 在启动 HTTP Server 之后,会从 DI 系统中获取所有的IHostedService
,来启动我们注册的 HostedService,参见上一章 。
IApplicationLifetime
IApplicationLifetime
用来实现 ASP.NET Core 的生命周期钩子,我们可以在 ASP.NET Core 停止时做一些优雅的操作,如资源的清理等。它有如下定义:
public interface IApplicationLifetime
{
CancellationToken ApplicationStarted { get; }
CancellationToken ApplicationStopping { get; }
CancellationToken ApplicationStopped { get; }
void StopApplication();
}
IApplicationLifetime
已被 ASP.NET Core 注册到 DI 系统中,我们使用的时候,只需要注入即可。它有三个CancellationToken
类型的属性,是异步方法终止执行的信号,表示 ASP.NET Core 生命周期的三个阶段:启动,开始停止,已停止。
public void Configure(IApplicationBuilder app, IApplicationLifetime appLifetime)
{
appLifetime.ApplicationStarted.Register(() => Console.WriteLine("Started"));
appLifetime.ApplicationStopping.Register(() => Console.WriteLine("Stopping"));
appLifetime.ApplicationStopped.Register(() =>
{
Console.WriteLine("Stopped");
Console.ReadKey();
});
app.Use(next =>
{
return async (context) =>
{
await context.Response.WriteAsync("Hello ASP.NET Core!");
appLifetime.StopApplication();
};
});
}
执行结果如下:
在上一章中我们提到过, IApplicationLifetime
的启动信号是在 WebHost 的StartAsync
方法中触发的,而没有提到停止信号的触发,在这里补充一下:
internal class WebHost : IWebHost
{
public async Task StopAsync(CancellationToken cancellationToken = default(CancellationToken))
{
....
// 设置 Task 的超时时间,上文在 IHostedService 中提到过
var timeoutToken = new CancellationTokenSource(Options.ShutdownTimeout).Token;
if (!cancellationToken.CanBeCanceled)
{
cancellationToken = timeoutToken;
}
else
{
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutToken).Token;
}
// 触发 Stopping 信号
_applicationLifetime?.StopApplication();
// 停止 Http Server
if (Server != null)
{
await Server.StopAsync(cancellationToken).ConfigureAwait(false);
}
// 停止 我们注册的 IHostService
if (_hostedServiceExecutor != null)
{
await _hostedServiceExecutor.StopAsync(cancellationToken).ConfigureAwait(false);
}
// 发送 Stopped 通知
_applicationLifetime?.NotifyStopped();
}
}
总结
本文详细介绍了对 WebHost 的配置,结合 上一章,对 ASP.NET Core 的启动流程也基本清楚了,下一章就来介绍一下请求管道的创建,敬请期待!
参考资料: