学习资料来源:
杨中科主讲 .Net Core 2022

基本概念

.NET是.NET Framework、.NET Core、Xamarin/Mono等等的统称,是一个平台
(.NET Framework以前被简称为.NET,是window下开发的,.NET Core是跨平台的开发,Xamarin/Mono是移动端开发的技术)
.NET5开始微软淡化了.NET Framework、.NET Core 概念,统一淡化为.NET
.NET Core是为云而生的一项技术

.NET Standard是.NET的一套标准(只是标准,不提供实现,可以被其他项目引用),.NET Framework、.NET Core、Xamarin/Mono等等都需要遵循这套标准。比如.NET Standard指定了一些类,而实现就是.NET Framework、.NET Core等这些去实现这些类。

dotnet cli是不用编辑器进行开发的(用命令行),在cmd里面写dotnet,如果回车后没有报错,说明cli环境安装好了。

::新建一个项目
dotnet new console
dotnet new winforms --name winformtest1

::运行项目
dotnet run

.csproj:是对项目的描述

发布:
配置文件设置里面的‘部署模式’,一种是‘依赖框架’,即运行的环境里面需要不是.NET,一种是独立,即不需要运行环境,之久就包含.NET运行所需的环境

生成单个文件:生成的文件比较少,生成时比较慢
启用ReadyToRun编译:启用的速度会更快
剪裁未使用的程序集:把用不到的包删掉

NuGet

相当于python里面的pip

dotnet Install-Package XXX --Version
Uninstall-Package XXX
Update-Package XXX

https://www.nuget.org

异步编程(不等)

关于Task理解的文章

1、基础知识

异步.png
只是能让Web服务器处理更多的请求,而不能提高单个处理的工作效率。所以异步编程知识提高web服务器请求数量,而不能提高时间效率。

2、async、await用法

async、await不等于多线程,只是简化了多线程的用法

static async Task Main(string[] args)
{
    string fileName = "d:/1.txt";
    File.Delete(fileName);
    File.WriteAllTextAsync(fileName, "hello");
    string s = await File.ReadAllTextAsync(fileName);
//如果不加await就得这样写
//Task s = File.ReadAllTextAsync(fileName);

    Console.WriteLine(s);
}

1、异步方法用async修饰,异步方法返回值一般是Task<T>,T是真正的返回值类型。

2、惯例一般异步方法名称以Async结尾。

3、即使方法没有返回值,最好把返回值声明为非泛型的Task

4、调用泛型方法时,一般在方法前加上await,这样拿到的返回值就是泛型指定的T类型。

5、异步方法的“传染性”:一个方法中如果有await调用,这个方法也必须修饰为async

// await其实就是把
//1、有返回值情况
Task<string> t = File.ReadAllTextAsync(@"d:\1.txt");
string s = t.Result;
//2、无返回值情况
File.WriteAllTextAsync(@"d:\1.txt","aaaaaaaaaa").Wait();
//简化成
string s = await File.ReadAllTextAsync(@"d:\1.txt");

3、异步方法

//编写异步方法
    static async Task<int> DownloadHtmlAsync(string url, string filename)
    {
        // using 资源回收
        using (HttpClient client = new HttpClient())
        {
            string html = await client.GetStringAsync(url);
            await File.WriteAllTextAsync(filename, html);
            return html.Length;
        } 
    }

//调用
int re = await DownloadHtmlAsync("https://www.youzack.com", @"d:\1.txt");

4、async、await原理(再查,没有很懂)

async的方法会被C#编译器编译成一个类,会根据await调用进行切分为多个状态,对async方法的调用会被拆分为对MoreNext的调用。用await看似是“等待”,经过编译后其实没有“wait”。

await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码。

    static async Task Main(string[] args)
    {
        //获取当前线程托管的线程ID
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < 10000; i++)
        {
            sb.Append("xxxxxxxx");
        }
        await File.WriteAllTextAsync(@"d:\1.txt", sb.ToString());
        // 打印线程ID
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    }
//结果前后输出的线程数值是不一致的

5、异步方法须知

异步方法的代码并不会自动在新线程中执行,除非把代码放到新线程中执行。

class Program1
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("之前:" + Thread.CurrentThread.ManagedThreadId);
        await CalcAsync(500);
        Console.WriteLine("之后:" + Thread.CurrentThread.ManagedThreadId);

    }

    public static async Task<double> CalcAsync(int n)
    {
        // 这个的线程不会有变化,因为CalcAsync没有实际的异步操作
        /*Console.WriteLine("CalcAsync:"+Thread.CurrentThread.ManagedThreadId);
        double result = 0;
        Random rand = new Random();
        for(var  i = 0; i < n*n; i++) 
        {
            result += rand.NextDouble();
        }
        return result;*/

        //通过Task.Run把它扔到新线程里面执行
        return await Task.Run(() =>
        {
            Console.WriteLine("CalcAsync:" + Thread.CurrentThread.ManagedThreadId);
            double result = 0;
            Random rand = new Random();
            for (var i = 0; i < n * n; i++)
            {
                result += rand.NextDouble();
            }
            return result;
        });
    }
}


为什么有的异步方法没有标async:
正常有async写法:

class Program
{
    static async Task Main(string[] args)
    {
        string s = await ReadAsync(1);
        Console.WriteLine(s);
    }
    static async Task<string> ReadAsync(int num)
    {
        if (num == 1)
        {
            string s = await File.ReadAllTextAsync(@"d:\1.txt");
            return s;
        }
        else if (num == 2) {
            string s = await File.ReadAllTextAsync(@"d:\2.txt");
            return s;
        }
        else
        {
            throw new ArgumentException("Parameter cannot be null.");
        }
    }
}

无async写法:

class Program
{
    static async Task Main(string[] args)
    {
        string s = await ReadAsync(1);
        Console.WriteLine(s);
    }
    static Task<string> ReadAsync(int num)
    {
        if (num == 1)
        {
            return File.ReadAllTextAsync(@"d:\1.txt");
        }
        else if (num == 2) {
            return File.ReadAllTextAsync(@"d:\2.txt");
        }
        else
        {
            throw new ArgumentException("Parameter cannot be null.");
        }
    }
}
//符合C#写法,因为都是返回Task类型,知识没有用async和await去包装返回string而已。
//只甩手Task,不“拆完再装”,只是普通的方法调用,这样做运行效率更高,不会造成线程浪费

async方法缺点:
1、异步方法会生成一个类,运行效率没有普通方法高
2、可能会占用非常多的线程

如果一个异步方法只是对别的异步方法调用的转发,并没有太多复杂的逻辑(比如等待A的结果,再调用B;把A调用的返回值拿到内部做一些处理再返回),那么就可以去掉async关键字。


异步方法中的暂停:
如果想在异步方法中暂停一段时间,不要用Thread.Sleep(),因为它会阻塞调用线程,而要用awaitTask.Delay()。
举例:下载一个网址,3秒后下载另一个。

6、CancellationToken

有时需要提前终止任务,比如:请求超时、用户取消请求。

很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号。

CancellationToken结构体
None:空
bool lsCancellationRequested是否取消(*)Register(Action callback)注册取消监听
ThrowlfCancellationRequested()如果任务被取消,执行到这句话就抛异常

通过CancellationTokenSource创建CancellationToken结构体:
CancelAfter()超时后发出取消信号Cancel()发出取消信号
CancellationToken Token
class Program
{
    static async Task Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        cts.CancelAfter(5000);
        CancellationToken cToken = cts.Token;
        await DownloadAsyn("https://www.youzack.com", 100, cToken);
    }

    static async Task DownloadAsyn(string url, int n, CancellationToken cancellationToken)
    {
        using(HttpClient client = new HttpClient())
        {
            for(int i = 0; i < n; i++)
            {
                string html = await client.GetStringAsync(url);
                Console.WriteLine($"{DateTime.Now}:{html}");
                //响应取消,不然即使加了取消也没用
                if (cancellationToken.IsCancellationRequested)
                {
                    Console.WriteLine("请求取消");
                    break;
                }
                //抛异常方式                                               
               //cancellationToken.ThrowIfCancellationRequested();
            }
        }
    }
}

另外一种抛出异常方式:

static async Task Download3Async(string url, int n, CancellationToken cancellationToken)
    {
        using(HttpClient client = new HttpClient())
        {
            for (int i = 0;i < n; i++)
            {
                var resp = await client.GetAsync(url, cancellationToken);
                string html = await resp.Content.ReadAsStringAsync();
                Console.WriteLine($"{DateTime.Now}:{html}");
            }
        }
    }

7、WhenAll

1、Task<Task> WhenAny(lEnumerable<Task>tasks)等,任何一个Task完成,Task就完成

2、Task<TResult[]> WhenAll<TResult>(paramsTask<TResult>[]tasks)等,所有Task完成,Task才完成。用于等待多个任务执行结束,但是不在乎它们的执行顺序

3、FromResult()创建普通数值的Task对象

class Program
{
    static async Task Main(string[] args)
    {
        Task<string> t1 = File.ReadAllTextAsync(@"d:\1.txt");
        Task<string> t2 = File.ReadAllTextAsync(@"d:\2.txt");
        string[] s = await Task.WhenAll(t1, t2);
        string s1 = s[0];
        string s2 = s[1];
        Console.WriteLine(s1);
        Console.WriteLine(s2);
    }
}

7、异步编程其他问题

  • async是提示编译器为异步方法中的await代码进行分段处理,而一个异步方法是否修饰了async对于方法的调用者来说没区别,所以对于接口中的方法或者抽象方法不能修饰为async
interface ITest
    {
        Task<int> GetCharCount(string file);

    }
    class Test : ITest
    {
        public async Task<int> GetCharCount(string file)
        {
            string strs = await File.ReadAllTextAsync(file);
            return strs.Length;
        }
    }
  • 异步与yield
  1. return 不仅能简化数据的返回,还能让数据处理“流水线化”,提升性能
static IEnumerable<string> Test2()
    {
        yield return "hello";
        yield return "cedar";
    }
//调用
foreach(var arg in Test2())
        {
            Console.WriteLine(arg);
        }

在旧版C#中,async方法中不能用yield。从C# 8.0开始,把返回值声明为IAsyncEnumerable(不要带Task),然后遍历的时候用await foreach()即可。

static async IAsyncEnumerable<string> Test3()
    {
        yield return "hello";
        yield return "cedar";
    }
//调用
await foreach(var arg in Test3())
        {
            Console.WriteLine(arg);
        }

LINQ

1、基础

LINQ中提供很多集合的扩展方法,配合lambda能简化数据处理

自己实现类似LINQ的where方法:

//方法1
static IEnumerable<int> MyWhere1(IEnumerable<int> item, Func<int,bool> f)
    {
        List<int> list = new List<int>();
        foreach (int x in item)
        {
            if(f(x)==true) list.Add(x);
        }
        return list;
    }
//方法2
static IEnumerable<int> MyWhere2(IEnumerable<int> item, Func<int, bool> f)
    {
        foreach (int x in item)
        {
            if (f(x) == true) yield return x;
        }
    }

2、LINQ常用扩展方法

大部分扩展方法在System.Linq命名空间里面,是IEnumerable<T>扩展方法

  • where方法:每一项数据都会经过predicate的测试,如果针对一个元素,predicate执行的返回值为true,那么这个元素就会放到返回值中
  • Count()方法:获取数据条数
  • Any()方法:是否至少有一条数据
  • Single:有且只有一条满足要求的数据
  • SingleOrDefault :最多只有一条满足要求的数据
  • First :至少有一条,返回第一条
  • FirstOrDefault :返回第一条或者默认值
  • Order() 对数据正序排序
  • OrderByDescending() 倒序排序
  • 多规则排序:可以在Order()、OrderByDescending()后继续写ThenBy () 、ThenByDescending()
  • Skip(n)跳过n条数据
  • Take(n)获取n条数据
  • 聚合函数:Max()、Min () 、Average () 、Sum () 、Count ()
  • GroupBy()方法参数是分组条件表达式,返回值为IGrouping<TKey, TSource>类型的泛型IEnumerable
  • Select方法:投影操作,类似于SQL的select name from Student;就是只取想要的字段而不取整张表数据。

3、匿名类型

var p = new{Name="tom", id=1};

4、链式调用

集合转换:有一些地方需要数组类型或者List类型的变量,我们可以用ToArray()方法和ToList()分别把lEnumerable<T>转换为数组类型和List<T>类型

LINQ中所有的扩展方法几乎都是针对IEnumerable接口的,而几乎所有能返回集合的都返回IEnumerable,所以是可以把几乎所有方法“链式使用”的

5、LINQ的查询语法

看的懂就行,不实用

var items = list.Where(e=>e.Salary>3000).OrderBy(e=>e.Age).Select(e=>new{e.Age,e.Name,XB=e.Gender?"男":"女"});
// 查询语法
var items = from e in list where e.Salary>3000 select new {e.Age, e.Name, XB=e.Gender>"男":"女"}

依赖注入

1、概念

依赖注入(DI)是控制反转(IOC)思想的实现方式。

代码控制反转的目的:从怎样创建XX对象=>我要XX对象

两种实现方式:
1)服务定位器
2)依赖注入

2、DI服务注册

服务:跟框架要的一个对象
注册服务:要的对象前需要将对象注册进去
服务容器:负责管理注册的服务
查询服务:创建对象及关联对象,管服务容器要服务的过程
对象生命周期:Transient(瞬态)、Scoped(范围)、Singleton(单例)

根据类型来获取和注册服务:
可以分别指定服务类型和实现类型。这两者可能相同也可能不同。服务类型可以是类也可以是接口,建议面向接口编程,更加灵活

.NET控制反转组件取名为Dependencylnjection,但它包含ServiceLocator的功能。

DI使用

1、安装包
Microsoft.Extensions.DependencyInjection

2、using Microsoft.Extensions.DependencyInjection

3、Servicecollection用来构造容器对象lServiceProvider。调用ServiceCollection的BuildServiceProvider()创建的ServiceProvider,可以用来获取BuildServiceProvider()之前

class Program
{
    static void Main(string[] args)
    {
        //没有使用依赖注入时的情况
        /*ITestService t = new TestServiceImpl();
        t.Name = "Test";
        t.SayHi();
        Console.Read();*/

        //创建存放服务的一个集合
        ServiceCollection services = new ServiceCollection();
        //添加TestServiceImpl这么一个服务
        services.AddTransient<TestServiceImpl>();
        //创建ServiceProvider对象,相当于服务定位器
        using (ServiceProvider sp = services.BuildServiceProvider())
        {
            //管服务定位器要一个服务
            TestServiceImpl t = sp.GetService<TestServiceImpl>();
            t.Name = "Test";
            t.SayHi();
        }
    }
    public interface ITestService
    {
        public string Name { get; set; }
        public void SayHi();
    }
    public class TestServiceImpl : ITestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine($"Hi, I'm {Name}");
        }
    }
    public class TestServiceImpl2 : ITestService
    {
        public string Name { get; set; }
        public void SayHi()
        {
            Console.WriteLine($"你好,我是{Name}");
        }
    }    
}

3、服务生命周期

1、给类构造函数中打印,看看不同生命周期的对象创建,使用serviceProvider.CreateScope()创建Scope。

2、如果一个类实现了IDisposable接口,则离开作用域之后容器会自动调用对象的Dispose方法。

3、不要在长生命周期的对象中引用比它短的生命周期的对象。在ASP.NET Core中,这样做默认会抛异常。

4、生命周期的选择:如果类无状态(没有成员变量,没有属性),建议为Singleton;如果类有状态,且有Scope控制,建议为Scoped,因为通常这种Scope控制下的代码都是运行在同一个线程中的,没有并发修改的问题;在使用Transient的时候要谨慎。

可以以using里面定一个Scope范围。

1、Transient

