.net 温故知新:【8】.NET 中的配置从xml转向json - XSpringSun - 博客园

mikel阅读(338)

来源: .net 温故知新:【8】.NET 中的配置从xml转向json – XSpringSun – 博客园

一、配置概述

在.net framework平台中我们常见的也是最熟悉的就是.config文件作为配置,控制台桌面程序是App.config,Web就是web.config,里面的配置格式为xml格式。
image

在xml里面有系统生成的配置项,也有我们自己添加的一些配置,最常用的就是appSettings节点,用来配置数据库连接和参数。
使用的话就引用包System.Configuration.ConfigurationManager 之后取里面的配置信息:System.Configuration.ConfigurationManager.AppSettings["ConnectionString"]

随着技术的发展这种配置方式显得冗余复杂,如果配置项太多层级关系参数表达凌乱,在.net core开始也将配置的格式默认成了json格式,包括现在很多的其它配置也是支持的,比如java中常用的yaml格式,为什么能支持这么多读取源和格式,其实质在于配置提供程序
目前.NET 中的配置是使用一个或多个配置提供程序执行的。 配置提供程序使用各种配置源从键值对读取配置数据,这些配置程序稍后我们会看到,读取的配置源可以是如下这些:

  • 设置文件,appsettings.json
  • 环境变量
  • Azure Key Vault
  • Azure 应用配置
  • 命令行参数
  • 已安装或已创建的自定义提供程序
  • 目录文件
  • 内存中的 .NET 对象
  • 第三方提供程序

二、配置初识

IConfiguration 接口是所有配置源的单个表示形式,给定一个或多个配置源,IConfiguration 类型提供配置数据的统一视图。
image

上图我们可能没有直观的感受,现在写一个例子来看看

(1). 新建控制台应用程序:
创建控制台使用的是.net 6.0 框架,vs 2022。
安装 Microsoft.Extensions.Configuration.Json NuGet 包,该包提供json配置文件读取。

Install-Package Microsoft.Extensions.Configuration.Json

image

(2). 添加appsettings.json 文件

{
  "person": {
    "name": "XSpringSun",
    "age": 18
  }
}

(3). 使用json提供程序读取json配置
new一个ConfigurationBuilder,添加json配置,AddJsonFile是在包中的IConfigurationBuilder扩展方法,其它配置提供程序也是用这种扩展方法实现。

        static void Main(string[] args)
        {

            IConfiguration configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build();

            Console.WriteLine(configuration["person:name"]);
            Console.WriteLine(configuration["person:age"]);

            Console.WriteLine("Hello, World!");
            Console.ReadLine();
        }

image

可以看到已经取到json配置文件中的值了,配置值可以包含分层数据。 分层对象使用配置键中的 : 分隔符表示。在下面的调试对象中我们可以看到实际configuration的Providers 提供程序数组有一个值,就是我们的JsonConfigurationProvider,并且JsonConfigurationProvider里面已经读取了json的数据存储在Data数组中。

对于如上几行代码干了什么呢:

  • 将 ConfigurationBuilder 实例化(new ConfigurationBuilder)。
  • 添加 “appsettings.json” 文件,由 JSON 配置提供程序识别(AddJsonFile(“appsettings.json”))。
  • 使用 configuration 实例获取所需的配置

三、选项模式

这样已经实现json进行配置读取,但是取值的方式似乎和以前没什么太大变法,所以.net提供了选项模式,选项模式就是使用类来提供对相关设置组的强类型访问。
我们创建一个Config类用来转换json:

namespace ConfigDemo
{
    public class Config
    {
        public Person? person { get; set; }
    }

    public class Person {
        public string? name { get; set; }
        public int age { get; set; }
    }
}

绑定配置

IConfiguration configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build();


            Config options = new Config();
            ConfigurationBinder.Bind(configuration, options);

            Person person = configuration.GetSection("person").Get<Person>();

            Console.WriteLine(options.person.name);
            Console.WriteLine(options.person.age);

            Console.WriteLine("-----------GetSection获取-------------");
            Console.WriteLine(person.name);
            Console.WriteLine(person.age);

image

用了两种方式获取配置,第一种使用ConfigurationBinder.Bind()将整个配置绑定到对象Config上,另外一种是使用IConfiguration的GetSection().Get<T>()并返回指定的类型。两种方式都可以使用,看实际需求和用途。

四、选项依赖注入

在控制台程序中我们引用DI注入包,然后演示下如何进行配置的注入。关于DI和IOC不清楚的看我上篇文章.net 温故知新:【7】IOC控制反转,DI依赖注入

  • 新建一个测试类TestOptionDI
    public class TestOptionDI
    {
        private readonly IOptionsSnapshot<Config> _options;
        public TestOptionDI(IOptionsSnapshot<Config> options)
        {
            _options = options;
        }

        public void Test()
        {
            Console.WriteLine("DI测试输出:");
            Console.WriteLine($"姓名:{_options.Value.person.name}");
            Console.WriteLine($"年龄:{_options.Value.person.age}");
        }
    }

在测试类中我们使用IOptionsSnapshot<T>接口作为依赖注入,还有其它不同定义的接口用来配置注入,关于选项接口:。

image

不同接口可以配合读取配置的不同方式起作用,IOptionsSnapshot接口可以在配置文件改变后不同作用域进行刷新配置。接着我们修改main方法,引入DI,并将AddJsonFile方法的参数reloadOnChange设置为true,optional参数是否验证文件存在,建议开发时都设置为true,这样如果文件有问题会进行报错。
注入配置这句services.AddOptions().Configure<Config>(e=>configuration.Bind(e))是关键,通过容器调用AddOptions方法注册,然后Configure方法里面是一个委托方法,该委托的作用就是将配置的信息绑定到Config类型的参数e上。注册到容器的泛型选项接口,这样在TestOptionDI类构造函数注入就能注入IOptionsSnapshot了,这里有点绕。

        static void Main(string[] args)
        {

            IConfiguration configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json",optional:true,reloadOnChange:true)
                .Build();
            
            //IServiceCollection 服务
            ServiceCollection services = new ServiceCollection();
            //注入配置
            services.AddOptions().Configure<Config>(e=>configuration.Bind(e));
            //注入TestOptionDI
            services.AddScoped<TestOptionDI>();

            using (var provider = services.BuildServiceProvider())
            {
                //获取服务
                var testOption = provider.GetRequiredService<TestOptionDI>();
                testOption.Test();
            }
            Console.ReadLine();
        }

image

为了测试IOptionsSnapshot接口在不同作用域会刷新配置,我们修改下main方法,用一个while循环在ReadLine时修改json文件值,不同的Scope里进行打印。

            using (var provider = services.BuildServiceProvider())
            {
                while (true)
                {
                    using (var scope = provider.CreateScope())
                    {
                        //获取服务
                        var testOption = scope.ServiceProvider.GetRequiredService<TestOptionDI>();
                        testOption.Test();
                    }
                    Console.ReadLine();
                }
            }

image

这个功能在web中使用很方便,因为框架的一次请求就是一个作用域,所以我们修改了配置,下次请求就能生效了,而不用重启服务。

五、其它配置

如最开始所说,不仅能配置json文件,由于各种提供程序,还可以配置其它的,但是根据配置的顺序会进行覆盖。我们只添加一个环境变量配置演示下:
首先添加提供程序包:Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables
然后添加环境变量配置代码AddEnvironmentVariables()

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

在VS中配置临时环境变量
image

这里有个扁平化配置,就是表示层级用冒号person:age
image

六、托管模式

对于web项目我们没有进行这么多操作它是怎么配置的呢,其实框架已经自动帮我们做了,其它非web项目也可以使用这种托管模式,在Microsoft.Extensions.Hosting 包中,只需要使用简单的代码就能配置好。

IHost host = Host.CreateDefaultBuilder(args).Build();
await host.RunAsync();

其加载配置的优先级:
image
通过分析我们对整个配置如何运行的机制有了一个大体的了解,如果想详细了解托管模式的还是建议看官方文档:.NET配置

.net 温故知新:【7】IOC控制反转,DI依赖注入 - XSpringSun - 博客园

mikel阅读(338)

来源: .net 温故知新:【7】IOC控制反转,DI依赖注入 – XSpringSun – 博客园

IOC控制反转

