C# Web开发教程(一)

学习曲线(DeepSeek推荐)

Web

  • 语言基础
- 委托与事件(回调机制)
- 泛型(类型安全的集合操作)
- LINQ(数据查询语言集成)
- 异步编程(async/await 处理I/O密集型任务)
  • ASP.NET Core 框架
- MVC 模式:理解Model(数据)、View(界面)、Controller(逻辑)的分层协作

- Web API开发:构建RESTful服务,用于前后端分离架构

- 核心机制:

	- 中间件(Middleware:请求处理管道)

	- 依赖注入(DI:解耦组件依赖)

	- 配置系统(如appsettings.json)
  • 数据库与ORM

- Entity Framework Core(EF Core):主流ORM,支持数据库操作面向对象化,自动生成SQL

- 数据库知识:基础SQL语句、事务管理、索引优化,搭配SQL Server/PostgreSQL等
  • 进阶与架构
- 身份认证与授权:Cookie、JWT、OAuth2等安全方案

- 性能优化:缓存(Redis)、异步处理、负载均衡

- 部署与运维:Docker容器化、CI/CD流水线、Azure/AWS云部署

项目结构

  • .net core.net framework的项目描述文件(xxx.csproj)是不一样的
    • .net core描述简洁
    • .net framework就比较啰嗦
// .net core 简洁

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>

// .net framework 详细(啰嗦)

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{A847C939-DEDB-4E2E-B4AA-43D2714BE047}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <RootNamespace>ConsoleApp1</RootNamespace>
    <AssemblyName>ConsoleApp1</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <ItemGroup>
    <None Include="App.config" />
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
  • 注意事项
- ".net framework"的项目描述文件中,可能由于协同开发的原因,导致"xxx.csproj"文件冲突,版本冲突==
- ".net core"就没有这种问题(默认包含所有的文件),除非你手动排除它(右键,点击"排除项目")
  • 知识点穿插
- 若想某些文件不参与项目,可以右键,然后点击"排除项目"
  • 发布项目注意事项
✅ 启用 ReadyToRun 编译: 编译后的文件会变大,但是程序的速度和性能会得到提升

✅ 剪裁未使用的程序集(预览): 未用到的库会被删除(若使用动态反射之类的逻辑,要慎选)

新的安装库的方式NuGet

  • 相当于Pythonpip命令,一键搞定所有的匹配库(以前很麻烦,要一个一个去找)

  • 安装方式

    • vs图形化安装(傻瓜式)

    • 命令行安装:

      • - 打开"程序包管理器控制台"
        - 粘贴命令(赋值PMC命令),例如: NuGet\Install-Package FluentFTP -Version 52.1.0
        - 卸载命令,前面加Un即可,例如: NuGet\UnInstall-Package FluentFTP -Version 52.1.0
        	- 偷懒方法: 连卸载都不用,在"xxx.csproj"文件中注释掉即可
            
            <Project Sdk="Microsoft.NET.Sdk">
          	......
              <ItemGroup>
              	<!--这行注释掉-->
                <PackageReference Include="FluentFTP" Version="52.1.0" />
              </ItemGroup>
        
            </Project>
        
        - 更新到最新版: Update-Package xxx
        
  • 知识点穿插: 公司内网部署NuGet服务器,供开发团队使用

异步编程

  • 通俗理解就两个字:不等

  • 误解点解析

    • - 异步编程能提高服务器运行的效率吗? 不行,它只是提升服务器能同时处理请求的数量
      	- 比如原本只能同时处理100个请求,现在能同时处理500个请求
      	- 用户原本需要等待5秒的时候,现在还是等待5秒,只不过等待的这段时间,用户可以去做别的事情
      
  • 传统开发方式现在开发方式

    • - 传统开发方式,多线程去实现异步,问题很多
      - 现在开发方式, async 和 await 的方式,化简了传统多线程的开发方式,容易很多
      
  • 异步方法就是用async关键字修饰的方法

- 返回值一般是Task<T>,例如 Task<int>
- 即使异步方法没有返回值,也最好把返回值声明为非泛型的Task
- 调用泛型方法时,一般在方法前加上"await",这样拿到的返回值就是泛型指定的T类型
- 一个方法中如果有await调用,则该方法必须加上async修饰("异步方法"的"传染性")
  • 使用普通方法示例: 文件写,读示例
using System;
using System.IO;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            string filename = @"D:\VS2019\Temp\123.txt";
            File.WriteAllText(filename, "Hello");
            string s = File.ReadAllText(filename);
            Console.WriteLine(s);
        }
    }
}

  • 使用异步方法实现写入文件
using System;
using System.IO;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    class Program
    {
      
       # async修饰,void修改为Task
       static async Task Main(string[] args)
        {
           
            string filename = @"D:\VS2019\Temp\123.txt";
            # 异步文件操作(如 WriteAllTextAsync)不会阻塞当前线程,适合 I/O 密集型操作
            # await修饰, 使用 File.WriteAllTextAsync 替代同步方法
            await File.WriteAllTextAsync(filename, "666");
            // 隐式返回 Task.CompletedTask(编译器自动处理,无需再显示返回)

        }
    }
}

  • 小拓展,再调用读取的异步方法
using System;
using System.IO;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    class Program
    {
       static async Task Main(string[] args)
        {
            string filename = @"D:\VS2019\Temp\123.txt";
            await File.WriteAllTextAsync(filename, "666");
            # 等待5秒以后再读
            await Task.Delay(5000);
            string content = await File.ReadAllTextAsync(filename);
            Console.WriteLine($"读取的内容: {content}");

        }
    }
}

  • 释疑: await可以理解为智能等待,释放当前任务的线程,让它可以做别的事情,而不是让该线程一直等待任务结束
    • 注意事项: 如果有没有添加await,调用异步方法的时候,就会出现不等,直接执行一段代码
await File.WriteAllTextAsync(...); // await智能等待,保证在文件写入完成后再执行下句代码
Console.WriteLine("在写入后执行"); 

var task = File.WriteAllTextAsync(...); // 调用异步方法,没有await,难以保证在文件写入完成后再执行下句代码
Console.WriteLine("立即执行"); // 此时文件写入可能未完成
  • 实例: 下载某网站的html代码,保存到本地文件
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    class Program
    {
        // static void Main(string[] args)
       static async Task Main(string[] args)
       {
            await DownloadHtmlAsync("https://www.youzack.com", @"D:\VS2019\Temp\123.txt");
       }
       
       // 自定义异步方法
       // 在异步编程中,单纯的Task表示没有返回值
       static async Task DownloadHtmlAsync(string url,string filename)
        {
        	// using语法糖用法
            using (HttpClient httpClient = new HttpClient())
            {
                string html = await httpClient.GetStringAsync(url);
                await File.WriteAllTextAsync(filename,html);
            }
        }
    }
}

- using语法糖解释: 编译器实际生成的代码如下

HttpClient client = new HttpClient();
try
{
    await client.GetStringAsync(url);
}
finally
{
    if (client != null)
        ((IDisposable)client).Dispose(); // 关键释放动作!
}

- 总的来说,两个作用
	- 保证HttpClient使用完以后,自动被释放出来(不被继续占用)
		- 好比图书馆借书,借的书应该及时归还,而不是看完以后,一直不还,别人就看不了
	- 语法糖,简化代码
  • 把上述实例修改一下,变成带返回值的实例
......

namespace ConsoleApp2
{
    class Program
    {
      
       static async Task Main(string[] args)
       {
            int res_num = await DownloadHtmlAsync("https://www.youzack.com", @"D:\VS2019\Temp\123.txt");
            Console.WriteLine($"OK...+{res_num}");
       }
		
		// Task<int>代表返回int类型(异步编程中,独有的返回值写法)
        static async Task<int> DownloadHtmlAsync(string url, string filename)
        {
            using (HttpClient httpClient = new HttpClient())
            {
                string html = await httpClient.GetStringAsync(url);
                await File.WriteAllTextAsync(filename, html);

                return html.Length;
            }
        }

    }
}
  • asyncawait原理解析
- 本质就是"语法糖",C#编译器在幕后做了很多工作
	- 比如把源码拆分成多个部分来分别执行
	- 比如"状态机"的调用
	
- 总结: 
	- async方法会被C#编译器编译成一个类,会根据await调用切分成多个状态(对async方法的调用,实质就是对MoveNext的调用)
	- await看似在等待,经过编译以后,其实并没有等
  • 注意事项: 当某些方法不支持asyncawait的写法时(一般是非常老旧的项目),可以这么写,但是有风险(比如死锁,造成程序一直卡在那等,一直不动)
using System;
using System.IO;

namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            // WriteAllTextAsync是异步方法,意味着主程序不会卡在这里,会继续执行下面的代码
            // 但是,加上Wait()以后,主程序就会在这里等,等待这个IO结束以后,才继续执行下面的代码
            File.WriteAllTextAsync(@"D:\VS2019\Temp\123.txt", "xxxxxxxxxxxxxx").Wait();
            Console.ReadLine();
        }
    }
}

- 注意事项: 在控制台程序中使用 ".Wait()"是相对安全的,因为它只有一个主线程。但在有UI线程的程序(如Windows窗体或WPF程序)中,如果在UI线程上使用 ".Wait()" 可能会导致界面卡死(死锁)[就需要用更优雅的 await 方式],所以需要谨慎使用
  • 有返回值的示例: 使用Result来接收异步方法返回的结果
using System;
using System.IO;
using System.Threading.Tasks;



namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            // ReadAllTextAsync方法返回一个Task对象
            Task<string> t = File.ReadAllTextAsync(@"D:\VS2019\Temp\123.txt");
            // TaskObj.Result阻塞等待
            string res = t.Result;
            Console.WriteLine($"值为{res.Substring(0,6)}");
            Console.WriteLine("读取完成!");
        }
    }
}

- 关键操作:当访问".Result" 属性时:
    如果文件已读完,直接返回结果
    如果还在读取中,主线程会被阻塞(暂停执行),直到文件读取完成
    相当于说:"我不管你现在读没读完,我就在这等着,直到你给我结果"
- 注意事项: Result这种方式也容易出现死锁的现象,导致主程序一直在那边等,卡住...

  • lam表达式中,如何使用asyncawait,线程池实例
// 正常实例
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;



namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
        	// 创建后台线程并加入无限打印任务
            ThreadPool.QueueUserWorkItem((obj) =>
            {
                while (true)
                {
                    Console.WriteLine("xxxxxxxxxx");
                }
            });
            // 前台线程(主线程)等待用户输入
            // 当主线程终止时,后台线程也随即终止
            Console.Read();
        }
    }
}

// 异步实例

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;



namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {

            ThreadPool.QueueUserWorkItem(async (obj) =>
            {
                while (true)
                {	
                	// 由于是异步操作,当前线程会被线程池回收,提高效率
                	// 每次循环可能使用不同的线程(线程池动态分配)
                    await File.WriteAllTextAsync(@"D:\VS2019\Temp\123.txt", "yyyyyyyyyyyyyyy");
                    Console.WriteLine("xxxxxxxxxx");

                }
            });
            Console.Read();
        }
    }
}

  • 实例演示: 发送网络请求以后,进行文件的写入与读取的异步操作
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;



namespace ConsoleApp4
{
    class Program
    {
        static async Task Main(string[] args)
        {
          	
          	// 发起网络请求异步操作
            using (HttpClient httpClient = new HttpClient())
            {
                string html = await httpClient.GetStringAsync("http://www.baid.com");
                Console.WriteLine(html);
            }
			
			// 等待网络请求异步操作结束以后,发起文件的写入与读取操作
            string myString = "66666666666666666666666666666666666";
            string fileName = @"D:\VS2019\Temp\123.txt";
            await File.WriteAllTextAsync(fileName, myString);
            Console.WriteLine("文件写入成功");
            await File.ReadAllTextAsync(fileName);
            Console.WriteLine("文件读取成功");
            Console.WriteLine("程序结束!");

        }
    }
}