class Program
{
    static void Main(string[] args)
    {
        ServiceCollection services = new ServiceCollection();
        services.AddTransient<TestServiceImpl>();
        //ServiceProvider==服务定位器
        using (ServiceProvider sp = services.BuildServiceProvider())
        {
            TestServiceImpl t = sp.GetService<TestServiceImpl>();
            t.Name = "Lili";
            t.SayHi();

            TestServiceImpl t1 = sp.GetService<TestServiceImpl>();
            Console.WriteLine(object.ReferenceEquals(t, t1));
        }
    }
}
//false

2、Singleton

class Program
{
    static void Main(string[] args)
    {
        ServiceCollection services = new ServiceCollection();
        //services.AddTransient<TestServiceImpl>();
        services.AddSingleton<TestServiceImpl>();
        //ServiceProvider==服务定位器
        using (ServiceProvider sp = services.BuildServiceProvider())
        {
            TestServiceImpl t = sp.GetService<TestServiceImpl>();
            t.Name = "Lili";
            t.SayHi();

            TestServiceImpl t1 = sp.GetService<TestServiceImpl>();
            Console.WriteLine(object.ReferenceEquals(t, t1));
        }
    }
}
//true

3、Scoped

class Program
{
    static void Main(string[] args)
    {
        ServiceCollection services = new ServiceCollection();
        //services.AddTransient<TestServiceImpl>();
        //services.AddSingleton<TestServiceImpl>();
        services.AddScoped<TestServiceImpl>();
        //ServiceProvider==服务定位器
        using (ServiceProvider sp = services.BuildServiceProvider())
        {
            TestServiceImpl temp;
            //创建scope范围,using里面就是scope范围
            using (IServiceScope scope1 = sp.CreateScope())
            {
                //在scope中获取scope相关的对象,scope1.ServiceProvider而不是sp
                TestServiceImpl t = scope1.ServiceProvider.GetService<TestServiceImpl>();
                t.Name = "lili";
                t.SayHi();
                temp = t;
                TestServiceImpl t1 = scope1.ServiceProvider.GetService<TestServiceImpl>();
                Console.WriteLine(object.ReferenceEquals(t,t1));
            }

            using (IServiceScope scope2 = sp.CreateScope())
            {
                //在scope中获取scope相关的对象,scope1.ServiceProvider而不是sp
                TestServiceImpl t = scope2.ServiceProvider.GetService<TestServiceImpl>();
                t.Name = "hhhh";
                t.SayHi();

                Console.WriteLine(object.ReferenceEquals(t, temp));
            }
        }
    }
}

4、服务定位器

  • 服务类型和实现类型不一致的注册
static void Main(string[] args)
    {
        ServiceCollection services = new ServiceCollection();
        //注册ITestService这个服务,实现类是TestServiceImpl
        services.AddScoped<ITestService, TestServiceImpl>();
        // 相当于 services.AddScoped(typeof(ITestService), typeof(TestServiceImpl)); ;
        //services.AddScoped<ITestService, TestServiceImpl2>();
        using (ServiceProvider sp = services.BuildServiceProvider())
        {
            //不管实现类是什么,只要ITestService这个服务
            ITestService ts1 = sp.GetService<ITestService>();
            ts1.Name = "Tom";
            ts1.SayHi();
            Console.WriteLine(ts1.GetType());
        }
    }

注册多个时:

services.AddScoped<ITestService, TestServiceImpl>();
services.AddScoped<ITestService, TestServiceImpl2>();

...

IEnumerable<ITestService> test = sp.GetServices<ITestService>();
     foreach (ITestService test2 in test)
     {
         Console.WriteLine(test2.GetType());
     }

5、依赖注入

依赖注入有传染性,创建一个对象的时候,也会依赖的其他服务也创建了

注入默认是构造函数注入

namespace DI会传染
{
    class Program
    {
        static void Main(string[] args)
        {
            ServiceCollection services = new ServiceCollection();
            services.AddScoped<Controller>();
            services.AddScoped<ILog, LogImpl>();
            services.AddScoped<IStorage, StorageImpl>();
            services.AddScoped<IConfig, ConfigImpl>();

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

    class Controller
    {
        private readonly ILog log;
        private readonly IStorage storage;

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

        }
    }

    interface ILog
    {
        public void Log(string msg);
    }

    class LogImpl : ILog
    {
        public void Log(string msg) { 
        Console.WriteLine($"日志:{msg}");
        }
    }

    interface IConfig
    {
        public string GetValue(string name);
    }
    //配置
    class ConfigImpl : IConfig
    {
        public string GetValue(string name)
        {
            return "hello";
        }
    }