大部分应用程序都是这样编写的:编译时依赖关系顺着运行时执行的方向流动,从而生成一个直接依赖项关系图。 也就是说,如果类 A 调用类 B 的方法,类 B 调用 C 类的方法,则在编译时,类 A 将取决于类 B,而 B 类又取决于类 C

image

应用程序中的依赖关系方向应该是抽象的方向,而不是实现详细信息的方向。
而这就是控制反转的思想。
应用依赖关系反转原则后,A 可以调用 B 实现的抽象上的方法,让 A 可以在运行时调用 B,而 B 又在编译时依赖于 A 控制的接口(因此,典型的编译时依赖项发生反转)。 运行时,程序执行的流程保持不变,但接口引入意味着可以轻松插入这些接口的不同实现。

image

上下不同的实现方式在于之前的依赖关系是A->B->C,控制反转后A->B接口->C接口,然后具体的B,C实现又是B->B接口 的反转依赖。这样的好处就是A只依赖B接口而不是依赖实现,具体我们要实现什么只需要按照业务需求进行编写,并且可以随时替换实现而不会影响A的实现,这种思想就是控制反转。

如下是顺序依赖:

        public class A
        {
            //依赖具体类
            public B b;
            public C c;
            public A(B _b, C _c) {
                b = _b;
                c = _c;
            }
            public void Listen()
            {
                b.SayHi();
                c.SayBye();
            }
        }

        public class B
        {
            public void SayHi()
            {
                Console.WriteLine("hi...");
            }
        }
        public class C
        {
            public void SayBye()
            {
                Console.WriteLine("bye...");
            }
        }

如下是控制反转:

        public class A
        {
            //依赖接口
            public IB b;
            public IC c;
            public A(IB _b, IC _c)
            {
                b = _b;
                c = _c;
            }
            public void Listen()
            {
                b.SayHi();
                c.SayBye();
            }
        }

        public interface IB
        {
            public void SayHi();
        }
        public interface IC
        {
            public void SayBye();
        }

DI依赖注入

.NET 支持依赖关系注入 (DI) 软件设计模式,这是一种在类及其依赖项之间实现控制反转 (IoC) 的技术。
我们首先用代码来看什么是DI,在.net提供的扩展包Microsoft.Extensions.DependencyInjection中来完成DI,nuget安装。

image

然后我们实现接口B和接口C,实现我们可以说英语,也可以说汉语,我们在SayHi和SayBye中输出汉语。

        public class B : IB
        {
            public void SayHi()
            {
                Console.WriteLine("你好...");
            }
        }

        public class C : IC
        {
            public void SayBye()
            {
                Console.WriteLine("再见...");
            }
        }

然后在服务容器中注册依赖关系。 .NET 提供了一个内置的服务容器 IServiceProvider。 服务通常在应用启动时注册,并追加到 IServiceCollection。 添加所有服务后,可以使用 BuildServiceProvider 创建服务容器,然后在容器中直接“要”对象而不用去管它如何实例化,并且DI具备传染性,假如B引用了D接口ID,那么我们注册B并在获取B实例时,引用的D接口也会被实例化。

            //IServiceCollection 服务
            IServiceCollection services = new ServiceCollection();
            //服务注册
            services.AddTransient<A>();
            services.AddTransient<IB, B>();
            services.AddTransient<IC, C>();
            //创建服务容器
            var serviceProvider = services.BuildServiceProvider();
            //获取服务
            var a = serviceProvider.GetRequiredService<A>();
            //使用
            a.Listen();
            Console.ReadKey();

image

这就是通过DI依赖注入的方式来实现IOC的思想,或许你会好奇为什么我们不直接实例化A,然后在构造方法里面传进去就行了,也就不依赖DI实现了。但是程序结构更复杂些呢,比如上面提到的B又有D,D又有F呢,这样在构造的时候不是一直要new很多对象,而且同一个接口的不同实现还要去找实例化处的代码进行修改。例如SayHI我想说英文呢?那么我们就可以实现一个BB,然后在服务注册的地方注册BB就可以了。

        public class BB : IB
        {
            public void SayHi()
            {
                Console.WriteLine("hello...");
            }
        }

替换注册BB services.AddTransient<IB, BB>(),而不用去改任何逻辑。
image

服务生命周期

在注册服务的时候我使用的AddTransient方法,表示注册的服务是瞬态的,也就是每次请求都是重新创建实例。同时还提供其它注册服务的方法。
image
服务有三种声明周期:

  • 瞬态
  • 作用域
  • 单例
  1. 瞬态
    服务是每次从服务容器进行请求时创建的。 这种生存期适合轻量级、 无状态的服务。 用 AddTransient 注册服务。在处理请求的应用中,在请求结束时会释放暂时服务。
  2. 作用域
    指定了作用域的生存期指明了每个客户端请求(连接)创建一次服务。 向 AddScoped 注册范围内服务。在处理请求的应用中,在请求结束时会释放有作用域的服务。
    ASP.NET 在处理一个请求的时候是一个作用域,同样我们自己也可以定义作用域。使用serviceProvider.CreateScope()创建作用域,在作用域释放后对象将被释放。
    image
    我们使用AddScoped添加对象,然后在作用域中取两个A对象进行比较,可以看到是True
    如果我们用AddTransient注册A,即使在作用域内两个对象比较也是不一样的,结果为False
    image
  3. 单例
    单例大家应该好理解,就是设计模式中的单例,使用AddSingleton 注册,在首次请求它们时进行创建;或者在向容器直接提供实现实例时由开发人员进行创建。 很少用到此方法,因为可能是线程不安全的,如果服务中有状态。

其它

在Microsoft.Extensions.DependencyInjection中只能用构造函数注入,其它框架还提供属性注入,比如autofac。至于原因不得而知,当然也看个人喜好。查了些资料说是构造函数注入更科学,在对象创建的瞬间对象的构造方法将服务实例化,避免逻辑问题。

.net 温故知新:【6】Linq是什么 - XSpringSun - 博客园

mikel阅读(314)

来源: .net 温故知新:【6】Linq是什么 – XSpringSun – 博客园

1、什么是Linq

关于什么是Linq 我们先看看这段代码。

            List<int> list = new List<int> { 1, 1, 2, 2, 3, 3, 3, 5, 7, 8, 10, 12 };
            var linqList = list.Where(t => t < 10)              //列表中值小于10
                           .GroupBy(t => t)                     //分组
                           .Where(t => t.Count() > 1)           //分组后出现次数大于1
                           .OrderByDescending(t => t.Count())   //按照出现次数倒序
                           .Select(t => t.Key);                 //选择值
            Console.WriteLine(string.Join(' ',linqList));


这段代码使用Linq对List列表进行筛选、分组、排序等一系列操作展示了Linq的强大和便捷,那么我们为什么需要学习Linq?可以看到这样一堆逻辑只几行Linq很快就可以实现,如果要我们自己实现方法去处理这个List肯定是比较繁琐的。
Linq是什么?如下是官方文档对于Linq的描述:

语言集成查询 (LINQ) 是一系列直接将查询功能集成到 C# 语言的技术统称。 数据查询历来都表示为简单的字符串,没有编译时类型检查或 IntelliSense 支持。 此外,需要针对每种类型的数据源了解不同的查询语言:SQL 数据库、XML 文档、各种 Web 服务等。 借助 LINQ,查询成为了最高级的语言构造,就像类、方法和事件一样。
对于编写查询的开发者来说,LINQ 最明显的“语言集成”部分就是查询表达式。 查询表达式采用声明性查询语法编写而成。 使用查询语法,可以用最少的代码对数据源执行筛选、排序和分组操作。 可使用相同的基本查询表达式模式来查询和转换 SQL 数据库、ADO .NET 数据集、XML 文档和流以及 .NET 集合中的数据。

Linq的使用频率和范围可以说是很高很广的,基本每天应该都会用到,那么Linq到底是什么呢?怎么实现的?
要学习Linq首先需要先了解委托Lambda 表达式,因为Linq是由 委托->Lambda->Linq 的一个变换过程。

2、委托

委托简单来讲就是指向方法的指针,就像变量是用来指向具体实现。例如String对象,我们定义一个对象string str="变量"那么str就是指向具体实例化对象的地址,String就是类型。
按照这个思路,如果我们要定义一个指向方法的变量,委托就是为了实现该目的。委托使用 delegate 关键字来声明委托类型。
用类似于定义方法签名的语法来定义委托类型。 只需向定义添加 delegate 关键字即可,如下我们定义一个比较两个数字的委托类型。