正确理解:await的"接力"特性

// 示例代码
await Task1();   // 第一个await
await Task2();   // 第二个await
  1. 当遇到第一个await

    • 如果Task1未完成 → 立即暂停当前方法,返回控制权
    • 不会执行Task2,甚至不会启动Task2
    • 整个方法在此"冻结"
  2. 当Task1完成后

    • 方法从第一个await后恢复执行
    • 开始执行Task2(启动第二个异步操作)
    • 遇到第二个await时,再次暂停

关键点图解:

开始执行
↓
执行Task1() → 启动异步操作
↓
遇到await Task1 → [如果未完成] 暂停方法
↳ 控制权返回给调用者
↳ Task2甚至还没被调用!
↓
[Task1完成] → 恢复执行
↓
调用Task2() → 启动第二个异步操作
↓
遇到await Task2 → 再次暂停

原始代码的具体分析:

// 1. 启动网络请求
string html = await httpClient.GetStringAsync(...);

// 2. 文件写入操作
await File.WriteAllTextAsync(...);
  1. 执行流程

    • 先启动网络请求(GetStringAsync返回Task<string>
    • 立即遇到await → 如果请求未完成,方法在此暂停
    • 文件写入操作根本不会执行,代码甚至还没运行到那一行!
  2. 恢复条件

    • 只有当网络请求完成后
    • 方法才会从await后恢复
    • 然后才开始执行文件写入操作

如何实现并行执行?

如果您希望两个操作同时进行,需要这样写:

// 同时启动两个任务(不等待)
var networkTask = httpClient.GetStringAsync("http://www.baidu.com");
var fileTask = File.WriteAllTextAsync(@"D:\Temp\123.txt", "content");

// 然后一起等待
await Task.WhenAll(networkTask, fileTask);

用现实世界比喻

假设您有两件事要做:

  1. 煮咖啡(耗时10分钟)
  2. 烤面包(耗时5分钟)

错误方式(顺序await)

await 煮咖啡(); // 站在咖啡机前等10分钟
await 烤面包(); // 然后才去烤面包
// 总耗时15分钟

正确并行方式

var 咖啡任务 = 煮咖啡(); // 按下咖啡机按钮就走开
var 面包任务 = 烤面包(); // 按下烤面包机按钮

// 做其他事情...
await Task.WhenAll(咖啡任务, 面包任务); // 等两者都完成
// 总耗时10分钟

await的本质是:

  • 不是"不等待",而是"优雅地等待"

  • 在等待期间释放线程资源,但保持代码顺序

    • 会有线程之间的切换,比如原来是线程1在处理,释放以后再继续处理,可能是线程2在处理

      • 注意事项,也有可能是线程1继续在服务(任务立马完成了,根本不用等)
        • 现实类比,接待服务员,然后你只点一个菜,接待服务员就顺手把这件事情做了,因为你立马完成,根本不需要等!
      // 不是同一个线程示例
      ......
      
      namespace ConsoleApp4
      {
          class Program
          {
              static async Task Main(string[] args)
              {   
      			// 打印结果: 1
                  Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                  StringBuilder sb = new StringBuilder();
                  for (int i = 0; i < 10000; i++)
                  {
                      sb.Append("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
                  }
      
                  string fileName = @"D:\VS2019\Temp\123.txt";
                  await File.WriteAllTextAsync(fileName, sb.ToString());
                  // 打印结果 8
                  Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
      
      
              }
          }
      }
      
      - 注意事项: await 不会让后续代码立即执行,它表示:"等待这个异步操作完成,再继续执行后面的代码"
      
      在等待期间:
      当前线程被释放(不阻塞),可以处理其他任务,但后续代码不会执行
      
      操作完成后:
      由线程池分配线程,继续执行 await 之后的代码,线程ID可能变化
      
      这种机制使得异步编程既能保持代码顺序的直观性,又能高效利用线程资源,避免阻塞。
      
      // 同一个线程示例
      ......
      
      namespace ConsoleApp4
      {
          class Program
          {
              static async Task Main(string[] args)
              {   
      			// 打印结果: 1
                  Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                  StringBuilder sb = new StringBuilder();
                  // 更改之处
                  for (int i = 0; i < 2; i++)
                  {
                      sb.Append("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
                  }
      
                  string fileName = @"D:\VS2019\Temp\123.txt";
                  await File.WriteAllTextAsync(fileName, sb.ToString());
                  // 打印结果 1
                  Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
      
      
              }
          }
      }
      
      - 注意事项: await 不会让后续代码立即执行,它表示:"等待这个异步操作完成,再继续执行后面的代码"
      
      在等待期间:
      当前线程被释放(不阻塞),可以处理其他任务,但后续代码不会执行
      
      操作完成后:
      由线程池分配线程,继续执行 await 之后的代码,线程ID可能变化
      
      这种机制使得异步编程既能保持代码顺序的直观性,又能高效利用线程资源,避免阻塞。
      
  • 要实现真正的并行,需要显式管理多个Task对象

  • 注意事项:asyncawait并不一定是成对出现的,以下实例演示(看着异步,实质是同步)

using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;



namespace ConsoleApp4
{
    class Program
    {
        static async Task Main(string[] args)
        {
          
			// 输出1
            Console.WriteLine("之前: " + Thread.CurrentThread.ManagedThreadId);
            double r = await CalcAsync(5000);
            Console.WriteLine($"r={r}");
            // 输出1
            Console.WriteLine("之后" + Thread.CurrentThread.ManagedThreadId);

        }

		
		// 自定义异步方法
        public static async Task<double> CalcAsync(int n)
        {
        	// 内部逻辑没有真正的异步操作(方法中没有使用await),这里输出1
            Console.WriteLine("CalcAsync: " + Thread.CurrentThread.ManagedThreadId);
            double res = 0;
            Random rand = new Random();
            for (int i = 0; i < n*n; i++)
            {
                res += rand.NextDouble();
            }
            return res;
        }
    }
}

- 线程行为特点:
    由于没有真正的异步操作,不会释放线程
    所有代码都在同一个线程上执行
    线程ID在整个过程中保持不变
    
- 缺少真正的异步操作:
    没有I/O操作(文件/网络)
    没有使用Task.Run或Task.Delay
    没有使用async方法中的await
    
    
......
namespace ConsoleApp4
{
    class Program
    {
        static async Task Main(string[] args)
        {
            
			// 输出1
            Console.WriteLine("之前: " + Thread.CurrentThread.ManagedThreadId);
            double r = await CalcAsync(5000);
            Console.WriteLine($"r={r}");
            // 输出4
            Console.WriteLine("之后" + Thread.CurrentThread.ManagedThreadId);

        }

		// 直接返回了 Task 对象,而不是在方法内部使用 await。这是实现异步方法的更高效方式
        public static Task<double> CalcAsync(int n)
        {
            // 使用Task.Run实现异步操作
            return Task.Run(() =>
            {
            	// 输出4
                Console.WriteLine("CalcAsync: " + Thread.CurrentThread.ManagedThreadId);
                double res = 0;
                Random rand = new Random();
                for (int i = 0; i < n * n; i++)
                {
                    res += rand.NextDouble();
                }
                return res;
            });
        }

    }
}

- 使用async 和 await 示例

	public static async Task<double> CalcAsync(int n)
        {
            return await Task.Run(() => {
                Console.WriteLine("CalcAsync: " + Thread.CurrentThread.ManagedThreadId);
                double res = 0;
                Random rand = new Random();
                for (int i = 0; i < n * n; i++)
                {
                    res += rand.NextDouble();
                }
                return res;
            });
        }

- 关键区别:直接返回 Task vs 使用 async/await

- 为什么不需要 async?
	- 没有使用 await,方法内部没有使用 await 关键字,async 关键字主要是为了在方法内使用 await,没有 await 就不需要 async

	- 直接返回 Task 对象:Task.Run() 已经返回了一个 Task<double> 对象,可以直接将这个 Task 返回给调用者,不需要额外的异步状态机制

- 编译器优化:当方法直接返回 Task 时,编译器不需要生成复杂的异步状态机,减少内存分配和方法调用的开销
  • 同步方法中返回异步操作
    • 好处: 避免不必要的状态机开销,对于简单传递任务的情况,省略async更高效
using System;
using System.IO;
using System.Threading.Tasks;

namespace ConsoleApp5
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            string res = await ReadFileTextAsync(1);
            Console.WriteLine($"结果为: {res}");

        }

		
		// 没有async修饰
        static Task<string> ReadFileTextAsync(int num)
        {
            if(num == 1)
            {	
            	// 返回一个异步操作对象
                return File.ReadAllTextAsync(@"D:\VS2019\Temp\123.txt");
            }
            else if(num == 2)
            {	// 返回一个异步操作对象
                return File.ReadAllTextAsync(@"D:\VS2019\Temp\456.txt");
            }
            else
            {	// 同步处理异常
                throw new ArgumentException();
            }
        }
    }
}

  • 修改成真正的异步方法,实例如下
......

namespace ConsoleApp5
{
    class Program
    {
        static async Task Main(string[] args)
        {
           ......

        }


        // static Task<string> ReadFileTextAsync(int num)
        static async Task<string> ReadFileTextAsync(int num)
        {
            if(num == 1)
            {	
            	// 新增await
                return await File.ReadAllTextAsync(@"D:\VS2019\Temp\123.txt");
            }
            else if(num == 2)
            {
            	// 新增await
                return await File.ReadAllTextAsync(@"D:\VS2019\Temp\456.txt");
            }
            else
            {
                throw new ArgumentException();
            }
        }
    }
}

  • 异步方法中的暂停
- 使用 await Task.Delay(),而不是 Thread.sleep()[阻塞调用线程]
// 新建winForm程序
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            using (HttpClient httpClient = new HttpClient())
            {
                string s1 = await httpClient.GetStringAsync("https://www.youzack.com");
                textBox1.Text = s1.Substring(0,20);
				// 这种写法会造成窗体卡死(用户体验很不好)
                Thread.Sleep(5000);

                string s2 = await httpClient.GetStringAsync("https://www.youzack.com");
                textBox1.Text = s2.Substring(21, 40);
            }
        }
    }
}