    interface IStorage
    {
        public void Save(string content, string name);
    }
    //存储
    class StorageImpl : IStorage
    {
        //readonly声明,这样只能在构造函数里面赋值,别的地方赋值不了
        private readonly IConfig config;
        public StorageImpl(IConfig config)
        {
            this.config = config;
        }
        public void Save(string content, string name)
        {
            string server = config.GetValue("server");
            Console.WriteLine($"向服务器{server}的文件名为{name}上传");

        }
    }
}

配置系统

1、入门

JSON文件配置

1、简单读取(一个节点一个节点地解析)

1、创建一个JSON文件,config.json,设置“如果较新则复制”(因为读取的是和程序同级的JSON文件,所以需要设置较新则复制)
2、安装Microsoft.Extensions.Configuration和Microsoft.Extensions.Configuration.Json

config.json

{
  "name": "yzk",
  "age": "18",
  "proxy": {"address": "aa","port": "80"}
}

Peogram.cs

class Program
{
    static void Main(string[] args)
    {
        ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
        configurationBuilder.AddJsonFile("config.json", optional: true, reloadOnChange: true);
        IConfigurationRoot configRoot = configurationBuilder.Build();
        string name = configRoot["name"];
        Console.WriteLine($"name={name}");
        string add = configRoot.GetSection("proxy:address").Value;
        Console.WriteLine($"add={add}");
        
    }
}

2、绑定读取配置(*)

1、绑定一个类,自动完成配置的读取
2、安装Microsoft.Extensions.Configuration.Binder

新增一个类

class Proxy
{
    public static string Address { get; set; }
    public static int Port { get; set; }
}

Program.cs

// 绑定读取配置
configRoot.GetSection("proxy").Get<Proxy>();
Console.WriteLine($"{Proxy.Address},{Proxy.Port}");

2、选项方式读取

1、推荐使用选项方式读取,和DI结合更好,且更好利用"reloadonchange”机制
2、安装
Microsoft.Extensions.Qptions
Microsoft.Extensions.Configuration.Binder
Microsoft.Extensions.Configuration
Microsoft.Extensions.Configuration.Json
3、读取配置的时候,DI要声明lOptions<T>、IOptionsMonitor<T>、IOptionsSnapshot<T>等类型。
lOptions<T>不会读取到新的值;和IOptionsMonitor相比,IOptionsSnapshot会在同一个范围内(比如ASP.NET Core一个请求中)保持一致。建议用IOptionsSnapshot

在读取配置的地方,用IOptionsSnapshot<T>注入。不要在构造函数里直接读取IOptionsSnapshot.Value,而是到用到的地方再读取,否则就无法更新变化。

IOptionsSnapshot 接口用于实时获取配置项的快照。它是基于 IOptions 接口的扩展,提供了在应用程序运行时获取最新配置值的能力。
优点:

  • 使用 IOptionsSnapshot 在应用程序运行时获取最新的配置值,而不需要重启应用程序。这样可以使配置的更改立即生效,而不需要停止和重新启动应用程序
  • 动态配置:使用 IOptionsSnapshot 可以实现动态配置,允许应用程序在运行时根据需要更改配置值。
  • 多租户支持:在多租户应用程序中,每个租户可能有不同的配置需求。IOptionsSnapshot 可以根据不同的租户提供相应的配置值,从而实现租户级别的配置管理

配置.png

class Program
{
    static void Main(string[] args)
    {
        ServiceCollection services = new ServiceCollection();
        services.AddScoped<TestController>();
        services.AddScoped<Test2>();
        
        ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
        configurationBuilder.AddJsonFile("config.json", optional: true, reloadOnChange: true);
        IConfigurationRoot configRoot = configurationBuilder.Build(); //程序配置的根节点
        
        //!!!最重要的一步
        services.AddOptions().Configure<Config>(e => configRoot.Bind(e))  //把Config对象绑定到configRoot根节点上,把AddOptions添加到DI容器上
            .Configure<Proxy>(e=>configRoot.GetSection("proxy").Bind(e));  //通过使用 Configure<T>() 方法,可以将配置节点绑定到特定的配置对象上

        using (var sp = services.BuildServiceProvider())
        {
            while (true)
            {
                using (var scope = sp.CreateScope()) //sp.GetRequiredService<T>() 方法可以直接从服务提供程序对象 sp 中获取所需的服务 T 的实例。
                {
                    var c = scope.ServiceProvider.GetRequiredService<TestController>(); //CreateScope() 方法用于创建一个作用域对象 scope
                    //通过 scope.ServiceProvider 可以获取作用域的服务提供程序对象
                    c.Test();
                    var c2 = scope.ServiceProvider.GetRequiredService<Test2>();
                    c2.Test();
                }

                Console.WriteLine("点击任意键继续");
                Console.ReadKey();
            }
        }
    }
}

更改.Config1binDebugnet6.0 下的config.json文件,可以发现即使不重启程序,配置也改变了

3、其他配置提供者

命令行方式配置

1、安装
Microsoft.Extensions.Configuration.CommandLine

2、configBuilder.AddCommandLine(args)

3、参数支持多种格式
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

class Program
{
    static void Main(string[] args)
    {
        。。。
        
        ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
        //configurationBuilder.AddJsonFile("config.json", optional: true, reloadOnChange: true);
        //命令行方式配置
        configurationBuilder.AddCommandLine(args);
        IConfigurationRoot configRoot = configurationBuilder.Build(); //程序配置的根节点

        。。。
    }
}

然后在命令行传递就行了

扁平化配置

对于config.json中的proxy格式,即复杂结构的配置,需要进行扁平化处理,对于配置的名字采用“层级配置”
例如:a:b:c 对于数组就这样配:a:b:c:0 a:b:c:1 ...

proxy:address=aaa  proxy:port=80

环境变量配置

1、安装
Microsoft.Extensions.Configuration.EnvironmentVariables

2、configurationBuilder. AddEnvironmentVariables()AddEnvironmentVariables()有无参数和有prefix参数的两个重载版本。无参数版本会把程序相关的所有环境变量都加载进来,由于有可能和系统中已有的环境变量冲突,因此建议用有prefix参数的AddEnvironmentVariables()。读取配置的时候,prefix参数会被忽略。

class Program
{
    static void Main(string[] args)
    {
        。。。
        
        ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();

        //configurationBuilder.AddJsonFile("config.json", optional: true, reloadOnChange: true);
        //命令行方式配置
        //configurationBuilder.AddCommandLine(args);
        //环境变量配置
        configurationBuilder.AddEnvironmentVariables("C1_");//自定义前缀,防止和其他环境变量冲突

        IConfigurationRoot configRoot = configurationBuilder.Build(); //程序配置的根节点

        。。。
    }
}

4、开发自己的配置提供者

流程:
编写ConfigurationProvider类实际读取配置
编写ConfigurationSource在Build中返回ConfigurationProvider对象
把ConfigurationSource对象加入IConfigurationBuilder
开发自己的配置提供者.png

占坑 有两节配置没有看

日志系统

1、Logging

日志级别:Trace<Debug<Information<Warning<Error<Critical
日志提供者 LoggingProvider:把日志输出到哪里

输出到控制台:
1、安装
Microsoft. Extensions.Logging
Microsoft.Extensions.Logging.Console

2、DI注入

services.AddLogging(logBuilder=>{logBuilder.AddConsole();})

3、需要记录日志的代码,注入ILogger<T>,T一般用当前类,这个类名会输出到日志,方便定位错误。然后调用LogInformation(),LogError等方法输出不同级别的日志。


插一个小知识,框架

class Program
{
    static void Main(string[] args)
    {
        ServiceCollection services = new ServiceCollection();  //用于注册和管理应用程序中的服务
    /*
     * BuildServiceProvider() 方法将在之前 ServiceCollection 中注册的服务构建为一个完整的 ServiceProvider
     * ServiceProvider 是依赖注入容器,它保存了所有已注册的服务,当应用程序需要服务时,可以从 ServiceProvider 中获取服务的实例。
     */
        using (var sp = services.BuildServiceProvider())
        {
            
        }
    }
}


Test1.cs

public class Test1
{
    private readonly ILogger<Test1> logger;

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