//比较两个数字
public delegate int Comparison(int i, int n);

接着我们定义委托变量comparison并指向方法ComparisonMax方法,该方法比较两个int大小,返回大的一个。

委托是和类平级的应以,理应放类同级别,但是C#支持类嵌套定义,所以我们把和本类关联性强的委托可以嵌套定义,委托变量comparison指向方法后,调用comparison(1, 2)执行委托方法并打印。
当然委托可以有返回值也可以定义void无返回值,关于委托的其它方面这里不再赘述,这里主要是为了看清Linq所以浅显的梳理下。
每次使用委托的时候我们都要定义比较麻烦,所以框架已经为我们定义好了两个类型,ActionFunc一个无返回值,一个有返回值,并且采用泛型定义了多个委托以满足我们日常使用。

有了这两个系列的委托类型,上面的方式我们也可以不定义委托直接使用Func<int,int,int> comparison = ComparisonMax;来实现。

3、Lambda

在看Lamda之前我们再看下委托方法的另外一种编写方式,匿名方法

delegate 运算符创建一个可以转换为委托类型的匿名方法
如下我们直接在委托变量后面使用delegate 将参数方法体直接写,而不用声明其名称的方式。


Func<int,int,int> comparison = delegate(int i,int n) { return i > n ? i : n; };
         

运行打印下结果:

从 C# 3 开始,lambda 表达式提供了一种更简洁和富有表现力的方式来创建匿名函数。 使用 => 运算符构造 Lambda
在 lambda 表达式中,lambda 运算符 将左侧的输入参数与右侧的 lambda 主体分开。

使用 Lambda 表达式来创建匿名函数。 使用 lambda 声明运算符=>(读作 goes to) 从其主体中分离 lambda 参数列表。 Lambda 表达式可采用以下任意一种形式:

其中第一种后面写表达式,第二种是使用大括号{}的代码块作为主体,语句 lambda 与表达式 lambda 类似,只是语句括在大括号中。
其实 表达式lambda 就是 语句lambda 在只有一行的情况下可以省略大括号和return。表达式 lambda 的主体可以包含方法调用。 不过若在表达式树中,则不得在 Lambda 表达式中使用方法调用。表达式树是另外一个东西,我们现在使用的ORM框架就是将lambda转换为SQL,这个过程使用表达式树技术,比如EF查询中,如果我们写一个Console.WriteLine()表达式树是没办法转换的,想一下这个调用对于sql查询来说是没有意义的,表达式树以后再讨论吧。

因此上面的匿名函数可以通过lambda变换为:


Func<int,int,int> comparison = (int i,int n) =>{ return i > n ? i : n; };
         

Lambda表达式参数类型也可以省略,输入参数类型必须全部为显式或全部为隐式;否则,便会生成 CS0748 编译器错误。
所以表达式还可以变换为:


Func<int,int,int> comparison = (i,n) =>{ return i > n ? i : n; };
         

将 lambda 表达式的输入参数括在括号中。 如果没有参数则直接写():Action ac = () => {Console.WriteLine();}或者Action ac = () => Console.WriteLine()
如果 lambda 表达式只有一个输入参数,则括号是可选:Func<int,int> fun = i => {return i++;}或者Func<int,int> fun = i =>i++
关于更多的lambda知识可以参看文档:Lambda 表达式

4、实现一个Linq

有了委托和Lambda 的知识,我们可以自己写一个简易的Linq实现,写一个where吧。
我们需要扩展List类的方法,当然不用扩展方法也是可以实现,直接写方法然后调用,但是为了还原框架实现方式,我们模仿IEnumerable类(List 继承至IEnumerable)。

关于扩展方法:

扩展方法使你能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。 扩展方法是一种静态方法,但可以像扩展类型上的实例方法一样进行调用。
扩展方法被定义为静态方法,但它们是通过实例方法语法进行调用的。 它们的第一个参数指定方法操作的类型。 参数前面是 this 修饰符。 仅当你使用 using 指令将命名空间显式导入到源代码中之后,扩展方法才位于范围中。

  • 定义扩展方法
    public static class MyLinq
    {
        public static List<T> MyLinqWhere<T>(this List<T> list, Func<T, bool> predicate)
        {
            List<T> tempList = new List<T>();
            foreach (var item in list)
            {
                if (predicate(item))
                {
                    tempList.Add(item);
                }
            }
            return tempList;
        }
    }

List类是泛型,所以我们定义泛型MyLinqWhere 方法,第一个参数使用this关键字修饰,然后predicate为一个输入参数是T返回时bool的委托用来进行对List里面的每一个元素进行筛选,返回的bool结果判断是否符合要求。
我们将符合要求的元素放到一个新的List里面最后返回该List。

  • 使用Linq方式调用自定义的where方法
     List<int> list = new List<int> { 1, 1, 2, 2, 3, 3, 3, 5, 7, 8, 10, 12 };
      var listWhere = list.MyLinqWhere(x => x < 7);
      Console.WriteLine(string.Join(' ', listWhere));

这样就实现了一个简单的Linq,虽然实际的IEnumerable扩展方法里面还有其它操作,但是通过这个过程我们知道了Linq的实现。
在IEnumerable扩展方法返回参数仍然是IEnumerable,所以可以像开始我们写的那样进行链式调用

5 Linq的另外一种写法

在刚开始的例子中我们换另外一种写法:

var linqList2 = from t in list
                   where t < 10
                   group t by t into t
                   where t.Count() > 1
                   orderby t.Count() descending
                   select t.Key;

输出的结果和方法调用,使用Lambda出来的结果是一样的。

这种方式称为语言集成查询,查询表达式采用声明性查询语法编写而成。 使用查询语法,可以用最少的代码对数据源执行筛选、排序和分组操作。 可使用相同的基本查询表达式模式来查询和转换 SQL 数据库、ADO .NET 数据集、XML 文档和流以及 .NET 集合中的数据。
这种写法只是一种语法方式,或者说语法糖,在编译阶段生成的代码和Lambda表达式生成的代码是一致的,虽然这种方法看起来比较炫酷,但是目前大家还是比较习惯Lambda的书写方式和阅读,了解就行了,要详细学习可以参看官方文档。

.net 温故知新:【5】异步编程 async await - XSpringSun - 博客园

mikel阅读(306)

来源: .net 温故知新:【5】异步编程 async await – XSpringSun – 博客园

1、异步编程

异步编程是一项关键技术,可以直接处理多个核心上的阻塞 I/O 和并发操作。 通过 C#、Visual Basic 和 F# 中易于使用的语言级异步编程模型,.NET 可为应用和服务提供使其变得可响应且富有弹性。

上面是关于异步编程的解释,我们日常编程过程或多或少的会使用到异步编程,为什么要试用异步编程?因为用程序处理过程中使用文件和网络 I/O,比如处理文件的读取写入磁盘,网络请求接口API,默认情况下 I/O API 一般会阻塞。
这样的结果是导致我们的用户界面卡住体验差,有些服务器的硬件利用率低,服务处理能力请求响应慢等问题。基于任务的异步 API 和语言级异步编程模型改变了这种模型,只需了解几个新概念就可默认进行异步执行。

现在普遍使用的异步编程模式是TAP模式,也就是C# 提供的 async 和 await 关键词,实际上我们还有另外两种异步模式:基于事件的异步模式 (EAP),以及异步编程模型 (APM) 。

APM 是基于 IAsyncResult 接口提供的异步编程,例如像FileStream类的BeginRead,EndRead就是APM实现方式,提供一对开始结束方法用来启动和接受异步结果。使用委托的BeginInvoke和EndInvoke的方式来实现异步编程。
EAP 是在 .NET Framework 2.0 中引入的,比较多的体现在WinForm编程中,WinForm编程中很多控件处理事件都是基于事件模型,经常用到跨线程更新界面的时候就会使用到BeginInvoke和Invoke。事件模式算是对APM的一种补充,定义了一系列事件包括完成、进度、取消的事件让我们在异步调用的时候能注册响应的事件进行操作。

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(DateTime.Now + " start");
        IAsyncResult result = BeginAPM();
        //EndAPM(result);
        Console.WriteLine(DateTime.Now + " end");

        Console.ReadKey();
    }


    delegate void DelegateAPM();
    static DelegateAPM delegateAPM = new DelegateAPM(DelegateAPMFun);

    public static IAsyncResult BeginAPM()
    {
        return delegateAPM.BeginInvoke(null, null);
    }

    public static void EndAPM(IAsyncResult result)
    {
        delegateAPM.EndInvoke(result);
    }
    public static void DelegateAPMFun()
    {
        Console.WriteLine("DelegateAPMFun...start");
        Thread.Sleep(5000);
        Console.WriteLine("DelegateAPMFun...end");

    }
}