// 修改为Task.Delay(5000)
......
namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            using (HttpClient httpClient = new HttpClient())
            {
                string s1 = await httpClient.GetStringAsync("https://www.youzack.com");
                textBox1.Text = s1.Substring(0,20);

                // Thread.Sleep(5000);
                // 不会堵塞主线程,窗体相应很丝滑,不会卡死,效果很好!
                await Task.Delay(5000);

                string s2 = await httpClient.GetStringAsync("https://www.youzack.com");
                textBox1.Text = s2.Substring(21, 40);
            }
        }
    }
}

  • 异步方法中的CancellationToken参数: 提前终止执行的信号

    • 应用场景: 请求超时 && 用户取消请求
    • 网络请求N次示例: 控制台持续输出网页源码N次
    // 正常请求示例(没有添加cancellationToken)
    using System;
    using System.IO;
    using System.Net.Http;
    using System.Threading.Tasks;
    
    namespace ConsoleApp5
    {
        class Program
        {
            static async Task Main(string[] args)
            {
                Console.WriteLine("Hello World!");
                await DownloadAsync("https://www.youzack.com", 500);
    
            }
    
            static async Task DownloadAsync(string url,int num)
            {
                using (HttpClient client = new HttpClient())
                {
                    for (int i = 0; i < num; i++)
                    {
                        var html = await client.GetStringAsync(url);
                        Console.WriteLine($"{DateTime.Now}: {html}");
                    }
                }
            }
        }
    }
    
    
    // 正常请求示例(添加cancellationToken)
    using System;
    using System.IO;
    using System.Net.Http;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApp5
    {
        class Program
        {
            static async Task Main(string[] args)
            {
                // 创建取消令牌源
                CancellationTokenSource cts = new CancellationTokenSource();
                // 设置5秒后自动取消
                cts.CancelAfter(5000);
                // 获取取消令牌
                CancellationToken cToken = cts.Token;
    
                // 开始下载任务(下载100次)
                await DownloadAsync("https://www.youzack.com", 100, cToken);
            }
    
    
            static async Task DownloadAsync(string url, int num, CancellationToken cancellationToken)
            {
                using (HttpClient client = new HttpClient())
                {
                    // 循环下载num次(100次)
                    for (int i = 0; i < num; i++)
                    {
                        // 异步获取网页内容
                        var html = await client.GetStringAsync(url);
                        Console.WriteLine($"{DateTime.Now}: {html}");
    
                        // 检查是否收到取消请求
                        if (cancellationToken.IsCancellationRequested)
                        {
                            Console.WriteLine("请求被取消");
                            break; // 跳出循环
                        }
                    }
                }
            }
        }
    }
    
    // 这里可以有另一种写法: 自动抛异常
    
    	static async Task DownloadAsync(string url,int num, CancellationToken cancellationToken)
            {
                using (HttpClient client = new HttpClient())
                {
                    for (int i = 0; i < num; i++)
                    {
                        ......
    
                        if (cancellationToken.IsCancellationRequested)
                        {
                            //Console.WriteLine("请求被取消");
                            //break;
                            // 自动抛异常
                            cancellationToken.ThrowIfCancellationRequested();
                        }
                    }
                }
            }
            
    // 获取响应的时候,还可以传递"取消令牌",实现自动触发异常(效果和上面的例子一模一样)
    
    	static async Task DownloadAsync(string url,int num, CancellationToken cancellationToken)
            {
                using (HttpClient client = new HttpClient())
                {
                    for (int i = 0; i < num; i++)
                    {	
                    	// 传递取消令牌
                        var res = await client.GetAsync(url, cancellationToken);
                        // var html = await client.GetStringAsync(url);
                        // 获取响应内容
                        var html = await res.Content.ReadAsStringAsync();
                        Console.WriteLine($"{DateTime.Now}: {html}");
    
                        //if (cancellationToken.IsCancellationRequested)
                        //{
                        //    //Console.WriteLine("请求被取消");
                        //    //break;
                        //    cancellationToken.ThrowIfCancellationRequested();
                        //}
                    }
                }
            }
    
    • 实战经验: 在asp.net开发中,CancellationToken无需开发者处理,开发者需要的做的,仅仅是转发就够用了
  • Task类的重要方法

- 以下方法中,任何一个Task完成,Task就算完成
    - Task<Task> 
    - WhenAny(IEnumerable<Task> tasks)
    
- 以下方法中,所有Task完成,Task才算完成,但不在乎执行顺序
	- Task<TResult[]>
	- WhenAll<TResult>(pamars Task<TResult[]tasks>)
	
- FromResult()创建普通数值的Task对象
  • WhenAll示例
 	static async Task Main(string[] args)
        {
            // 获取3个Task对象,传给WhenAll并发(顺序是不确定的,但保证所有任务完成)
            Task<string> t1 = File.ReadAllTextAsync(@"D:\VS2019\Temp\123.txt");
            Task<string> t2 = File.ReadAllTextAsync(@"D:\VS2019\Temp\456.txt");
            Task<string> t3 = File.ReadAllTextAsync(@"D:\VS2019\Temp\789.txt");

            string[] sList = await Task.WhenAll(t1,t2,t3);

            string s1 = sList[0];
            string s2 = sList[1];
            string s3 = sList[2];

            Console.WriteLine(s1);
            Console.WriteLine(s2);
            Console.WriteLine(s3);



        }

yield关键字

  • 作用: 简化迭代器(Iterator) 的实现,它允许你按需生成序列中的元素(延迟执行),而无需一次性创建整个集合
    • 意义: 当整个集合很大的时候,内存一次性加载所有的数据,鸭梨山大.如果分成"按需索取",性能会提升许多!
  • 示例
// 不使用yield
......
namespace ConsoleApp5
{
    class Program
    {
        
        static void Main(string[] args)
        {
            var list1 = GetNumber(101);
            // 一次性释放所有
            Console.WriteLine($"[{string.Join(", ", list1)}]");
        }
		
		// 生成多条数据的数组
        static IEnumerable<int> GetNumber(int max)
        {
            var list = new List<int>();
            for (int i = 0; i < max; i++)
            {
                list.Add(i);
            }

            return list;
        }
    }
}


// yield示例
......
namespace ConsoleApp5
{
    class Program
    {
       
        static void Main(string[] args)
        {
                  
            foreach(var num in GetNumber(1000000))
            {
                
                if (num > 100) break;
                Console.WriteLine(num);
            }


        }

		
        static IEnumerable<int> GetNumber(int max)
        {
          
            for (int i = 0; i < max; i++)
            {
                yield return i;
            }
            
        }

    }
}

// 读取文件示例

......

namespace ConsoleApp5
{
    class Program
    {
    
        static void Main(string[] args)
        {
            
          
            foreach (var line in ReadFileLines(@"D:\VS2019\Temp\123.txt"))
            {
                Console.WriteLine(line);
            }

        }


        static IEnumerable<string> ReadFileLines(string filePath)
        {
            using (var reader = new StreamReader(filePath))
            {
                while (!reader.EndOfStream)
                {	
                	// 每次只加载一行,并记录下一次结果(节省性能开销)
                    yield return reader.ReadLine();
                }
            }
        }



    }
}


Linq模块

  • 作用: 让数据处理变得更简单,无需自己再去自定义方法去处理数据

  • 学习这个模块的时候,需要先对委托lam表达式有一定的了解

  • 委托复习

    • // 声明委托类型并运行(不带参,不带返回值)
      
      using System;
      
      namespace ConsoleApp6
      {
      	// 声明委托类型
          delegate void MiddleTrader();
          class Program
          {	
          	// 写这里也一样的效果
              // delegate void MiddleTrader();
              static void Main(string[] args)
              {
              	// 可以看出,委托实际就是一种数据类型
                  MiddleTrader middleTrader = TestDeleate;
                  middleTrader();
              }
      
              static void TestDeleate()
              {
                  Console.WriteLine("Test Delegate!!!");
              }
      
      
              
      
          }
      }
      
      
    • // 声明委托类型并运行(带参,带返回值)
      using System;
      
      namespace ConsoleApp6
      {
          // delegate void MiddleTrader();
          class Program
          {
          	// 声明委托类型(带返回值,带参)
              delegate int MiddleTrader1(int i1,int i2);
      
              static void Main(string[] args)
              {
                  //MiddleTrader middleTrader = TestDeleate;
                  //middleTrader();
      			
      			// 应用
                  MiddleTrader1 middleTrader1 = Add;
                  var res1 = middleTrader1(1, 2);
                  Console.WriteLine(res1);
              }
      
              static void TestDeleate()
              {
                  Console.WriteLine("Test Delegate!!!");
              }
      
              static int Add(int a, int b)
              {
                  return a+ b;
              }
      
          }
      }
      
      
      // 使用泛型委托示例(既能接受加法,也能接收字符串)
      
      using System;
      
      namespace ConsoleApp6
      {
          
          class Program
          {
          	// delegate int MiddleTrader1(int i1,int i2);
              // 声明泛型委托
              delegate T ManyKind<T>(T a, T b);
      
              static void Main(string[] args)
              {
                 
      
                  ManyKind<int> manyKind1 = Add;
                  var res1 = manyKind1(1, 2);
                  Console.WriteLine(res1);
      
                  ManyKind<string> manyKind2 = Cat;
                  var res2 = manyKind2("king", "ing");
                  Console.WriteLine(res2);
              }
      
      
              static int Add(int a, int b)
              {
                  return a + b;
              }
      
              static string Cat(string a, string b)
              {
                  return a + b;
              }
      
          }
      }
      
      
      
      
      • 注意事项: 自定义委托在实战中应用的比较少,一般用微软写好的Action(无返回值)和Func(有返回值)
      using System;
      
      namespace ConsoleApp6
      {
         
          class Program
          {
             
      
              static void Main(string[] args)
              {
                 // 声明action
                  Action myAction = TestDeleate;
                  myAction();
      			
      			// Func实例
                  Func<int, int, int> CalAdd = Add;
                  var res1 = CalAdd(1, 2);
                  Console.WriteLine(res1);
      
                  Func<string, string, string> CalConcat = Cat;
                  var res2 = CalConcat("King", "ing");
                  Console.WriteLine(res2);
      
              }
      
              static void TestDeleate()
              {
                  Console.WriteLine("Test Delegate!!!");
              }
      
              static int Add(int a, int b)
              {
                  return a + b;
              }
      
              static string Cat(string a, string b)
              {
                  return a + b;
              }
      
      
      
      
      
      
          }
      }
      
      
    • 匿名委托: 旧写法用delegate来声明,新写法用lam表达式

      • using System;
        
        namespace ConsoleApp6
        {
           
            class Program
            {
              
        
                static void Main(string[] args)
                {
                    
                    Action<string, int> myAction = delegate (string s, int i)
                     {
                         Console.WriteLine($"{s}---{i}");
                     };
                    myAction("king",123);
        
                    Func<int, int, int> myFunc = delegate (int i1, int i2)
                      {
                          return i1 + i2;
                      };
                    var res1 = myFunc(1,2);
                    Console.WriteLine(res1);
        
                }
        
            }
        }
        
        // 参数可以简写(编译器自动推断)
        		Action<string, int> myAction = (s, i) =>
                    {
                        Console.WriteLine($"{s}---{i}");
                    };
                    myAction("king",123);
        
                    Func<int, int, int> myFunc = (int i1, int i2) =>
                      {
                          return i1 + i2;
                      };
        
                    var res1 = myFunc(1,2);
                    Console.WriteLine(res1);
                    
        // 只有一行且没有返回值的简写
         Action myAction = () => Console.WriteLine("666");
         myAction();
         
         // 只有一行代码,可以省略return
         		Func<int, int, int> myFunc = (i1, i2) => i1 + i2;
                 var res = myFunc(1, 2);
                 Console.WriteLine(res);
                 
         // 只有一个参数,可以省略括号
         Action<int> myAction = i => Console.WriteLine(i);
         myAction(666);
        

拓展方法

  • 作用: 允许开发者在不修改原始类型定义、不创建子类的前提下,"添加"新方法到现有类型(包括结构体接口等)
  • 本质: 是静态方法的语法糖,但可以像实例方法一样调用,极大提高了代码的可读性和流畅性
  • 语法:
- "静态类"中的"静态方法"
	- 扩展方法必须在 static 类中定义为 static 方法
	
- this 关键字修饰第一个参数
	- 第一个参数前加 this 关键字,指明要扩展的类型和实例

- 调用方式
	- 通过被扩展类型的实例调用(如同调用自己的方法)
  • 自定义实例演示
using System;

namespace ConsoleApp6
{
   
    class Program
    {
        

        static void Main(string[] args)
        {
           
           // 像调用实例方法那样,去调用该"拓展方法"
            Person p = new Person() { Name = "Jim Green" };
            p.Report();
        }
    }

	
	// 自定义类
    class Person
    {
        public string Name { get; set; }
    }

	// 静态类
    static class PersonExtension
    {
    	// 自定义静态方法,第一个参数为this
        public static void Report(this Person person)
        {
            Console.WriteLine($"姓名为: {person.Name}");
        }
    }
}

  • string类型添加拓展方法示例
