04 应用配置、日志、路由和静态文件
应用配置
在开发过程中,有些时候我们都会有配置应用的需求。
在 ASP.NET 中,我们一般使用 Web.config 来进行配置。但是在 ASP.NET Core 中 ,你们会发现 Web.config 已经没了,因为它被 appsettings.json 这个配置文件取代了。
基本用法
如果你还记得前两节课内容的话,你应该知道在主机创建后,服务容器里就已经为我们默认注册了一些服务,其中一个就是 IConfiguration 服务。
这个服务可以被注入到Startup类的构造函数中,它为我们提供了应用配置相关的服务。
假设有如下配置项:
// 键不区分大小写。例如,`ConnectionString` 和 `connectionstring` 被视为等效键。"ConnectionString": "data source=.;initial catalog=db;user id=sa","WebSetting": { "WebName": "ASP.NET Core", "Title": "Hello Title", "Behavior": { "IsCheckIp": true, "MaxConnection": 300 }}
首先,创建Startup类的构造函数,并注入 IConfiguration
private readonly IConfiguration _configuration;public Startup(IConfiguration configuration){ _configuration = configuration;}
之后,就可以在任何一个方法中使用它了
app.Run(async context => { var connStrDefault = _configuration.GetConnectionString("default"); var connStr = _configuration["connectionString"]; var title = _configuration["WebSetting:Title"]; var isCheckIp = _configuration["WebSetting:Behavior.IsCheckIp"]; await context.Response.WriteAsync($"{connStrDefault}\r connStr \r title \r isCheckIp"); });
绑定配置模型对象
使用字符串键名获取配置值很不优雅,也容易出错。我们可以把配置项绑定到对象。
首先,创建配置项结构对应的配置模型。
public class Behavior{ public bool IsCheckIp { get; set; } public int MaxConnection { get; set; }}public class WebSetting{ public string WebName { get; set; } public string Title { get; set; } public Behavior Behavior { get; set; }}public class AppSetting{ public string ConnectionString { get; set; } public WebSetting WebSetting { get; set; }}
然后,创建配置模型对象,并通过 IConfiguration 服务绑定
// 全部绑定var appSetting = new AppSetting();_configuration.Bind(appSetting);// 部分绑定var behavior = new Behavior();_configuration.GetSection("Behavior").Bind(behavior);app.Run(async context => { var connStrDefault = appSetting.ConnectionString; var title = appSetting.WebSetting.Title; var isCheckIp = appSetting.WebSetting.Behavior.IsCheckIp; var maxConnection = appSetting.WebSetting.Behavior.MaxConnection; await context.Response.WriteAsync($"{connStrDefault} \r {title} \r {isCheckIp} \r {maxConnection}"); });
注册配置选项
首先,在 ConfigureServices 方法中,注册AppSetting绑定的配置服务
services.Configure<AppSetting>(_configuration);
然后,在需要的地方利用参数把IOptions注入进来:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IOptions<AppSetting> appOptions){ app.Run(async context => { var connStrDefault = appOptions.Value.ConnectionString; var title = appOptions.Value.WebSetting.Title; var isCheckIp = appOptions.Value.WebSetting.Behavior.IsCheckIp; var maxConnection = appOptions.Value.WebSetting.Behavior.MaxConnection; await context.Response.WriteAsync($"{connStrDefault} \r {title} \r {isCheckIp} \r {maxConnection}"); });}
自定义配置文件
有的时候配置参数可能会比较很多,想放在单独的文件中进行配置。
创建customSetting.json配置文件,假设有如下配置:
{ "Name": "Rick", "Age": 99}
首先,在 ConfigureServices 方法中,创建配置生成器,添加customSetting.json文件,注册CustomSetting绑定的配置服务
var config = new ConfigurationBuilder()
.AddJsonFile("customSetting.json")
.Build();
services.Configure(config);
然后,在需要的地方利用参数把IOptions注入进来:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IOptions appOptions, IOptions customOptions)
{
var name = customOptions.Value.Name;
var age = customOptions.Value.Age;
}
多环境
ASP.NET Core 在应用启动时读取环境变量 ASPNETCORE_ENVIRONMENT,并将该值存储在 IWebHostEnvironment.EnvironmentName 中。
ASPNETCORE_ENVIRONMENT 可设置为任意值,但框架提供三个值:
Development (开发)
Staging(演示、预览)
Production(生产)
// 可以获取IWebHostEnvironment,判断不同的环境
if (env.IsProduction() || env.IsStaging() || env.IsEnvironment("Demo"))
{
// 要加载的中间件
}
Startup方法多环境
ConfigureServices方法和Configure方法,可以为不同的环境单独定义单独方法。
运行时会优先调用方法中环境名与当前环境匹配的方法。如果找不到匹配的方法,就会使用调用默认方法。
www.public void ConfigureDemoServices(IServiceCollection services)
{
}
public void ConfigureDemo(IApplicationBuilder app, IWebHostEnvironment env)
{
app.Run(async context => { await context.Response.WriteAsync("ConfigureDemo"); });
}
Startup类多环境
还可以为不同的环境定义单独的 Startup
类(例如 StartupDevelopment
)。
运行时会选择Startup类名后缀与当前环境名称匹配的Startup类。如果找不到匹配的 Startup{EnvironmentName}
,就会使用 Startup
类。
当应用需要为各环境之间存在许多代码差异的多个环境配置启动时,这种方法非常有用。
public class StartupDevelopment
{
public void ConfigureServices(IServiceCollection services)
{
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.Run(async context => { await context.Response.WriteAsync("StartupDevelopment"); });
}
}
public class StartupProduction
{
public void ConfigureServices(IServiceCollection services)
{
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.Run(async context => { await context.Response.WriteAsync("StartupProduction"); });
}
}
修改主机配置启动类
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
//webBuilder.UseStartup();
webBuilder.UseStartup(Assembly.GetExecutingAssembly().FullName);
});
日志
日志是使用频率最高的东西了,给我们开发调试程序提供了必要的信息。
ASP.NET Core中内置了一个通用日志接口ILogger
,虽然实现了多种内置的日志提供程序,例如
Console
Debug
EventSource
EventLog
但内置的日志提供程序不支持记录到文件(只有Linux下才会)和数据库。
我们可以在ASP.Net Core中自由的指定日志提供程序,并将日志发送到指定的位置。
在我们创建主机时, WebHost.CreateDefaultBuilder方法默认添加了2个日志提供器(控制台和DEBUG输出),并且默认读取appsetting.json配置文件中的配置。
日志级别
ASP.NET Core中提供了6种日志级别(按严重性从低到高排列)。
跟踪 = 0
有关通常仅用于调试的信息。这些消息可能包含敏感应用程序数据,因此不得在生产环境中启用它们。默认情况下禁用。
调试 = 1
有关在开发和调试中可能有用的信息。示例:
Entering method Configure with flag set to true.
由于日志数量过多,因此仅当执行故障排除时,才在生产中启用Debug
级别日志。信息 = 2
用于跟踪应用的常规流。这些日志通常有长期价值。示例:
Request received for path /api/todo
警告 = 3
表示应用流中的异常或意外事件。可能包括不会中断应用运行但仍需调查的错误或其他条件。
Warning
日志级别常用于已处理的异常。示例:FileNotFoundException for file quotes.txt.
错误 = 4
表示无法处理的错误和异常。这些消息指示的是当前活动或操作(例如当前 HTTP 请求)中的失败,而不是整个应用中的失败。日志消息示例:
Cannot insert record due to duplicate key violation.
严重 = 5
需要立即关注的失败。例如数据丢失、磁盘空间不足。
创建日志
我们需要通过依赖注入获取一个实现ILogger的泛型接口对象,我们可以用刚才创建的中间件试验。
public async Task InvokeAsync(HttpContext httpContext, ILogger logger)
{
// ILogger为了提供了6个可用的输出日志方法,分别对应了6个不同的日志级别
logger.LogDebug($"Path:{httpContext.Request.Path}");
await httpContext.Response.WriteAsync($"Path:{httpContext.Request.Path}");
await _next(httpContext);
}
手动添加日志提供器
我们只需要在Program.cs中使用ConfigureLogging方法就可以配置我们需要的日志提供程序了。
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(builder => builder.AddEventLog())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
});
除了在Program.cs添加日志提供器之外,我们还可以在Startup.cs中添加日志提供器。
在Startup.cs中,我们可以为Configure方法添加第三个参数ILoggerFactory loggerFactory
, 并使用该参数添加日志提供器。
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
loggerFactory.AddConsole();
loggerFactory.AddDebug();
}
集成NLog
使用Nuget安装NLog和NLog.Web.AspNetCore
创建 nlog.config 配置文件
<?xml version="1.0" encoding="utf-8" ?> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="Info"
internalLogFile="c:\temp\internal-nlog.txt"> layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" /> layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />更改 program.cs
using System;
using NLog.Web;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
public static void Main(string[] args)
{
var logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
try
{
logger.Debug("init main");
CreateHostBuilder(args).Build().Run();
}
catch (Exception exception)
{
//NLog: catch setup errors
logger.Error(exception, "Stopped program because of exception");
throw;
}
finally
{
// Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
NLog.LogManager.Shutdown();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup();
})
.UseNLog(); // NLog: Setup NLog for Dependency injection
异常处理
ASP.NET Core 默认提供了一个DeveloperExceptionPage中间件,由于它的异常信息比较敏感,所以只适合在开发环境下;如果再生产环境下,我们可能想自定义异常处理,比如显示一个异常友好页面,那我们可以这么做。
// 检查当前环境名称是不是Development
if (env.IsDevelopment())
{
// 开发人员异常页面中间件
app.UseDeveloperExceptionPage();
}
else
{
// 自定义异常页面
app.UseExceptionHandler("/Error");
app.Map("/Error", ErrorHandle);
}
if (env.IsProduction() || env.IsStaging() || env.IsEnvironment("Demo"))
{
app.UseExceptionHandler("/Error");
app.Map("/Error", ErrorHandle);
}
// 测试异常页面
app.Run(context =>
{
throw new Exception("Error");
});
还可以封装一个异常处理中间件,这样就可以统一处理异常,比如记录日志,发送告警邮件什么的
ASP.NET Core 约定中间件类必须包括:
具有类型为 RequestDelegate 的参数的公共构造函数
RequestDelegate 就是请求委托
名为 Invoke 或 InvokeAsync 的公共方法,此方法必须满足两个条件
返回 Task;
接受类型 HttpContext 的第一个参数
public class CustomExceptionMiddleware
{
private readonly RequestDelegate _next;
public CustomExceptionMiddleware(RequestDelegate next)
{
_next = next;
}
public Task InvokeAsync(HttpContext context, ILogger logger)
{
try
{
return _next(context);
}
catch (Exception e)
{
// 异常处理
logger.LogError(e.Message);
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return context.Response.WriteAsync(e.Message);
}
}
}
创建添加中间件扩展方法
public static class CustomMiddlewareExtensions
{
public static IApplicationBuilder UseCustomException(this IApplicationBuilder app)
{
return app.UseMiddleware();
}
}
路由
路由负责将请求地址映射到终结点。并向这些终结点传入的请求,可以把终结点理解为MVC 控制器中的Action。
路由还能根据配置好的路由信息,生成映射到某个终结点的地址。
路由中间件
ASP.NET Core 3.0 使用了更加完善的终结点(端点)路由,以便对应用程序内的路由提供更多的控制。
端点就是HTTP请求根地址后面的那部分,
两个中间件之间,是可以设置其他中间件的,在这之间的中间件可以利用来自端点路由中间件的路由信息,处理一些其它逻辑,比如授权。
在ASP.NET Core 2.x 中是没有这两个东西的,3.0 版本把路由部分独立了出来,这是因为不管是MVC、Web API、Razor Pages、SingelR,本就可以使用同一套路由逻辑,它们不同的只是端点。分成两个部分,就可以达到复用统一的目的,只需要为不同的框架设置不同的端点即可。
// 负责匹配请求和配置的路由,并把解析出来的路由信息写进请求,以供下一个中间件使用
app.UseRouting();
app.UseAuthorization();
// 负责配置路由端点和中间件,根据路由信息调用相应的中间件
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
在UseEndpoints中,是可以配置多种路由端点,ASP.NET Core 提供了一组用于创建端点的扩展方法
app.UseEndpoints(endpoints =>
{
// MVC
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// Web API
endpoints.MapControllers();
});
静态文件
ASP.NET Core Web应用中,静态文件的功能需要静态文件中间件提供支持。
静态文件存储在项目的 Web 根目录中。默认目录是 {content root}/wwwroot,但可通过 UseWebRoot 方法更改目录 。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles();
}
libman
库管理器 (LibMan) 是一个轻量型客户端(前端)库获取工具。LibMan 可从文件系统或从内容分发网络 (CDN) 下载前端库和框架。支持的 CDN 包括 CDNJS、jsDelivr 和 unpkg。
LibMan 提供以下优势:
只会下载所需的库文件。
无需使用其他工具(例如 Node.js、npm 和 WebPack),即可获取库中文件的子集。
可将文件放置在特定位置,无需执行生成任务,也不需手动进行文件复制。
捆绑和压缩
绑定和压缩指的是Web应用的静态文件,它们起到了性能优化的功能。
捆绑
绑定就是将多个静态文件合并到一个静态文件中,绑定可减少请求数量。
压缩
压缩就是在不影响静态文件提供的功能的情况下,删除其中不必要的字符。
配置捆绑和压缩
使用 Nuget 添加 BuildBundlerMinifier 包,然后,创建bundleconfig.json配置文件
[
{
"outputFileName": "wwwroot/css/site.min.css",
"inputFiles": [
"wwwroot/css/site.css"
]
},
{
"outputFileName": "wwwroot/js/site.min.js",
"inputFiles": [
"wwwroot/js/site.js"
],
"minify": {
"enabled": true
}
}
]
outputFileName:要输出的绑定文件的名称。inputFiles:要捆绑在一起的文件的数组。minify:压缩选项。可选,默认值minify: { enabled: true }