如上代码我使用委托实现异步调用,BeginAPM 方法使用 BeginInvoke 开始异步调用,然后 DelegateAPMFun 异步方法里面停5秒。看下下面的打印结果,是 main 方法里面的打印在前,异步方法里面的打印在后,说明该操作是异步的。

其中一行代码EndAPM(result)被注释了,调用了委托 EndInvoke 方法,该方法会阻塞程序直到异步调用完成,所以我们可以放到适当的位置用来获取执行结果,这类似于TAP模式的await 关键字,放开改行代码执行下。

以上两种方式已不推荐使用,编写理解起来比较晦涩,感兴趣的可以自行了解下,而且这种方式在.net 5里面已经不支持委托的异步调用了,所以如果要运行需要在.net framework框架下。
TAP 是在 .NET Framework 4 中引入的,是目前推荐的异步设计模式,也是我们本文讨论的重点方向,但是TAP并不一定是线程,他是一种任务,理解为工作的异步抽象,而非在线程之上的抽象。

2、async await

使用 async await 关键字可以很轻松的实现异步编程,我们子需要将方法加上 async 关键字,方法内的异步操作使用 await 等待异步操作完成后再执行后续操作。

class Program
{

    static void Main(string[] args)
    {
        Console.WriteLine(DateTime.Now + " start");
        AsyncAwaitTest();
        Console.WriteLine(DateTime.Now + " end");
        Console.ReadKey();
    }

    public static async void AsyncAwaitTest()
    {
        Console.WriteLine("test start");
        await Task.Delay(5000);
        Console.WriteLine("test end");
    }
}

AsyncAwaitTest 方法使用 async 关键字,使用await关键字等待5秒后打印”test end”。在 Main 方法里面调用 AsyncAwaitTest 方法。

使用 await 在任务完成前将控制让步于其调用方,可让应用程序和服务执行有用工作。 任务完成后代码无需依靠回调或事件便可继续执行。 语言和任务 API 集成会为你完成此操作。
使用await 的方法必须使用 async 关键字,如果我们 Main 方法里面想等待 AsyncAwaitTest 则 Main 方法需要加上 async 并返回 Task。

3、async await 原理

将上面 Main 方法不使用 await 调用的方式编译后使用ILSpy反编译dll,使用C# 4.0才能看到编译器为我们做了什么。因为4.0不支持 async await 所以会反编译到具体代码,4.0 以后的反编译后会直接显示 async await 语法。

通过反编译后可以看到在异步方法里面重新生成了一个泛型类 d__1 实现接口IAsyncStateMachine,然后调用Start方法,Start中进行了一些线程处理后调用 stateMachine.MoveNext() 即调用d__1实例化对象的MoveNext方法。

public static void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
	if (stateMachine == null)
	{
		ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);
	}
	Thread currentThread = Thread.CurrentThread;
	Thread thread = currentThread;
	ExecutionContext executionContext = currentThread._executionContext;
	ExecutionContext executionContext2 = executionContext;
	SynchronizationContext synchronizationContext = currentThread._synchronizationContext;
	try
	{
		stateMachine.MoveNext();
	}
	finally
	{
		SynchronizationContext synchronizationContext2 = synchronizationContext;
		Thread thread2 = thread;
		if (synchronizationContext2 != thread2._synchronizationContext)
		{
			thread2._synchronizationContext = synchronizationContext2;
		}
		ExecutionContext executionContext3 = executionContext2;
		ExecutionContext executionContext4 = thread2._executionContext;
		if (executionContext3 != executionContext4)
		{
			ExecutionContext.RestoreChangedContextToThread(thread2, executionContext3, executionContext4);
		}
	}
}

我们再看编译器为生成的类 <AsyncAwaitTest>d__1 :

MoveNext方法将 AsyncAwaitTest 逻辑代码包含进去了,我们的源代码因为只有一个 await 操作,如果有多个 await 操作,那么MoveNext里面应该还会有多个分段逻辑,将不同段的MoveNext放入不同的状态分段块。
在该类中也有一个if判断,按照 1__state 状态参数,最开始调用的时候是-1,执行进来 num != 0 则执行我们的业务代码if里面的,这个时候会顺序执行业务代码,直到碰到 await 则执行如下代码

awaiter = Task.Delay(5000).GetAwaiter();
if (!awaiter.IsCompleted)
{
    num = (<> 1__state = 0);

    <> u__1 = awaiter;

    < AsyncAwaitTest > d__1 stateMachine = this;

    <> t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
    return;
}

在该过程中 <> t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine) 将 await 句和状态机进行传递调用 AwaitUnsafeOnCompleted方法,该方法一直跟下去会找到线程池的操作。

// System.Threading.ThreadPool
internal static void UnsafeQueueUserWorkItemInternal(object callBack, bool preferLocal)
{
    s_workQueue.Enqueue(callBack, !preferLocal);
}

程序将封装的任务放入线程池进行调用,这个时候异步方法就切换到了另一个线程,或者在原线程上执行(如果异步方法执行时间比较短可能就不会进行线程切换,这个主要看调度程序)。
执行完成 await 后状态 1__state 已经更改了为 0,程序会再次调用 MoveNext 进入 else 之后没有return和其它逻辑,则继续执行到结束。
可以看到这是一个状态控制的执行逻辑,是一种“状态机模式”的设计模式,对于 Main 方法调用 AsyncAwaitTest 逻辑此刻进入if,碰到await则进入线程调度执行,如果异步方法切换到其它线程调用,则方法 Main 继续执行,当状态机执行切换到另外一个状态后再次 MoveNext 直到执行完异步方法。

4、async 与 线程

有了上面的基础我们知道 async 与 await 通常是成对配合使用的,当我们的方法标记为异步的时候,里面的耗时操作就需要 await 进行标记等待完成后执行后续逻辑,调用该异步方法的调用者可以决定是否等待,如果不用 await 则调用者异步执行或者就在原线程上执行异步方法。

如果 async 关键字修改的方法不包含 await 表达式或语句,则该方法将同步执行,可选择性通过 Task.Run API 显式请求任务在独立线程上运行。
可以将 AsyncAwaitTest 方法改为显示线程运行:

public static async Task AsyncAwaitTest()
{
    Console.WriteLine("test start");
    await Task.Run(() =>
    {
        Thread.Sleep(5000);
    });
    Console.WriteLine("test end");
}

5、取消任务 CancellationToken

如果不想等待异步方法完成,可以通过 CancellationToken 取消该任务,CancellationToken 是一个struct,通常使用 CancellationTokenSource 来创建 CancellationToken,因为CancellationTokenSource 有一些列的[方法]用于我们取消任务而不用去操作CancellationToken 结构体。

CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken ct = cts.Token;

然我改造下方法,将 CancellationToken 传递到异步方法,cts.CancelAfter(3000) 3秒钟后取消任务,我们监听CancellationToken 如果 IsCancellationRequested==true 则直接返回 。

static void Main(string[] args)
{
    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken ct = cts.Token;
    cts.CancelAfter(3000);

    Console.WriteLine(DateTime.Now + " start");
    AsyncAwaitTest(ct);
    Console.WriteLine(DateTime.Now + " end");
    Console.ReadKey();
}

public static async Task AsyncAwaitTest(CancellationToken ct)
{
    Console.WriteLine("test start");
    await Task.Delay(5000);
    Console.WriteLine(DateTime.Now + " cancel");
    if (ct.IsCancellationRequested) {
        return;
    }
    //ct.ThrowIfCancellationRequested();
    Console.WriteLine("test end");
}