using System;

namespace ConsoleApp6
{
 
    class Program
    {
       

        static void Main(string[] args)
        {
           
            // 使用
            string text = "Hello";
            string reversed = text.Reverse(); // 像实例方法一样调用
            Console.WriteLine(reversed); // 输出 "olleH"


        }
    }

    public static class StringExtensions // 必须是静态类
    {
        // this + 要扩展的类型(string)作为第一个参数
        public static string Reverse(this string input)
        {
            if (string.IsNullOrEmpty(input)) return input;
            char[] chars = input.ToCharArray();
            Array.Reverse(chars);
            return new string(chars);
        }
    }



}

  • 底层原理
// 原始代码
text.Reverse();

// 编译后实际执行
StringExtensions.Reverse(text); 
  • Linq实例
using System;
using System.Linq; // 必须引入Linq,数组才有where(where属于"拓展方法")

namespace ConsoleApp7
{
    class Program
    {
        static void Main(string[] args)
        {
           
            int[] nums = new int[] { 1,2,3,4,5,6,7,8,9 };
            // 使用where可以快速方便实现数据过滤的效果
            var res = nums.Where(a=>a>3);
            foreach (var item in res)
            {
                Console.WriteLine(item);
            }
           
        }
    }
}

// 自定义实例(稍显麻烦)

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp7
{
    class Program
    {
        static void Main(string[] args)
        {
           

            int[] nums = new int[] { 1,2,3,4,5,6,7,8,9 };
            // 运用
            var res = myWhere(nums, a => a > 3);
            foreach (var item in res)
            {
                Console.WriteLine(item);
            }
           
        }

		// IEnumerable<T>是泛型集合,这里T指定为Int
		// int类型数组显然符合这个条件
		// 最后再传入Func类型的函数对象(传入int,输出bool)f
        static IEnumerable<int> myWhere(IEnumerable<int> items,Func<int,bool> f)
        {
            List<int> res = new List<int>();
            foreach (var item in items)
            {
                if (f(item) == true)
                {
                    res.Add(item);
                }
            }

            return res;
        }
    }
}


// 性能优化自定义实例,变成yield

......
namespace ConsoleApp7
{
    class Program
    {
        static void Main(string[] args)
        {
            ......

        }

        static IEnumerable<int> myWhere(IEnumerable<int> items, Func<int, bool> f)
        {
            List<int> res = new List<int>();
            foreach (var item in items)
            {
                if (f(item) == true)
                {
                    // res.Add(item);
                    // 返回的是一个迭代器对象
                    yield return item;
                }
            }

            // return res;
        }

    }
}

  • Linq常用方法介绍,先初始化数据
// Employee.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp8
{
    class Employee
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public int Age { get; set; }

        public bool Gender { get; set; }

        public int Salary { get; set; }

        public override string ToString()
        {
            return $"Id={Id},Name={Name},Age={Age},Gender={Gender},Salary={Salary}";
        }

    }
}

  • Where方法: 筛选出符合条件的所有对象
  • Count方法: 统计符合条件的数据个数
  • Any方法: 只要集合中有一个对象满足条件,就返回true,否则返回false
using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp8
{
    class Program
    {
        static void Main(string[] args)
        {
            
            List<Employee> list = new List<Employee>();
            initEmployee(list);
            // Where示例
            var res = list.Where(obj => obj.Age > 30);
            foreach (var obj in res)
            {
                Console.WriteLine(obj.Age);
            }
            // Count和Any示例
            Console.WriteLine(list.Count()); // 8
            Console.WriteLine(list.Count(obj=>obj.Age>30)); // 5
            Console.WriteLine(list.Count(obj=>obj.Age>30 && obj.Salary > 8000)); // 2

            Console.WriteLine(list.Any(obj=>obj.Salary > 8000)); // True
            Console.WriteLine(list.Any(obj=>obj.Salary > 800000)); // False
            
           


        }
		
		// 初始化数据
        static void initEmployee(List<Employee> list)
        {
            list.Add(new Employee { Id = 1, Name = "jerry", Age = 28, Gender = true, Salary = 5000 });
            list.Add(new Employee { Id = 2, Name = "jim", Age = 33, Gender = true, Salary = 3000 });
            list.Add(new Employee { Id = 3, Name = "lily", Age = 35, Gender = false, Salary = 9000 });
            list.Add(new Employee { Id = 4, Name = "lucy", Age = 16, Gender = false, Salary = 2000 });
            list.Add(new Employee { Id = 5, Name = "kimi", Age = 25, Gender = true, Salary = 1000 });
            list.Add(new Employee { Id = 6, Name = "nancy", Age = 35, Gender = false, Salary = 8000 });
            list.Add(new Employee { Id = 7, Name = "zack", Age = 35, Gender = true, Salary = 8500 });
            list.Add(new Employee { Id = 8, Name = "jack", Age = 33, Gender = true, Salary = 8000 });
        }


    }
}

  • 获取一条数据的几种方法
- Single: 有且只有一条满足要求的数据(返回多条则触发异常)
- SingleOrDefault: 最多只有一条满足要求的数据(返回多条则触发异常,没有结果则返回null)
- First: 至少有一条,返回一条(没有则触发异常)
- FirstOrDefault: 返回第一条或默认值(null)

// Single演示

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp8
{
    class Program
    {
        static void Main(string[] args)
        {
            
            List<Employee> list = new List<Employee>();
            initEmployee(list);
            
            // 这种写法也是可以的,支持链式查询
            // var item = list.Where(obj => obj.Name == "jerry").Single();
            // 如果变成这句,由于返回了多条数据,Single会自动触发异常
            // IEnumerable<Employee> items = list.Where(obj => obj.Salary == 8000);
            IEnumerable<Employee> items = list.Where(obj => obj.Name == "jerry");
            
            // Single()会提取IEnumerable类型集合中,符合要求的Employee对象
            Employee item = items.Single();
            // Id=1,Name=jerry,Age=28,Gender=True,Salary=5000
            Console.WriteLine(item);


        }

        static void initEmployee(List<Employee> list)
        {
            list.Add(new Employee { Id = 1, Name = "jerry", Age = 28, Gender = true, Salary = 5000 });
            list.Add(new Employee { Id = 2, Name = "jim", Age = 33, Gender = true, Salary = 3000 });
            list.Add(new Employee { Id = 3, Name = "lily", Age = 35, Gender = false, Salary = 9000 });
            list.Add(new Employee { Id = 4, Name = "lucy", Age = 16, Gender = false, Salary = 2000 });
            list.Add(new Employee { Id = 5, Name = "kimi", Age = 25, Gender = true, Salary = 1000 });
            list.Add(new Employee { Id = 6, Name = "nancy", Age = 35, Gender = false, Salary = 8000 });
            list.Add(new Employee { Id = 7, Name = "zack", Age = 35, Gender = true, Salary = 8500 });
            list.Add(new Employee { Id = 8, Name = "jack", Age = 33, Gender = true, Salary = 8000 });
        }


    }
}

// SingleOrDefault演示
		   var res = list.SingleOrDefault(obj => obj.Name == "Tom");
            if(res == null)
            {
                Console.WriteLine("查询结果为空");
            }
            else
            {
                Console.WriteLine(res);
            }
            // 没找到数据,返回0
		   int[] nums = new int[] {1,2,3,4,5,6 };
		   // var data = nums.SingleOrDefault(num => num > 5); // 返回6
            var data = nums.SingleOrDefault(num => num > 10);
            Console.WriteLine(data);
var data = list.First(obj => obj.Salary == 8000);
// 有两条数据符合要求,但只选择第一条数据: Id=6,Name=nancy,Age=35,Gender=False,Salary=8000
Console.WriteLine(data);
  • 排序的几种方法介绍
- OrderBy: 升序排列
- OrderByDescending: 降序排列
- ThenBy: 链式查询
		   //  var objs = list.OrderByDescending(obj => obj.Salary);
		   var objs = list.OrderBy(obj => obj.Salary);
            foreach (var obj in objs)
            {
                Console.WriteLine(obj.Salary);
            }
            
            1000
            2000
            3000
            5000
            8000
            8000
            8500
            9000
            
            // 数组排序示例
            List<int> nums = new List<int> {2,3,6,8,9,4,1 };
            var res = nums.OrderBy(num => num);
            foreach (var num in res)
            {
                Console.WriteLine(num);
            }
            
            // 链式查询
            var objs = list.OrderBy(obj => obj.Age).ThenBy(obj => obj.Salary);
            foreach (var obj in objs)
            {
                Console.WriteLine(obj);
            }
- Skip: 跳过几条数据
- Take: 获取几条数据
		   // 跳过两条数据,然后获取前三条数据
		   var objs = list.Skip(2).Take(3);
            foreach (var obj in objs)
            {
                Console.WriteLine(obj);
            }
  • 聚合函数
- Max
- Min
- Average
- Count
- Sum
 var obj = list.Max(obj=>obj.Age);
 Console.WriteLine(obj);
 
 var res = list.Where(obj => obj.Age > 30).Average(obj => obj.Salary);
 Console.WriteLine(res);
  • GroupBy分组函数

// 根据年龄进行分组
var objSets = list.GroupBy(obj => obj.Age);
foreach (var objset in objSets)
{	
	// Key就是"分组键",这里会被赋值为Age
    Console.WriteLine(objset.Key);
    // 统计每组Age中,Salary最大的记录
    Console.WriteLine(objset.Max(obj=>obj.Salary));

    foreach (var obj in objset)	
    {
    Console.WriteLine(obj.Age);
    }
}

- 返回结果
28
28
33
33
33
35
35
35
35
16
16
25
25


// 如果想对分组以后的年龄进行排序,可以这么做
var objSets = list.GroupBy(obj => obj.Age).OrderBy(group=>group.Key);

  • 投影: 把集合中的每一项转换成另一种类型
- Select(): 从集合中提取特定的值或转换元素
// 从Employee提取Age并转换为IEnumerable<int>类型集合
IEnumerable<int> groupSet = list.Select(obj => obj.Age);
foreach (int group in groupSet)
{
	Console.WriteLine(group);
}

Console.WriteLine("-------------------------");

var items = list.Where(obj => obj.Age > 30);
foreach (var item in items)
{
	Console.WriteLine(item);
}

- 返回结果:
28
33
35
16
25
35
35
33
-------------------------
Id=2,Name=jim,Age=33,Gender=True,Salary=3000
Id=3,Name=lily,Age=35,Gender=False,Salary=9000
Id=6,Name=nancy,Age=35,Gender=False,Salary=8000
Id=7,Name=zack,Age=35,Gender=True,Salary=8500
Id=8,Name=jack,Age=33,Gender=True,Salary=8000
            
            
// string类型实例
IEnumerable<string> groupSet = list.Select(obj => obj.Name);
foreach (string group in groupSet)
{
	Console.WriteLine(group);
}
Console.WriteLine("-------------------------");

- 返回结果:
jerry
jim
lily
lucy
kimi
nancy
zack
jack
-------------------------
            
            
  • 使用Select提取两个字段示例
IEnumerable<string> groupSet = list.Select(obj => obj.Name + "--" + obj.Age);
foreach (string group in groupSet)
{
	Console.WriteLine(group);
}

Console.WriteLine("-------------------------");

- 返回结果:
jerry--28
jim--33
lily--35
lucy--16
kimi--25
nancy--35
zack--35
jack--33
-------------------------	
  • 示例: 提取Salary大于5000的记录,并根据Gender筛选男或者女
var items = list.Where(obj => obj.Salary > 5000).Select(obj=>obj.Gender?"男":"女");
foreach (var item in items)
{
    Console.WriteLine(item);
}

- 结果:
	女
    女
    男
    男
  • 使用Select实现类型转换示例