    public void Test()
    {
        logger.LogDebug("开始执行数据库同步");
        logger.LogDebug("连接数据库成功");
        logger.LogWarning("查找数据失败,重试第一次");
        
        logger.LogWarning("查找失败重试第二次");
        logger.LogError("查找数据最终失败");
        try
        {
            File.ReadAllText("A:/1.txt");
            logger.LogDebug("读取文件成功!");
        }
        catch (Exception e)
        {
            logger.LogError(e,"读取文件失败");
        }
    }
}

Program.cs

class Program
{
    static void Main(string[] args)
    {
        ServiceCollection services = new ServiceCollection();  //用于注册和管理应用程序中的服务
        //logBuilder => { logBuilder.AddConsole(); } 是一个匿名函数,它会被自动转换为满足 Action<ILoggingBuilder> 委托类型的实例
        services.AddLogging(logBuilder =>
        {
            logBuilder.AddConsole();
            logBuilder.SetMinimumLevel(LogLevel.Trace);
        });
        services.AddScoped<Test1>();
    /*
     * BuildServiceProvider() 方法将在之前 ServiceCollection 中注册的服务构建为一个完整的 ServiceProvider
     * ServiceProvider 是依赖注入容器,它保存了所有已注册的服务,当应用程序需要服务时,可以从 ServiceProvider 中获取服务的实例。
     */
        using (var sp = services.BuildServiceProvider())
        {
            var test1 = sp.GetRequiredService<Test1>();
            test1.Test();
        }
    }
}



Event Viewer:Window记录日志的地方,一般不推荐使用,不过如果Window有什么故障的话,可以在这里查查看

2、Nlog

.NET没有内置的文本日志提供者,第三方有Log4Net(老牌,目前不推荐)、NLog、Serilog等。
1、安装
NLog.Extensions.Logging
2、项目根目录下建nlog.config (也可以其他名,但要单独配置)
遵循约定大于配置,在官网复制对应版本的代码(可能会爆红,但是不影响程序运行)

3、增加logBuilder.AddNLog()

日志分类:不同级别或者不同模块的日志记录到不同的地方
日志过滤:项目不同节点需要记录的日志不同,严重错误可以调用短信Provider等

demo:
新建一个新类,放到SystemServices这个命名空间下,然后输出一些日志,然后调用很多次日志执行的代码。

3、结构化日志

结构化日志就是JSON格式的日志,比普通文本更有利于日志分析。

4、集中化日志

集群化部署环境中,有N多服务器,如果每隔服务器都把日志记录到本地文件,不便于查询、分析。需要把日志保存到集中化的日志服务器中。

5、Serilog

1、NLog也可以配置结构化日志,但是配置麻烦,推荐用Serilog
2、安装:Serilog.AspNetCore
3、Log.Jogger= new LoggerConfiguration().MinimumLevel.Debug()
.Enrich.FromLogContext()
.WriteTo.Console(new JsonFormatter().CreateLogger();
builder.AddSerilog();

4、要记录的结构化数据通过占位符来输出:
logger.LogWarning("新增用户{@person}", new{Id=3,Name="zack"});
5、同样可以输出到文件、数据库、MongoDB等

6、Exceptionless(集中日志)

1、到Exceptionless官网注册、登录后,新建一个项目,按照向导输入公司名字、项目名字后,网站就会给出一个apikey
2、项目NuGet安装Serilog的Exceptionless插件:Serilog.Sinks.Exceptionless。
3、在程序的最开始加上一句ExceptionlessClient.Default.Startup("拿到的apiKey"),然后Serilog的配置中加上一句: .WriteTo.Exceptionless()

Entity Framework Core (EF Core)

1、EF Core 简介

1、ORM:Object Relational Mapping 让开发者用对象操作的形式操作关系数据库
2、有哪些ORM:EF Core、Dapper、SqlSugar、FreeSql等

2、搭建EF Core环境

EF Core 是对于底层ADO.NET Core的封装,因此ADO.NET Core 支持的数据库不一定被EF Core支持。
EF Core支持所有主流数据库,也可以自己实现Provider支持其他数据库。

经典步骤:建实体类;建DbContext;生成数据库;编写调用EF Core的业务代码

1、Book.cs

public class Book
{
    public long Id { get; set; }
    public string Title { get; set; }
    public DateTime PubTime { get; set; }
    public double Price { get; set; }
}

Person.cs

public class Person
{
    public long Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

2、创建实现IEntityTypeConfiguration接口的实体配置类,配置实体类和数据库表的对应关系
BookConfig.cs

public class BookConfig:IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder.ToTable("T_Books");
        builder.Property(b => b.Title).HasMaxLength(50);
    }
}

PersonConfig.cs

public class PersonConfig:IEntityTypeConfiguration<Person>
{
    public void Configure(EntityTypeBuilder<Person> builder)
    {
        builder.ToTable("T_Persons");
    }
}

3、创建继承DbContext的类
MyDbContext.cs

public class MyDbContext:DbContext
{
    //有哪些实体
    public DbSet<Book> Books { get; set; }
    public DbSet<Person> Persons { get; set; }
    