因为我们是手动通过代码判断状态结束异步,所以即使在3秒后就已经结束了任务,但是await Task.Delay(5000) 任然会等待5秒执行完。还有一种方式就是我们不判断是否取消,直接调用ct.ThrowIfCancellationRequested() 给我们判断,这个方法如果,但是任然不能及时结束。这个时候我们还有另外一种处理方式,就是将CancellationToken 传递到 await 的异步API方法里,可能会立即结束,也可能不会,这个要取决异步实现。

public static async Task AsyncAwaitTest(CancellationToken ct)
{
    Console.WriteLine("test start");
    //传递CancellationToken 取消
    await Task.Delay(5000,ct);
    Console.WriteLine(DateTime.Now + " cancel");
    
    //手动处理取消
    //if (ct.IsCancellationRequested) {
    //    return;
    //}

    //调用方法处理取消
    //ct.ThrowIfCancellationRequested();
    Console.WriteLine("test end");
}

6、注意项

在异步方法里面不要使用 Thread.Sleep 方法,有两种可能:
1、Sleep在 await 之前,则会直接阻塞调用方线程等待Sleep。
2、Sleep在 await 之后,但是 await 执行在调用方的线程上也会阻塞调用方线程。
所以我们应该使用 Task.Delay 用于等待操作。那为什么我上面的 Task.Run 里面使用了 Thread.Sleep呢,因为 Task.Run 是显示请求在独立线程上运行,所以我知道这里写不会阻塞调用方,上面我只是为了演示,所以不建议用。

.net 温故知新:【4】NuGet简介和使用 - XSpringSun - 博客园

mikel阅读(314)

来源: .net 温故知新:【4】NuGet简介和使用 – XSpringSun – 博客园

在包管理以前我们在项目中引用第三方包通常是去下载dll放到项目中再引用,后来逐渐发展成各种包管理工具,nuget就是一种工具,适用于任何现代开发平台的基本工具可充当一种机制,通过这种机制,开发人员可以创建、共享和使用有用的代码。 通常,此类代码捆绑到“包”中,其中包含编译的代码(如 DLL)以及在使用这些包的项目中所需的其他内容。
Linux 我们可以使用apt、yum来安装软件,js 可以使用npm来搭建下载,Java 有maven管理包,而对于.net nuget就是同样效果和机制的工具。

NuGet 客户端工具

要使用 NuGet,作为软件包使用者或创建者,可以使用命令行接口 (CLI) 工具以及 Visual Studio 中的 NuGet 功能。

CLI工具可以使用 dotnet CLI 或 nuget.exe CLI。

dotnet CLI 随某些 Visual Studio 工作负载一起安装,例如 .NET Core 。从 Visual Studio 2017 开始dotnet CLI 将自动随任何与 .NET Core 相关的工作负载一起安装。
dotnet CLI 适用于 .NET Core 和 .NET Standard 项目(SDK 样式的项目类型),以及任何其他 SDK 样式项目(例如,面向 .NET Framework 的 SDK 样式项目)
也就是说安装VS的时候会自动包含在工作负载中,对于.net 5 也可以直接安装.NET SDK,如之前.net 知新:【1】 .Net 5 基本概念文章介绍中可以看到.NET SDK是包含了CLI的。
而对于.NET Framework(仅限非 SDK 样式项目),使用 nuget.exe CLI。这种方式现在基本不使用,因为我们一般不会去单独安装,都是安装VS后直接使用就行了,除非你还在使用Visual Studio 2017 以前的版本。

至于他们有什么区别呢?

第一个是以前.NET Framework时期使用包管理的方式是使用单独的 packages.config 文件进行管理。

但是不建议使用packages.config,.NET Framework可以在VS中右键点击packages.config迁移到PackageReference。

现在.net 5的项目默认使用 PackageReference,包保留在 global-packages 文件夹中(而不是解决方案中的 packages 文件夹中)。
PackageReference 仅列出那些直接安装在项目中的 NuGet 包,不会显示引用包所包含的低级依赖更加简洁。

比如我们使用nuget安装NPOI包,它的依赖如下:

在.net framework的packages.config文件中看到NPOI和它的依赖项

在.net 5项目文件中只有NPOI

第二个就是两个工具的功能有差异

某些高级功能无法使用的时候我们就需要用命令的方式。

visual studio 使用 nuget

在VS里面有两种方式管理nuget包。第一种是右键项目->管理程序nuget包 进入导UI界面。

可以进行程序包的查找和安装的包管理,对包进行卸载更新。 在右上角有一个程序包源,可以进行包源设置,设置包源地址。默认是将 NuGet.org 用作 NuGet 客户端的包存储库。
所以我们配置应使用以下 V3 API 终结点:

https://api.nuget.org/v3/index.json

NuGet.org 是 NuGet 包的公用主机,NuGet 技术还支持在云中(如在 Azure DevOps 上)、在私有网络中或者甚至直接在本地文件系统以私密方式托管包。
https://www.nuget.org/ 打开NuGet.org站点可以进行包搜索和包的上传等。

另外一种方式就是工具->nuget管理器->程序包管理器控制台 ,调出控制台后就可以使用cli命令进行nuget包管理了。
所以有时候我们搜索文章的时候看到别人添加包,命令dotnet add package Newtonsoft.Json 我们要知道这是nuget包添加,程序包管理器控制台执行,或者在ui界面搜索包可视化操作添加,以前的 nuget.exe CLI添加包是install 命令,要注意区分下。

创建发布包

首先需要设置属性,创建包需要以下属性。

  • PackageId,包标识符,在托管包的库中必须是唯一的。 如果未指定,默认值为 AssemblyName。
  • Version,窗体 Major.Minor.Patch[-Suffix] 中特定的版本号,其中 -Suffix 标识预发布版本。 如果未指定,默认值为 1.0.0。
  • 包标题应出现在主机上(例如 nuget.org)
  • Authors,作者和所有者信息。 如果未指定,默认值为 AssemblyName。
  • Company,公司名称。 如果未指定,默认值为 AssemblyName。

在 Visual Studio 中,可以在项目属性中设置这些值(在解决方案资源管理器中右键单击项目,选择“属性” ,然后选择“包” 选项卡)。 也可以直接在项目文件 (.csproj) 中设置这些属性。

在包的 NuGet.org 页面上所示的包说明可以在 .csproj 文件中的 设置,或者通过 .nuspec 文件中的 $description 拉取。
.nuspec 文件是包含包元数据的 XML 清单,.nuspec 当你创建包时将生成。

运行 pack 命令

运行dotnet pack 命令会打包解决方案中可打包的所有项目,也可以在项目属性上设置“在构建时生成NutGet包”。

具有 .nupkg 扩展名的 NuGet 包只是一个 zip 文件。 若要轻松查看任何包的内容,只需将扩展名更改为 .zip 并按常规方法展开内容。 尝试将包上传到主机前,请务必将扩展名改回 .nupkg。
命令执行完成后打包后生成的文件路径会显示在控制台上,到目录查看到ConsoleAppNet5.1.0.0.nupkg 包,并复制一个改成zip验证。

发布到 nuget.org

登录到nuget.org,使用 Microsoft 帐户进行登录,然后选择upload上传,选择了文件后会进行自动校验,如果有问题处理后再重新上传。
还可以通过命令的方式去上传,但是需要api密钥,自己去看下官网好了。

.net 温故知新:【3】.net 5 项目结构说明和发布部署 - XSpringSun - 博客园

mikel阅读(297)

来源: .net 温故知新:【3】.net 5 项目结构说明和发布部署 – XSpringSun – 博客园

.net 5的项目目录结构和.net framework有些明显的变化,包括显示结构和项目文件,从这两个方面看看有哪些变化。

项目目录结构

就以上篇用的demo项目为例(【.net 知新:【2】 .Net Framework 、.Net 5、 .NET Standard的概念与区别】),先看看.net 5项目目录结构。

.net5 项目和原来.net framework项目最大的不同在于引用和项目文件,.net 5变成了依赖项,里面清晰的区分了包、分析器、框架、项目等内容,这样分门别类更方便我们查找和管理引用。

.net framework的所有引用都显示在一起,包括其它项目、nuget包、dll等等引用。但是他们最终都是用项目文件来管理这些东西,所以我们再看看他们项目文件的差异。

项目文件

在两个项目中都引用了nuget包 Newtonsoft.Json,添加了ClassLibraryTest项目引用,建了一个Class1.cs的类文件。
在项目里面找到项目文件.csproj,打开两个文件对比,左边是.net 5右边是.net framework 4.6.1。