// Cat.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp8
{
    class Cat
    {
        public string nickName { get; set; }
        public int Age { get; set; }
    }
}

// 主程序

IEnumerable<Cat> items = list.Select(obj => new Cat { nickName = obj.Name, Age = obj.Age });
// Cat类型
foreach (Cat item in items)
{
	Console.WriteLine($"{item.nickName}--{item.Age}");
}

匿名类型(使用Var来声明)

Cat c1 = new Cat { nickName = "Kitty" };
// 声明匿名类型
var obj1 = new { Name = "Jim", Age = 20, Hobby = "Basketball", Department = "Net" };
Console.WriteLine(c1.nickName);
// 输出 Net
Console.WriteLine(obj1.Department);
  • 使用Select从别的类型数据提取字段,然后填充给匿名类型示例
var itmes = list.Select(obj => new { Name = obj.Name, Age = obj.Age, Gender=obj.Gender?"男":"女" });
foreach (var item in itmes)
	{
		Console.WriteLine(item);
	}
  • 集合转换
- ToArray(): 把IEnumerable<T>集合类型转换为"数组"
- ToList(): 把IEnumerable<T>集合类型转换为"列表"
IEnumerable<Employee> items = list.Where(obj => obj.Salary > 4500);
List<Employee> list1 = items.ToList();
Employee[] arr = items.ToArray();
  • 查询语法(SQL语法的风格): 比较少用到,但要了解一下(看着别扭...)
 var items = from obj in list
 			where (obj.Salary > 4500)
 			select (new { Name = obj.Name, Age = obj.Age, Gender = obj.Gender ? "男" : "女" });


foreach (var item in items)
{
Console.WriteLine(item);
}

知识点穿插

  • 在C#中,List(列表)和Array(数组)是两种完全不同的类型,它们具有显著区别

C# 中的区别

特性 Array(数组) List(列表)
类型定义 固定长度数据结构(如 int[] 动态集合(泛型类 List
长度 创建后长度固定 长度动态可变(自动扩容)
内存分配 连续内存块 内部基于数组实现,但封装了动态扩容逻辑
功能方法 基础操作(如索引访问) 提供丰富方法(Add(), Remove(), Find()等)
命名空间 语言内置(System.Array System.Collections.Generic
示例代码 int[] arr = new int[5]; List list = new List();

关键差异:

  • 长度可变性:数组长度固定,列表长度动态。
  • 功能扩展:列表提供更多便捷方法(如添加、删除、搜索)。
  • 性能:数组访问更快;列表在频繁插入/删除时可能触发扩容(有额外开销)。

依赖注入

  • 通俗理解
- 自己发电:准备一大堆东东
- 用电厂的电(依赖注入)
  • 引入实例: 从头到尾自己组装
using System;

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {
         
		   // 接口引用指向实现类对象
            ITestService t = new TestServiceImp1();
            t.Name = "Jim Green";
            t.SayHi();
        }
    }



    public class TestServiceImp1 : ITestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine($"I'm {Name}");
        }
    }

    public interface ITestService
    {
        public string Name { get; set; }
        public void SayHi() { }
    }

}

  • 实例演示: 展示如何注册一个服务(TestServiceImp1)并从中获取实例,同时说明了服务的生命周期(这里是Transient)
using Microsoft.Extensions.DependencyInjection; // 引入依赖注入框架
using System;

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {
			
			// 创建服务容器
            ServiceCollection services = new ServiceCollection();
            // 将 TestServiceImp1 注册为瞬时(Transient)服务
            services.AddTransient<TestServiceImp1>();
            // 构建服务提供者
            using (ServiceProvider sp = services.BuildServiceProvider())
            {
            	// 解析服务
                TestServiceImp1 t = sp.GetService<TestServiceImp1>();
                t.Name = "Jim Green";
                t.SayHi();
            }

  
        }
    }



    public class TestServiceImp1 : ITestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine($"I'm {Name}");
        }
    }

    public interface ITestService
    {
        public string Name { get; set; }
        public void SayHi() { }
    }

}

  • 服务生命周期
生命周期 注册方法 特点
Transient AddTransient<> 每次请求创建新实例(最常用)
Scoped AddScoped<> 每个作用域一个实例(如 Web 请求)
Singleton AddSingleton<> 整个应用一个实例
  • 引用生命周期,检验是否属于同一对象
	static void Main(string[] args)
        {

            ServiceCollection services = new ServiceCollection();
            services.AddTransient<TestServiceImp1>();
            using (ServiceProvider sp = services.BuildServiceProvider())
            {
                TestServiceImp1 t = sp.GetService<TestServiceImp1>();
                t.Name = "Jim Green";
                t.SayHi();
				
				// 创建新示例
                TestServiceImp1 t1 = sp.GetService<TestServiceImp1>();
				// 返回False,说明AddTransient创建了两个实例
                Console.WriteLine(object.ReferenceEquals(t,t1));
            }

           
        }
  • 补充说明
- using Microsoft.Extensions.DependencyInjection
	- 提供服务容器,服务注册,解析
static void Main(string[] args)
{
    // 创建服务容器
    ServiceCollection services = new ServiceCollection();
    
    // 注册服务
    services.AddTransient<TestServiceImp1>();
    
    // 构建服务提供者
    using (ServiceProvider sp = services.BuildServiceProvider())
    {
        // 解析服务
        TestServiceImp1 t = sp.GetService<TestServiceImp1>();
        t.Name = "Jim Green";
        t.SayHi();
    }
}
  • 运行效果: 与直接new对象的效果相同,但创建对象的方式由依赖注入容器管理
- 小结: 在小型项目中,DI 的优势可能不明显,甚至会觉得 "多此一举"。但在大型项目中,随着类数量增加、依赖关系复杂化,DI 带来的解耦、可维护性、可测试性等优势会变得至关重要,能显著降低团队协作成本和后期维护难度。这也是为什么几乎所有现代大型框架(如ASP.NET Core、Spring 等)都将依赖注入作为核心特性。
  • AddScoped实例演示
using Microsoft.Extensions.DependencyInjection;
using System;

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {

            ServiceCollection services = new ServiceCollection();
            // 将接口映射为TestServiceImp1类实例
            // 容器会创建 TestServiceImp1 类的实例,并以 ITestService 接口类型返回(多态特性)
            services.AddScoped<ITestService,TestServiceImp1>();
            using (ServiceProvider sp = services.BuildServiceProvider())
            {
				// 每次请求服务,就创建一个TestServiceImp1实例
                ITestService t = sp.GetService<ITestService>();
                // t 虽然声明为 ITestService 接口类型,但实际指向的是 TestServiceImp1 实例
                t.Name = "Jim Green";
                t.SayHi();

            }

         
        }
    }



    public class TestServiceImp1 : ITestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine($"I'm {Name}");
        }
    }

    public interface ITestService
    {
        public string Name { get; set; }
        public void SayHi() { }
    }

}

  • 非泛型写法:替换一句代码,一模一样的效果(泛型的兼容性更好)
......

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {

            ......
            // services.AddScoped<ITestService,TestServiceImp1>();
            services.AddSingleton(typeof(ITestService),typeof(TestServiceImp1));
            using (ServiceProvider sp = services.BuildServiceProvider())
            {

              ......
            }

         
        }
    }

  .......

}

  • 本质: 依然是建立 “接口 - 实例” 的映射关系,并指定服务生命周期
  • 手动类型转换示例(看上去有点啰嗦)
......

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {

            ServiceCollection services = new ServiceCollection();
            services.AddScoped<ITestService, TestServiceImp1>();
            using (ServiceProvider sp = services.BuildServiceProvider())
            {
			   // 啰嗦
			   // typeof(ITestService)返回的是object对象,所以要转换一下类型
                ITestService t = (ITestService)sp.GetService(typeof(ITestService));
                t.Name = "Jim Green";
                t.SayHi();

            }

        }
    }



    public class TestServiceImp1 : ITestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine($"I'm {Name}");
        }
    }

    public interface ITestService
    {
        public string Name { get; set; }
        public void SayHi() { }
    }

}

  • GetRequiredService<T>(): 若服务容器中没有对应类型的T服务,则自动触发异常
using Microsoft.Extensions.DependencyInjection;
using System;

namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {

            ServiceCollection services = new ServiceCollection();
            // 注册接口服务
            services.AddScoped<ITestService, TestServiceImp1>();
            using (ServiceProvider sp = services.BuildServiceProvider())
            {

                // ITestService t = sp.GetRequiredService<ITestService>();
                // 报错: 并没有找到对应的服务
                TestServiceImp1 t = sp.GetRequiredService<TestServiceImp1>();
                t.Name = "Jim Green";
                t.SayHi();

            }
 
        }
    }



    public class TestServiceImp1 : ITestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine($"I'm {Name}");
        }
    }

    public interface ITestService
    {
        public string Name { get; set; }
        public void SayHi() { }
    }

}

- 修复方式1: 注册TestServiceImp1服务(耦合性高,完全不推荐这种方式,类一多,根本忙不过来...)
......
services.AddScoped<ITestService, TestServiceImp1>();
services.AddScoped<TestServiceImp1>();
......

- 修复方式2: 手动强制转换类型
......
ITestService t = sp.GetRequiredService<ITestService>();
// 强制转换类型......
TestServiceImp1 t1 = (TestServiceImp1)t;
t1.Name = "Jim Green";
t1.SayHi();

  • 注意事项
- GetRequiredService 与 GetService 的区别:
	- GetService<T>():当服务未注册时,返回 null。
	- GetRequiredService<T>():当服务未注册时,直接抛出异常(而不是返回 null),强制要求服务必须存在。
  • 一个接口对应多个实现” 的注册与获取方式---GetServices<>()
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;


namespace ConsoleApp9
{
    class Program
    {
        static void Main(string[] args)
        {

            ServiceCollection services = new ServiceCollection();
            // 同一个接口,注册为多个服务
            services.AddScoped<ITestService, TestServiceImp1>();
            services.AddScoped<ITestService, TestServiceImp2>();
            using (ServiceProvider sp = services.BuildServiceProvider())
            {
				
				// 获取服务集合,遍历展示
                IEnumerable<ITestService> items = sp.GetServices<ITestService>();
                foreach (var item in items)
                {	
                	// ConsoleApp9.TestServiceImp1
				  // ConsoleApp9.TestServiceImp2
                    Console.WriteLine(item.GetType());
                }
               
            }

        }
    }



    public class TestServiceImp1 : ITestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine($"I'm {Name}");
        }
    }


	// 新增实现类
    public class TestServiceImp2 : ITestService
    {
        public string Name { get; set; }
        public void SayHi() { Console.WriteLine($"I'm {Name},I'm in TestServiceImp2"); }
    }

    public interface ITestService
    {
        public string Name { get; set; }
        public void SayHi() { }
    }


}

  • 依赖注入针对构造函数,可以自动传参,大大方便了调用者
以下代码包含三个主要部分:
	- 接口定义:IConfig、IStorage、ILog 三个接口,定义了不同服务的规范。
	- 实现类:ConfigImp1、StorageImp1、LogImp1 分别实现上述接口,提供具体功能。
	- 控制器(Controller):作为业务逻辑的入口,依赖 ILog 和 IStorage 服务完成操作。
	- 依赖注入配置:在 Main 方法中注册服务并通过容器解析实例。
using System;
using Microsoft.Extensions.DependencyInjection;

namespace ConsoleApp10
{
    class Program
    {
        static void Main(string[] args)
        {

            var services = new ServiceCollection();
            
            services.AddScoped<Controller>();
            services.AddScoped<IStorage,StorageImp1>();
            services.AddScoped<ILog,LogImp1>();
            services.AddScoped<IConfig,ConfigImp1>();

            using (var sp = services.BuildServiceProvider())
            {
            	// 从容器获取 Controller 实例(自动注入其依赖的 ILog 和 IStorage)
                var c = sp.GetRequiredService<Controller>();
                c.Test();
            }

            Console.ReadLine();
        }
    }