    //链接哪个数据库
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        string connStr = "Server=.;Database=demol;Trusted_Connection=True;MultipleActiveResultSets=true";
        optionsBuilder.UseSqlServer(connStr);
    }
    
    //从哪加载这些EntityTypeConfiguration
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        //this.GetType()拿到当前DbContext类    this.GetType().Assembly表示当前项目程序集
        modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
    }
}



Migration数据库迁移

面向对象的ORM(对象关系映射)开发中,数据库不是程序员手动创建的,而是由Migration工具生成的。关系数据库只是盛放模型数据的一个媒介而已。

根据对象的定义变化,自动更新数据库中的表以及表结构的操作,叫做迁移(Migration)

迁移可以分为多步(项目进化),也可以回滚


4、Migration

  • 安装 Package Microsoft.EntityFrameworkCore.Tools
  • Add-Migration InitialCreate 自动在项目的Migrations文件夹中生成操作数据库的C#代码 (dotnet ef migrations add InitialCreate 【InitialCreate 是此次操作名称】)
  • Update-database 执行代码,执行后才会对数据库有对应操作(dotnet ef database update)

数据库.png

BookConfig.cs增加数据库字段映射规则

public class BookConfig:IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder.ToTable("T_Books");
        builder.Property(b => b.Title).HasMaxLength(50).IsRequired();//HasMaxLength:长度  IsRequired:允许为空
        builder.Property(b => b.AuthorName).HasMaxLength(20).IsRequired();

    }
}

3、EF Core 增删改查

1、增:

static async Task Main(string[] args)
    {
        //ctx=逻辑上的数据库
        using (MyDbContext ctx = new MyDbContext())
        {
            //插入
            Book b = new Book() { Title = "000",PubTime=DateTime.Parse("2023/02/02"),AuthorName="name1",Price=00};
            Book b1 = new Book() { Title = "222",PubTime=DateTime.Parse("2023/02/03"),AuthorName="name2",Price=11};
            Book b2 = new Book() { Title = "333",PubTime=DateTime.Parse("2023/02/04"),AuthorName="name3",Price=33};
            Book b3 = new Book() { Title = "444",PubTime=DateTime.Parse("2023/02/05"),AuthorName="name4",Price=44};
            Book b4 = new Book() { Title = "555",PubTime=DateTime.Parse("2023/02/06"),AuthorName="name5",Price=55};
            ctx.Books.Add(b);//把b对象加入到Books这个逻辑上的表里面
            ctx.Books.Add(b1);
            ctx.Books.Add(b2);
            ctx.Books.Add(b3);
            ctx.Books.Add(b4);
            await ctx.SaveChangesAsync();//Update-database
        }
    }

2、查
DBSet实现了IEnumerable<T>接口,因此可以对DBSet使用Linq操作进行数据查询。EF Core会把Linq操作转换为SQL语句。面向对象,而不是面向数据库(SQL)

IQueryable<Book> books = ctx.Books.Where(b => b.Id > 3);
foreach (var book in books)
   {
      Console.WriteLine(book.Title);
   }

3、改

var b = ctx.Books.Single(b => b.Title == "333");
b.AuthorName = "修改后名字";
await ctx.SaveChangesAsync();

4、删

var b = ctx.Books.Single(b => b.Id == 2);
ctx.Books.Remove(b);
await ctx.SaveChangesAsync();



net5 批量修改、删除多条数据的方法有局限性,性能低下,查出来然后一条条Update、Delete,而不能执行Update...Where...

4、EF Core实体配置