相对于.net framewokr而言,.net 5项目文件会少很多内容:
在.net framework中所有引用类库都包含在项目文件中,.net 5是包含在框架中。
在.net framework中所有包含文件描述都在项目文件中,.net 没有任何项目包含文件的描述。

所以.net 5的项目文件描述信息在哪儿呢,现在我在项目中排除类文件Class1.cs。

再打开两个项目文件对比,折叠起其他项。

在.net 5中排除项用<Compile Remove="Class1.cs" />"在编译的时候移除Class1.cs。
在.net framework中因为是包含了所有的项目文件,所以排除就将<Compile Include="Class1.cs" />移除就行了。

.net 5中的这个小的改动会让我们的项目文件大大减少,试想如果我们的文件成千上万个那么.csproj的大小和阅读.net 5就会是巨大优势。
.net 5中默认是包含所有文件,如果要排除某文件直接去除包含项就行了,但是作为一个正常项目不会有太多的排除而是大量的包含,所以.net 5的改动优化相当精妙。
.net 5可以直接双击项目就能在vs中打开.csproj,.net framework需要到目录中去打开。

项目发布

可在两种模式下发布使用 .NET 创建的应用程序,模式会影响用户运行应用的方式。
将应用作为独立应用,生成的应用程序将包含 .NET 运行时和库,以及该应用程序及其依赖项。 应用程序的用户可以在未安装 .NET 运行时的计算机上运行该应用程序。
如果将应用发布为依赖于框架的应用,生成的应用程序将仅包含该应用程序本身及其依赖项。 应用程序的用户必须单独安装 .NET 运行时。
默认情况下,这两种发布模式都会生成特定于平台的可执行文件。 不使用可执行文件也可以创建依赖于框架的应用程序,这些应用程序是跨平台的。

首先我们项目右键发布,选择文件夹方式发布,然后就生成了发布配置。

点击编辑或者设置可以进行发布项配置。如上面官方文档描述,有【独立】和【依赖框架】两种方式。
如果发布【依赖框架】那么运行环境需要安装.net 运行时,并且在发布配置“目标运行时”可以选择“可移植”,因为运行时是自主安装不需要包含,所以不需要发布指定的运行时。

如果选择【独立】那么“目标运行时”只能选择特定的。因为包含了.net运行时和库,所以需要进行选择。如果不选择特定平台,这样就没办法将.net运行时和库正确的发布。

发布项目,然后到发布目录看下两种方式的文件区别

  • 依赖框架->可移植

  • 独立->win-x64(太长了截取一部分)

以上就是发布的简单介绍,采用独立的方式发布时间会久一点,另外在发布配置里面还有个“文件发布选项”,有几个配置简单说明下,有兴趣的可以对比下发布的文件区别。

  1. 生成单个文件:这个就是字面意思,通过将所有依赖应用程序的文件捆绑到一个二进制文件中,这种方式适用于将项目用作第三方库或者应用程序,方便传输管理。
  2. 启用ReadyToRun编译:可以通过将应用程序集编译为 ReadyToRun (R2R) 格式来改进 .NET Core 应用程序的启动时间和延迟。R2R 二进制文件通过减少应用程序加载时实时 (JIT) 编译器需要执行的工作量来改进启动性能。
  3. 裁剪未使用的程序集:也是字面意思,目前还是预览版,无法可靠地分析各种有问题的代码模式(主要集中在反射使用),应用程序的生成时间分析可能会导致运行时失败。这个功能最有用的应该是独立发布的方式,通过裁剪以减小部署大小。

linux 服务器利用宝塔面板部署.net 6(.net core)服务端程序图文步骤_宝塔安装.net环境-CSDN博客

mikel阅读(373)

来源: linux 服务器利用宝塔面板部署.net 6(.net core)服务端程序图文步骤_宝塔安装.net环境-CSDN博客

使用宝塔可视化操作发布.net core 项目
随着.net core 跨平台技术的兴起,微软.net拥抱云原生,支持跨平台,可以使基于.net core技术的服务端程序轻松移植到基于Linux的云服务器上,本文以图文的方式介绍如何利用阿里云轻量应用服务器安装宝塔面板部署基于.net core的后端服务器接口程序并正常运行。
步骤
准备一台linux服务器
安装宝塔镜像及开放端口(网上教程有很多,基本都是傻瓜式安装,这里不做过多赘述)
登录宝塔会看到这样的一个面板
点击左边网站选项进入网站列表
点击添加站点填写站点信息(根目录填写存放.net core 项目的跟目录,就是有 项目名称.dll的目录)添加完成后点击提交
发布.netcore 程序
注意,部署模式选择独立,否则需要现在linux服务器装.netcore运行环境。详细请看
https://docs.microsoft.com/zh-cn/dotnet/core/install/linux-centos
发布完成后并确保咱们项目没问题之后,就可以进入宝塔去上传了。
进入第5步选择的根目录中,上传打包之后的文件
进入软件商店搜索并下载守护进程软件
安装完成后进入并添加守护进程
启动命令后的端口可以随意指定,注意,是没有被占用过的。
准备妥当后点击确定
进入网站列表点击刚创建的网站填写反向代理
代理地址就是上一步启动命令中的填写的地址。
location / {
      proxy_pass http://localhost:5052;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection keep-alive;
      proxy_set_header Host $host;
      proxy_cache_bypass $http_upgrade;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
    }
至此整个配置过程已经大功告成,我们可以通过浏览器访问一下.net api的swagger文档:https://api.xxxx.com/swagger/index.html 已经可以访问了:
结语
.net跨平台,超高的性能,简易的部署方式给后端程序员又提供了一种新的选择,以上便是通过阿里云轻量应用服务器采用宝塔面板全界面操作的全过程,简单实用而且很方便!
————————————————
版权声明:本文为CSDN博主「是扬不是羊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_45602658/article/details/129299814

.net 温故知新:【2】 .Net Framework 、.Net 、 .NET Standard的概念与区别 - XSpringSun - 博客园

mikel阅读(280)

来源: .net 温故知新:【2】 .Net Framework 、.Net 、 .NET Standard的概念与区别 – XSpringSun – 博客园

作为了解历史和演进过程,我们需要将 .Net Framwork 、.Net、 .Net Stander几个概念进行下理解。
.net 代表跨平台框架,从.net 5开始就统一叫.net,废弃原来的.net core 叫法。由于太多名字防止混淆,我们就不管.net core了。

.NET Framework

在未来.NET Framework或许成为过去时,目前还是有很多地方在使用的。这一套体系对于做C#的老coder应该是再熟悉不过了,新入坑的也就用不着费力去学习。

.NET Framework 是一种技术,支持生成和运行 Windows 应用及 Web 服务。
.NET Framework 包括公共语言运行时 (CLR) 和 .NET Framework 类库。 公共语言运行时是 .NET Framework 的基础。
可将运行时看作一个在执行时管理代码的代理,它提供内存管理、线程管理和远程处理等核心服务,并且还强制实施严格的类型安全以及可提高安全性和可靠性的其他形式的代码准确性。

.Net

.net 就是由.net core 演进而来,在底层有很多性能和架构优化改造,上层应用api和用法和.NET Framework大多数相同。

.NET 是一种用于构建多种应用的免费开源开发平台,使用 .NET 时,无论你正在构建哪种类型的应用(web,api、桌面应用…),代码和项目文件看起来都一样。 可以访问每个应用的相同运行时、API 和语言功能。
NET 是开放源代码,使用 MIT 和 Apache 2 许可证。 .NET 是 .NET Foundation 的项目。
Microsoft 支持在 Windows、macOS 和 Linux 上使用 .NET。 它会定期更新以保证安全和质量。
.NET 支持三种编程语言:C#、F#、Visual Basic。

.NET Standard

.NET Standard 是针对多个 .NET 实现推出的一套正式的 .NET API 规范。 推出 .NET Standard 的背后动机是要提高 .NET 生态系统中的一致性。 但是,.NET 5 采用不同的方法来建立一致性,这种新方法在很多情况下都不需要 .NET Standard。

