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
<Controller> 全类型名一致,可以包含通配符(* 和 ?) - 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 timersSerilog
基本概念
- 方便实现结构化、集中日志服务
 
依赖包
# 基础包
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(); });
