NetCore基础知识
概述
官方文档
.NET Core的优点
- 支持独立部署,不互相影响;
- 彻底模块化;
- 没有历史包袱,运行效率高
- 不依赖于IIS
- 跨平台
- 符合现代开发理念:依赖注入、单元测试等
.NET Core 和 .NET Framework 不同点
- .NET Core 不支持:ASP.NET WebForms、WCF服务器端、WF、.NET Remoting、Appdomain
- 支持部分 Windows-Only 的特性,但是无法跨平台: WinForm、WPF、注册表、Event Log、AD等
.NET Standard
- .NET Standard只是标准,不是实现
- .NET Standard只是规范,.NET Standard类库可以被支持其版本的.NET Framework、.NET Core、Xamarin等引用。
- 而.NET Core类库、.NET Framework类库则不可以被其他版本应用。
- 如果编写一个公用的类库,尽量选择.NET Standard,并且尽量用低版本。
官方版本说明
.NET Framework支持到.NET Standard 2.0为止。微软官方说明
从.NET 5开始,微软开始统一为.NET,后续默认 .NET 就是指的 .NET Core。
程序的发布
- 部署模式:依赖框架;独立(推荐);
- 依赖框架:需要目标电脑已经安装运行时环境
- 独立:每个应用有独立的运行环境,相互没有干扰
- 目标运行时:应用运行的系统
- 生成单个文件
- ReadyToRun
- 将应用程序集编译为 ReadyToRun (R2R) 格式来改进 .NET Core 应用程序的启动时间和延迟。
- R2R 二进制文件更大,因为它们包含中间语言 (IL) 代码和相同代码的本机版本。
- 包含大量代码的大多数应用程序都会获得很大的性能增益,具有少量代码的应用程序很可能不会获得显著改进。
- 裁剪未使用的程序集。
- 由于无法可靠地分析各种有问题的代码模式(主要集中在反射使用),应用程序的生成时间分析可能会导致运行时失败。
NuGet
安装
- Install-Package 包名
- -Version 指定版本
- Uninstall-Package 卸载包
- Update-Package 更新到最新版本
VS无法连接网络和解决方法
原因:VS2013/VS2019 访问 https 默认使用的协议为 Tls1.1,但是Nuget官方网站只支持Tls1.2
解决办法为在程序包管理控制台运行如下命令:
[Net.ServicePointManager]::SecurityProtocol=[Net.ServicePointManager]::SecurityProtocol-bOR [Net.SecurityProtocolType]::Tls12
具体步骤如下:工具 -- 库程序包管理器(N) -- 程序包管理器控制台(o) -- 底部弹出控制台输入界面
异步编程
async/await
- 异步方法的返回值一般是Task
<T>
,T是真正的返回值类型 - 异步方法名字一般以 Async 结尾
- 即使方法没有返回值,也最好把返回值声明为非泛型的 Task
- 调用泛型方法时,一般在方法前加上 await 获取异步结果 T
取消任务
使用 CancellationToken 和 CancellationTokenSource 对象
CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken ct = cts.Token;
ASP.NET Core开发中,一般不需要自己处理CancellationToken、CancellationTokenSource,只要做到“能转发CancellationToken就转发”即可。
Task类的重要方法
WhenAny(IEnumerable<Task> tasks)
任何一个Task完成,Task就完成WhenAll<TResult>(params Task<TResult>[] tasks)
所有Task完成,Task才完成- FromResult() 创建普通数值的Task对象
底层原理
- await、async是“语法糖”,最终编译成“状态机调用”
- async 的方法会被 C# 编译器编译成一个类,会根据 await 调用进行切分为多个状态
- 对 async 方法的调用会被拆分为对 MoveNext 的调用
异步与多线程
- await 调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码。
- 异步方法的代码并不会自动在新线程中执行,除非把代码放到新线程中执行。
async 方法标记
- async是提示编译器为异步方法中的await代码进行分段处理的,而一个异步方法是否修饰了async对于方法的调用者来讲没区别的,因此对于接口中的方法或者抽象方法不能修饰为async。
- 在旧版C#中,async 方法中不能用 yield。从C# 8.0 开始,把返回值声明为 IAsyncEnumerable (不要带Task),然后遍历的时候用 await foreach() 即可
async 方法缺点
- 异步方法会生成一个类,运行效率没有普通方法高;
- 可能会占用非常多的线程;
异步方法不一定需要 async 标记
- 返回值为 Task 的不一定都要标注 async,标注 async 只是让我们可以更方便的await而已
- 如果一个异步方法只是对别的异步方法调用的转发,并没有太多复杂的逻辑,那么就可以去掉async关键字。
- 取消 async 标记,可以避免多次拆装调用其他异步方法的 Task 对象,可以提高运行效率,不会造成线程浪费
依赖注入
依赖注入(Dependency Injection,DI)是控制反转(Inversion of Control,IOC)思想的实现方式。
依赖注入简化模块的组装过程,降低模块之间的耦合度
控制反转的两种实现方式:
服务定位器(ServiceLocator):向服务定位器获取服务
IDbConnection conn = ServiceLocator.GetService<IDbConnection>();
依赖注入(Dependency Injection,DI):容器管理服务,直接申请注入服务
class Demo { public IDbConnection Conn { get; set; } }
DI 基本概念
- 服务(service):对象;
- 注册服务:将类交给容器管理;
- 服务容器:负责管理注册的服务;
- 查询服务:创建对象及关联对象;
- 对象生命周期:Transient(瞬态) ; Scoped(范围); Singleton(单例);
安装包
Install-Package Microsoft.Extensions.DependencyInjection
引入命名空间
using Microsoft.Extensions.DependencyInjection
服务定位器
.NET控制反转组件取名为DependencyInjection,但它包含ServiceLocator的功能。
注册服务可以分别指定服务类型(service type)和实现类型(implementation type),这两者可能相同,也可能不同。
服务类型可以是类,也可以是接口,建议面向接口编程,更灵活。
获取服务:每一种方法都包括泛型方法和参数方法两种
- T GetService
<T
>() 如果获取不到对象,则返回null。 - T GetRequiredService
<T
>() 如果获取不到对象,则抛异常 - IEnumerable
<T
> GetServices<T
>() 适用于可能有很多满足条件的服务
- T GetService
ServiceProvider 实现了 IDisposable 接口,离开作用域之后容器会自动调用对象的 Dispose 方法
依赖注入
.Net 的依赖注入采用的是构造函数注入
依赖注入是有“传染性”的,如果一个类的对象是通过DI创建的,那么这个类的构造函数中声明的所有服务类型的参数都会被DI赋值;
但是如果一个对象是手动创建的,那么这个对象就和DI没有关系,它的构造函数中声明的服务类型参数就不会被自动赋值。
.Net 使用 DI 完整示例
主函数使用服务定位器调用具体的业务
internal class Program
{
static void Main(string[] args)
{
// 定义容器
ServiceCollection services = new ServiceCollection();
// 注册服务
services.AddScoped<Controller>(); // 服务与类型为同一个类
services.AddScoped<ILog, LogImp>(); // 使用接口注册
services.AddScoped<IConfig, ConfigImp>(); // 使用接口注册
// 获取服务执行业务
using (ServiceProvider provider = services.BuildServiceProvider())
{
var controller = provider.GetRequiredService<Controller>();
controller.Excute();
}
}
}
编写控制器代码自动注入服务
internal class Controller
{
private readonly ILog log;
private readonly IConfig config;
// 构造函数自动注入日志服务和配置服务
// 采用接口的形式,不用关注服务具体的实现
// 自动注入的参数从容器自动获取
public Controller(ILog log, IConfig config)
{
this.log = log;
this.config = config;
}
// 执行业务测试代码
public void Excute()
{
Console.WriteLine("Controller is running...");
this.log.Info("Test log info.");
this.config.Set("Name", "zhangsan");
}
}
编写服务接口及实现类
internal interface ILog
{
public void Info(string message);
}
internal class LogImp : ILog
{
public void Info(string message)
{
Console.WriteLine("Info: "+message);
}
}
internal interface IConfig
{
public void Set(string key, string value);
}
internal class ConfigImp : IConfig
{
public void Set(string key, string value)
{
Console.WriteLine($"set {value} to {key}.");
}
}
运行结果
Controller is running...
Info: Test log info.
set zhangsan to Name.
生命周期
- 如果一个类实现了 IDisposable 接口,则离开作用域之后容器会自动调用对象的Dispose方法。
- 不要在长生命周期的对象中引用比它短的生命周期的对象。
- 生命周期的选择:
- 如果类无状态,建议为Singleton;
- 如果类有状态,且有Scope控制,建议为Scoped,因为通常这种Scope控制下的代码都是运行在同一个线程中的,没有并发修改的问题;
- 在使用Transient的时候要谨慎。
总结
- 关注于接口,而不是关注于实现,各个服务可以更弱耦合的协同工作。在编写代码的时候,我们甚至都不知道具体的服务是什么。
- 第三方DI容器:Autofac :支持属性注入、基于名字注入、基于约定的注入等。
配置系统
- .NET 中的配置系统支持丰富的配置源,包括文件(json、xml、ini等)、注册表、环境变量、命令行、Azure Key Vault等
- 可以配置自定义配置源,可以跟踪配置的改变,可以按照优先级覆盖
安装包
安装基础包
Install-Package Microsoft.Extensions.Configuration
根据使用的配置文件类型安装对应的包,比如使用 .json 类型配置文件
Install-Package Microsoft.Extensions.Configuration.Json
安装绑定读取配置包
Install-Package Microsoft.Extensions.Configuration.Binder
创建配置文件
创建一个json文件,比如config.json,文件属性设置“如果较新则复制”。
{ "Name": "张三", "Age": 28, "Proxy": { "Address": "127.0.0.1", "Port": "8000" } }
读取配置
- optional 参数表示这个文件是否可选
- reloadOnChange 参数表示如果文件修改了是否重新加载配置
// 创建 ConfigurationBuilder
ConfigurationBuilder builder = new ConfigurationBuilder();
// 添加配置文件
// AddJsonFile 是 Microsoft.Extensions.Configuration.Json 包中定义的 ConfigurationBuilder 的扩展方法
builder.AddJsonFile("config.json",true,true);
// 创建配置
IConfigurationRoot root = builder.Build();
// 使用原始读取方法获取配置,获取的为string类型
string name = root.GetSection("Name").Value;
Console.WriteLine(name);
// 使用绑定到指定类的读取方法
// 需要安装 Microsoft.Extensions.Configuration.Binder
Proxy proxy = root.GetSection("Proxy").Get<Proxy>();
Console.WriteLine($"{proxy.Address}:{proxy.Port}");
选项方式读取配置(常用)
推荐和 DI 结合使用选项方式读取
需要安装包:
Install-Package Microsoft.Extensions.Options Install-Package Microsoft.Extensions.Configuration.Binder
在读取配置的地方,注入 IOptions
<T
>、IOptionsMonitor<T
>、IOptionsSnapshot<T
>(推荐)建议用 IOptionsSnapshot
不要在构造函数里直接读取 IOptionsSnapshot.Value,而是到用到的地方再读取,否则就无法更新变化。
根据配置复制程度可以把整个配置创建成实体类,也可以把配置的一部分创建成实体,根据需要导入部分节点
// 创建配置类实体 internal class Config { public string Name { get; set; } public int Age { get; set; } public Proxy Proxy { get; set; } } internal class Proxy { public string Address { get; set; } public string Port { get; set; } }
// 在控制器中使用 IOptionsSnapshot 方式注入配置 // 注入整个配置 internal class Controller { private readonly IOptionsSnapshot<Config> config; public Controller(IOptionsSnapshot<Config> config) { this.config = config; } // 执行业务测试代码 public void Excute() { Console.WriteLine("Controller is running..."); Console.WriteLine($"Name: {config.Value.Name}\nAge: {config.Value.Age}\nProxy: {config.Value.Proxy}"); } } // 注入部分配置节点 internal class Controller2 { private readonly IOptionsSnapshot<Proxy> proxyOpt; public Controller2(IOptionsSnapshot<Proxy> proxyOpt) { this.proxyOpt = proxyOpt; } public void Excute() { Console.WriteLine("Controller2 is running"); Console.WriteLine($"Proxy: {proxyOpt.Value.Address}:{proxyOpt.Value.Port}"); } }
// 在主方法中配置 DI 并调用具体业务 static void Main(string[] args) { // 定义容器并注册服务(跟依赖注入使用方法一样) ServiceCollection services = new ServiceCollection(); services.AddScoped<Controller, Controller>(); services.AddScoped<Controller2, Controller2>(); // 创建配置 ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddJsonFile("config.json",true,true); IConfigurationRoot root = builder.Build(); // 设置配置项 services.AddOptions() // 将 Config 对象绑定到配置的根节点 .Configure<Config>(e => root.Bind(e)) // 将 Proxy 对象绑定到配置的 Proxy 节点 .Configure<Proxy>(e=>root.GetSection("Proxy").Bind(e)); // 获取服务执行业务 using (ServiceProvider provider = services.BuildServiceProvider()) { var controller = provider.GetRequiredService<Controller>(); controller.Excute(); var controller2 = provider.GetRequiredService<Controller2>(); controller2.Excute(); } } /* 运行结果 Controller is running... Name: 张三 Age: 28 Proxy: ConsoleApp1.Proxy Controller2 is running Proxy: 127.0.0.1:8000 */
配置修改与作用域
三种不同的选项类型
- IOptions
<T
> 不更新配置项 - IOptionsMonitor
<T
> 任何时刻都更新配置项 - IOptionsSnapshot
<T
> 更新配置项并在同一作用域类保持一致
- IOptions
reloadOnChange 参数需要设置为 true 才能实时加载
// 获取服务执行业务 using (ServiceProvider provider = services.BuildServiceProvider()) { while (true) { // 手动创建作用域 using (var scope = provider.CreateScope()) { // 根作用域,中途修改不会影响 var controller = provider.GetRequiredService<Controller>(); controller.Excute(); // 从手动创建的 scope 中获取的 ServiceProvider 将刷新配置文件 var newController = scope.ServiceProvider.GetRequiredService<Controller>(); newController.Excute(); } Console.ReadLine(); } } /* Controller is running... Name: 张三 Age: 18 Proxy: ConsoleApp1.Proxy Controller is running... Name: 张三 Age: 28 Proxy: ConsoleApp1.Proxy */
其他配置文件提供者
其他配置文件提供者与 Json 格式配置相似,均采用了 ConfigurationBuilder 的扩展方法 Add*** 来添加配置
还支持命令行、环境变量、 ini、xml 、Azure、阿里云等配置源
对于环境变量、命令行等简单的键值对结构,如果想要进行复杂结构的配置,需要进行“扁平化处理”。对于配置的名字需要采用“层级配置”。
"Proxy": { "Address": "127.0.0.1", "Port": "8000" } 扁平化处理后为 Proxy:Address=127.0.0.1 Proxy:Port=8000
命令行配置
安装依赖包
Install-Package Microsoft.Extensions.Configuration.CommandLine
添加配置
configBuilder.AddCommandLine(args);
args 参数支持多种格式,但格式不能混用格式不能混用
server=127.0.0.1 --server=127.0.0.1 --server 127.0.0.1 /server=127.0.0.1 /server 127.0.0.1
环境变量配置
安装依赖包
Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables
添加配置
configBuilder.AddEnvironmentVariables(); configBuilder.AddEnvironmentVariables(prefix);
prefix 参数表示只加载以 prefix 前缀的配置,加载进来后会自动忽略掉前缀
自定义配置提供者
基本步骤
- 定义实现 IConfigurationProvider 接口的类 XXXConfigurationProvider
- 一般继承自 ConfigurationProvider
- 如果是从文件读取,可以继承自 FileConfigurationProvider
- 重写 Load 方法,把“扁平化数据”设置到Data属性即可
- 定义实现 IConfigurationSource 接口的类 XXXConfigurationSource
- 如果是从文件读取,可以继承自 FileConfigurationSource
- 在 Build 方法中返回 XXXConfigurationProvider 对象
- 使用 configurationBuilder.Add(new XXXConfigurationSource()) 即可添加配置
- 为了简化使用,一般提供一个 IConfigurationBuilder 的扩展方法 AddXXX
示例:开发 web.config 提供者
实现读取 web.config 里的 connectionStrings 和 appSettings
web.config 文件
<?xml version="1.0" encoding="utf-8"?> <configuration> <connectionStrings> <add name="connstr1" connectionString="Data Source=.;Initial Catalog=DemoDB;User ID=sa;Password=123456" providerName="System.Data.SqlClient"/> <add name="connstr2" connectionString="Data Source=.;Initial Catalog=DemoDB2;User ID=sa;Password=000000" providerName="System.Data.SqlClient"/> </connectionStrings> <appSettings> <add key="Smtp:Server" value="smtp.test.com" /> <add key="Smtp.Port" value="25" /> </appSettings> </configuration>
Program.cs 主函数
// 定义容器并注册测试服务 ServiceCollection services = new ServiceCollection(); services.AddScoped<TestSmtpController, TestSmtpController>(); services.AddScoped<TestConnnectStringController, TestConnnectStringController>(); // 定义配置并添加配置源 ConfigurationBuilder builder = new ConfigurationBuilder(); builder.Add(new WebConfigurationSource() { Path = "Web.config" }); // 绑定配置到 DI 容器 IConfigurationRoot root = builder.Build(); services.AddOptions() // 绑定 Stmp 到 Smtp 节点,将获取到 Smtp:xxx 形式扁平化数据的配置 .Configure<Smtp>(e => root.GetSection("Smtp").Bind(e)) // 绑定 ConnectionStrings 到根节点获取数组 // ConnectionStringCollection 需要有对应的 ConnectionStrings 属性才能获取到 ConnectionStrings:i:xxx 形式扁平化数据的配置 .Configure<ConnectionStringCollection>(e=>root.Bind(e)); // 测试 using (var provider = services.BuildServiceProvider()) { TestSmtpController controller = provider.GetRequiredService<TestSmtpController>(); controller.Test(); TestConnnectStringController controller2 = provider.GetRequiredService<TestConnnectStringController>(); controller2.Test(); } /* smtp.test.com:25 Name: connstr1 Providername: System.Data.SqlClient ConnectionString: Data Source=.;Initial Catalog=DemoDB;User ID=sa;Password=123456 Name: connstr2 Providername: System.Data.SqlClient ConnectionString: Data Source=.;Initial Catalog=DemoDB2;User ID=sa;Password=000000 */
WebConfigurationProvider 配置提供者
internal class WebConfigurationProvider : FileConfigurationProvider { public WebConfigurationProvider(FileConfigurationSource source) : base(source){} // 业务代码:复制读取配置文件并转化为扁平化数据放入 Data 中 public override void Load(Stream stream) { // Key 大小写不敏感 var data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase); XmlDocument xml = new XmlDocument(); xml.Load(stream); // 根据 XPath 获取 connectionStrings 子节点 var connStrNodes = xml.SelectNodes("/configuration/connectionStrings/add"); if (connStrNodes != null) for (var i = 0; i < connStrNodes.Count; i++) { XmlNode node = connStrNodes[i]; // 获取需要提取的节点属性 // ConnectionStrings 需要与实体类中接收该数组参数的属性名一致 data[$"ConnectionStrings:{i}:Name"] = node.Attributes["name"].Value; data[$"ConnectionStrings:{i}:ConnStr"] = node.Attributes["connectionString"].Value; data[$"ConnectionStrings:{i}:ProviderName"] = node.Attributes["providerName"].Value; } // 同理获取 appSettings 子节点 var settingNodes = xml.SelectNodes("/configuration/appSettings/add"); if (settingNodes != null) foreach (XmlNode node in settingNodes) { var name = node.Attributes["key"].Value.Replace('.', ':'); data[$"{name}"] = node.Attributes["value"].Value; } // 将扁平化结构数据赋值给 Data this.Data = data; } }
ConnectionString 和 ConnectionStringCollection 实体类
public class ConnectionString { public string Name { get; set; } public string ConnStr { get; set; } public string Providername { get; set; } }
public class ConnectionStringCollection { // 需要与实体类中接收该数组参数的属性名一致 public List<ConnectionString> ConnectionStrings { get; set; }= new List<ConnectionString>(); }
Smtp 实体类(略)
TestConnnectStringController 和 TestSmtpController 测试控制器(略)
AddWebConfigFile 扩展方法
// 将扩展方法声明到 Microsoft.Extensions.Configuration 中可以在调用时不用引入额外的命名空间 namespace Microsoft.Extensions.Configuration { static public class WebConfigExtension { static public IConfigurationBuilder AddWebConfigFile(this IConfigurationBuilder builder,string? path =null) { // 添加配置源 // 设置默认配置源可以方便调用 builder.Add(new WebConfigurationSource() { Path = path ?? "Web.config" }); // 返回 IConfigurationBuilder 可以方便链式调用 return builder; } } }
主函数中调用 AddWebConfigFile 扩展方法
// 定义配置并添加配置源 ConfigurationBuilder builder = new ConfigurationBuilder(); builder.AddWebConfigFile()
多配置源优先级
- 按照注册到 ConfigurationBuilder 的顺序,后注册的优先级高,如果配置名字重复,后注册的值覆盖先注册的值。
- .NET 提供了 user-secrets 机制, user-secrets 的配置不放到源代码中,保证私密配置不会被泄露
- 一般仅用于开发环境存放内部私密信息
- Nuget 安装:Microsoft.Extensions.Configuration.UserSecrets
- 在VS项目上点右键【管理用户机密】,编辑配置文件,user-secrets 的配置会放到本地
- configBuilder.AddUserSecrets
<Program
>() 添加配置
- user-secrets 配置应该添加在最前面使其优先级最低,生产环境配置将覆盖 user-secrets 中的相同配置
日志系统
基本概念
- 日志级别:Trace<Debug
< Information
< Warning< Error
< Critical - 日志提供者(LoggingProvider):日志输出到哪里
- 需要记录日志的代码,注入 ILogger
<T
> 即可,T 一般就用当前类,这个类的名字会输出到日志,方便定位错误。 - 然后调用 LogInformation()、LogError 等方法输出不同级别的日志。
- 类似于配置系统,编写 LoggingProvider 可以实现自定义的日志
依赖包
基础包
Install-Package Microsoft.Extensions.Logging
日志提供者
# 控制台日志 Install-Package Microsoft.Extensions.Logging.Console # Nlog Install-Package NLog.Extensions.Logging
快速入门
主函数
// 1. 创建服务容器并注册服务 var services = new ServiceCollection(); services.AddScoped<Controller, Controller>(); // 2. 添加 LoggingProvider,可以添加多个 // AddLogging 是 ServiceCollection 的扩展方法 // 可以添加第三方 LoggingProvider,比如 NLog services.AddLogging(loggerBuilder => { // 添加打印到控制台,要其他 Provider 安装对应的 LoggingProvider 依赖包即可 loggerBuilder.AddConsole(); // 设置日志级别,也可以用配置文件进行设置 loggerBuilder.SetMinimumLevel(LogLevel.Trace); }); // 3. 调用服务进行测试 using (var service = services.BuildServiceProvider()) { service.GetRequiredService<Controller>().Test(); }
测试 Controller
internal class Controller { private readonly ILogger logger; public Controller(ILogger<Controller> logger) { this.logger = logger; } public void Test() { this.logger.LogInformation("This is a test info."); } }
NLog
概况
- NLog 是一个基于.NET平台编写的日志记录类库
依赖包
# 安装主模块
Install-Package Nlog
# 日志系统扩展包
Install-Package NLog.Extensions.Logging
配置文件
NLog.LogManager.Setup().LoadConfiguration(builder => { builder.ForLogger().FilterMinLevel(LogLevel.Info).WriteToConsole(); builder.ForLogger().FilterMinLevel(LogLevel.Debug).WriteToFile(fileName: "file.txt"); });
(推荐)也可以使用配置文件:根目录下创建配置文件 nlog.config (小写)
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <!-- 配置输出目标 --> <targets> <target name="logfile" xsi:type="File" fileName="file.txt" /> </targets> <!-- 配置输出规则,将不同的日志级别输出到不同的 output --> <rules> <logger name="*" minlevel="Info" writeTo="logfile" /> </rules> </nlog>
target 配置输出目标
- name:输出名称
- type:输出类型,Console -- 输出到控制台,File -- 输出到文件
- fileName:日志文件名,可以使用表达式
- layout:日志格式,可以使用表达式
- archiveAboveSize:单个日志文件最大字节数,可以避免单个文件太大
- maxArchiveFiles:日志存档文件的数量最大值,超过最大值时旧的文件会被删掉;
- maxArchiveDays:设定保存若干天的日志存档
<targets>
<target name="logfile" xsi:type="File" fileName="file.txt" />
<target name="logconsole" xsi:type="Console" />
<target xsi:type="File" name="debugfile" fileName="${basedir}/logs/${shortdate}.log"
layout="${longdate} ${uppercase:${level}} ${message}" />
</targets>
rules 配置日志规则
- 日志按照日志规则定义 logger 顺序依次匹配 name 和 level 输出,直到遇到 final=true 就不再继续匹配
- name:配置名称,与注入的 ILogger
<Controlle
r> 全类型名一致,可以包含通配符(* 和 ?) - minlevel / maxlevel:日志最低 / 最高等级,Trace
< Debug
< Info< Warn
< Error `< Fatal - writeTo:输出到指定 target
- final:日志按照日志规则定义的前后书序依次匹配输出,直到遇到 final=true 就不再继续匹配
<rules>
<logger name="*" minlevel="Info" writeTo="logconsole" />
<logger name="*" minlevel="Debug" writeTo="debugfile" />
</rules>
Layout 配置格式
可以配置 3 种输出格式
<target xsi:type="File" name="csvFileExample" fileName="./CsvLogExample.csv">
<layout xsi:type="CsvLayout" delimiter="Tab" withHeader="false">
<column name="time" layout="${longdate}" />
<column name="level" layout="${level:upperCase=true}"/>
<column name="message" layout="${message}" />
<column name="stacktrace" layout="${stacktrace:topFrames=10}" />
<column name="exception" layout="${exception:format=ToString}"/>
</layout>
</target>
filters 配置过滤器
<logger name="*" writeTo="file">
<filters defaultAction="Log">
<when condition="length('${message}') > 100" action="Ignore" />
</filters>
</logger>
记录日志
方法一:直接调用 Logger 对象
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); logger.Debug("Test debug message."); logger.Info("Hello {Name}", "Earth"); logger.Error(ex, "Something bad happened");
方法二:使用依赖注入添加 LoggingProvider
services.AddLogging(loggingBuilder => loggingBuilder.AddNLog());
手动刷新流 Flush/Shutdown
- NLog 默认在线程关闭时自动刷新流
NLog.LogManager.Shutdown(); // Flush and close down internal threads and timers
Serilog
基本概念
- 方便实现结构化、集中日志服务
依赖包
# 基础包
Install-Package Serilog.AspNetCore
# 配置包
Install-Package Serilog.Settings.AppSettings
配置文件
可以使用代码配置
Log.Logger = new LoggerConfiguration() .ReadFrom.AppSettings() ... // Other configuration here, then .CreateLogger()
也可使用配置文件,在 App.config or Web.config 中创建
<appSettings
> 节点<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="serilog:minimum-level" value="Verbose" /> <!-- More settings here -->
结构化日志
结构化日志示例:
{"Timestamp":"2023-03-02T13:59:41.9244663+08:00","Level":"Information","MessageTemplate":"This is a test info.","Properties":{"SourceContext":"Logging.Controller"}} {"Timestamp":"2023-03-02T13:59:42.0260409+08:00","Level":"Information","MessageTemplate":"Processing {@SensorInput}","Properties":{"SensorInput":{"Latitude":25,"Longitude":134},"SourceContext":"Logging.Controller"}}
普通简单类型数据和数组、字典类型:直接使用
var fruit = new[] { "Apple", "Pear", "Orange" }; Log.Information("In my bowl I have {Fruit}", fruit); //{ "Fruit": ["Apple", "Pear", "Orange"] }
Objects 类型:使用
var sensorInput = new { Latitude = 25, Longitude = 134 }; Log.Information("Processing {@SensorInput}", sensorInput);
复杂类型可以自定义 Destructure
// 只保存 HttpRequest 对象的 RawUrl 和 Method Log.Logger = new LoggerConfiguration() .Destructure.ByTransforming<HttpRequest>( r => new { RawUrl = r.RawUrl, Method = r.Method }) .WriteTo...
强制转化为字符串:使用
{ $name }
var unknown = new[] { 1, 2, 3 } Log.Information("Received {$Data}", unknown); // Received "System.Int32[]"
记录日志
使用依赖注入的方法
services.AddLogging(loggingbuilder => { // 使用代码配置 Serilog Log.Logger = new LoggerConfiguration().MinimumLevel.Debug() .Enrich.FromLogContext() .WriteTo.Console(new JsonFormatter()) .CreateLogger(); // 添加到 loggingbuilder loggingbuilder.AddSerilog(); });