所以.net standard 是 .Net Api 规范,不是实现。其作用是为了提高.net 一致性,只要框架支持就能使用.net standard规范去实现。
但是!.NET 5 采用不同的方法来建立一致性,也就是说.net 5 开始过度到.net 框架后。不用.net Standard去实现一致性,但是.net5+也是支持.net standard!(名字有点绕晕啊)
NET Standard并未弃用 对于可由多个 .NET 实现使用的库,仍需要 .NET Standard。比如在 .NET Framework 和 .NET 上都要使用的内库就需要按照.net standard规范,这样两个框架都能用,但是要看.net standard版本支持,下图对照。
在创建类库的时候就可以选择不同的支持框架。

各种 .NET 实现以特定版本的 .NET Standard 为目标。 每个 .NET 实现版本都会公布它所支持的最高 .NET Standard 版本,这种声明意味着它也支持以前的版本。

例子说明

1、创建一个.NET Standard 类库,添加一个简单的测试方法。

public class NetStandardTest
{
    public static void PrintLocation()
    {
        //打印FileStream 路径
        Console.WriteLine(typeof(FileStream).Assembly.Location);
        //打印NetStandardTest 路径
        Console.WriteLine(typeof(NetStandardTest).Assembly.Location);
    }
}

在这个测试方法里面我们加了两行打印代码。主要是打印FileStream路径,同时我们创建的.NET Standard类库为2.0,因为我们接下来要创建.net framework 的控制台,它不支持2.1。

2、创建.net 5,.net framework 4.6.1 控制台程序

创建好两个控制台项目,在主方法里面调用内库方法。

static void Main(string[] args)
{
    NetStandardTest.PrintLocation();

    Console.ReadKey();
}

解决方案结构如下

3、运行分析

从结果看我们看到同一个.NET Standard类库,引用在不同的框架上,调用同一个FileStream的地址是不一样的。
然后我们在.NET Standard类库里面F12定位到FileStream看到程序集如下

三个地址我们并列对比下:

C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.8\System.Private.CoreLib.dll
C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll
C:\Users\Administrator\.nuget\packages\netstandard.library\2.0.3\build\netstandard2.0\ref\netstandard.dll

我们将三个dll 用dnspy反编译下,找到FileStream,随便找一个BeginRead方法看下代码。

  • .net standard

  • .net framework

  • .net 5

从上面三个结果对比可以看到.net standard里面是没有实现的,只是定义了方法,也就是说定义了一种标准,说明我的类里面有哪些东西。
调用的时候再根据我们当前使用的框架去找到对应框架的实现。这就是为什么.net standard能对多框架引用,也是为什么2.1不能被.net framework使用,因为它没有去实现2.1新增的api。
至于为什么我们调用.net standard的dll会被转到对应框架的dll呢,这是利用Type Forwarding方式实现跨程序集类型转移的技术成为“垫片(Shim)”,这是实现程序集跨平台复用的重要手段。
关于这个垫片技术可以看下这篇文章https://www.cnblogs.com/artech/p/how-to-cross-platform-03.html

自此我们已经基本搞清楚了几个概念和体验它们是如何演进而来,以后也许用不太多.net standard了,等不断升级以后直接就用.net 类库就行,也不用去理解这么多概念和技术,可以理解这些是过渡迭代的产物。

.net 温故知新:【1】 .Net 5 基本概念和开发环境搭建 - XSpringSun - 博客园

mikel阅读(239)

来源: .net 温故知新:【1】 .Net 5 基本概念和开发环境搭建 – XSpringSun – 博客园

最近一两年搞了很多其它事情,.net web方面的基本没做,之前做过几个小的项目零星的学习了些,从.net core 发布后其实都没正真的系统学习过。
就是上手做项目,平时也有关注和看些资料,所以项目写点业务逻辑还是没问题的,最近琢磨着重新系统学习下.net。
因为以后不叫.net core统一叫.net x, 所以就从当前稳定版本.net 5 开始吧,反正.net 6 还没正式发布而且也不会对体系进行大改,说不定搞着搞着就到6了,所以标题写成.net 5+。
平时除了工作生活也没太多空闲时间,对于我来说可能是个漫长的学习过程。熟话说开篇有益嘛,希望不会太监,一是给自己做学习笔记以免遗忘,二是和新同学一起学习,岂不快哉。
我们第一步从.net 开发环境搞起。

.NET SDK 和 运行时

首先我们先了解下.net sdk和运行时的概念。

.NET SDK 是一组用于开发和运行 .NET 应用程序的库和工具

SDK 下载包括以下组件:

  • NET CLI。 可用于本地开发和持续集成脚本的命令行工具。
  • dotnet 驱动程序。 用于运行依赖于框架的应用的 CLI 命令。
  • Roslyn 和 F# 编程语言编译器。
  • MSBuild 生成引擎。
  • .NET 运行时。 提供类型系统、程序集加载、垃圾回收器、本机互操作和其他基本服务。
  • 运行时库。 提供基元数据类型和基本实用程序。
  • ASP.NET Core 运行时。 为连接 Internet 的应用(如 Web 应用、IoT 应用和移动后端)提供基本服务。
  • 桌面运行时。 为 Windows 桌面应用(包括 Windows 窗体和 WPF)提供基本服务。

运行时下载包括以下组件:

  • (可选)桌面或 ASP.NET Core 运行时。
  • .NET 运行时。 提供类型系统、程序集加载、垃圾回收器、本机互操作和其他基本服务。
  • 运行时库。 提供基元数据类型和基本实用程序。
  • dotnet 驱动程序。 用于运行依赖于框架的应用的 CLI 命令。

这里要注意[运行时]和[.NET运行时],运行时就是上面包含的那些东西,.NET运行时包含在运行时里面。
.Net 运行时也就是那个CLR,运行时库就是基类库 (BCL),这些名字确实挺迷惑的,也许我们平时说的.net 运行时≈运行时,但是在理解概念的时候要搞清楚。

.NET CLR 是包含 Windows、macOS 和 Linux 支持的跨平台运行时。 CLR 处理内存分配和管理。 CLR 也是一个虚拟机,不仅可执行应用,还可使用实时 JIT 编译器生成和编译代码。
运行时库也称为框架库或基类库 (BCL)。 这些库为许多常规用途类型和特定于工作负载的类型和实用工具功能提供实现。

你可以看到 SDK 是包含运行时的,SDK还包含了一个重要的东西就是CLI,CLI工具是用于开发、生成、运行和发布 .NET 应用程序的跨平台工具链。
例如如下一行代码命令就是CLI 帮我们干事,在当前目录下创建 C# 控制台应用程序项目:

dotnet new console 

简单点说就是SDK可以让我们开发程序,运行时让我们可以运行写好的程序,当然安装了SDK就没必要再装运行时了。
如果和java做类比,sdk 相当于 jdk,运行时 相当于 jre。
当然我们一般不会用cli开发,因为我们有更好的工具 visual studio。

SDK 安装

SDK安装有两种方式,第一种是通过工具安装的时候一起安装,第二种是自己下载SDK包安装。

1、使用 Visual Studio 安装

visual studio 2019 下载

不同目标 .NET SDK 版本所需的 Visual Studio 最低版本。

对于.net 5来说则最少需要 visual studio 2019 版本 16.8 。

如果你已安装 Visual Studio,则可以使用以下步骤检查你的版本。
打开 Visual Studio。
选择“帮助” > “Microsoft Visual Studio”。
从“关于”对话框中读取版本号。

我本地已经安装了visual studio 2019 本来准备通过更新修改vs方式测试安装的。但是我本地环境vs有问题,一直报错。

没有装的同学可以直接下载,然后按需选择功能直接安装就OK。
捣鼓了下,没办法修复,无法升级。所以最后得重新安装,因为.net 5 最低支持16.8,我的16.7!。
如果没办法卸载vs 使用安装器目录下C:\Program Files (x86)\Microsoft Visual Studio\Installer 运行setup.exe,然后再启动installer安装。

2、下载并手动安装

.NET 5.0 下载

这是第二种方式,如果你不需要vs的话可以用这种方式,直接下载安装SDK。
然后就可以使用CLI创建项目了,应该很少人会不使用工具去编程,毕竟没啥理由。

.NET 5 项目创建

在卸载重装了了Visual Studio 2019 后我们看看创建项目。
创建一个控制台程序,如下图可以看到有两个选项,一个是.net Framework,一个是.net core上运行的。
不知道后面正式版VS2022会不会修改这个名称叫法,毕竟后面不再叫.net core了。

