学习资料来源:
杨中科主讲 .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
异步编程(不等)
1、基础知识
只是能让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
- 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 可以根据不同的租户提供相应的配置值,从而实现租户级别的配置管理
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
占坑 有两节配置没有看
日志系统
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)
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语句的框架
存在合法的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代码
通过代码查看EFCore的SQL语句
1、方法1:标准日志
public static readonly ILoggerFactory MyLoggerFactory = LoggerFactory.Create(builder=>{builder.AddConsole();});
optionsBuilder.UseLoggerFactory(MyLoggerFactory);