    class ConfigImp1 : IConfig
    {
        public string GetValue(string value)
        {
            Console.WriteLine($"I'm in ConfigImp1,your value is {value}");
            return value;
        }
    }

	// 实现类:具体的存储逻辑,依赖 IConfig
    class StorageImp1 : IStorage
    {
        private readonly IConfig config;
		
		// 依赖关系:StorageImp1 依赖 IConfig,通过构造函数注入获取 IConfig 实例(无需手动 new,由 DI 容器自动提供)
        public StorageImp1(IConfig config)
        {
            this.config = config;
        }

        public string Save(string content,string name)
        {
            string server = config.GetValue("server");
            Console.WriteLine($"向服务器{server}的文件名为{name}上传{content}");
            return $"向服务器{server}的文件名为{name}上传{content},上传动作完成!";
        }
    }

    class LogImp1 : ILog
    {
        public string Log(string content)
        {
            Console.WriteLine($"I'm in LogImp1,your log is {content}");
            return $"I'm in LogImp1,your log is {content}";
        }
    }

    class Controller
    {
        private readonly ILog log; // 依赖注入的日志服务
        private readonly IStorage storage; // 依赖注入的存储服务
		
		// 依赖关系:通过构造函数注入获取 ILog 和 IStorage 实例,无需关心它们的具体实现和依赖(如 IStorage 依赖的 IConfig 由容器自动处理)
        public Controller(ILog log, IStorage storage)
        {
            this.log = log;
            this.storage = storage;
        }

        public void Test()
        {
            log.Log("开始上传");
            this.storage.Save("xxxxxxxxxxxxx", "1.txt");
            log.Log("上传完毕");
        }
    }

    interface IConfig
    {
        public string GetValue(string name);
    }

    interface IStorage
    {
        public string Save(string content, string name);
    }

    interface ILog
    {
        public string Log(string content);
    }
}


- 好处: 依赖链自动解析,当获取 Controller 实例时,容器会自动创建其依赖的 ILog(LogImp1)和 IStorage(StorageImp1);而 StorageImp1 依赖的 IConfig(ConfigImp1)也会被容器自动创建并注入,形成完整的依赖链。

  • 注意事项
- 如果没有注册 services.AddScoped<ILog, LogImp1>() 和 services.AddScoped<IStorage, StorageImp1>(),调用 sp.GetRequiredService<Controller>() 时不会自动填充 Controller 构造函数的参数,反而会直接抛出异常

- 依赖注入容器的 “自动填充构造函数参数” 能力,完全依赖于预先注册的服务映射关系。只有当构造函数中所有参数的类型(或其实现)都已在容器中注册时,容器才能成功解析并创建实例

- 如果缺少任何一个依赖的注册,GetRequiredService<T>() 会直接报错

JSON配置

  • 安装工具库Microsoft.Extensions.configurationMicrosoft.Extensions.configuration.Json
  • 先用比较土的方式来读取配置
- 新建 config.json,放一点实验数据(文件属性-较新则复制)

{
  "name": "Jim Green",
  "age": "18",
  "proxy": {"address": "192.168.1.1"}
}
using System;
using Microsoft.Extensions.Configuration; // 引入配置库
using Microsoft.Extensions.Configuration.Json; // 引入json库

namespace ConsoleApp11
{
    class Program
    {
        static void Main(string[] args)
        {
            // 创建构建器容器实例
            ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
            // 把json文件加进容器实例
            // - optional: true:表示该文件是可选的(不存在也不会报错)
            //  - reloadOnChange: true:表示当文件内容变化时会重新加载配置
            configurationBuilder.AddJsonFile("config.json", optional: true, reloadOnChange: true);
            // 创建json文件对应的的配置实例
            IConfiguration configuration = configurationBuilder.Build();
            // 读取各个字段
            string name = configuration["name"];
            string age = configuration["age"];
            string proxyAddress = configuration["proxy:address"];  // 读取嵌套对象的属性,使用冒号分隔层级
            // Jim Green--18--192.168.1.1
            Console.WriteLine($"{name}--{age}--{proxyAddress}");

        }
    }
}

配置1

  • 另一种读取配置文件的方式: 把config.json当做一个,然后读取配置
// 安装 Microsoft.Extensions.Configuration.Binder
// 实现目标: 从 JSON 配置文件中读取并解析嵌套的proxy配置信息,并将其映射到自定义的Proxy类对象

// 新建类

......
namespace ConsoleApp11
{	
	// 对应配置文件
    class Proxy
    {
        public string Address { get; set; }
        public int Port { get; set; }
        
        // 修改默认的输出行为,返回Address
        public override string ToString()
        {
            return Address; 
        }
    }

    class Config
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public Proxy proxy { get; set; }
    }
}

// 主程序
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;

namespace ConsoleApp11
{
    class Program
    {
        static void Main(string[] args)
        {
    
            ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
            configurationBuilder.AddJsonFile("config.json", optional: true, reloadOnChange: true);
            IConfiguration configuration = configurationBuilder.Build();
            // - 从配置中获取proxy节点(对应 JSON 中的"proxy": {"address": "192.168.1.1"})
            // - 将proxy节点的内容自动映射到Proxy类的实例中(需要Proxy类定义与 JSON 结构匹配)
            Proxy proxy = configuration.GetSection("proxy").Get<Proxy>();
            // 192.168.1.1--80
            Console.WriteLine($"{proxy.Address}--{proxy.Port}");

            Console.ReadLine();



        }
    }
}

- 补充: 这里也可以映射整个config对象,然后通过config对象再点出来需要的值

ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonFile("config.json", optional: true, reloadOnChange: true);
IConfiguration configuration = configurationBuilder.Build();

// 获取整个配置对象
var config = configuration.Get<Config>();
// - 点出现需要的值
Console.WriteLine($"{config.Name}--{config.Age}--{config.proxy}");
......

  • 高级用法: 配置类+依赖注入,实现读取配置文件并展示配置项的值(支持自动重新加载[当config.json文件内容变化])
// 安装 Microsoft.Extensions.Options;
// TestController.cs

using Microsoft.Extensions.Options;
using System;

namespace ConsoleApp11
{	
	// 传入配置对象,输出配置对象的信息
    class TestController
    {
    	// 声明存储配置选项的快照对象(理解为就是配置对象)
        private readonly IOptionsSnapshot<Config> optConfig;
		
		// 新增构造方法,用于依赖注入
        public TestController(IOptionsSnapshot<Config> optConfig)
        {
            this.optConfig = optConfig;
        }

        public void Test()
        {
            Console.WriteLine(optConfig.Value.Age);
            Console.WriteLine("------------------");
            Console.WriteLine(optConfig.Value.Age);
        }
    }
}



// 主程序

using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.DependencyInjection;

namespace ConsoleApp11
{
    class Program
    {
        static void Main(string[] args)
        {
          
			
			// 创建服务容器并注册服务(指定声明周期)
            ServiceCollection services = new ServiceCollection();
            services.AddScoped<TestController>();
			
			// 构建指定的json文件配置对象
            ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
            configurationBuilder.AddJsonFile("config.json", optional: true, reloadOnChange: true);
            IConfiguration configuration = configurationBuilder.Build();
            
			// 将配置绑定到Config类,并注册到依赖注入容器
            services.AddOptions().Configure<Config>(e=>configuration.Bind(e));
			
			// 获取服务
            using (var sp = services.BuildServiceProvider())
            {
                var c = sp.GetRequiredService<TestController>();
                c.Test();
            }


        }
    }
}

配置和依赖注入

  • 新增第二个配置类的依赖注入
......

namespace ConsoleApp11
{
    class TestController
    {
        ......
    }


    class TestProxy
    {
        private readonly IOptionsSnapshot<Proxy> optProxy;

        public TestProxy(IOptionsSnapshot<Proxy> optProxy)
        {
            this.optProxy = optProxy;
        }

        public void Test()
        {
            Console.WriteLine(optProxy.Value.Address);
        }

    }
}


using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.DependencyInjection;

namespace ConsoleApp11
{
    class Program
    {
        static void Main(string[] args)
        {
          

            ServiceCollection services = new ServiceCollection();
            services.AddScoped<TestController>();
            # 注册服务
            services.AddScoped<TestProxy>();

            ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
            configurationBuilder.AddJsonFile("config.json", optional: true, reloadOnChange: true);
            IConfiguration configuration = configurationBuilder.Build();
			
			# 把Proxy类和配置文件绑定并注册到依赖服务
            services.AddOptions().Configure<Config>(e => configuration.Bind(e))
                                 .Configure<Proxy>(e => configuration.GetSection("proxy").Bind(e));

            using (var sp = services.BuildServiceProvider())
            {
                var c = sp.GetRequiredService<TestController>();
                c.Test();

                var proxy = sp.GetRequiredService<TestProxy>();
                proxy.Test();
            }

        }
    }
}

配置2

命令行环境变量的方式来调整配置文件

  • 命令行配置方式
......
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
// configurationBuilder.AddJsonFile("config.json", optional: true, reloadOnChange: true);
// 换成这句
configurationBuilder.AddCommandLine(args);
IConfiguration configuration = configurationBuilder.Build();

- 终端: D:\VS2019\Projects\Demo\ConsoleApp11\bin\Debug\net5.0>ConsoleApp11.exe name=Tom age=20

项目配置

  • 环境变量配置示例
- 先在系统的环境变量设置键值对
	- variable name: age
	- variable value: 18

......
 ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(); 
 // 引入环境变量,导入配置
 configurationBuilder.AddEnvironmentVariables();
 IConfiguration configuration = configurationBuilder.Build();
 ......

环境变量

  • 注意事项: 环境变量的命名方式不要和别的变量冲突,在命名方式上面要显著和其他变量区分开来,比如vs_xxxvs_yyy

  • 多配置源

- 顺序: 顺序在后面的,会覆盖前面的配置,优先级比较高
......
configurationBuilder.AddEnvironmentVariables();
configurationBuilder.AddCommandLine(args);
# 最终的配置以"json文件"为主
configurationBuilder.AddJsonFile("config.json", optional: true, reloadOnChange: true);
......
  • UserSecrets: 敏感信息配置(该文件不和项目文件一起存放,安装库单独放一个地方)
- 安装 Microsoft.Extensions.Configuration.UserSecrets
- 右键项目-管理用户机密-配置这个新生成的json文件,例如
	{
      "name": "Kate Green",
      "age": 188
    }
    
- 使用方法:
	ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
    configurationBuilder.AddEnvironmentVariables();
    # 新增配置
    configurationBuilder.AddUserSecrets<Program>();
    IConfiguration configuration = configurationBuilder.Build();
	

日志系统

  • 日志级别:
- 依赖库: using Microsoft.Extensions.Logging;
- Trace < Debug < Information < Warning < Error < Critical
- LoggingProvider: 把日志输出到控制台,文件,数据库等待
  • 日志测试实例: 通过依赖注入获取日志服务并输出不同级别的日志信息
// TestLogger类

using Microsoft.Extensions.Logging;

namespace ConsoleAppLogSys12
{
    class TestLogger
    {
        private readonly ILogger<TestLogger> logger;

        public TestLogger(ILogger<TestLogger> logger)
        {
            this.logger = logger;
        }

        public void TestProgramLog()
        {
            logger.LogDebug("开始调试...logDebug");
            logger.LogWarning("开始调试...logWarning");
            logger.LogError("开始调试...logError");

        }
    }
}