但是我们选了.net core 后可以选择我们的目标框架,如果装了多个版本。

到此我们的.net 5 开发环境就搭建起来了,下一次我们再来探究下.net framwork,.net 5 和 .net standard的概念和区别。

.net 温故知新:【8】.NET 中的配置从xml转向json - XSpringSun - 博客园

mikel阅读(243)

来源: .net 温故知新:【8】.NET 中的配置从xml转向json – XSpringSun – 博客园

一、配置概述

在.net framework平台中我们常见的也是最熟悉的就是.config文件作为配置,控制台桌面程序是App.config,Web就是web.config,里面的配置格式为xml格式。
image

在xml里面有系统生成的配置项,也有我们自己添加的一些配置,最常用的就是appSettings节点,用来配置数据库连接和参数。
使用的话就引用包System.Configuration.ConfigurationManager 之后取里面的配置信息:System.Configuration.ConfigurationManager.AppSettings["ConnectionString"]

随着技术的发展这种配置方式显得冗余复杂,如果配置项太多层级关系参数表达凌乱,在.net core开始也将配置的格式默认成了json格式,包括现在很多的其它配置也是支持的,比如java中常用的yaml格式,为什么能支持这么多读取源和格式,其实质在于配置提供程序
目前.NET 中的配置是使用一个或多个配置提供程序执行的。 配置提供程序使用各种配置源从键值对读取配置数据,这些配置程序稍后我们会看到,读取的配置源可以是如下这些:

  • 设置文件,appsettings.json
  • 环境变量
  • Azure Key Vault
  • Azure 应用配置
  • 命令行参数
  • 已安装或已创建的自定义提供程序
  • 目录文件
  • 内存中的 .NET 对象
  • 第三方提供程序

二、配置初识

IConfiguration 接口是所有配置源的单个表示形式,给定一个或多个配置源,IConfiguration 类型提供配置数据的统一视图。
image

上图我们可能没有直观的感受,现在写一个例子来看看

(1). 新建控制台应用程序:
创建控制台使用的是.net 6.0 框架,vs 2022。
安装 Microsoft.Extensions.Configuration.Json NuGet 包,该包提供json配置文件读取。

Install-Package Microsoft.Extensions.Configuration.Json

image

(2). 添加appsettings.json 文件

{
  "person": {
    "name": "XSpringSun",
    "age": 18
  }
}

(3). 使用json提供程序读取json配置
new一个ConfigurationBuilder,添加json配置,AddJsonFile是在包中的IConfigurationBuilder扩展方法,其它配置提供程序也是用这种扩展方法实现。

        static void Main(string[] args)
        {

            IConfiguration configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build();

            Console.WriteLine(configuration["person:name"]);
            Console.WriteLine(configuration["person:age"]);

            Console.WriteLine("Hello, World!");
            Console.ReadLine();
        }

image

可以看到已经取到json配置文件中的值了,配置值可以包含分层数据。 分层对象使用配置键中的 : 分隔符表示。在下面的调试对象中我们可以看到实际configuration的Providers 提供程序数组有一个值,就是我们的JsonConfigurationProvider,并且JsonConfigurationProvider里面已经读取了json的数据存储在Data数组中。

对于如上几行代码干了什么呢:

  • 将 ConfigurationBuilder 实例化(new ConfigurationBuilder)。
  • 添加 “appsettings.json” 文件,由 JSON 配置提供程序识别(AddJsonFile(“appsettings.json”))。
  • 使用 configuration 实例获取所需的配置

三、选项模式

这样已经实现json进行配置读取,但是取值的方式似乎和以前没什么太大变法,所以.net提供了选项模式,选项模式就是使用类来提供对相关设置组的强类型访问。
我们创建一个Config类用来转换json:

namespace ConfigDemo
{
    public class Config
    {
        public Person? person { get; set; }
    }

    public class Person {
        public string? name { get; set; }
        public int age { get; set; }
    }
}

绑定配置

IConfiguration configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json")
                .Build();


            Config options = new Config();
            ConfigurationBinder.Bind(configuration, options);

            Person person = configuration.GetSection("person").Get<Person>();

            Console.WriteLine(options.person.name);
            Console.WriteLine(options.person.age);

            Console.WriteLine("-----------GetSection获取-------------");
            Console.WriteLine(person.name);
            Console.WriteLine(person.age);

image

用了两种方式获取配置,第一种使用ConfigurationBinder.Bind()将整个配置绑定到对象Config上,另外一种是使用IConfiguration的GetSection().Get<T>()并返回指定的类型。两种方式都可以使用,看实际需求和用途。

四、选项依赖注入

在控制台程序中我们引用DI注入包,然后演示下如何进行配置的注入。关于DI和IOC不清楚的看我上篇文章.net 温故知新:【7】IOC控制反转,DI依赖注入

  • 新建一个测试类TestOptionDI
    public class TestOptionDI
    {
        private readonly IOptionsSnapshot<Config> _options;
        public TestOptionDI(IOptionsSnapshot<Config> options)
        {
            _options = options;
        }

        public void Test()
        {
            Console.WriteLine("DI测试输出:");
            Console.WriteLine($"姓名:{_options.Value.person.name}");
            Console.WriteLine($"年龄:{_options.Value.person.age}");
        }
    }

在测试类中我们使用IOptionsSnapshot<T>接口作为依赖注入,还有其它不同定义的接口用来配置注入,关于选项接口:。

image

不同接口可以配合读取配置的不同方式起作用,IOptionsSnapshot接口可以在配置文件改变后不同作用域进行刷新配置。接着我们修改main方法,引入DI,并将AddJsonFile方法的参数reloadOnChange设置为true,optional参数是否验证文件存在,建议开发时都设置为true,这样如果文件有问题会进行报错。
注入配置这句services.AddOptions().Configure<Config>(e=>configuration.Bind(e))是关键,通过容器调用AddOptions方法注册,然后Configure方法里面是一个委托方法,该委托的作用就是将配置的信息绑定到Config类型的参数e上。注册到容器的泛型选项接口,这样在TestOptionDI类构造函数注入就能注入IOptionsSnapshot了,这里有点绕。

        static void Main(string[] args)
        {

            IConfiguration configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json",optional:true,reloadOnChange:true)
                .Build();
            
            //IServiceCollection 服务
            ServiceCollection services = new ServiceCollection();
            //注入配置
            services.AddOptions().Configure<Config>(e=>configuration.Bind(e));
            //注入TestOptionDI
            services.AddScoped<TestOptionDI>();

            using (var provider = services.BuildServiceProvider())
            {
                //获取服务
                var testOption = provider.GetRequiredService<TestOptionDI>();
                testOption.Test();
            }
            Console.ReadLine();
        }

image

为了测试IOptionsSnapshot接口在不同作用域会刷新配置,我们修改下main方法,用一个while循环在ReadLine时修改json文件值,不同的Scope里进行打印。

            using (var provider = services.BuildServiceProvider())
            {
                while (true)
                {
                    using (var scope = provider.CreateScope())
                    {
                        //获取服务
                        var testOption = scope.ServiceProvider.GetRequiredService<TestOptionDI>();
                        testOption.Test();
                    }
                    Console.ReadLine();
                }
            }

image

这个功能在web中使用很方便,因为框架的一次请求就是一个作用域,所以我们修改了配置,下次请求就能生效了,而不用重启服务。

五、其它配置

如最开始所说,不仅能配置json文件,由于各种提供程序,还可以配置其它的,但是根据配置的顺序会进行覆盖。我们只添加一个环境变量配置演示下:
首先添加提供程序包:Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables
然后添加环境变量配置代码AddEnvironmentVariables()

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

在VS中配置临时环境变量
image

这里有个扁平化配置,就是表示层级用冒号person:age
image

六、托管模式

对于web项目我们没有进行这么多操作它是怎么配置的呢,其实框架已经自动帮我们做了,其它非web项目也可以使用这种托管模式,在Microsoft.Extensions.Hosting 包中,只需要使用简单的代码就能配置好。

IHost host = Host.CreateDefaultBuilder(args).Build();
await host.RunAsync();

其加载配置的优先级:
image
通过分析我们对整个配置如何运行的机制有了一个大体的了解,如果想详细了解托管模式的还是建议看官方文档:.NET配置