主要的约定配置规则:
1、表名采用DbContext中的对应的DBSet的属性名
2、数据表列的名字采用实体类属性的名字,列的数据类型采用和实体类属性类型最兼容的类型
3、数据表列的可空性取决于对应实体类属性的可空性
4、名字为id的属性为主键,如果主键为short、int或者long类型,默认采用自增字段,如果主键为Guid类型,默认采用默认的Guid生成机制生成主键值

两种配置方式
1、Data Annotation
把配置以特性(Annotation)的形式标注在实体类中

[Table("T_Books")]
public class Book
{
}

缺点:耦合
优点:简单

2、Fluent Api

builder.ToTable("T_Books");
//把配置写到单独的配置类中

缺点:复杂
优点:解耦

大部分功能可以重叠,但是不建议这样做

5、Fluent Api

Fluent API

//1、视图与实体类映射
modelBuilder.Entity<Blog>().ToView("blogsView);

//2、排除属性映射
modelBuilder.Entity<Blog>().Ignore(b=>b.Name2);

//3、配置列名
modelBuilder.Entity<Blog>().Property(b=>b.BlogId).HasColumnName("blog_id");

//4、配置列数据类型
modelBuilder.Property(e=>e.Title).HasColumnType("varchar(200)")

//5、配置主键
modelBuilder.Entity<Student>().HasKey(c=>c.Number);
//支持复合主键,但是不建议使用

//6、生成列的值
modelBuilder.Entity<Student>().Property(b=>b.Number).ValueCreatedOnAdd();

//7、可以用HasDefaultValue()为属性设定默认值
modelBuilder.Entity<Student>().Property(b=>b.Age).hasDefaultValue(6)

//8、索引
modelBuilder.Entity<Blog>().HasIndex(b=>b.Url);
//复合索引
modelBuilder.Entity<Blog>().hasIndex(p=>new{p.FirstName,p.Lastname});
//唯一索引 
IsUnique()
//聚合索引
IsClustered()

//9、用EF Core太多高级特性的时候需要谨慎,尽量不要和业务逻辑混合在一起

Fluent API众多用法
Fluent API中很多方法都有多个重载方法,比如HasIndex、Property()
把Number属性定义为索引,可以用下面两种方法

builder.HasIndex("Number")

builder.HasIndex(b=>b.Number") //推荐使用这种,因为这样利用的是C#的强类型检查机制

6、EF Core 主键

自增主键
1、EF Core支持多种主键生成策略:自动增长;Guid;Hi/Lo算法等

2、自动增长
优点:简单
缺点:数据库迁移以及分布式系统中比较麻烦,并发性能差
long、int等类型主键,默认自增,因为是数据库生成的值,所以SaveChange后会自动把主键的值更新到Id属性

3、自增字段的代码中不能为Id赋值,必须保持默认值0.否则运行的时候会出错

Guid主键

1、生成一个全局唯一的Id,适用于分布式系统。
优点:简单、高并发、全局唯一
缺点:磁盘空间占用大

2、Guid值不连续,用Guid类型做主键的时候,不能把主键设置为聚集索引(特点:顺序存储),因为聚集索引是按照顺序保存主键的,因此用Guid做主键性能差。
在MySQL中,插入频繁的表不要用Guid做主键
在SQLServer中,不要把Guid设置为聚集索引

3、Guid是在客户端赋值的,自己赋值或者是EF Core赋值

混合自增和Guid(非复合主键)
1、用自增列做物理的主键,用Guid列做逻辑上的主键。把自增列设置为表的主键,而业务上查询数据把Guid当做主键使用。不仅保证性能,而且利用了Guid的优点,而且减轻了主键自增性导致主键值可预测带来的安全性问题

Hi/Lo算法
1、主键值由高位(Hi)和低位(Lo),高位由数据库生成,两个高位之间间隔若干个值,由程序在本地生成低位,低位的值在本地自增生成。不同进程或集群中不同服务器获取的Hi值不会重复,而本地进程计算的Lo则可以保证可以在本地高效的生成主键值。但是该算法不是EF Core标准。

7、深入了解Migration

  • Update-Database XXX(dotnet ef database update XXX):把数据库回滚到XXX的状态,迁移脚本不动
  • Remove-Migration(dotnet ef migrations remove):删除最后一次的迁移脚本
  • Script-Migration(`dotnet ef migrations script XX1 XX2):生成迁移SQL代码,XX1到XX2之间版本的SQL脚本

8、反向工程

1、根据数据库表反向生成实体类

2、命令

Scaffold-DbContext 
'Server=.;Database=demol;Trusted_Connection=True;MultipleActiveResultSets=true'
Microsoft.EntityFrameworkCore.SqlServer

不建议用

9、EF Core操作数据库底层原理

把C#代码转换为SQL语句的框架

数据库操作底层原理.png


存在合法的C#语句无法被翻译为SQL语句的情况

var persons = ctx.TPersons.Where(p=>IsOk(P.Name));
foreach(var p in persons){
    Console.WriteLine(p.Name);
}
...

static bool IsOk(string s){
    return s.Contains("杨);
}

以上情况编译通过但是运行不通过,无法翻译为SQL代码

数据库操作底层原理2.png



通过代码查看EFCore的SQL语句

1、方法1:标准日志

public static readonly ILoggerFactory MyLoggerFactory = LoggerFactory.Create(builder=>{builder.AddConsole();});

optionsBuilder.UseLoggerFactory(MyLoggerFactory);
最后编辑:2023年08月16日 ©著作权归作者所有