// 主程序

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace ConsoleAppLogSys12
{
    class Program
    {
        static void Main(string[] args)
        {
         
            ServiceCollection services = new ServiceCollection();
            services.AddScoped<TestLogger>();
			// 配置日志系统(添加控制台日志输出器)
            services.AddLogging(logBuilder => logBuilder.AddConsole());

            using (var sp = services.BuildServiceProvider())
            {
                var t1 = sp.GetRequiredService<TestLogger>();
                t1.TestProgramLog();
            }
        }
    }
}

- 运行结果:

warn: ConsoleAppLogSys12.TestLogger[0]
      开始调试...logWarning
fail: ConsoleAppLogSys12.TestLogger[0]
      开始调试...logError
      
 - 注意事项: 默认情况下,Debug 级别的日志可能不会显示,因为日志系统默认的最低日志级别是 Information,要显示 Debug 级别日志需要额外配置

            services.AddLogging(logBuilder => {
                logBuilder.AddConsole();
                // 新增这句
                logBuilder.SetMinimumLevel(LogLevel.Trace);
            });

dbug: ConsoleAppLogSys12.TestLogger[0]
      开始调试...logDebug
warn: ConsoleAppLogSys12.TestLogger[0]
      开始调试...logWarning
fail: ConsoleAppLogSys12.TestLogger[0]
      开始调试...logError
  • 若想捕获"报错信息",可以这么写
......

namespace ConsoleAppLogSys12
{
    class TestLogger
    {
        ......

        public void TestProgramLog()
        {
            logger.LogDebug("开始调试...logDebug");
            ......

            try
            {
                File.ReadAllText("D:\\123.txt");
                logger.LogDebug("读取文件成功!!!");
            }
            catch(Exception ex)
            {
            	// 输出异常信息
                logger.LogError(ex, "读取文件失败!!!");
            }

        }
    }
}

  • Window系统专属---EventLog配置
- 安装using Microsoft.Extensions.Logging.EventLog;
- 查看: 事件管理器-Windows日志-应用程序
- 本次实例一直报错,故障还未解决: System.PlatformNotSupportedException:“EventLog access is not supported on this platform
......
using Microsoft.Extensions.Logging.EventLog;
using System.Runtime.InteropServices;
......

namespace ConsoleAppLogSys12
{
    class Program
    {
        static void Main(string[] args)
        {
         
          ......

            services.AddLogging(logBuilder =>
            {
                logBuilder.AddConsole();
              
                // 仅在 Windows 上添加 EventLog
                if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                {
                    logBuilder.AddEventLog();
                }


                logBuilder.SetMinimumLevel(LogLevel.Trace);
            });

            using (var sp = services.BuildServiceProvider())
            {
               ......
            }
        }
    }
}

  • Nlog库的使用: 把日志保存到独立的文件中
- Install-Package NLog.Extensions.Logging
  • 项目根目录新建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"
      xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd">
	autoReload="true"
	internalLogLevel="Info"
	internalLogFile="d:\temp\internal-nlog-AspNetCore.txt">

	<!-- enable asp.net core layout renderers -->
	<extensions>
		<add assembly="NLog.Web.AspNetCore"/>
	</extensions>

	<!-- the targets to write to -->
	<targets>
		<!-- File Target for all log messages with basic details -->
		<target xsi:type="File" name="allfile" fileName="d:\temp\nlog-AspNetCore-all-${shortdate}.log"
				layout="${longdate}|${event-properties:item=EventId_Id:whenEmpty=0}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}" />

		<!-- File Target for own log messages with extra web details using some ASP.NET core renderers -->
		<target xsi:type="File" name="ownFile-web" fileName="d:\temp\nlog-AspNetCore-own-${shortdate}.log"
				layout="${longdate}|${event-properties:item=EventId_Id:whenEmpty=0}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />

		<!--Console Target for hosting lifetime messages to improve Docker / Visual Studio startup detection -->
		<target xsi:type="Console" name="lifetimeConsole" layout="${MicrosoftConsoleLayout}" />
	</targets>

	<!-- rules to map from logger name to target -->
	<rules>
		<!--All logs, including from Microsoft-->
		<logger name="*" minlevel="Trace" writeTo="allfile" />

		<!--Output hosting lifetime messages to console target for faster startup detection -->
		<logger name="Microsoft.Hosting.Lifetime" minlevel="Info" writeTo="lifetimeConsole, ownFile-web" final="true" />

		<!--Skip non-critical Microsoft logs and so log only own logs (BlackHole) -->
		<logger name="Microsoft.*" maxlevel="Info" final="true" />
		<logger name="System.Net.Http.*" maxlevel="Info" final="true" />

		<logger name="*" minlevel="Trace" writeTo="ownFile-web" />
	</rules>
</nlog>
  • 注意事项
- 类似 internalLogFile="d:\temp\internal-nlog-AspNetCore.txt" 文件路径的配置,默认是在C盘,但是windows系统默认C盘需要很大的权限,所以这里修改为D盘比较稳妥,不然有可能碰到权限问题!
// 主程序

......
using NLog.Extensions.Logging; # 引入


namespace ConsoleAppLogSys12
{
    class Program
    {
        static void Main(string[] args)
        {
         
           ......

            services.AddLogging(logBuilder =>
            {	
            	......
                logBuilder.AddConsole();
                # 新增
                logBuilder.AddNLog();
                logBuilder.SetMinimumLevel(LogLevel.Trace);
            });

            using (var sp = services.BuildServiceProvider())
            {
               ......
            }
        }
    }
}

  • 运行结果
- nlog-AspNetCore-all-2025-08-12.log

	- 用于记录所有日志源的基础信息(包括系统框架、第三方库等),适合全面追踪程序运行状态,不区分是否为 Web 场景
	
- nlog-AspNetCore-own-2025-08-12.log

	- 主要用于记录应用自身业务日志,并附加 Web 上下文信息(如请求 URL、Action),适合 Web 应用中定位具体请求的问题

2025-08-12 09:25:37.6198|0|DEBUG|ConsoleAppLogSys12.TestLogger|开始调试...logDebug 
2025-08-12 09:25:37.6571|0|WARN|ConsoleAppLogSys12.TestLogger|开始调试...logWarning 
2025-08-12 09:25:37.6571|0|ERROR|ConsoleAppLogSys12.TestLogger|开始调试...logError 
2025-08-12 09:25:37.7320|0|ERROR|ConsoleAppLogSys12.TestLogger|读取文件失败!!! System.IO.FileNotFoundException: Could not find file 'D:\123.txt'.
File name: 'D:\123.txt'
......
D:\VS2019\Projects\Demo\ConsoleAppLogSys12\TestLogger.cs:line 29

  • 利用nlog.config 配置,实现根据不同的命名空间输出各自的日志
// TestLogger.cs

......
namespace ConsoleAppLogSys12
{
    class TestLogger
    {
        private readonly ILogger<TestLogger> logger;

        public TestLogger(ILogger<TestLogger> logger)
        {
            this.logger = logger;
        }

        public void TestProgramLog()
        {
            logger.LogDebug("开始调试...logDebug");
            logger.LogWarning("开始调试...logWarning");
            logger.LogError("开始调试...logError");

            try
            {
                File.ReadAllText("D:\\123.txt");
                logger.LogDebug("读取文件成功!!!");
            }
            catch(Exception ex)
            {
                logger.LogError(ex, "读取文件失败!!!");
            }

        }
    }
}

// TestLogger2.cs

......
namespace SystemServers
{
    class TestLogger2
    {
        private readonly ILogger<TestLogger2> logger;

        public TestLogger2(ILogger<TestLogger2> logger)
        {
            this.logger = logger;
        }

        public void TestProgramLog2()
        {
            logger.LogDebug("开始调试...logDebug2");
            logger.LogWarning("开始调试...logWarning2");
            logger.LogError("开始调试...logError2");

            try
            {
                File.ReadAllText("D:\\456.txt");
                logger.LogDebug("读取文件成功2222!!!");
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "读取文件失败222!!!");
            }

        }
    }
}

// 主程序

......

namespace ConsoleAppLogSys12
{
    class Program
    {
        static void Main(string[] args)
        {
         
            ServiceCollection services = new ServiceCollection();
            services.AddScoped<TestLogger>();
            services.AddScoped<TestLogger2>();

            services.AddLogging(logBuilder =>
            {
                logBuilder.AddConsole();
                // 关键配置
                logBuilder.AddNLog();
                logBuilder.SetMinimumLevel(LogLevel.Trace);
            });

            using (var sp = services.BuildServiceProvider())
            {
                var t = sp.GetRequiredService<TestLogger>();
                t.TestProgramLog();

                var t1 = sp.GetRequiredService<TestLogger2>();
                t1.TestProgramLog2();
            }
        }
    }
}

// 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"
      xsi:schemaLocation="http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd">
	autoReload="true"
	internalLogLevel="Info"
	internalLogFile="d:\temp\internal-nlog-AspNetCore.txt">

	<!-- enable asp.net core layout renderers -->
	<extensions>
		<add assembly="NLog.Web.AspNetCore"/>
	</extensions>

	<targets>
		<!-- 控制台目标(targetConsole):输出到控制台 -->
		<target xsi:type="Console" name="targetConsole"
				layout="${longdate}|${level:uppercase=true}|${logger}|${message}" />

		<!-- 系统服务日志文件(sysServicesFile):记录SystemServers命名空间的日志 -->
		<!-- 系统服务日志文件(sysServicesFile):记录SystemServers命名空间的日志 -->
		<target xsi:type="File" name="SystemServersFile"
		fileName="d:\temp\nlog-system-servers-${shortdate}.log"
		layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}" />

		<!-- 默认日志文件(defaultFile):记录所有Warn及以上级别的日志 -->
		<target xsi:type="File" name="defaultFile"
				fileName="d:\temp\nlog-default-${shortdate}.log"  
		layout="${longdate}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}" />
	</targets>

	<!-- rules to map from logger name to target -->
	<rules>
	
		<!-- 调整规则顺序,确保SystemServers命名空间的日志优先处理 -->
		<logger name="SystemServers.*" minlevel="Trace" writeTo="SystemServersFile" final="true" />
		<!-- 修正目标名称引用 -->
		<logger name="*" minlevel="Warn" maxlevel="Fatal" writeTo="targetConsole" />
		<logger name="*" minlevel="Warn" writeTo="defaultFile" />
	</rules>
</nlog>
  • 运行结果: 生成的日志文件各自输出log,各自独立,不互相影响
- nlog-default-2025-08-12.log
- nlog-system-servers-2025-08-12.log
// TestLogger类的输出内容
2025-08-12 10:27:03.9965|WARN|ConsoleAppLogSys12.TestLogger|开始调试...logWarning 
2025-08-12 10:27:04.0164|ERROR|ConsoleAppLogSys12.TestLogger|开始调试...logError 
2025-08-12 10:27:04.0932|ERROR|ConsoleAppLogSys12.TestLogger|读取文件失败!!! System.IO.FileNotFoundException: Could not find file 'D:\123.txt'.
......
D:\VS2019\Projects\Demo\ConsoleAppLogSys12\TestLogger.cs:line 29

// TestLogger2类的输出内容
2025-08-12 10:27:04.0932|DEBUG|SystemServers.TestLogger2|开始调试...logDebug2 
2025-08-12 10:27:04.0932|WARN|SystemServers.TestLogger2|开始调试...logWarning2 
2025-08-12 10:27:04.0932|ERROR|SystemServers.TestLogger2|开始调试...logError2 
2025-08-12 10:27:04.1131|ERROR|SystemServers.TestLogger2|读取文件失败222!!! System.IO.FileNotFoundException: Could not find file 'D:\456.txt'.
......
D:\VS2019\Projects\Demo\ConsoleAppLogSys12\TestLogger2.cs:line 30

  • 结构化日志配置库: Serilog
- 安装: Serilog.Asp.NetCore
- 作用: 输出json类型的日志
......

namespace ConsoleAppLogSys12
{
    class TestLogger
    {
        private readonly ILogger<TestLogger> logger;

        public TestLogger(ILogger<TestLogger> logger)
        {
            this.logger = logger;
        }

        public void TestProgramLog()
        {
            logger.LogDebug("开始调试...logDebug");
            logger.LogWarning("开始调试...logWarning");
            logger.LogError("开始调试...logError");
			
			// 新增
            User user = new User { Name="Jim Green",Email="xxxx@google.com" };
            logger.LogDebug("注册了一个{@person}", user);

            try
            {
                File.ReadAllText("D:\\123.txt");
                logger.LogDebug("读取文件成功!!!");
            }
            catch(Exception ex)
            {
                logger.LogError(ex, "读取文件失败!!!");
            }

        }
    }

    class User
    {
        public string Name { get; set; }
        public string Email { get; set; }
    }
}

// 主程序

......
using Serilog;
using Serilog.Formatting.Json;

namespace ConsoleAppLogSys12
{
    class Program
    {
        static void Main(string[] args)
        {
         
            ServiceCollection services = new ServiceCollection();
            services.AddScoped<TestLogger>();
            services.AddScoped<TestLogger2>();

            services.AddLogging(logBuilder =>
            {
               
				
				// 配置
                Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Debug()
                .Enrich.FromLogContext()
                .WriteTo.Console(new JsonFormatter())
                .CreateLogger();
				// 将Serilog集成到Microsoft日志框架
                logBuilder.AddSerilog();

            

                logBuilder.SetMinimumLevel(LogLevel.Trace);
            });

            using (var sp = services.BuildServiceProvider())
            {
                var t = sp.GetRequiredService<TestLogger>();
                t.TestProgramLog();

               ......
            }
        }
    }
}


{"Timestamp":"2025-08-12T11:49:16.8586600+08:00","Level":"Debug","MessageTemplate":"开始调试...logDebug","Properties":{"SourceContext":"ConsoleAppLogSys12.TestLogger"}}
{"Timestamp":"2025-08-12T11:49:16.8951519+08:00","Level":"Warning","MessageTemplate":"开始调试...logWarning","Properties":{"SourceContext":"ConsoleAppLogSys12.TestLogger"}}
{"Timestamp":"2025-08-12T11:49:16.8953574+08:00","Level":"Error","MessageTemplate":"开始调试...logError","Properties":{"SourceContext":"ConsoleAppLogSys12.TestLogger"}}
}}
{"Timestamp":"2025-08-12T11:49:16.9740798+08:00","Level":"Error","MessageTemplate":"读取文件失败!!!","Exception":"System.IO.FileNotFoundException: Could not find file 'D:\\123.txt'.\r\nFile name: 'D:\\123.txt'\r\n   at System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle)\r\n   at System.IO.FileStream.CreateFileOpenHandle(FileMode mode, FileShare share, FileOptions options)\r\n   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)\r\n   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)\r\n   at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks)\r\n   at System.IO.File.InternalReadAllText(String path, Encoding encoding)\r\n   at System.IO.File.ReadAllText(String path)\r\n   at ConsoleAppLogSys12.TestLogger.TestProgramLog() in D:\\VS2019\\Projects\\Demo\\ConsoleAppLogSys12\\TestLogger.cs:line 32","Properties":{"SourceContext":"ConsoleAppLogSys12.TestLogger"}}
{"Timestamp":"2025-08-12T11:49:17.0144093+08:00","Level":"Debug","MessageTemplate":"开始调试...logDebug2","Properties":{"SourceContext":"SystemServers.TestLogger2"}}
{"Timestamp":"2025-08-12T11:49:17.0145002+08:00","Level":"Warning","MessageTemplate":"开始调试...logWarning2","Properties":{"SourceContext":"SystemServers.TestLogger2"}}
{"Timestamp":"2025-08-12T11:49:17.0145608+08:00","Level":"Error","MessageTemplate":"开始调试...logError2","Properties":{"SourceContext":"SystemServers.TestLogger2"}}
{"Timestamp":"2025-08-12T11:49:17.0388197+08:00","Level":"Error","MessageTemplate":"读取文件失败222!!!","Exception":"System.IO.FileNotFoundException: Could not find file 'D:\\456.txt'.\r\nFile name: 'D:\\456.txt'\r\n   at System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle)\r\n   at System.IO.FileStream.CreateFileOpenHandle(FileMode mode, FileShare share, FileOptions options)\r\n   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)\r\n   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)\r\n   at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks)\r\n   at System.IO.File.InternalReadAllText(String path, Encoding encoding)\r\n   at System.IO.File.ReadAllText(String path)\r\n   at SystemServers.TestLogger2.TestProgramLog2() in D:\\VS2019\\Projects\\Demo\\ConsoleAppLogSys12\\TestLogger2.cs:line 30","Properties":{"SourceContext":"SystemServers.TestLogger2"}}
  • 如果想把log输出到本地文件,这里可以这么配置
- 安装文件输出插件: Install-Package Serilog.Sinks.File
// 主程序中的 services.AddLogging 部分修改如下
services.AddLogging(logBuilder =>
{
    Log.Logger = new LoggerConfiguration()
        .MinimumLevel.Debug()
        .Enrich.FromLogContext()
        .WriteTo.Console(new JsonFormatter()) // 保留控制台输出(可选)
        // 添加文件输出配置
        .WriteTo.File(
            path: "logs/log.txt", // 日志文件路径(logs文件夹会自动创建)
            formatter: new JsonFormatter(), // 输出JSON格式(可选,也可改用文本格式)
            rollingInterval: RollingInterval.Day, // 按天滚动日志(每天一个文件)
            retainedFileCountLimit: 7, // 保留最近7天的日志文件
            encoding: Encoding.UTF8 // 编码格式
        )
        .CreateLogger();

    logBuilder.AddSerilog(); // 关键:将Serilog集成到日志框架
    logBuilder.SetMinimumLevel(LogLevel.Trace);
});
  • 测试效果
- 项目根目录底下会生成 log目录-log20250812.txt

{"Timestamp":"2025-08-12T13:25:41.2995937+08:00","Level":"Debug","MessageTemplate":"开始调试...logDebug","Properties":{"SourceContext":"ConsoleAppLogSys12.TestLogger"}}
{"Timestamp":"2025-08-12T13:25:41.3691525+08:00","Level":"Warning","MessageTemplate":"开始调试...logWarning","Properties":{"SourceContext":"ConsoleAppLogSys12.TestLogger"}}
{"Timestamp":"2025-08-12T13:25:41.3694702+08:00","Level":"Error","MessageTemplate":"开始调试...logError","Properties":{"SourceContext":"ConsoleAppLogSys12.TestLogger"}}
{"Timestamp":"2025-08-12T13:25:41.4135421+08:00","Level":"Debug","MessageTemplate":"注册了一个{@person}","Properties":{"person":{"Name":"Jim Green","Email":"xxxx@google.com","_typeTag":"User"},"SourceContext":"ConsoleAppLogSys12.TestLogger"}}
{"Timestamp":"2025-08-12T13:25:41.4843319+08:00","Level":"Error","MessageTemplate":"读取文件失败!!!","Exception":"System.IO.FileNotFoundException: Could not find file 'D:\\123.txt'.\r\nFile name: 'D:\\123.txt'\r\n   at System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle)\r\n   at System.IO.FileStream.CreateFileOpenHandle(FileMode mode, FileShare share, FileOptions options)\r\n   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)\r\n   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)\r\n   at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks)\r\n   at System.IO.File.InternalReadAllText(String path, Encoding encoding)\r\n   at System.IO.File.ReadAllText(String path)\r\n   at ConsoleAppLogSys12.TestLogger.TestProgramLog() in D:\\VS2019\\Projects\\Demo\\ConsoleAppLogSys12\\TestLogger.cs:line 32","Properties":{"SourceContext":"ConsoleAppLogSys12.TestLogger"}}
{"Timestamp":"2025-08-12T13:25:41.5504778+08:00","Level":"Debug","MessageTemplate":"开始调试...logDebug2","Properties":{"SourceContext":"SystemServers.TestLogger2"}}
{"Timestamp":"2025-08-12T13:25:41.5506034+08:00","Level":"Warning","MessageTemplate":"开始调试...logWarning2","Properties":{"SourceContext":"SystemServers.TestLogger2"}}
{"Timestamp":"2025-08-12T13:25:41.5506785+08:00","Level":"Error","MessageTemplate":"开始调试...logError2","Properties":{"SourceContext":"SystemServers.TestLogger2"}}
{"Timestamp":"2025-08-12T13:25:41.5910288+08:00","Level":"Error","MessageTemplate":"读取文件失败222!!!","Exception":"System.IO.FileNotFoundException: Could not find file 'D:\\456.txt'.\r\nFile name: 'D:\\456.txt'\r\n   at System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle)\r\n   at System.IO.FileStream.CreateFileOpenHandle(FileMode mode, FileShare share, FileOptions options)\r\n   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)\r\n   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)\r\n   at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks)\r\n   at System.IO.File.InternalReadAllText(String path, Encoding encoding)\r\n   at System.IO.File.ReadAllText(String path)\r\n   at SystemServers.TestLogger2.TestProgramLog2() in D:\\VS2019\\Projects\\Demo\\ConsoleAppLogSys12\\TestLogger2.cs:line 30","Properties":{"SourceContext":"SystemServers.TestLogger2"}}

  • 注意事项: 如果注释掉formatter参数,则输出非JSON格式的日志
operties":{"SourceContext":"SystemServers.TestLogger2"}}
2025-08-12 13:28:34.976 +08:00 [DBG] 开始调试...logDebug
2025-08-12 13:28:35.045 +08:00 [WRN] 开始调试...logWarning
2025-08-12 13:28:35.045 +08:00 [ERR] 开始调试...logError
2025-08-12 13:28:35.060 +08:00 [DBG] 注册了一个{"Name":"Jim Green","Email":"xxxx@google.com","$type":"User"}
2025-08-12 13:28:35.131 +08:00 [ERR] 读取文件失败!!!
System.IO.FileNotFoundException: Could not find file 'D:\123.txt'.
File name: 'D:\123.txt'
   at System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle)
   at System.IO.FileStream.CreateFileOpenHandle(FileMode mode, FileShare share, FileOptions options)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks)
   at System.IO.File.InternalReadAllText(String path, Encoding encoding)
   at System.IO.File.ReadAllText(String path)
   at ConsoleAppLogSys12.TestLogger.TestProgramLog() in D:\VS2019\Projects\Demo\ConsoleAppLogSys12\TestLogger.cs:line 32
2025-08-12 13:28:35.193 +08:00 [DBG] 开始调试...logDebug2
2025-08-12 13:28:35.193 +08:00 [WRN] 开始调试...logWarning2
2025-08-12 13:28:35.193 +08:00 [ERR] 开始调试...logError2
2025-08-12 13:28:35.217 +08:00 [ERR] 读取文件失败222!!!
System.IO.FileNotFoundException: Could not find file 'D:\456.txt'.
File name: 'D:\456.txt'
   at System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle)
   at System.IO.FileStream.CreateFileOpenHandle(FileMode mode, FileShare share, FileOptions options)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks)
   at System.IO.File.InternalReadAllText(String path, Encoding encoding)
   at System.IO.File.ReadAllText(String path)
   at SystemServers.TestLogger2.TestProgramLog2() in D:\VS2019\Projects\Demo\ConsoleAppLogSys12\TestLogger2.cs:line 30
posted @ 2025-07-04 11:26  清安宁  阅读(222)  评论(0)    收藏  举报