.Net 6 基础

更新见语雀文档地址

1. IOC 框架

1.1 IOC 概念

  1. 之前我们写程序的时候所有对象都是程序员手动new的,当项目大了之后这样做的坏处
    • 各模块之间耦合严重
    • 想要更换为其他实现类的时候很麻烦
    • 有的程序员只关心“给我一个实现了***接口的类”,它不想关心这个类是怎么来的
  2. 因此就诞生了IOC ( Ilnversion of Control,控制反转)容器。使用IoC容器后,不再是由程序员自己new对象,而是由框架帮你new对象。
  3. 现在IoC有很多: Spring.Net、Unity.Castle、AutoFac等。目前最火的就是AutoFac.使用IOC容器的时候,一般都是建议基于接口编程,也就是把方法定义到接口中,然后再编写实现类。
  4. 在使用的时候声明接口类型的变量、属性,由容器负责赋值。接口、实现类一般都是定义在单独的项目中,这样减少互相的耦合。

1.2 Net Core 内置框架

1. DI概念

  1. DI(Dependency Injection),即“依赖注入”,组件之间依赖关系由容器在运行期决定,即容器动态的将某个依赖关系注入到组件当中
  2. 服务(service):service管理的就是对象;
  3. 注册服务: 服务容器负责管理注册的服务;
  4. 查询服务:创建对象及关联对象;
  5. 对象生命周期: Transient(瞬态);Scoped(范围); Singleton(单例);

2. .NET中使用DI

  1. 根据类型来获取和注册服务。
    • 可以分别指定服务类型(service type)和实现类型(implementation type)。
    • 这两者可能相同,也可能不同。服务类型可以是类,也可以是接口,建议面向接口编程,更灵活。
  2. .NET控制反转组件取名为 Dependencylnjectic 但它包含ServiceLocator的功能。
  3. Install-Package Microsoft.Extensions.DependencyInjection
  4. using Microsoft.Extensions.Dependencylnjection
  5. ServiceCollection用来构造容器对象lServiceProvider。
  6. 调用ServiceCollection的BuildServiceProvider()创建的ServiceProvider,可用来获取BuildServiceProvider()之前ServiceCollection中的对象。

ServiceCollection services = new ServiceCollection();
// 管理TestServicelmpl类
services.AddTransient<TestServicelmpl>();
//创建容器对象
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
 {
     TestServicelmpl testService =serviceProvider.GetService<TestServicelmpl>();
     testService.Name = "tom";
     testService.SayHi();
 }

3. 生命周期

  1. 给类构造函数中打印,看看不同生命周期的对象创建使用serviceProvider.CreateScope()创建Scope。
  2. 如果一个类实现了IDisposable接口,则离开作用域之后容器会自动调用对象的Dispose方法。
  3. 不要在长生命周期的对象中引用比它短的生命周期的对象。在ASP.NET Core中,这样做默认会抛异常。
  4. 生命周期的选择:如果类无状态,建议为Singleton;
  5. 如果类有状态,且有Scope控制,建议为Scoped,因为通常这种Scope控制下的代码都是运行在同一个线程中的,没有并发修改的问题;
  6. 在使用Transient的时候要谨慎。
3.1 AddTransient、AddSingleton、AddScoped的区别
  1. AddSingleton的生命周期:
    • **项目启动-项目关闭 相当于静态类 只会有一个 **
  2. AddScoped的生命周期:
    • **请求开始-请求结束 在这次请求中获取的对象都是同一个 **
  3. AddTransient的生命周期:
    • 请求获取-(GC回收-主动释放) 每一次获取的对象都不是同一个

4. lServiceProvider的服务定位器方法

  1. T GetService()如果获取不到对象,则返回null
  2. object GetService(Type serviceType)
  3. T GetRequiredService()如果获取不到对象,则抛异常
  4. object GetRequiredService(Type serviceType)
  5. lEnumerable GetServices()适用于可能有很多满足条件的服务
  6. lEnumerable GetServices(Type serviceType)

5.DI 依赖注入

  1. 依赖注入是有传染性”的,如果一个类的对象是通过DI创建的,那么这个类的构造函数中声明的所有服务类型的参数都会被DI赋值;但是如果一个对象是程序员手动创建的,那么这个对象就和DI没有关系,它的构造函数中声明的服务类型参数就不会被自动赋值。
  2. .NET的DI默认是构造函数注入。
  3. 举例:编写一个类,连接数据库做插入操作,并且记录日志(模拟的输出),把Dao、日志都放入单独的服务类。connstr见备注

6. 综合案例

  1. 需求说明
    • 有配置服务、日志服务,然后再开发一个邮件发送器服务。
    • 可以通过配置服务来从文件、环境变量、数据库等地方读取配置,可以通过日志服务来将程序运行过程中的日志信息写入文件、控制台、数据库等。
  2. 实现
    • 创建四个.NET Core类库项目,ConfigServices是配置服务的项目,LogServices是日志服务的项目,MailServices是邮件发送器的项目,
    • 然后再建一个.NET Core控制台项目MailServicesConsole来调用MailServices。
    • MailServi项目引用ConfigServices项目和LogServices项目,而MailServicesConsole项目引用MailServices项目。
    • 编写类库项目LogServices,创建ILogProvider接口。编写实现类ConsoleLogProvider。
    • 编写一个ConsoleLogProviderExtensions定义扩展方法AddConsoleLog,namespace和IServiceCollection一致。
  3. 日志部分
  • 接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LogServices
{
    public interface ILogProvider
    {
        public void LogInfo(string msg);
        public void LogError(string msg);
    }
}
  • 实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LogServices
{
    public class ConsoleLogProvider : ILogProvider
    {
        public void LogError(string msg)
        {
            Console.WriteLine($"ERROR{msg}");
        }

        public void LogInfo(string msg)
        {
            Console.WriteLine($"INFO{msg}");
        }
    }
}
  1. 配置
    • 接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConfigServices
{
    public interface IConfigureServices
    {
        public String GetValue(string name);

    }

}

  • 实现
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConfigServices
{
    public class FileConfigurationService:IConfigureServices
    {
        public String FilePath { get; set;}


        public String GetValue(string name)
        {
             var kv=File.ReadLines(FilePath).Select(e=>e.Split('=')).Select(str => new { Name = str[0], Value = str[1] })
                .SingleOrDefault(kv=>kv.Name==name);
            return kv.Value ?? null;
        }
        
        
    }
}
  1. 邮件部分
    • 接口
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MailServices
{
    public interface IMailServices
    {
        public void Send(string title,String to,string Content);
        
    }
}

  • 实现
using ConfigServices;
using LogServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MailServices
{
    public class MailServices : IMailServices
    {
        private readonly ILogProvider log;

        private readonly IConfigureServices configure;

        public MailServices(ILogProvider log, IConfigureServices configure)
        {
            this.log = log;
            this.configure = configure;
        }

        public void Send(string title, string to, string Content)
        {
            log.LogInfo("开始发送邮件");
            Console.WriteLine($"标题:{title},内容{Content},收件人{to}");
            var address = this.configure.GetValue("StmpServer");
            var userName = this.configure.GetValue("UserName");
            var password = this.configure.GetValue("Password");
            Console.WriteLine($"邮件服务器信息:地址:{address}, 用户名:{userName},密码:{password},");
            log.LogInfo("发送完成");
        }
    }
}

  1. 测试
/*
配置文件内容
StmpServer=qq.com
UserName=123
Password=123
*/

using Microsoft.Extensions.DependencyInjection;
using System;
using System.Text;
using LogServices;
using MailServices;
using ConfigServices;

namespace ConsoleApp1 
{
    internal class Program
    {
        static void Main(string[] args)
        {
            //创建容器
            IServiceCollection services = new ServiceCollection();
            //注册服务
            services.AddScoped<ILogProvider, ConsoleLogProvider>();
            services.AddScoped<IMailServices, MailServices.MailServices>();
            //services.AddScoped<IConfigureServices, ConfigureService>();
            services.AddScoped(typeof(IConfigureServices),e=>new FileConfigurationService {FilePath= @"mail.ini" });

            using (var  s=services.BuildServiceProvider()) 
            {
                var mailService = s.GetRequiredService<IMailServices>();
                mailService.Send("hh","22222@qq.com","222222222222222222222");
            }
            Console.ReadLine();
        }


       
    }
}

1.3 AutoFac 框架

1.为什么使用AutoFac?

  • Autofac是.NET领域最为流行的IOC框架之一,传说是速度最快的一个:
  • 优点:
    • 它是C#语言联系很紧密,也就是说C#里的很多编程方式都可以为Autofac使用,例如可以用Lambda表达式注册组件
    • 较低的学习曲线,学习它非常的简单,只要你理解了IoC和DI的概念以及在何时需要使用它们
    • XML配置支持
    • 自动装配
    • 与Asp.Net MVC 3集成
    • 微软的Orchad开源程序使用的就是Autofac,从该源码可以看出它的方便和强大
  • 理解IoC和DI的区别:
    • 控制反转(IoC/Inverse Of Control): 调用者不再创建被调用者的实例,由autofac框架实现(容器创建)所以称为控制反转。
    • 依赖注入(DI/Dependence injection) : 容器创建好实例后再注入调用者称为依赖注入。

2. 性能

有人专门做了测试:
image.pngimage.png

3.使用AutoFac

3.1 安装AutoFac
  1. Client项目右键->管理 NuGet 程序包->Autofac。
  2. image.png
3.2 AutoFac基本方法
  1. builder.RegisterType().As():注册类型及其实例
//注册接口IDAL的实例SqlDAL
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<SqlDAL>().As<IDAL>();

IContainer container = builder.Build();
//解析IDAL的实例SqlDAL
SqlDAL sqlDAL = (SqlDAL)container.Resolve<IDAL>();
  1. IContainer.Resolve():解析某个接口的实例。
  2. builder.RegisterType().Named(string name):为一个接口注册不同的实例。
    • 有时候难免会碰到多个类映射同一个接口,比如SqlDAL和OracleDAL都实现了IDAL接口,为了准确获取想要的类型,就必须在注册时起名字。
public enum DBType
{
Sql, Oracle
}

builder.RegisterType<SqlDAL>().Keyed<IDAL>(DBType.Sql);
builder.RegisterType<OracleDAL>().Keyed<IDAL>(DBType.Oracle);

IContainer container = builder.Build();
SqlDAL sqlDAL = (SqlDAL)container.ResolveKeyed<IDAL>(DBType.Sql);
//解析IDAL的特定实例OracleDAL
OracleDAL oracleDAL = (OracleDAL)container.ResolveKeyed<IDAL>(DBType.Oracle);
  1. IContainer.ResolveNamed(string name):解析某个接口的“命名实例”。
  2. builder.RegisterType().Keyed(Enum enum):以枚举的方式为一个接口注册不同的实例。
    • 有时候我们会将某一个接口的不同实现用枚举来区分,而不是字符串
DBManager manager = container.Resolve<DBManager>(new NamedParameter("name", "SQL"));
public class DBManager 
{   
    IDAL dal;
    public DBManager (string name,IDAL  _dal)
    {
        Name = name;
        dal= _dal;
    }
}
  1. IContainer.ResolveKeyed(Enum enum):根据枚举值解析某个接口的特定实例。
  2. builder.RegisterType().InstancePerDependency():用于控制对象的生命周期,每次加载实例时都是新建一个实例,默认就是这种方式
  3. builder.RegisterType().SingleInstance():用于控制对象的生命周期,每次加载实例时都是返回同一个实例
  4. IContainer.Resolve(NamedParameter namedParameter):在解析实例T时给其赋值。
  5. builder.RegisterAssemblyTypes(Assembly assembly):注册程序集下所有类型
builder.RegisterAssemblyTypes(typeof(Program).Assembly).AsImplementedInterfaces();
//或者  AsImplementedInterfaces表示注册的类型,以接口的方式注册

builder.RegisterAssemblyTypes(typeof(IRepository<>).Assembly).Where(t => t.IsClass && t.Name.EndsWith("Repository")).AsImplementedInte
3.3 IOC 注册
1. 类型注册
  • 类型注册:使用RegisterType进行注册。
//注册Autofac组件
    ContainerBuilder builder = new ContainerBuilder();
    //注册实现类Student,当我们请求IStudent接口的时候,返回的是类Student的对象。
    builder.RegisterType<Student>().As<IStudent>();
    //上面这句也可改成下面这句,这样请求Student实现了的任何接口的时候,都会返回Student对象。
    //builder.RegisterType<Student>().AsImplementedInterfaces();
    IContainer container = builder.Build();
    //请求IStudent接口
    IStudent student = container.Resolve<IStudent>();
    student.Add("1001", "Hello");
  • 类型注册(别名):假如一个接口有多个实现类,可以在注册时起别名。
ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterType<Dog>().Named<IAnimalSleep>("Dog");
    builder.RegisterType<Cat>().Named<IAnimalSleep>("Cat");
    IContainer container = builder.Build();
  
    var dog = container.ResolveNamed<IAnimalSleep>("Dog");
    dog.Sleep();
    var cat = container.ResolveNamed<IAnimalSleep>("Cat");
    cat.Sleep();
  • 类型注册(枚举):假如一个接口有多个实现类,也可以使用枚举的方式注册。
public enum AnimalType
{
    Dog,
    Cat
}

ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<Dog>().Keyed<IAnimalSleep>(AnimalType.Dog);
builder.RegisterType<Cat>().Keyed<IAnimalSleep>(AnimalType.Cat);
IContainer container = builder.Build();

var dog = container.ResolveKeyed<IAnimalSleep>(AnimalType.Dog);
dog.Sleep();
var cat = container.ResolveKeyed<IAnimalSleep>(AnimalType.Cat);
cat.Sleep();
2. 实例注册
ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterInstance<IStudent>(new Student());
    IContainer container = builder.Build();
    IStudent student = container.Resolve<IStudent>();
    student.Add("1001", "Hello");
3. Lambda册
  • Lambda注册
ContainerBuilder builder = new ContainerBuilder();
           builder.Register(c => new Student()).As<IStudent>();
           IContainer container = builder.Build();
 
           IStudent student = container.Resolve<IStudent>();
           student.Add("1001", "Hello");
  • Lambda注册(NamedParameter)
ContainerBuilder builder = new ContainerBuilder();
      builder.Register<IAnimalSleep>((c, p) =>
          {
              var type = p.Named<string>("type");
              if (type == "Dog")
              {
                  return new Dog();
              }
              else
              {
                  return new Cat();
              }
          }).As<IAnimalSleep>();
      IContainer container = builder.Build();

      var dog = container.Resolve<IAnimalSleep>(new NamedParameter("type", "Dog"));
      dog.Sleep();
4.程序集注册
  • 如果有很多接口及实现类,假如觉得这种一一注册很麻烦的话,可以一次性全部注册,当然也可以加筛选条件。
ContainerBuilder builder = new ContainerBuilder();
    Assembly assembly = Assembly.Load("LinkTo.Test.Autofac.Service");   //实现类所在的程序集名称
    builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces();  //常用
    //builder.RegisterAssemblyTypes(assembly).Where(t=>t.Name.StartsWith("S")).AsImplementedInterfaces();  //带筛选
//builder.RegisterAssemblyTypes(assembly).Except<School>().AsImplementedInterfaces();  //带筛选
        
     IContainer container = builder.Build();
        //单实现类的用法
        IStudent student = container.Resolve<IStudent>();
        student.Add("1001", "Hello");

        //多实现类的用法
        IEnumerable<IAnimalSleep> animals = container.Resolve<IEnumerable<IAnimalSleep>>();
        foreach (var item in animals)
        {
            item.Sleep();
        }
5. 泛型注册
ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterGeneric(typeof(List<>)).As(typeof(IList<>));
            IContainer container = builder.Build();
            IList<string> list = container.Resolve<IList<string>>();
6. 默认注册
ContainerBuilder builder = new ContainerBuilder();
            //对于同一个接口,后面注册的实现会覆盖之前的实现。
            //如果不想覆盖的话,可以用PreserveExistingDefaults,这样会保留原来注册的实现。
            builder.RegisterType<Dog>().As<IAnimalSleep>();
            builder.RegisterType<Cat>().As<IAnimalSleep>().PreserveExistingDefaults();  //指定为非默认值
            IContainer container = builder.Build();
            var dog = container.Resolve<IAnimalSleep>();
            dog.Sleep();
3.4 IoC-注入
  1. 构造函数注入
ContainerBuilder builder = new ContainerBuilder();
            builder.RegisterType<AnimalWagging>();
            builder.RegisterType<Dog>().As<IAnimalBark>();
            IContainer container = builder.Build();
            AnimalWagging animal = container.Resolve<AnimalWagging>();
            animal.Wagging();
  1. 属性注入
ContainerBuilder builder = new ContainerBuilder(); 
Assembly assembly = Assembly.Load("LinkTo.Test.Autofac.Service");  //实现类所在的程序集名称
builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces().PropertiesAutowired();    //常用
IContainer container = builder.Build();
ISchool school = container.Resolve<ISchool>();
school.LeaveSchool();
3.5 IoC-生命周期
  1. PerDependency
    • Per Dependency:为默认的生命周期,也被称为"transient"或"factory",其实就是每次请求都创建一个新的对象。
ContainerBuilder builder = new ContainerBuilder();
Assembly assembly = Assembly.Load("LinkTo.Test.Autofac.Service");//实现类所在的程序集名称                  
builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces().PropertiesAutowired().
InstancePerDependency();    //常用

IContainer container = builder.Build();
ISchool school1 = container.Resolve<ISchool>();
ISchool school2 = container.Resolve<ISchool>();
Console.WriteLine(school1.Equals(school2));
  1. Single Instance
    • Single Instance:就是每次都用同一个对象。
ContainerBuilder builder = new ContainerBuilder();
Assembly assembly = Assembly.Load("LinkTo.Test.Autofac.Service");        //实现类所在的程序集名称        

builder.RegisterAssemblyTypes(assembly).AsImplementedInterfaces().
PropertiesAutowired().SingleInstance();   //常用

IContainer container = builder.Build();

ISchool school1 = container.Resolve<ISchool>();
ISchool school2 = container.Resolve<ISchool>();
Console.WriteLine(ReferenceEquals(school1, school2));
  1. Per Lifetime Scope
    • Per Lifetime Scope:同一个Lifetime生成的对象是同一个实例。
ContainerBuilder builder = new ContainerBuilder();
    builder.RegisterType<School>().As<ISchool>().InstancePerLifetimeScope();
    IContainer container = builder.Build();
    ISchool school1 = container.Resolve<ISchool>();
    ISchool school2 = container.Resolve<ISchool>();
    Console.WriteLine(school1.Equals(school2));
    using (ILifetimeScope lifetime = container.BeginLifetimeScope())
    {
        ISchool school3 = lifetime.Resolve<ISchool>();
        ISchool school4 = lifetime.Resolve<ISchool>();
        Console.WriteLine(school3.Equals(school4));
        Console.WriteLine(school2.Equals(school3));
    }
  1. InstancePerRequest: ASP.Net MVC专用,每个请求一个对象。InstancePerRequest()
  2. InstancePerMatchingLifetimeScope
    • 在一个做标识的生命周期域中,每一个依赖或调用创建一个单一的共享的实例。打了标识了的生命周期域中的子标识域中可以共享父级域中的实例。若在整个继承层次中没有找到打标识的生命周期域,则会抛出异常:DependencyResolutionException。
  3. InstancePerOwned
    • 在一个生命周期域中所拥有的实例创建的生命周期中,每一个依赖组件或调用Resolve()方法创建一个单一的共享的实例,并且子生命周期域共享父生命周期域中的实例。若在继承层级中没有发现合适的拥有子实例的生命周期域,则抛出异常:DependencyResolutionException。
3.6 IoC-通过配置文件使用Autofac
  1. 组件安装
    • Client项目右键->管理 NuGet 程序包->Autofac.Configuration及Microsoft.Extensions.Configuration.Xml。
    • image.png
    • image.png
  2. 配置文件
    • 新建一个AutofacConfigIoC.xml文件,在其属性的复制到输出目录项下选择始终复制
<?xml version="1.0" encoding="utf-8" ?>
<autofac defaultAssembly="LinkTo.Test.Autofac.IService">
  <!--无注入-->
  <components name="1001">
    <type>LinkTo.Test.Autofac.Service.Student, LinkTo.Test.Autofac.Service</type>
    <services name="0" type="LinkTo.Test.Autofac.IService.IStudent" />
    <injectProperties>true</injectProperties>
  </components>
  <components name="1002">
    <type>LinkTo.Test.Autofac.Service.Dog, LinkTo.Test.Autofac.Service</type>
    <services name="0" type="LinkTo.Test.Autofac.IService.IAnimalBark" />
    <injectProperties>true</injectProperties>
  </components>
  <!--构造函数注入-->
  <components name="2001">
    <type>LinkTo.Test.Autofac.Service.AnimalWagging, LinkTo.Test.Autofac.Service</type>
    <services name="0" type="LinkTo.Test.Autofac.Service.AnimalWagging, LinkTo.Test.Autofac.Service" />
    <injectProperties>true</injectProperties>
  </components>
  <!--属性注入-->
  <components name="3001">
    <type>LinkTo.Test.Autofac.Service.School, LinkTo.Test.Autofac.Service</type>
    <services name="0" type="LinkTo.Test.Autofac.IService.ISchool" />
    <injectProperties>true</injectProperties>
  </components>
</autofac>
//加载配置
            ContainerBuilder builder = new ContainerBuilder();
            var config = new ConfigurationBuilder();
            config.AddXmlFile("AutofacConfigIoC.xml");
            var module = new ConfigurationModule(config.Build());
            builder.RegisterModule(module);
            IContainer container = builder.Build();
            //无注入测试
            IStudent student = container.Resolve<IStudent>();
            student.Add("1002", "World");
            //构造函数注入测试
            AnimalWagging animal = container.Resolve<AnimalWagging>();
            animal.Wagging();
            //属性注入测试
            ISchool school = container.Resolve<ISchool>();
            school.LeaveSchool();
3.7 ASP.NET MVC与AtuoFac
  1. 首先在函数Application_Start() 注册自己的控制器类,一定要引入Autofac.Integration.Mvc.dll
  2. 注入依赖代码
    • 首先声明一个Student学生类
    • 然后声明接口及其实现
    • 最后添加控制器StudentController,并注入依赖代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using Autofac;
using AtuoFacOfMVC4.Models;
using System.Reflection;
using Autofac.Integration.Mvc;


namespace AtuoFacOfMVC4
{
   public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            var builder = new ContainerBuilder();
            SetupResolveRules(builder);
            builder.RegisterControllers(Assembly.GetExecutingAssembly());
            var container = builder.Build();
            DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

            AreaRegistration.RegisterAllAreas();
            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            AuthConfig.RegisterAuth();
        }
        private void SetupResolveRules(ContainerBuilder builder)
        {
            builder.RegisterType<StudentRepository>().As<IStudentRepository>();
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace AtuoFacOfMVC4.Models
{
    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Graduation { get; set; }
        public string School { get; set; }
        public string Major { get; set; }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AtuoFacOfMVC4.Models
{
    public interface IStudentRepository
    {
        IEnumerable<Student> GetAll();
        Student Get(int id);
        Student Add(Student item);
        bool Update(Student item);
        bool Delete(int id);
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using AtuoFacOfMVC4.Models;

namespace AtuoFacOfMVC4.Controllers
{
    public class StudentController : Controller
    {
        readonly IStudentRepository repository;
        //构造器注入
        public StudentController(IStudentRepository repository)
        {
            this.repository = repository;
        }

        public ActionResult Index()
        {
            var data = repository.GetAll();
            return View(data);
        }

    }
}
3.8 AOP
  1. 组件安装
    • Client项目右键->管理 NuGet 程序包->Autofac.Extras.DynamicProxy。
    • image.png
  2. 拦截器
  3. 测试代码
    • 注意:对于以类方式的注入,Autofac Interceptor要求类的方法必须为virtual方法。如AnimalWagging类的Wagging()、WaggingAsync(string name)都加了virtual修饰符
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Castle.DynamicProxy;

namespace LinkTo.Test.Autofac.Client
{
    /// <summary>
    /// 拦截器:需实现IInterceptor接口。
    /// </summary>
    public class CallLogger : IInterceptor
    {
        private readonly TextWriter _output;

        public CallLogger(TextWriter output)
        {
            _output = output;
        }

        /// <summary>
        /// 拦截方法:打印被拦截的方法--执行前的名称、参数以及执行后的返回结果。
        /// </summary>
        /// <param name="invocation">被拦截方法的信息</param>
        public void Intercept(IInvocation invocation)
        {
            //空白行
            _output.WriteLine();

            //在下一个拦截器或目标方法处理之前的处理
            _output.WriteLine($"调用方法:{invocation.Method.Name}");

            if (invocation.Arguments.Length > 0)
            {
                _output.WriteLine($"参数:{string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())}");
            }

            //调用下一个拦截器(若存在),直到最终的目标方法(Target Method)。
            invocation.Proceed();

            //获取被代理方法的返回类型
            var returnType = invocation.Method.ReturnType;

            //异步方法
            if (IsAsyncMethod(invocation.Method))
            {
                //Task:返回值是固定类型
                if (returnType != null && returnType == typeof(Task))
                {
                    //定义一个异步方法来等待目标方法返回的Task
                    async Task Continuation() => await (Task)invocation.ReturnValue;
                    //Continuation()中并没有使用await,所以Continuation()就如同步方法一样是阻塞的。
                    invocation.ReturnValue = Continuation();
                }
                //Task<T>:返回值是泛型类型
                else
                {
                    //获取被代理方法的返回类型
                    var returnTypeT = invocation.Method.ReflectedType;
                    if (returnTypeT != null)
                    {
                        //获取泛型参数集合,集合中的第一个元素等价于typeof(Class)。
                        var resultType = invocation.Method.ReturnType.GetGenericArguments()[0];
                        //利用反射获得等待返回值的异步方法
                        MethodInfo methodInfo = typeof(CallLogger).GetMethod("HandleAsync", BindingFlags.Public | BindingFlags.Instance);
                        //调用methodInfo类的MakeGenericMethod()方法,用获得的类型T(<resultType>)来重新构造HandleAsync()方法。
                        var mi = methodInfo.MakeGenericMethod(resultType);
                        //Invoke:使用指定参数调用由当前实例表示的方法或构造函数。
                        invocation.ReturnValue = mi.Invoke(this, new[] { invocation.ReturnValue });
                    }
                }

                var type = invocation.Method.ReturnType;
                var resultProperty = type.GetProperty("Result");

                if (resultProperty != null)
                    _output.WriteLine($"方法结果:{resultProperty.GetValue(invocation.ReturnValue)}");
            }
            //同步方法
            else
            {
                if (returnType != null && returnType != typeof(void))
                    _output.WriteLine($"方法结果:{invocation.ReturnValue}");
            }
        }

        /// <summary>
        /// 判断是否异步方法
        /// </summary>
        public static bool IsAsyncMethod(MethodInfo method)
        {
            return 
                (
                    method.ReturnType == typeof(Task) || 
                    (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
                );
        }

        /// <summary>
        /// 构造等待返回值的异步方法
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="task"></param>
        /// <returns></returns>
        public async Task<T> HandleAsync<T>(Task<T> task)
        {
            var t = await task;
            return t;
        }
    }
}
ContainerBuilder builder = new ContainerBuilder();

            //注册拦截器
            builder.Register(c => new CallLogger(Console.Out));
            builder.Register(c => new CallTester());

            //动态注入拦截器

            //这里定义了两个拦截器,注意它们的顺序。
            builder.RegisterType<Student>().As<IStudent>().InterceptedBy(typeof(CallLogger), typeof(CallTester)).EnableInterfaceInterceptors();

            //这里定义了一个拦截器
            builder.RegisterType<AnimalWagging>().InterceptedBy(typeof(CallLogger)).EnableClassInterceptors();
            builder.RegisterType<Dog>().As<IAnimalBark>();

            IContainer container = builder.Build();
            IStudent student = container.Resolve<IStudent>();
            student.Add("1003", "Kobe");

            AnimalWagging animal = container.Resolve<AnimalWagging>();
            animal.Wagging();

            Task<string> task = animal.WaggingAsync("哈士奇");
            Console.WriteLine($"{task.Result}");

4. 实战

  1. IUserDao层
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace IDao
{
    public interface IUserDao
    {
        public void GetUser();
        public void AddUser();
        public void UpdateUser();
        public void DeleteUser();
    }
}
  1. UserDao层
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using IDao;
namespace Dao
{
    public class UserDao : IUserDao
    {
        public void AddUser()
        {
            Console.WriteLine("添加用户");
        }

        public void DeleteUser()
        {
            Console.WriteLine("删除用户");
        }

        public void GetUser()
        {
            Console.WriteLine("查询用户");
        }

        public void UpdateUser()
        {
            Console.WriteLine("修改用户");
        }
    }
}
  1. IUserService层
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace IServices
{
    public interface IUserService
    {
        public void GetUser();
        public void AddUser();
        public void UpdateUser();
        public void DeleteUser();
    }
}
  1. UserService层
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using IDao;
using IServices;
namespace Services
{
    public class UserService : IUserService
    {
        IUserDao userDao;

        public IUserDao UserDao {private get => userDao; set => userDao = value; }

        public void AddUser()
        {
            UserDao.AddUser();
        }

        public void DeleteUser()
        {
            UserDao.DeleteUser();
        }

        public void GetUser()
        {
            UserDao.GetUser();
        }

        public void UpdateUser()
        {
           UserDao.UpdateUser();
        }
    }
}
  1. test层
using Autofac;
using IServices;
using System;
using System.Reflection;

namespace ConsoleApp2
{  
    internal class Program
    {
        private static IUserService userService;
        private static IContainer container;
        static void Main(string[] args)
        {
            //注册
            Register();
            userService = container.Resolve<IUserService>();
            userService.GetUser();
        }

        public static void Register()
        {
            ContainerBuilder builder = new ContainerBuilder();

            var IDao = Assembly.Load("IDao");
            var Dao = Assembly.Load("Dao");
            var IServices = Assembly.Load("IServices");
            var Services = Assembly.Load("Services");

            //根据名称约定(服务层的接口和实现均以BLL结尾),实现服务接口和服务实现的依赖
            // 我这里使用的Service
            builder.RegisterAssemblyTypes(IServices, Services)
              .Where(t => t.Name.EndsWith("Service"))
              .AsImplementedInterfaces().PropertiesAutowired().InstancePerDependency();

            //根据名称约定(数据访问层的接口和实现均以DAL结尾),实现数据访问接口和数据访问实现的依赖
            builder.RegisterAssemblyTypes(IDao, Dao)
              .Where(t => t.Name.EndsWith("Dao"))
              .AsImplementedInterfaces().PropertiesAutowired().InstancePerDependency();

            //创建一个Autofac的容器来管理这些类
            container = builder.Build();
        }
    }
}

2. Net Core 概念

2.1 什么是Net Core

  • .NET Core是适用于 Windows、Linux 和 macOS 的免费、开源托管的计算机软件框架,是微软开发的第一个官方版本,具有跨平台能力的应用程序开发框架 (Application Framework),未来也将会支持 FreeBSD 与 Alpine 平台,也是微软在一开始发展时就开源的软件平台 。

2.2 Net Core 优点

  1. 支持独立部署,不互相影响;
  2. 彻底模块化;
  3. 没有历史包袱,运行效率高
  4. 不依赖于IIS
  5. 跨平台
  6. 符合现代开发理念:依赖注入、单元测试等

2.3 Net Core 和Net Framework 区别

  1. 不支持:ASP. NET WebForms、WCF服务器端、WF、.NET Remoting、Appdomain
  2. 部分Windows-Only的特性. NET core,但是无法跨平台:WinForm、WPF、注册表、Event Log、AD等

2.4 Net Standard

  1. Net Standard 在Net Core中的地位
  2. .NET Standard只是规范,一个.NET Standard类库可以被支持其版本的.NETFramework、.NET Core、 Xamarin等引用。而.NET Core类库、.NET Framework类库则不可以。如果编写一个公用的类库,尽量选择. NET Standard,并且尽量使用低版本。

3. Nuget 使用

3.1 什么是Nuget ?

  1. Nuget是一个.NET平台下的开源的项目,它是Visual Studio的扩展。在使用Visual Studio 或.NET CLI开发基于.NET 或.NET Framework的应用时,Nuget能把在项目中添加、移除和更新引用的工作变得更加快捷方便。

3.2 使用Nuget

  1. 可以在nuget 官网进行寻找包
  2. nuget 可以通过命令行和图形化界面进行安装包
    • 在VS中打开Nuget 中进行搜索点击安装即可
    • 在Nuget 官网找到要安装的包,然后在Nuget程序包管理器界面中使用命令即可安装。
  3. 常用命令
    1. 安装
      • install-package <程序包名> -source <本地路径>
      • 安装指定版本使用install-package <程序包名> –version <版本号>
    2. 更新
      • update-package –reinstall <程序包名>
    3. 卸载
      • uninstall-package<程序包名> -force
    4. 还原
      • nuget restore MySolution.sln

4. 异步编程

4.1 同步编程和异步编程

  1. 所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。
  2. 异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。
  3. 例如:B/S模式中的ajax请求,具体过程是:客户端发出ajax请求->服务端处理->处理完毕执行客户端回调,在客户端(浏览器)发出请求后,仍然可以做其他的事

4.2 同步编程和异步编程区别

  1. 同步是多个线程同时访问同一资源,等待资源访问结束,浪费时间,效率低
  2. 异步:访问资源时在空闲等待时同时访问其他资源,实现多线程机制
  3. 异步处理就是,你现在问我问题,我可以不回答你,等我用时间了再处理你这个问题.
  4. 同步不就反之了,同步信息被立即处理 – 直到信息处理完成才返回消息句柄;
  5. 异步信息收到后将在后台处理一段时间 – 而早在信息处理结束前就返回消息句柄

4.3 async和 await基本使用

4.3.1 异步方法

  1. 异步方法
    • 用async关键字修饰的方法
  2. 异步方法的返回值一般是Task, T是真正的返回值类型,Task. 惯例:异步方法名字以Async结尾。
  3. 即使方法没有返回值,也最好把返回值声明为非泛型的Task.
  4. 调用异步方法时,一般在方法前加上await关键字,这样拿到的返回值就是泛型指定的T类型;
  5. 异步方法的 传染性
    • 一个方法中如果有await调用,则这个方法也必须修饰为async
    • 案例
  6. 如果同样的功能,既有同步方法,又有异步方法,那么首先使用异步万法。.NET5根多椎架中的方法也都支持异步: Main、 WinForm事件处理函数。
  7. 对于不支持的异步方法怎么办?
    • 使用 Wait() (无返回值) ; Result (有返回值)去解决
    • 可能造成死锁,尽量不要使用。
  8. 异步委托
static async Task Main(string[] args)
{
    string fileName =” d:/1. txt" ;
        File. Delete(fi 1 eName) ;
    File. WriteAl lTextAsync(fileName,"hello async");
    string s = await File.ReadAllTextAsync(fileName) ;
    Console.WriteLine(s) ;
}
/*
Task<string> t = File. ReadAllTextAsync(@ e: \temp\a\l. txt ) ;
string s = t. Result;
string s = File. ReadAllTextAsync(@" e: \temp\a\1. txt' ). Result;
Console. WriteLine (s. Substring(0, 20)) ;*/

File. WriteAllTextAsync(@ "e: \temp\a\l. txt","aaaaaaaaaaaaaaaaaaaaaaa").Wait() ;
ThreadPool. QueueUserWorkItem(async (obj) => {
    while (true)
{
    await File. WriteAllTextAsync (@" e:\temp\a\1. txt","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
    Console. WriteLine ("XXXXXXXXX”) ;
                        }
                        Console. Read() ;

4.4 async与await 原理

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

4.5 async 背后的线程切换

  1. await调用的等待期间,.NET会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码。
  2. 验证代码
  3. 优化思路: 到要等待的时候,如果发现已经执行结束了,那就没必要再切换线程了,剩下的代码就继续在之前的线程上继续执行了。
static async Task Main(string[]args)
{
    Console.writeLine(Thread.CurrentThread. ManagedThreadId) ;StringBui1der sb = new StringBuilder ( ;
                                                                                                    for(int i=0; i<10000;i++)
 {                                                                                                                sb.Append("xXXXXXXXXXXXXXXXXXXXXXXXXx");                                                                  await File.WriteA11TextAsync(@"e: \templa\1.txt", sb.ToString());
 }
  Console.WriteLine(Thread.CurrentThread. ManagedThreadId) ;
}

4.6 为什么有点异步方法没有标记Async

  1. 只甩手Task,不“拆完了再装”反编译上面的代码:只是普通的方法调用。优点:运行效率更高,不会造成线程浪费。

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

static Task<string>ReadFileAsync(int num){
    switch (num){
            case l: 
               return  File.ReadAllTextAsync("d:/1.txt");
            case 2:
               return  File.ReadAllTextAsync("d:/2.txt");
           default:
               throw new ArgumentException("num invalid");
    }
}

4.7 在异步编程中暂停线程

  1. 如果想在异步方法中暂停一段时间,不要用 Thread.Sleep(),因为它会阻塞调用线程,而要用awaitTask.Delay()。

4.8 CancellationToken

  1. 有时需要提前终止任务,比如:请求超时、用户取消请求。很多异步方法都有CancellationToken参数,用于获得提前终止执行的信号。
  2. CancellationToken结构体
  3. 实例
None:空
bool lsCancellationRequested是否取消(*)
Register(Action callback)注册取消监听
ThrowlfCancellationRequested()如果任务被取消,执行到这句话就抛异常。

CancellationTokenSource 用来创建CancellationToken
CancelAfter()超时后发出取消信号
Cancel() 发出取消信号
using System;
using System.Text;

namespace ConsoleApp1 // Note: actual namespace depends on the project name.
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
           
            CancellationTokenSource cts = new CancellationTokenSource();    
            CancellationToken token = cts.Token;
            cts.CancelAfter(5000);//五秒后取消任务
            await Test("https://www.bilibili.com/", 200, token);
          
        }


        static async Task Test(string url,int n, CancellationToken token) 
        {
            using (HttpClient client = new HttpClient()) 
            {
                for (int i = 0; i < n; i++)
                {
                    // await client.GetAsync(url,token); 使用异步方法自带取消任务处理,抛出异常

                    /* await client.GetAsync(url) 
                     * token.ThrowIfCancellationRequested(); 不传参数给异步方法,然后自己手动抛出异常
                     */

               //通过IsCancellationRequested 属性来判断是否取消了任务,做出相应的处理,尽量使用自己处理和系统处理的方式
                    var html =await client.GetStringAsync(url,token);
                    Console.WriteLine($"{DateTime.Now}:{html}");
                    if (token.IsCancellationRequested) 
                    {
                        Console.WriteLine("请求被取消");
                        break;
                    }
                }

            }
            
        }
    }
}

4.9 Task类的重要方法:

  1. Task WhenAny(lEnumerabletasks)等,任何一个Task完成,Task就完成
  2. Task<TResult[]> WhenAll(paramsTask[] tasks)等,所有Task完成,Task才完成。用于等待多个任务执行结束,但是不在乎它们的执行顺序。
  3. FromResult()创建普通数值的Task对象。
using System;
using System.Text;
namespace ConsoleApp1 // Note: actual namespace depends on the project name.
{
    internal class Program
    {
        static async Task Main(string[] args)
        {

            string[] files = Directory.GetFiles(@"");
            Task<int>  [] counter=new Task<int>[files.Length];  

            for (int i = 0; i < files.Length; i++)
            {
                string fileName = files[i];
                Task<int> task = Test(fileName);
                counter[i] = task;
                    
            }
            int [] counts=await Task.WhenAll(counter);
            Console.WriteLine(counts.Sum());
        }


        static async Task<int> Test(string fileName) 
        {
          string s= await File.ReadAllTextAsync(fileName);
          return s.Length;
            
        }
    }
}

4.10 异步编程其他问题

  1. 接口中的异步方法
    • async是提示编译器为异步方法中的await代码进行分段处理的,而一个异步方法是否修饰了async对于方法的调用者来讲没区别的,因此对于接口中的方法或者抽象方法不能修饰为async。
  2. 异步与yield:
  3. 在旧版C#中,async方法中不能用yield。从C# 8.0开始,把返回值声明为IAsyncEnumerable(不要带Task),然后遍历的时候用await foreach()即可。
复习: yield return不仅能够简化数据的返回,而且可以让数据处理“流水线化”,提升性能。
static lEnumerable<string>Test()
{
  yield return "hello";
  yield return "yzk";
  yield return "youzack";
}
static async Task Main(string]args)
{
  await foreach(var s in Test())
  {
  console.WriteLine(s);
  }
}

static async IAsyncEnumerable<string> Test()
{
   yield return "hello";
   yield return "yzk";
   yield return "youzack";
}

4.11 异步编程不等于多线程

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

5. Linq

5.1 为什么要学Linq

  1. 为什么要学LINQ?让数据处理变得简单:
统计一个字符串中每个字母出现的频率(忽略大小写),然后按照从高到低的顺序输出出现频率高于2次的单词和其出现的频率。
 var items = s.Where(c => char.lsLetter(c))//过滤非字母
 .Select(c => char.ToLower(c))//大写字母转换为小写
 .GroupBy(c => c)//根据字母进行分组
 .Where(g=>g.Count()>2)//过滤掉出现次数<=2
 .OrderByDescending(g =>g.Count())//按次数排序
 .Select(g=>new { Char=g.Key,Count=g.Count()});

5.2 Linq 原理

  1. 模拟where方法
static IEnumerable<int> MyWhere1(IEnumerable<int> items,Func<int, bool> f)
{
    List<int> result = new List<int>();
    foreach(int i in items)
     {
         if(f(i)==true)I{
             result.Add(i);
             return result;
         }
     }

5.3 常用扩展方法

  1. LINQ中提供了大量类似Where扩展方法,简化数据处理。大部分都在System.Linq命名空间中。
  2. Where方法:
    • 每一项数据都会经过predicate的测试,如果针对一个元素,predicate执行的返回值为true,那么这个元素就会放到返回值中。
    • Where参数是一个lambda表达式的匿名名方法,方法的参数e表示当前判断的元素对象。参数的名字不一定非要叫e,不过一般lambda表达式中的变量名长度都不长。
  3. Count(方法: 获取数据条数。
  4. **Any()方法:**是否至少有一条数据,返回值是bool ,Any 还可以指定条件表达式。**bool b=list.Any(p=>p.Age>5);等价于bool b= list.Where(p=>p.Age>5).Any(); **
  5. 获取一条数据(是否带参数的两种写法):
    • Single:有且只有一条满足要求的数据
    • SingleOrDefault:最多只有一条满足要求的数据;
    • First :至少有一条,返回第一条;
    • FirstOrDefault :返回第一条或者默认值;
  6. 排序:
    • list.OrderBy(p=>p.Age)对数据正序排序;
    • list.OrderByDescending(p=>p.age) 倒序排序;
    • 指定多个排序规则,**list.OrderByDescending(p=>p.Age).ThenBy(p=>p.Salary)**,也支持ThenByDescending()。
  7. 限制结果集,获取部分数据:
    • Skip(n)跳过n条数据
    • Take(n)获取n条数据。
    • 案例:获取从第2条开始获取3条数据var orderedltems1 = list.Skip(2).Take(3);
    • Skip()、Take()也可以单独使用。
  8. 聚合函数:
    • Max()、Min ()、Average ()、Sum ()、Count()。
    • LINQ中所有的扩展方法几乎都是针对lEnumerable接口的,而几乎所有能这但集合的都返回IEnumerable,所以是可以把几乎所有方法“链式使用”的。
    • 例如:list.Where(e=> e.Age > 30).Min(e=>e.Salary)
  9. 分组:
    • GroupBy()方法参数是分组条件表达式
    • 返回值为IGrouping<TKey, TSource>类型的泛型IEnumerable,也就是每一组以一个IGrouping对象的形式返回。
    • IGrouping是个继承自IEnumerable的接口,IGrouping中Key属性表示这一组的分组数据的值。
  10. 投影:
  • 把集合中的每一项转换为另外一种类型。
  • lEnumerable ages = list.Select(e=>e.Age);
  • lEnumerablenames=list.Select(e=>e.Gender?“男”:“女”);
  • var dogs = list.Select(p=>new Dog{NickName=e.Name,Age=e.Age}};
  1. **集合转换:**有一些地方需要数组类型或者List类型的变量,我们可以用别把ToArray)方法和ToList()分别把lEnumerable转换为数组类型和List类型。
  2. 集合操作
  • Except(items1)排除当前集合中在items中存在的元素
  • Union(items1)把当前集合和item1中组合
  • Intersect(item1)把当前集合和item1取交集
  1. SelectMany :把集合中每个对象的另外集合值重新拼接为一个新的集合
 注意不会去重,如果需要去查重需要自己调用Distinct()
IEnumerable<Person> ps=teachers.SelectMany(t=>t.Students)
    foreach(var s in ps)
{
    Console.WriteLine(s);
}

  1. **Join()方法:**Join 可以实现和数据库一样的Join效果,对有关联关系的数据进行联合查询
//查询Id=1的狗,并查询狗主人的姓名
// dogs.Where(d=>d.id>1) 集合和 masters 进行连接,
// 狗集合里面的 d=>d.MasterId  和 狗主人集合的Id  m=>m.Id 相等
// (d,m)=>new{DogName=d.Name,MasterName=m.Name} 是查询结果
var result =dogs.Where(d=>d.id>1).Join(masters,d=>d.MasterId,m=>m.Id,(d,m)=>new{DogName=d.Name,MasterName=m.Name});
foreach(var item in result)
{
    Console.WriterLine(item,DogName+","+item.MasterName);
}
  1. 测试用例
IEnumerable<IGrouping<int, Employee>> items = list.GroupBy(e => e.Age);
foreach(IGrouping<int,Employee> g in items)
 {
     Console.WriteLine(g. Key);
     foreach(Employee e in g)
     {
         Console.WriteLine(e);
     }
     Console.WriteLine("**************");
 }
/* 统计每一个字母出现的次数,并且保留出现次数两次以上的字母 */
string s = "hello World, Hahah, heiheihei";
var items = s.Where(c => char.IsLetter(c)).Select(c =>char.ToLower(c))
     .GroupBy(c =>c).Select(g =>new { g.Key,Count =
     g. Count () }).OrderByDescending(g =>g.Count).Where(g=> g. Count > 2);

5.4 Linq 基础语法

  1. 以from item in items 开始,items 为待处理的数据集合,item为每一项的变量名;最后要加上select,表示结果的数据。
  2. 查询:var r= from d in dogs select d;
  3. 排序:var r =from d in dogs order by d.Id descending select d;
  4. join
  var items =from d in dogs  join m in master on d.MasterId equals 
      m.Id  select new {DogName=d.Name,MasterName=m.Name};
  1. group by
var items =from d in dogs
           group d by d.Age into g 
           select new {g.Keym,MaxId=g.Max(d=>d.Id)}
  1. 混用:只有Where ,Select,OrderBy Join 等这些使用linq写法,如果要用Max、Min、Count、Average、Sum、Any、First、FirstOrDefault,Single,SingleOrDefault,Distinct、Skip,Take等,则使用lamda写法(编译后是同一个东西,可以混用)
var r1= from p in list 
        group p by p.Age into g
        select new {Age=g.key,MaxSalary=g.Max(p.Salary),Count=g.Count()};

6. 配置

6.1 配置系统入门

  1. 传统Web.config配置
  2. 为了兼容,仍然可以使用Web.config和ConfigurationManager类,但不推荐。
  3. .NET中的配置系统支持丰富的配置源,包括文件(json、xml、ini等)、注册表、环境变量、命令行Azure Key Vault等,还可以配置自定义配置源。可以跟踪配置的改变,可以按照优先级覆盖。

6.2 Json 文件配置

  1. 创建一个json文件,文件名随意,比如config.json ,设置“如果较新则复制”。
  2. NuGet安装Microsoft.Extensions.Configuration和Microsoft.Extensions.Configuration.Json。
  3. 编写代码,先用简单的方式读取配置。

6.2.1 读取Json 配置的方法

{
  "Age": 11,
  "Name": "ssr",
  "student": {"Address": "芜湖"}
}
//读取配置原始方法
/*optional参数表示这个文件是否可选。初学时,建议optional设置为false,
这样写错了的话能够及时发现。*/
//reloadOnchange参数表示如果文件修改了,是否重新加载配置

ConfigurationBuilder configBuilder = new ConfigurationBuilder();
configBuilder.AddJsonFile("config.json", optional: false, reloadOnChange: false);
IConfigurationRoot config = configBuilder.Build();
string name = config["name"];
string proxyAddress = config.GetSection("proxy:address").Value;

6.2.2 绑定读取配置(*)

  1. 可以绑定一个类,自动完成配置的读取。
  2. NuGet安装:Microsoft.Extensions.Configuration.Binder
  3. Server server = configRoot.GetSection(“proxy”).Get();
{
  "age": 11,
  "name": "ssr",
  "teacher": {"address": "芜湖","name":"ssr","age": 18}
}
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();

configurationBuilder.AddJsonFile ("config.json", optional:false,reloadOnChange:false);
IConfigurationRoot config = configurationBuilder.Build();

//读取teacher中的值并转化为一个对象
var teacher=   config.GetSection("teacher").Get<Teacher>();

Console.WriteLine($" name={teacher.Name},address={teacher.Address},age={teacher.Age}");

Console.ReadKey();


class Teacher 
{
    public string? Name{ get; set; }

    public string? Address { get; set; }

    public int Age { get; set; }
}

6.2.3 选项方式读取配置

  1. 推荐使用选项方式读取,和DI结合更好,且更好利用"reloadonchange”机制。
  2. NuGet安装:Microsoft.Extensions.OptionsMicrosoft.Extensions.Configuration.Binder,当然也需要Microsoft.Extensions.ConfigurationMicrosoft.Extensions.Configuration.Json
  3. 读取配置的时候,DI要声明IOptionsIOptionsMonitorIOptionsSnapshot等类型。lOptions不会读取到新的值; 和IOptionsMonitor相比,IOptionsSnapshot会在同一个范围内(比如ASP.NET Core一个请求中) 保持一致。建议IOptionsSnapshot
  4. 在读取配置的地方,用IOptionsSnapshot注入
  5. 不要在构造函数里直接读取IOptionsSnapshot.Value, 是到用到的地方再读取,否则就无法更新变化。
  6. 代码
{
  "total": 1100,
  "num": 10,
  "teacher": {"address": "芜湖","name":"ssr","age": 18}
}
public class Config
 {

     public int Total { get; set; }
     public int Num { get; set; }

     public Teacher? Teacher { get; set; }


 }
public class Teacher
{
    public int Age { get; set; }

    public string?  Address { get; set; }

    public string? Name { get; set; }
}
public class TestConfig
{
    /// <summary>
    /// 声明一个snapshot字段
    /// </summary>
    private readonly IOptionsSnapshot<Config> snapshot;
    /// <summary>
    /// 使用构造器方式进行注入值
    /// </summary>
    /// <param name="snapshot"></param>
    public TestConfig(IOptionsSnapshot<Config> snapshot)
    {
          this.snapshot = snapshot;
    }

    /// <summary>
    ///  读取值
    /// </summary>
    public void Test() 
    {
        var config= snapshot.Value;

        Console.WriteLine($"config.Num={config.Num},config.Teacher.Name={config.Teacher.Name}");  
    }
}
public class TestTeacher
{
    /// <summary>
    /// 声明一个snapshot字段
    /// </summary>
    private readonly IOptionsSnapshot<Teacher> snapshot;
    /// <summary>
    /// 使用构造器方式进行注入值
    /// </summary>
    /// <param name="snapshot"></param>
    public TestTeacher(IOptionsSnapshot<Teacher> snapshot)
    {
        this.snapshot = snapshot;
    }
    /// <summary>
    ///  读取值
    /// </summary>
    public void Test()
    {
        var teacher = snapshot.Value;

        Console.WriteLine($"teacher.Name={teacher.Name}");
    }
}
//创建ServiceCollection对象用来创建容器对象
ServiceCollection services = new ServiceCollection();

/*添加要管理的类*/
services.AddScoped<TestConfig>();
services.AddScoped<TestTeacher>();
//添加AddOptions,让DI来管理Options
var options= services.AddOptions();

/*读取配置*/
ConfigurationBuilder builder =new ConfigurationBuilder();
builder.AddJsonFile("config.json",optional:false,reloadOnChange:true);
IConfigurationRoot configRoot= builder.Build();

/*绑定类*/
//将json文件中的属性和config对象中的属性进行绑定
options.Configure<Config>(e => configRoot.Bind(e))
//将json文件中的teacher对象和teacher中的对象进行绑定
    .Configure<Teacher>(e=>configRoot.GetSection("teacher").Bind(e));


//创建容器对象
using (ServiceProvider provider= services.BuildServiceProvider())
{
    //从容器中获取TestConfig对象
    var testConfig = provider.GetRequiredService<TestConfig>();
    var testTeacher = provider.GetRequiredService<TestTeacher>();
    //调用方法
    testConfig.Test();
    testTeacher.Test();
}

6.3 命令行方式进行配置

  1. 配置框架还支持从命令行参数、环境变量等地方读取。
  2. 从命令行中读取配置,NuGet安装Microsoft.Extensions.Configuration.CommandLine。
  3. configBuilder.AddCommandLine(args)
  4. 参数支持多种格式,比如: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(注意在键值之间加空格)。格式不能混用。
  5. 从环境变量中读取配置,需要安装Microsoft.Extensions.Configuration.EnvironmentVariables。
  6. 然后configurationBuilder.AddEnvironmentVariables()**,AddEnvironmentVariables()**有无参数和有prefix参数的两个重载版本。无参数版本会把程序相关的所有环境发重郁加载进来,由于有可能和系统中已有的环境变量冲突,因此建议用有prefix参数的AddEnvironmentVariables()。读取配置的时候,prefix参数会被忽略。
  7. 对于环境变量、命令行等简单的键值对结构,如果想要进行复杂结构的配置,需要进行“扁平化处理”
    例如proxy:address=80 \\给proxy下的address赋值等于80proxy:ids:0=10 给proxy下的ids数组第一个元素赋值
  8. 从命令行/环境变量读取代码
// 在VS2022的调试属性里面添加命令行参数
num:10  teacher:name=ssr

//在VS2022的调试属性里面添加环境变量
名称                     值
myNum                    10
myTeacher:name           ssr
public class Config
{

    public int Total { get; set; }
    public int Num { get; set; }

    public Teacher? Teacher { get; set; }
}

public class Teacher
{
    public int Age { get; set; }

    public string?  Address { get; set; }

    public string? Name { get; set; }
}
public class TestConfig
{
    /// <summary>
    /// 声明一个snapshot字段
    /// </summary>
    private readonly IOptionsSnapshot<Config> snapshot;
    /// <summary>
    /// 使用构造器方式进行注入值
    /// </summary>
    /// <param name="snapshot"></param>
    public TestConfig(IOptionsSnapshot<Config> snapshot)
    {
          this.snapshot = snapshot;
    }

    /// <summary>
    ///  读取值
    /// </summary>
    public void Test() 
    {
        var config= snapshot.Value;

        Console.WriteLine($"config.Num={config.Num},config.Teacher.Name={config.Teacher.Name}");  
    }
}
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using 读取Json文件配置;

//创建ServiceCollection对象用来创建容器对象
ServiceCollection services = new ServiceCollection();

/*添加要管理的类*/
services.AddScoped<TestConfig>();

//添加AddOptions,让DI来管理Options
var options= services.AddOptions();

/*读取配置*/
ConfigurationBuilder builder =new ConfigurationBuilder();
//从控制台读取参数
// builder.AddCommandLine(args);


// 从环境变量读取参数,读取以my开头的参数,绑定参数时会自动忽略prefix参数
builder.AddEnvironmentVariables(prefix:"my");

IConfigurationRoot configRoot= builder.Build();

/*绑定类*/
//将json文件中的属性和config对象中的属性进行绑定
options.Configure<Config>(e => configRoot.Bind(e)); 

//创建容器对象
using (ServiceProvider provider= services.BuildServiceProvider())
{
    //从容器中获取TestConfig对象
    var testConfig = provider.GetRequiredService<TestConfig>();
    //调用方法
    testConfig.Test();
}


6.4 自定义配置提供者

  1. 安装 Microsoft.Extensions.Configuration.Binder(提供绑定)、
    ** ** Microsoft.Extensions.Configuration.FileExtensions(从文件读取)
  2. 开发一个直接或者间接实现IConfiguratidnProvider接口的类XXXConfigurationProvider.一般继承首ConfigurationProvider。如果是从文件读取,可以继承自FileConfigurationProvider. 重写Load方法,把“扁平化数据”设置到Data属性即可。
  3. 再开发一个实现了IConfigurationSource接口的类XXXConfigurationSource.如果是从文件读取,可以继承自FileConfigurationSource。在Build方法中返回上面的ConfigurationProvider对象。
  4. 然后使用即可,configurationBuilder.Add(new ConfigurationSource())即可。为了简化使用,一般提供一个IConfigurationBuilder的扩展方法。
  5. 整体流程:编写ConfigurationProvider类实际读取配置;编写ConfigurationSource在Build中返回ConfigurationProvider对象;把ConfigurationSource对象加入IConfigurationBuilder。
  6. 代码
<configuration>
	<connectionStrings>
		<add name="connStr1" connectionString="server:127.0.0.1" providerName="Mysql"/>
		<add name="connStr2" connectionString="server:127.0.0.1" providerName="Sql Server"/>
	</connectionStrings>

	<appSettings>
		<add key="name" value="ssr"/>
		<add key="age" value="12"/>
		<add key="sex" value="男"/>
	</appSettings>
</configuration>
public class Config
{
    public Config()
    {
        conStrs = new List<ConnStr>();
    }

    public List<ConnStr> conStrs { get;set;}

    public string?  Name { get; set; }
    public string? Age { get; set; }
    public string? Sex { get; set; }

  
}

public class  ConnStr
{
    public string? Name { get; set; }
    public string? ConnectionString { get; set; }
    public string? ProviderName { get; set; }
}
public class FXConfigurationProvider : FileConfigurationProvider
{
    /// <summary>
    /// FXConfigurationProvider 就是提供数据的
    /// </summary>
    /// <param name="source"></param>
    public FXConfigurationProvider(FileConfigurationSource source) : base(source)
    {
        //调用父类的构造器去初始化FileConfigurationSource对象
    }

    public override void Load(Stream stream)
    {
       //创建一个忽略大小写的字典
       var data= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); 
       XmlDocument xmlDocument = new XmlDocument();
       xmlDocument.Load(stream);

        int i = 0;
        #region 读取connectionStrings节点
        XmlNodeList? nodes= xmlDocument.SelectNodes ("/configuration/connectionStrings/add");
        foreach (var item in nodes.Cast<XmlNode>())
        {
            string name = item.Attributes["name"].Value;
            string connectionString = item.Attributes["connectionString"].Value;
            string providerName = item.Attributes["providerName"].Value;
            
            //只要像之前扁平填充数据即可
            data[$"conStrs:{i}:name"] = name;
            data[$"conStrs:{i}:connectionString"] = connectionString;
            data[$"conStrs:{i++}:providerName"] = providerName;
        }
        #endregion


        #region 读取appSettings节点
        XmlNodeList? nodeList = xmlDocument.SelectNodes("/configuration/appSettings/add");
        foreach (var item in nodeList.Cast<XmlNode>())
        {
            string key = item.Attributes["key"].Value;
            string value = item.Attributes["value"].Value;
            //只要像之前扁平填充数据即可
            data[key]= value;
        }
        #endregion
        this.Data = data;
    }
}
public class FXConfigurationSource : FileConfigurationSource
{
    /// <summary>
    /// FXConfigurationSource 提供资源
    /// </summary>
    /// <param name="builder"></param>
    /// <returns></returns>
    public override IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        EnsureDefaults(builder); //处理默认值问题
        return new FXConfigurationProvider(this);
    }
}
public class TestConfig
{
    private readonly IOptionsSnapshot<Config> options;

    public TestConfig(IOptionsSnapshot<Config> options) 
    {
      this.options = options;   
    }

    /// <summary>
    ///  读取值
    /// </summary>
    public void Test()
    {
        var config = options.Value;

        Console.WriteLine($"config.Name={config.Name},config.conStrs[0].ProviderName={config.conStrs[0].ProviderName}");
    }


}
public static class FXConfiguration
{
    public static IConfigurationBuilder AddFXConfig(this IConfigurationBuilder configuration, string path = null)
    {
        if (path == null) path = "web.config";
        configuration.Add(new FXConfigurationSource() { Path = path });
        return configuration;
    }

}
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using 自定义配置读取;

//创建ServiceCollection对象用来创建容器对象
ServiceCollection services = new ServiceCollection();

/*添加要管理的类*/
services.AddScoped<TestConfig>();

//添加AddOptions,让DI来管理Options
var options = services.AddOptions();

/*读取配置*/
ConfigurationBuilder builder = new ConfigurationBuilder();
//添加自定义FXConfigurationSource
//builder.Add(new FXConfigurationSource() { Path= "web.config" });
//使用拓展方法
builder.AddFXConfig ();


IConfigurationRoot configRoot = builder.Build();

/*绑定类*/
//将json文件中的属性和config对象中的属性进行绑定
options.Configure<Config>(e => configRoot.Bind(e));

//创建容器对象
using (ServiceProvider provider = services.BuildServiceProvider())
{
    //从容器中获取TestConfig对象
    var test = provider.GetRequiredService<TestConfig>();
    //调用方法
    test.Test();
}

6.5 开发数据库配置提供者

6.5.1 第一步

在数据库中建一张表,默认名字是T_Configs,这个表名允许自定义为其他名字,具体见后续步骤。表必须有Id、Name、Value三个列,Id定义为整数、自动增长列,Name和Value都定义为字符串类型列,列的最大长度根据系统配置数据的长度来自行确定,Name列为配置项的名字,Value列为配置项的值。
允许具有相同Name的多行数据,其中Id值最大的一条的值生效,这样就实现了简单的配置版本管理。因此,如果不确认一个新的配置项一定成功的话,可以先新增一条同名的配置,如果出现问题,只要把这条数据删除就可以回滚到旧的配置项。
Name列的值遵循.NET中配置的“多层级数据的扁平化”,如下都是合法的Name列的值:

Api:Jwt:Audience
Age
Api:Names:0
Api:Names:1

Value列的值用来保存Name类对应的配置的值。Value的值可以是普通的值,也可以使用json数组,也可以是json对象。比如下面都是合法的Value值:

["a","d"]
{"Secret": "afd3","Issuer": "youzack","Ids":[3,5,8]} 
ffff
3

**注意:**因为非双引号包裹的键或者值不是合法的Json格式,比如{‘Secret’: ‘afd3’}或者{Secret: ‘afd3’},所以它们不会被本组件支持,请使用标准的双引号包裹的Json格式:{“Secret”: “afd3”}
下面这个数据就是后续演示使用的数据:

6.5.2 第二步

创建一个ASP.NET 项目,演示案例是使用Visual Studio 2019创建.NET Core 3.1的ASP.NET Core MVC项目,但是Zack.AnyDBConfigProvider的应用范围并不局限于这个版本。通过NuGet安装开发包:

Install-Package Zack.AnyDBConfigProvider

6.5.3 配置数据库的连接字符串

虽然说项目中其他配置都可以放到数据库中了,但是数据库本身的连接字符串仍然需要单独配置。它既可以配置到本地配置文件中,也可以通过环境变量等方式配置,下面用配置到本地json文件来举例。打开项目的appsettings.json,增加如下节点:

"ConnectionStrings": {
  "conn1": "Server=127.0.0.1;database=youzack;uid=root;pwd=123456"
}

接下来在Program.cs里的CreateHostBuilder方法的webBuilder.UseStartup();之前增加如下代码:

webBuilder.ConfigureAppConfiguration((hostCtx, configBuilder)=>{
	var configRoot = configBuilder.Build();
	string connStr = configRoot.GetConnectionString("conn1");
	configBuilder.AddDbConfiguration(() => new MySqlConnection(connStr),reloadOnChange:true,reloadInterval:TimeSpan.FromSeconds(2));
});

在 .Net 6中你可以使用如下的代码:

var webBuilder = builder.Host;
webBuilder.ConfigureAppConfiguration((hostCtx, configBuilder) => {
    var configRoot = builder.Configuration;
    string connStr = configRoot.GetConnectionString("conn1");
    configBuilder.AddDbConfiguration(() => new MySqlConnection(connStr), reloadOnChange: true, reloadInterval: TimeSpan.FromSeconds(2));
});

上面代码的第3行用来从本地配置中读取到数据库的连接字符串,然后第4行代码使用AddDbConfiguration来添加Zack.AnyDBConfigProvider的支持。我这里是使用MySql数据库,所以使用new MySqlConnection(connStr)创建到MySQL数据库的连接,你可以换任何你想使用的其他数据库管理系统。reloadOnChange参数表示是否在数据库中的配置修改后自动加载,默认值是false。如果把reloadOnChange设置为true,则每隔reloadInterval这个指定的时间段,程序就会扫描一遍数据库中配置表的数据,如果数据库中的配置数据有变化,就会重新加载配置数据。AddDbConfiguration方法还支持一个tableName参数,用来自定义配置表的名字,默认名称为T_Configs。不同版本的开发工具生成的项目模板不一样,所以初始代码也不一样,所以上面的代码也许并不能原封不动的放到你的项目中,请根据自己项目的情况来定制化配置的代码。

6.5.4 第四步:

剩下的就是标准的.NET 中读取配置的方法了,比如我们要读取上面例子中的数据,那么就如下配置。
首先创建Ftp类(有IP、UserName、Password三个属性)、Cors类(有string[]类型的Origins、Headers两个属性)。
然后在Startup.cs的ConfigureServices方法中增加如下代码:

services.Configure<Ftp>(Configuration.GetSection("Ftp"));
services.Configure<Cors>(Configuration.GetSection("Cors"));

然后在Controller中读取配置:

public class HomeController : Controller
{
	private readonly ILogger<HomeController> _logger;
	private readonly IConfiguration config;
	private readonly IOptionsSnapshot<Ftp> ftpOpt;
	private readonly IOptionsSnapshot<Cors> corsOpt;

	public HomeController(ILogger<HomeController> logger, IConfiguration config, IOptionsSnapshot<Ftp> ftpOpt, IOptionsSnapshot<Cors> corsOpt)
	{
		_logger = logger;
		this.config = config;
		this.ftpOpt = ftpOpt;
		this.corsOpt = corsOpt;
	}

	public IActionResult Index()
	{
		string redisCS = config.GetSection("RedisConnStr").Get<string>();
		ViewBag.s = redisCS;
		ViewBag.ftp = ftpOpt.Value;
		ViewBag.cors = corsOpt.Value;
		return View();
	}
}

点击查看关于这个组件的原理讲解

6.6 多源配置和UserSecrets

6.6.1 多源配置

  1. 为什么要多源配置
    • 某个网站需要自定义配置;
    • 程序员的同一台机器上,开发调试环境和测试环境用不同的配置。
  2. 按照注册到ConfigurationBuilder的顺序,“后来者居上”,后注册的优先级高,如果配置名字重复,用后注册的值。
  3. image.png

6.6.2 UserSecrets

  1. 不能泄露到源码中的配置放到user-secrets即可。
  2. 一般吧user-secrets优先级放到普通json文件之后。
  3. 如果开发人员电脑重装系统等原因造成本地的配置文件删除了,就需要重新配置。
  4. 并不是生产中的加密,只适用于开发。
  5. Nuget安装: Microsoft.Extensions.Configuration.UserSecrets
  6. 在VS项目上点右键【管理用户机密】,编辑这个配置文件。看看这个文件在哪里。会自动在csproj中的UserSecretsld就是文件夹的名字。
  7. configBuilder.AddUserSecrets()

7. 日志

7.1 日志基本概念

  1. 日志级别:Trace<Debug <Information <Warning< Error < Critical
  2. 日志提供者(LoggingProvider):把日志输出到哪里。控制台、文件、数据库等。
  3. .NET的日志非常灵活,对于业分代码只要注入日志对象记录日志即可,具体哪些日志输出到哪里、什么样的格式、是否输出等都有配置或者初始化代码决定。

7.1.1 微软自带日志使用

  1. 把日志信息写入控制台
  2. Nuget:Microsoft.Extensions.Logging、Microsoft.Extensions.Logging.Console
  3. DI 注入
services.AddLogging(LogBuiler=>{
    LogBuilder.AddConsole(); //
});
  1. 需要记录日志的代码,注入ILogger即可,T一般就用当前类,这个类的名字会输出到日志,方便定位错误然后调用LogInformation()、LogError等方法输出不同级别的日志,还支持输出异常对象。
  2. 完整代码:
 public class Test
    {
        private readonly ILogger<Test> logger;
        /// <summary>
        /// 采用构造器注入,注入logger对象
        /// </summary>
        /// <param name="logger"></param>
        public Test(ILogger<Test> logger) 
        {
          this.logger = logger; 
        }

        public void Print()
        {
            logger.LogCritical("系统崩溃了");
            logger.LogDebug("xx出错了");
            logger.LogError("抛出异常");
            logger.LogInformation("xx完成");
            logger.LogTrace("xx");
            logger.LogWarning("警告");
        }
    }
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using 日志演示;

ServiceCollection service = new ServiceCollection();

/*-------添加要管理的类以及各种包------------*/

//添加日志插件
service.AddLogging(logBuilder => { 
    logBuilder.AddConsole(); //把日志输入到控制台
    logBuilder.SetMinimumLevel(LogLevel.Debug); //设置最低输出日志等级
});
//添加要托管的类
service.AddScoped<Test>();

//构建容器
using (ServiceProvider provider= service.BuildServiceProvider())
{
    //获取对象
    var test= provider.GetRequiredService<Test>();
    test.Print();
}

7.1.2 其他日志提供者

  1. Console只适合开发阶段,运行阶段需要输出到文件等。
  2. 采用和Configuration类似的扩展机制,不仅内置了Debug、事件查看器、文件、Azure日志等提供者,还可以扩展。Provider可以共存。
  3. Event Log: .windows Only 在 Windows下部署的程序、网站运行出错、不正常,先去EventLog看看。NuGet安装:Microsoft.Extensions.Logging.EventLog。然后logBuilder.AddEventLog()。

7.2 文本日志NLog使用

  1. .NET没有内置文本日志提供者。第三方有Log4Net、NLog、Serilog等。老牌的Log4Net另搞一套,不考

虑。

  1. NuGet安装:NLog.Extensions.Logging( using NLog.Extensions.Logging;)。项目根目录下建nlog.config,注意文件名的大小写(考虑linux),也可以是其他文件名,但是需要单独配置。
  2. 增加**logBuilder.AddNLog();**
  3. 代码
<?xml version="1.0" encoding="utf-8" ?>
<!-- XSD manual extracted from package NLog.Schema: https://www.nuget.org/packages/NLog.Schema-->
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xsi:schemaLocation="NLog NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogFile="console-example-internal.log"
      internalLogLevel="Info" >

	<!-- the targets to write to -->
	<targets>
		<!-- write logs to file -->
		<target xsi:type="File" name="logfile" fileName="console-example.log"
				layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />
		<target xsi:type="Console" name="logconsole"
				layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />
	</targets>

	<!-- rules to map from logger name to target -->
	<rules>
		<logger name="*" minlevel="Trace" writeTo="logfile,logconsole" />
	</rules>
</nlog>
public class Test
{
    private readonly ILogger<Test> logger;
    /// <summary>
    /// 采用构造器注入,注入logger对象
    /// </summary>
    /// <param name="logger"></param>
    public Test(ILogger<Test> logger) 
    {
      this.logger = logger; 
    }

    public void Print()
    {
        logger.LogCritical("系统崩溃了");
        logger.LogDebug("xx出错了");
        logger.LogError("抛出异常");
        logger.LogInformation("xx完成");
        logger.LogTrace("xx");
        logger.LogWarning("警告");
    }
}
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using 日志演示;

ServiceCollection service = new ServiceCollection();
/*-------添加要管理的类以及各种包------------*/
//添加日志插件
service.AddLogging(logBuilder => {
// configure Logging with NLog
logBuilder.ClearProviders();
logBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);//设置最低输出日志等级
logBuilder.AddNLog();
});
//添加要托管的类
service.AddScoped<Test>();

//构建容器
using (ServiceProvider provider= service.BuildServiceProvider())
{
//获取对象
var test= provider.GetRequiredService<Test>();
test.Print();
}

7.3 NLog日志分级和过滤

  1. 为什么要日志分类?
    • 不同级别或者不同模块的日志记录到不同的地方。
  2. 为什么要日志过滤?
    • 项目不同阶段(比如刚上线和稳定后)需要记录的日志不同。
    • 严重错误可以调用短信Provider等。
  1. archiveAboveSize 参数:为“单个日志文件超过多少字节就把日志存档”,单位为字节,这样可以避免单个文件太大。
  2. maxArchiveFiles 参数:如果不设定maxArchiveFiles参数,则文件日志存档文件的数量会一直增加,而如果设定maxArchiveFiles参数后,则最多保存maxArchiveFiles指定数量个数的存档文件,旧的会被删掉;当然也可以不设置maxArchiveFiles参数,而设置maxArchiveDays参数,这样可以设定保存若干天的日志存档。
  3. 这些不同参数的起到什么作用?“滚动日志”策略。
  4. rules
    • rules节点下可以添加多个logger,每个logger都有名字(name属性),name是通配符格式的。
    • logger节点的minlevel属性和maxlevel属性,表示这个logger接受日志的最低级别和最高级别。
    • 日志输出时,会从上往下匹配rules节点下所有的logger,若发现当前日志的分类名和level符合这个logger的name的通配符,就会把日志输出给这个logger。如果匹配多个logger,就把这条日志输出给多个logger。但是如果一个logger设置了final=“true”,那么如果匹配到这个logger,就不继续向下匹配其他logger了。

7.4 结构化日志和集中日志服务

  1. 结构化日志比普通文本更利于日志的分析。

image.png

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

7.4.1 结构化日志组件Serilog

  1. Nuget 安装Serilog.AspNetCore 包
  2. Log.Logger=new LoggerConfiguration() .MinimumLevel.Debug() //日志输出最低级别 .Enrich.FromLogContent() //使用更加丰富的日志信息 .WriteTo.Console(new JsonFormatter()).CreateLogger(); //写入控制台,输出格式为json
  3. logBuilder.AddSerilog();
  4. 同样可以输出到文件、数据库、MongoDB等。
  5. 代码
 public class Test
    {
        private readonly ILogger<Test> logger;
        /// <summary>
        /// 采用构造器注入,注入logger对象
        /// </summary>
        /// <param name="logger"></param>
        public Test(ILogger<Test> logger) 
        {
          this.logger = logger; 
        }

        public void Print()
        {
            logger.LogCritical("系统崩溃了");
            logger.LogDebug("xx出错了");
            logger.LogError("抛出异常");
            logger.LogInformation("xx完成");
            logger.LogTrace("xx");
            logger.LogWarning("警告");
        }
    }
using Microsoft.Extensions.DependencyInjection;
using Serilog.Formatting.Json;
using Serilog;
using 日志演示;

ServiceCollection service = new ServiceCollection();

/*-------添加要管理的类以及各种包------------*/

//添加日志插件
service.AddLogging(logBuilder => {
 // configure Logging with Serilog

 Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug() //日志输出最低级别
.Enrich.FromLogContext()  //使用更加丰富的日志信息
.WriteTo.Console(new JsonFormatter()).CreateLogger(); //写入控制台,输出格式为js

logBuilder.AddSerilog();

});
//添加要托管的类
service.AddScoped<Test>();

//构建容器
using (ServiceProvider provider= service.BuildServiceProvider())
{
    //获取对象
    var test= provider.GetRequiredService<Test>();
    test.Print();
}

7.4.2 总结

  1. 普通项目用NLog输出到文本文件即可,根据需要设定过滤、分类规则。
  2. 集群部署的项目用Serilog+“集中式日志服务“ 如果需要记录结构化日志,再进行结构化输出。
  3. 如果用云服务的就够了,就用云服务的,免得自己部署;如果想自己控制日志数据就用自部署Exceptionless或者ELK(难度大)等。

8. EF Core

8.1 EF Core 简介

8.1.1 EF Core 介绍

EF Core 可用作对象关系映射程序 (O/RM),这可以实现以下两点:

  • 使 .NET 开发人员能够使用 .NET 对象处理数据库。
  • 无需再像通常那样编写大部分数据访问代码。

类似于EF的ORM框架有:Dapper、Sqlsugar、FreeSql

8.1.2 EF与其他ORM的比较

  1. Entity Framework Core(EF Core)是微软官方的ORM框架。
    • 优点:功能强大、官方支持、生产效率高、力求屏蔽底层数据库差异;
    • 缺点:复杂、上手门槛高、不熟悉EFCore的话可能会进坑。
  2. Dapper。优点:简单,N分钟即可上手,行为可预期性强;缺点:生产效率低,需要处理底层数据库差异。
  3. EF Core是模型驱动(1Model-Driven)的开发思想,Dapper是数据库驱动(DataBase-Driven)的开发思想的。没有优劣,只有比较。
  4. 性能:Dapper等≠性能高;EF Core≠性能差。
  5. EF Core是官方推荐、推进的框架,尽量屏蔽底层数据库差异,.NET开发者必须熟悉,根据的项目情况再决定用哪个。

8.1.3 EF Core 和EF 比较

  1. EF有 DB First、Model First、Code First。EF Core不支持模型优先,推荐使用代码优先,遗留系统可以使用Scaffold-DbContext来生成代码实现类似DBFirst的效果,但是推荐用Code First 。
  2. 会对实体上的标注做校验,EF Core追求轻量化,不校验。
  3. 熟悉EF的话,掌握EFCore会很容易,很多用法都移植过来了。EF Core又增加了很多新东西。
  4. EF中的一些类的命名空间以及一些方法的名字在EF Core中稍有不同。
  5. EF不再做新特性增加。

8.2 搭建EF Core 环境

8.2.1 使用Code Frist 创建表

  1. Nuget 安装:Microsoft .EntityFrameWorkCore.SqlServer
  2. 建实体类
public class Book
{
    public long Id { get; set; }   //Id

    public string? Title { get; set; } // 标题

    public DateTime PubTime { get; set; } //发布日期

    public double Price { get; set; } //单价
}
  1. 建配置类 继承于 IEntityTypeConfiguration(可以使用约定,不一定需要建立配置类)
public class BookEntityConfig : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder.ToTable ("T_Books");
    }
}
  1. 创建继承于DbContext 的类
 public class TestDbContext:DbContext
    {
        //只有声明了DbSet<Book>才会生成表
        public  DbSet<Book> Books { get; set; }
        /// <summary>
        /// 配置连接字符串
        /// </summary>
        /// <param name="optionsBuilder"></param>
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            base.OnConfiguring(optionsBuilder);
            string connStr = "Data Source=.;Initial Catalog=demo1;User ID=sa;Password=123456;Trusted_Connection=True;MultipleActiveResultSets=true";
            optionsBuilder.UseSqlServer(connStr);
        }
        /// <summary>
        /// 创建当前程序集所有实现了IEntityTypeConfiguration的类的Model
        /// </summary>
        /// <param name="modelBuilder"></param>
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
        }
    }
  1. Migration 数据库迁移
    • **Migration 概念:**根据对象的定义变化,自动更新数据库中的表以及表的结构的操作,叫做Migration
      迁移可以分为多步(项目进化),也可以回滚。
  2. 使用Migration数据生成工具
    • Nuget 安装 **Microsoft.EntityFrameworkCore.Tools**
    • 在程序包管理器控制台中执行**Add-Migration InitialCreate(名字随便起)**会自动在项目的Migration文件夹中生成操作数据库的C#代码
    • 在程序包管理器控制台中执行**Update-database**,就会执行Migration文件夹C#生成表的代码

8.2.2 修改表结构

  1. 项目开发中,根据需要,可能会在已有实体中修改、新增、删除表、列等。
  2. 例如想要限制Title的最大长度为50,Title字段设置为“不可为空”,并且想增加一个不可为空且最大长度为20的AuthorName(作者名字)属性。首先在Book实体类中增加一个AuthorName属性
  3. 修改BookEntityConfig `
public class BookEntityConfig : IEntityTypeConfiguration<Book>
{
  public void Configure(EntityTypeBuilder<Book> builder)
  {
    builder.ToTable("T_Books");
    builder.Property(e =>e.Title).HasMaxLength(50).IsRequired();
     builder.Property(e =>e.AuthorName).HasMaxLength(20).IsRequired();
  }
}

4、执行Add-Migration AddAuthorName_ModifyTitle(取名字有意义) 。
5、Update-Database

8.3 EF Core 增删改查

8.3.1 增加数据

  1. EF Core 中推荐使用异步方法,EF core 会自动跟踪(Track)实体类对象以及DbSet的改变
  2. 添加单条数据
TestDbContext testDbContext =null;
using (testDbContext=new TestDbContext()) 
{
    /* 如果主键是Id,且是自动增长的,
     * 那么在SaveChanges()之后,会自动给Id赋值
     * 必须给主键赋值默认值0,否则会报错
     */
    Book book = new Book()
    {
        AuthorName = "ssr",
        Price = 112.21,
        PubTime = DateTime.Now,
        Title= "西游记",
    };
    //将数据添加到内存中
    testDbContext.Books.Add(book);
    //将内存中的数据写入到数据库 ,相当于Update-database
    testDbContext.SaveChanges();
}
  1. 添加多条数据
TestDbContext testDbContext =null;

using (testDbContext=new TestDbContext()) 
{
    for (int i=0;i<10; i++) 
    {
        Book book = new Book()
        {
            AuthorName = "ssr",
            Price = 112.21,
            PubTime = DateTime.Now,
            Title = "西游记"+i,
        };
        testDbContext.Books.Add(book);
    }
    /* 当我们调用SaveChanges方法来执行增、删、改时其操作内部
     * 都用一个transaction包裹着(自动完成的),
     * 不用我们自己去调用事务。*/
    testDbContext.SaveChanges();
}

8.3.2 查询数据

  1. DbSet 实现了IEnumerable接口,因此可以对DbSet 实施Linq操作来进行数据查询。EF Core 会把Linq 操作转换为SQL 语句。从而实现面向对象,而不是面向数据库(SQL)。
  2. 使用Id进行查询
TestDbContext db = new TestDbContext();
var stu1= db.Books.Find(1);
var stu2= db.Books.where(s=>s.Id=1).FirstOrDefault();
  1. 根据条件进行查询
// 实例化继承自dbCoetent的派生类
TestDbContext db = new TestDbContext();
// 根据查询条件如果数据返回类型是IQueryable说明是延迟查询
var query= db.Books.Where(s=> s.Id>3);
//只有在用的时候才进行查询,这就是延迟查询
foreach(var iteam in query)
{
    Console.WriteLine(item);
}
  1. 查询所有
// 实例化继承自dbCoetent的派生类
TestDbContext db = new TestDbContext();
var query= db.Books.ToList();
foreach(var iteam in query)
{
    Console.WriteLine(item);
}
  1. 分页查询
/*.Skip() 跳过指定的条数
.Take() 获取从指定数据开始的前几条数据
int pageIndex=1;//第几页数据
int pageSize=5;//每页几条数据
.Skip((pageIndex-1)*pageSize).Take(pageSize);
*/

// 实例化继承自dbCoetent的派生类
TestDbContext db = new TestDbContext();
//分页查询一定要排序,pageIndex,pageSize 是做分页的时候,传过来的来参数

// EF 调用Skip之前必须使用OrderBy
var query=db.Books.OrderBy(s=>s.Id).Skip((pageIndex-1)*pageSize).Take(pageSize).ToList();
  1. GroupBy
TestDbContext db = new TestDbContext();
var groups = db.Books.GroupBy(b =>b.AuthorName).Select
    (g =>new 
             { AuthorName= g.Key,
               BooksCount = g.Count(),
               MaxPrice = g.Max(b =>b.Price)
             }
    );

foreach(var g in groups)
{
    Console.WriteLine($"作者名:{g.AuthorName},著作数量:{g.BooksCount},最贵的价格:{g.MaxPrice}");
}

8.3.3 修改数据

  1. 要对数据进行修改,首先需要把要修改的数据查询出来,然后再对查询出来的对象进行修改,然后再执行SaveChangesAsync()保存修改。
  2. 单条数据修改
TestDbContext testDbContext =null;

using (testDbContext=new TestDbContext()) 
{
    //修改id=1的数据
    var book= testDbContext.Books.SingleOrDefault(b=>b.Id==1);
   book.Title = "SSR的编程书";
  await  testDbContext.SaveChangesAsync();
}
  1. 修改多条值
TestDbContext testDbContext =null;

using (testDbContext=new TestDbContext()) 
{
    // ExecuteUpdateAsync EF Core 7 新增的批量修改的方法

    /**  给每一个本价格大于100的书,价格增加10
     * SetProperty(要修改的属性,要修改的值)
     * 
     * **/

    await testDbContext.Books.Where(e => e.Price > 100).
        ExecuteUpdateAsync(s => 
        s.SetProperty(b => b.Price,b=>b.Price+10)
        .SetProperty(b=>b.PubTime,b=>DateTime.Now));

}

8.3.4 删除数据

  1. 删除也是先把要修改的数据查询出来,然后再调用DbSet或者DbContext的Remove方法把对象删除,然后再执行SaveChangesAsync()保存修改。
  2. 单条数据删除
TestDbContext testDbContext =null;

using (testDbContext=new TestDbContext()) 
{
    //删除Id=11的数据
   var book= testDbContext.Books.SingleOrDefault(b=>b.Id==11);

   testDbContext.Books.Remove(book);//也可以写成testDbContext.Remove(book);
    testDbContext.SaveChangesAsync();
}
  1. 删除多条值
TestDbContext testDbContext =null;

using (testDbContext=new TestDbContext()) 
{
    // ExecuteDeleteAsync EF Core 7 新增的批量删除的方法
    await testDbContext.Books.Where(e => e.Title.StartsWith("西游记")).
        ExecuteDeleteAsync();

}

8.4 EF Core 实体的配置

8.4.1 约定配置

  1. 表名采用DbContext中的对应的DbSet的属性名。
  2. 数据表列的名字采用实体类属性的名字,列的数据类型采用和实体类属性类型最兼容的类型。
  3. 数据表列的可空性取决于对应实体类属性的可空性。
  4. 名字为Id的属性为主键,如果主键为short, int或者long类型,则默认采用自增字段,如果主键为Guid类型,则默认采用默认的Guid生成机制生成主键值。

8.4.2 两种配置方式

  1. Data Annotation
    • 把配置以特性(Annotation)的形式标注在实体类中
    • 优点:简单;
    • 缺点:耦合;
[Table("T_Books")]
public class Books
{
}
  1. Fluent API
    • **builder.ToTable("T_Books");**
    • 把配置单独写到配置类中
    • 优点:解耦
    • 缺点:复杂
    • 大部分功能重叠。可以混用,但是不建议混用。

8.4.3 Fluent API 配置

  1. 视图与实体类映射: **modelBuilder.Entity<Blog>().ToView("blogsView");**(继承IEntityTypeConfiguration不需要点Entity(),直接点ToView() 就行,其他的一样)
  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. 配置主键:** 默认把名字为Id或者“实体类型+Id“ 的属性作为主键,可以用HasKey(来配置其他属性作为主键**。**modelBuilder.Entity<Student>().HasKey(c =>c.Number);**支持复合主键,但是不建议使用。
  6. 生成列的值:**modelBuilder.Entity<Student>().Property(b =>b.Number).ValueGeneratedOnAdd();**
  7. 设置默认值:**modelBuilder.Entity<Student>().Property(b =>b.Age).HasDefaultValue(6);**
  8. 索引: **modelBuilder.Entity<Blog>().HasIndex(b =>b.Url);****复合索引:**modelBuilder.Entity<Person>().HasIndex(p =>new { p.FirstName,p.LastName ));**
    唯一索引:**IsUnique();**
    聚集索引: **IsClustered();**

8.5 EF Core 主键选择

8.5.1 自增主键

  1. EF Core支持多种主键生成策略:自动增长;Guid;Hi/Lo算法等。
  2. 自动增长:
    • 优点:简单;
    • 缺点:数据库迁移以及分布式系统中比较麻烦;并发性能差。long、int等类型主键,默认是自增。因为是数据库生成的值,所以SaveChanges后会自动把主键的值更新到Id属性。

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

8.5.2 GuId 主键

  1. Guid算法(或UUID算法)生成一个全局唯一的Id。适合于分布式系统,在进行多数据库数据合并的时候很简单。
    优点:简单,高并发,全局唯一;
    缺点:磁盘空间占用大。
  2. Guid值不连续,使用Guid类型做主键的时候,不能把主键设置为聚集索引。因为聚集索引是按照顺序保存主键的,因此用Guid做主键性能差。比如MySQL的InnoDB引擎中主键是强制使用聚集索引的。有的数据库支持部分的连续Guid,比如SQLServer中的NewSequentialId(),但也不能解决问题。在SQLServer等中,不要把Guid主键设置为聚集索引; 在MySQL中,插入频繁的表不要用uid做主键。
  3. Guid用法:既可以让EF Core给赋值,也可以手动赋值(推荐)。

8.5.3 其他方案

  1. 混合自增和Guid(非复合主键)。用自增列做物理的主键,而用uid列做逻辑上的主键。把自增列设置为表的主键,而在业务上查询数据时候把Guid当主键用。在和其他表关联以及和外部系统通讯的时候(比如前端显示数据的标识的时候)都是使用Guid列。不仅保证了性能,而且利用了Guid的优点,而且减轻了主键自增性导致主键值可被预测带来的安全性问题。
  2. Hi/Lo算法:EF Core支持Hi/Lo算法来优化自增列。主键值由两部分组成:高位(Hi)和低位(Lo),高位由数据库生成,两个高位之间间隔若干个值,由程序在本地生成低位,低位的值在本地自增生成。不同进程或者集群中不同服务器获取的Hi值不会重复,而本地进程计算的Lo则可以保证可以在本地高效率的生成主键值。但是Hi/Lo算法不是EF Core的标准。

8.6 Migrations 深入

8.6.1 Migrations 文件和数据介绍

  1. 使用迁移脚本,可以对当前连接的数据库执行编号更高的迁移,这个操作叫做“向上迁移”(Up),也可以:

把数据库回退到旧的迁移,这个操作叫“向下迁移”(Down)。

  1. 除非有特殊需要,否则不要删除Migrations文件夹下的代码。
  2. 数据库的__EFMigrationsHistory表;记录当前数据库曾经应用过的迁移脚本,按顺序排列。

8.6.2 Migrations其他命令

  1. **Update-Database XXX** 把数据库回滚到XXX的状态,迁移脚本不动。
  2. **Remove-migration** 删除最后一次的迁移脚本
  3. **Script-Migration **生成迁移SQL代码。
  4. 可以生成版本D到版本F的SQL脚本**Script-Migration D F**
  5. 生成版本D到最新版本的SQL脚本:**Script-Migration D**

8.7 EF Core 反向工程(DB Frist)

  1. 根据数据库表来反向生成实体类
  2. Nuget安装 **Microsoft.EntityFrameworkCore.Sqlserver**** 、****Microsoft.EntityFrameworkCore.Tools**
  3. 在nuget程序管理控制台输入:**Scanffold-DbContext 连接字符串 使用的包**
Scaffold-DbContext  
    "Data Source=.;Initial Catalog=demo1;User ID=sa;Password=123456;Trusted_Connection=True;MultipleActiveResultSets=true"
Microsoft.EntityFrameworkCore.Sqlserver

8.8 EF Core 底层如何操作数据库

  1. image.png
  2. EF Core 把C#代码转换为SQL 语句的框架。

8.9 通过代码查看EF Core的SQL语句

  1. 标准日志
//在 XXDbContext 类中定义
public static readonly ILoggerFactoryMyLoggerFactory
= LoggerFactory.Create(builder =>{ builder.AddConsole();});

// 在OnConfiguring 方法中调用
optionsBuilder.UseLoggerFactory(MyLoggerFactory

  1. 简单日志
// XXDbContext的OnConfiguring 方法中调用
optionsBuilder. LogTo(msg=> {
   Console.WriteLine (msg);
});
  1. ToQueryString():IQueryable 有拓展方法 ToQueryString() 可以获得SQL,不需要真的执行查询才能获取SQL语句;只能获取查询操作的
IQueryable<Person> persons =ctx.Persons.Where(p => p.Name.ToLower()=="杨中科"&& p.BirthDay==DateTime.Now);
string sql =persons.ToQueryString();
Console.WriteLine("这也就是我要的!!!"+sql) ;

8.10 EF 使用不同的数据库

  1. 因为迁移脚本不能跨数据库。通过给Add-Migration命令添加-OutputDir 参数的形式来在同一个项目中为不同的数据库生成不同的迁移脚本。
  2. image.png
  3. image.png

8.11 EF Core 关系

8.11.1 EF Core 配置关系的函数

  1. **一对一:HasOne(...).WithOne(...);**
  2. **也可以配置多对一:HasMany(…).WithOne(…);**
  3. **一对多:HasOne(...).WithMany(...);**
  4. **多对多:HasMany(...).WithMany(...);**

8.11.2 配置一对一关系

  1. 必须显式的在其中一个实体类中声明一个外键属性。因为一对一关系,外键建在哪个表都可以,需要我们自己指定一个,否则EF不知道在哪个表创建外键
  2. 代码:
//订单表
public class Order
{
    public long Id { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public Delivery Delivery { get; set; }
}
//快递单
public class Delivery
{
    public long Id { get; set; }
    public string CompanyName { get; set; }
    public String Number { get; set; }
    public Order Order { get; set; }
    public long OrderId { get; set; }   // 显示声明一个外键属性
}
public class OrderConfig : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.ToTable("T_Orders");
        // Order有一个 Delivery,Delivery有一个Order,且Delivery表中的外键是OrderId
        builder.HasOne<Delivery>(o => o.Delivery).WithOne(d => d.Order).HasForeignKey<Delivery>(d => d.OrderId);
    }
}

8.11.3 配置一对多

8.11.3.1 配置双向导航属性
  1. 对于主从结构的“一对多”表关系,一般是声明双向导航属性。
  2. 配置在任何一端都可以,考虑到单向导航一般用HasOne(…).WithMany(…),即把关系配置到多端。
  3. 可以不创建外键属性,EF Core 会自动创建一个影子外键,但是不创建外键属性的话,如果需要获取外键列的值,就需要关联查询,效率低,所以建议添加如果添加了外键属性,需要通过**HasForeignKey(c => c.TheArticleId)**声明一下它是外键属性。
  4. 如果需要查询关联属性,需要使用Include(),最终会生成Join查询sql。
  5. 代码:
// 文章
public class Article
{
    public long Id { get; set; }//主键
    public string Title { get; set; }//标题
    public string Content { get; set; }//内容
    public List<Comment> Comments { get; set; } = new List<Comment>(); //此文章的若干条评论
}
class ArticleConfig : IEntityTypeConfiguration<Article>
{
    public void Configure(EntityTypeBuilder<Article> builder)
    {
        builder.ToTable("T_Articles");

         // 在一端配置
        //builder.HasMany<Comment>(a => a.Comments).WithOne(c => c.Article).IsRequired();


    }
}
// 评论
public class Comment
{
    public long Id { get; set; }
    public Article Article { get; set; }
    public long TheArticleId { get; set; }
    public string Message { get; set; }
}
public class CommentConfig : IEntityTypeConfiguration<Comment>
{
    public void Configure(EntityTypeBuilder<Comment> builder)
    {
        builder.ToTable("T_Comments");
        //在多端配置:一个评论属于一篇文章,一篇文章有多个评论
        builder.HasOne<Article>(c => c.Article).WithMany(a => a.Comments)
            .IsRequired().HasForeignKey(c => c.TheArticleId);
    }
}
8.11.3.2 配置单向导航属性
  1. 如果表属于被很多表引用的基础表,则用单项导航属性。如用户id是很多表的外键,如果用户实体中添加所有的导航属性,会非常多。这种情况应该用单向导航属性。
  2. image.png
  3. 代码
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    class UserConfig : IEntityTypeConfiguration<User>
    {
        public void Configure(EntityTypeBuilder<User> builder)
        {
            builder.ToTable("T_Users");
        }
    }
    public class Order
    {
        public int Id { get; set; }
        public User User { get; set; }
    }
    public class OrderConfig : IEntityTypeConfiguration<Order>
    {
        public void Configure(EntityTypeBuilder<Order> builder)
        {
            builder.ToTable("T_Orders");
            builder.HasOne<User>(o => o.User).WithMany()//单向导航属性WithMany参数为空即可
                .IsRequired();
        }
    }
8.11.3.3 自引用
  1. EF Core 中的实体自己引用自己,常用在设计菜单实体。
  2. 代码:
class OrgUnit
{
	public long Id { get; set; }
	public string Name { get; set; }
	public OrgUnit Parent { get; set; }
	public List<OrgUnit> Children { get; set; } = new List<OrgUnit>();
}
class OrgUnitConfig : IEntityTypeConfiguration<OrgUnit>
{
    public void Configure(EntityTypeBuilder<OrgUnit> builder)
    {
        //配置自引用
        builder.HasOne<OrgUnit>(u => u.Parent).WithMany(p => p.Children);
    }
}

8.11.4 配置多对多

  1. 多对多需要中间表,EF会为我们创建中间表
  2. 在一端进行配置就行了,在下边的student 和Teacher中都可以
  3. 代码:
//学生表
class Student
{
	public long Id { get; set; }
	public string Name { get; set; }
	public List<Teacher> Teachers { get; set; } = new List<Teacher>();
}

//教师表
class Teacher
{
	public long Id { get; set; }
	public string Name { get; set; }
	public List<Student> Students { get; set; } = new List<Student>();
}


//在学生表中配置
public class StudentConfig : IEntityTypeConfiguration<Student>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.ToTable("T_Students");

         // 一个学生有多个老师,一个老师有多个学生
        builder.HasMany<Teacher>(s => s.Teachers).WithMany(t=>t.Students).
        // UsingEntity()方法并不是必须的,我们可以通过它指定中间表名称
        UsingEntity(j=>j.ToTable("T_Students_Teachers"));
    }
}


8.12 IEnumerable和IQueryable

  1. 普通集合的Where()返回的是IEnumerable,在内存中过滤(客户端评估),而EF Core中的Where 方法返回的IQueryable,把查询操作翻译成SQL语句(服务器端评估)。
  2. 若强制将EF Core 的集合的where方法的返回值改为IEnumerable,进行的操作就是先把所有的数据返回,然后在内存中进行过滤,会导致效率非常低。若使用的是IQueryable,则先再数据库中进行过滤,然后在返回数据。

8.13 IQueryable的延迟查询

  1. IQueryable只是代表一个“可以放到数据库服务器去执行的查询”,它没有立即执行,只是“可以被执行”而已。
  2. 对于IQueryable接口调用非终结方法的时候不会执行查询,而调用终结方法的时候则会立即执行查询。
  3. 终结方法:遍历、ToArray()、ToList()、Min()、Max()、Count()等;
  4. 非终结方法:GroupBy()、OrderBy()、Include()、Skip()、Take()等。
  5. 简单判断:一个方法的返回值类型如果是IQueryable类型,那么这个方法一般就是非终结方法,否则就是终结方法。
  6. 为什么延迟执行? 可以在实际执行之前,分布构建IQueryable。
  7. IQueryable是一个待查询的逻辑,因此它是可以被重复使用的。
IQueryable<Book> books = ctx.Books.Where(b=>b.Price <= 8);
Console.WriteLine(books.Count());
Console.WriteLine(books.Max(b=>b.Price));
var books2 = books.Where(b=>b.PubTime.Year>2000);
//生成的SQL是不同的

8.14 IQueryable 底层怎么读取数据

  1. **DataReader:**分批从数据库服务器读取数据。内存占用小、DB连接占用时间长
  2. **DataTable:**把所有数据都一次性从数据库服务器都加载到客户端内存中。内存占用大,节省DB连接。
  3. IQueryable 底层调用的就是DataReader
    • 优点:节省客户端内存。
    • 缺点:如果处理的慢,会长时间占用连接。
  4. 想一次把数据读取到内存中,可以使用ToArray()、ToArrayAsync()、ToList()、ToListAsync()。
  5. 什么时候需要一次加载
    • 场景1:遍历IQueryable并且进行数据处理的过程很耗时。
    • 场景2:如果方法需要返回查询结果,并且在方法里销毁DbContext的话,是不能返回IQueryable的。必须一次性加载返回。
    • 场景3:多个IQueryable的遍历嵌套。很多数据库的ADO.NET Core Provider是不支持多个DataReader同时执行的。把连接字符串中的MultipleActiveResultSets=true删掉,其他数据库不支持这个。

8.15 EF Core 中的异步方法

  1. 异步方法大部分是定义在**Microsoft.EntityFrameworkCore**这个命名空间下**EntityFrameworkQueryableExtensions**等类中的扩展方法,用完记得using。
  2. 常见的异步方法: **AddAsync()、AddRangeAsync()、AlIAsync()、AnyAsync、AverageAsync、ContainsAsync、CountAsync、FirstAsync、FirstOrDefaultAsync、ForEachAsync、LongCountAsync、MaxAsync、MinAsync、SingleAsync、SingleOrDefaultAsync、SumAsync等**
  3. IQueryable 的这些异步的拓展方法都“立即执行”方法,而GroupBy、OrderBy、Join、Where等“非立即执行”方法则没有对应的异步方法。因为非立即执行方法并没有实际执行SQL语句,并不是消耗IO操作。

8.15 EF Core 执行原生SQL语句

  1. 原生SQL语句的情景:非查询语句、实体查询、任意SQL语句。

8.15.1 执行非查询语句

  1. EF Core 提供了dbCtx.Database.ExecuteSqlInterpolated()和dbCtx.Database.ExecuteSqlInterpolatedAsync() 方法来执行原生的非查询SQL语句。
  2. 字符串内插语法:string name="ssr"; string str= $"我是{name}";
  3. 字符串内插如果赋值给string 变量,就是字符串拼接,字符串内插如果赋值给FormattableString 变量,编译器就会构造FormattableString对象,会自动做SQL防注入处理。
  4. ExecuteSqlInterpolated()的参数FormattableString类型,因此ExecuteSqlInterpolated会进行参数化SQL处理。

8.15.2 执行实体相关查询原生SQL语句

  1. 如果要执行的原生SQL是一个查询语句,并且查询的结果也能对应一个实体,就可以调用对应实体的DbSet的FromSqlInterpolated()方法来执行一个查询sQL语句,同样使用字符串来内插来传递参数。
  2. 代码:**IQueryable<Book> books =ctx.Books.FromSqllnterpolated(@$"select * from T_Books where DatePart(year,PubTime)>{year} order by newid()"); **
  3. FromSqlInterpolated方法返回的是IQueryable,因此可以在实际执行IQueryable之前,可以对IQueryable进行进一步的操作,然后把分页、分组、二次过滤、排序、include 添加上去。
IQueryable<Book> books =ctx.Books.
    FromSqllnterpolated(@$"select * from T_Books where DatePart(year,PubTime)>{year} order by newid()"); 
foreach(Book b in books.Skip(3).Take(6))
{
    ....
}
  1. 局限性:
    • SQL查询必须返回实体类型对应数据库表的所有列。
    • 结果集中的列名必须与属性映射到的列名匹配。
    • 只能单表查询,不能使用Join语句进行关联查询。但是可以在查询后面使用Include()来进行关联数据的获取。

8.15.2 执行任意原生SQL查询语句。

  1. 使用ADO.NET 的场景
    • FromSqllnterpolated()只能单表查询,但是在实现报表查询等的时候,SQL语句通常是非常复杂的,不仅要多表Join,而且返回的查询结果一般也都不会和一个实体类完整对应因此需要一种执行任意SQL查询语句的机制。
    • EF Core中允许把视图或存储过程映射为实体,因此可以把复杂的查询语句写成视图或存储过程,然后再声明对应的实体类,并且在DbContext中配置对应的DbSet。
    • 不推荐写存储过程;项目复杂查询很多,导致:视图太多;非实体的DbSet; DbSet膨胀。
  2. 推荐使用原生ADO 或者Dapper

8.16 EF Core 如何知道数据改变了

  1. 快照更改跟踪方式去跟踪数据 。首次跟踪一个实体的时候,EFCore会创建这个实体的快照。执行SaveChanges()等方法时, EF Core将会把存储的快照中的值与实体的当前值进行比较。
  2. 实体状态:
    • 已添加(Added): DbContext正在跟踪此实体,但数据库中尚不存在该实体。
    • 未改变(Unchanged): DbContext正在跟踪此实体,该实体存在于数据库中,其属性值和从数据库中读取到的值一致,未发生改变。
    • 已修改(Modified): DbContext正在跟踪此实体,并存在于数据库中,并且其部分或全部属性值已修改。
    • 已删除(Deleted): DbContext正在跟踪此实体,并存在于数据库中,但在下次调用SaveChanges时要从数据库中删除对应数据。
    • 已分离(Detached): DbContext未跟踪该实体。
  3. SaveChanges() 操作
  • “已分离”和"未改变"的实体, SaveChanges()忽略;
  • “已添加"的实体, SaveChanges()插入数据库
  • “已修改”的实体, SaveChanges()更新到数据库;
  • “已删除"的实体, SaveChanges()从数据库删除;
  1. 使用DbContext的Entry()方法来获得实体在EF Core中的跟踪信息对象EntityEntry。EntityEntry类的State属性代表实体的状态,通过DebugView.LongView属性可以看到实体的变化信息。

8.17 EF Core 优化之AsNoTracking

  1. 如果通过DbContext查询出来的对象只是用来展,不会发生状态改变,则可以使用AsNoTracking()来“禁用跟踪"。
  2. 如果查询出来的对象不会被修改、删除等,那么查询时可以AsNoTracking(),就能降低内存占用。

8.18 EF Core 实体状态跟踪的妙用

  1. 直接更新一条数据
Book bl =new Book {fId=10}; //跟踪通过ld定位b1.Title = "yzk";
var entryl = ctx.Entry(bl);
entry1.Property("Title").IsModified = true;
Console.WriteLine(entry1.DebugView.LongView);ctx.SaveChanges();

//查看生成的SQI,只更新Title列。
  1. 直接删除一条数据
Book bl = new Book{ Id=28 };
ctx.Entry(bl).State =EntityState.Deleted;
ctx.SaveChanges();
//查看生成的SQL。
  1. 上面的技巧代码可读性低、可维护性不强,而且使用不当有可能造成不容易发现的Bug。带来的性能提升也是微乎其微的,因此不推荐使用,知道即可。

8.19 EF Core 全局查询筛选器

  1. 全局查询筛选器:EF Core 会自动将这个查询筛选器应用于涉及这个实体类型的所有Ling查询。
  2. 场景:软删除、多租户。
  3. 用法:
builder.HasQueryFilter(b=>b.lsDeleted==false); //在配置文件中配置全局筛选器

ctx.Books.IgnoreQueryFilters().Where(b =>b.Title.Contains("o")).ToArray();//忽略全局查询

8.20 EF Core 悲观并发控制

  1. 并发控制:避免多个用户同时操作资源造成的并发冲问题。
  2. 最好的解决方案:非数据库解决方案。
  3. 数据库层面的两种策略:悲观、乐观。
  4. 悲观并发控制
    • 悲观并发控制一般采用行锁、表锁等排它锁对资源进行锁定,确保同一时间只有一个使用者操作被锁定的资源。
    • EF Core没有封装悲观并发控制的使用,需要开发人员编写原生SQL语句来使用悲观锁并发控制。
  5. MySQL实现
/*如果有其他的查询操作也使用for update来查询Id=1的这条数据的话,
那些查询就会被挂起,一直到针对这条数据的更新操作完成从而释放这个行锁,代码才会继续执行。*/
MySQL 行锁 :select * from T_Houses where ld=1 for update
  1. 锁是和事务相关的,因此通过 **BeginTransactionAsync()**创建一个事务,并且在所有操作完成后调用C**ommitAsync()**提交事务。
  2. var h1 = await ctx.Houses.FromSqllnterpolated($"select * fromT_Houses where ld=1 for update").SingleAsync();
  3. 悲观锁并发控制的使用比较简单,且是独占的、排他的,系统并发量很大的话,会严重影响性能,如果使用不当,会导致死锁。

8.21 乐观并发控制(推荐)

  1. 乐观并发控制原理:
/*当Update的时候,如果数据库中的Owner值已经被其他操作者更新为其他值了,
那么where语句的值就会为false,因此这个Update语句影响的行数就是0,
EF Core就知道“发生并发冲突"了,因此
SaveChanges()方法就会抛出
DbUpdateConcurrencyException异常。*/ 

Update T_Houses set Owner='新值' 
where ld=1 and Owner='旧值'

8.21.1 EF Core 中单个列的并发设置

/*1. 把被鬓发修改的属性使用IsConCurrencyToken() 设置为并发令牌。
2.builder.Property(h=>h.Owner).IsConcurrencyToken();
3.catch(DbUpdateConcurrencyException ex)
{
   var entry=ex.Entries.First();
   var dbValues= await entry.GetDatabaseValuesAsync();
   string newOwner = dbValues.GetValue<string>(nameof(House.Owner));
Console.WriteLine($"并发冲突,被{newOwner}提前抢走了");

}
*/

8.21.2 EF Core 中多个列的并发设置

  1. SQLServer数据库可以用一个byte[]类型的属性做并发令牌属性,然后使用IsRowVersion()把这个属性设置为RowVersion类型,这样这个属性对应的数据库列就会被设置为ROWVERSION类型。对于ROWVERSION类型的列,在每次插入或更新行时,数据库会自动为这一行的ROWVERSION类型的列其生成新值。
  2. 在SQLServer中, timestamp和rowversion是同一种类型的不同别名而已。
  3. 实体以及配置
class House
{
public long ld { get; set; }
public string Name{ get; set; }
public string Owner { get; set;}
public byte[] RowVer { get; set; }
}
builder.Property(h => h.RowVer).IsRowVersion();
  1. 在Mysql等数据库中,虽然也有类似的timestamp 类型,但是由于timestamp类型的精度不够,并不适合在高并发的系统。
  2. 在非SQL Server 中,可以将并发令牌列的值更新为Guid的值,修改其他属性知道同时,并手动修改RowVer的值,使用h1.RowVer=new Guid();手动更新并发令牌的属性值。

8.21.3 总结

  1. 乐观并发控制能够避免悲观锁带来的性能、死锁等问题,因此推荐使用乐观并发控制而不是悲观锁。
  2. 如果有一个确定的字段要被进行并发控制,那么使用IsConcurrencyToken()把这个字段设置为并发令牌即可;
  3. 如果无法确定一个唯一的并发令牌列,那么就可以引入一个额外的属性设置为并发令牌,并且在每次更新数据的时候,手动更新这一列的值。如果用的是SQLServer数据库,那么也可以采用RowVersion列,这样就不用开发者手动来在每次更新数据的时候,手动更新并发令牌的值了。

9. 表达式树

9.1 什么是表达式树

  1. 表达式树(Expression Tree):树形数据结构表达代码,以表示逻辑运算,以便可以在运行时访问逻辑运算的结构。
  2. Expression 类型,在使用时一般要声明一个委托类型。
  3. 从lambda 表达式来生成表达式树:Expression<Func<Book,bool>>el=(b)=>b.price>5; 其中参数类型为Book,返回值为bool。
  4. **Expression<Func<Book,bool>>e1=(b)=>b.price>5; ****Func<Book,bool> e2=(b)=>b.price>5;**的区别:
    • Expression对象储存了运算逻辑,它把运算逻辑保存成抽象语法树(AST),可以在运行时动态获取运算逻辑。而普通委托则没有。
    • Expression 对象和普通委托放入Where中进行查询时,Expression的语法会被翻译成SQL语句,而普通委托则是先获取所有的数据,再在客户端进行筛选。

9.2 查看表达式树结构

  1. Visual Studio中调试程序,然后用【快速监视】的方式查看变量e的值,展开Raw View。
  2. 整个表达式树是一个“或”(OrElse) 类型的节点,左节点(Left)是b.Price>5表达式,右节点(Right)是b.AuthorName.Contains(“杨中科”)表达式。而b.Price>5这个表达式又是一个“大于”(GreaterThan)类型的节点, 左节点(Left)是b.Price,右节点(Right)是5。
    • Expression<Func<Book,bool>>e1=(b)=>b.price>5; Expression<Func<Book,bool>>e2=(b)=>b.Contains("杨中科");
    • **list.where(e1).where(e2);**
  3. ,如果报BadImageFormatException异常,需要将降低版本到**.Net Core 3.1**
  4. 在控制台打印表达式树结构:
    • lnstall-Package ExpressionTreeToString;
    • Expression<Func<Book, bool>>e=b=>b.AuthorName.Contains("杨中科")||b.Price>30;
    • Console.WriteLine(e.ToString("Object notation", "C#"));

9.3 动态构建表达式树

  1. 构建表达式树常用的类,ParameterExpression、BinaryExpression、MethodCallExpression、 ConstantExpression等类 几乎都没有提供构造方法,因此无法创建实例。
  2. 通常调用Expression 类的 Parameter、MakeBinary、Call、ConStant等静态方法来生成表达式树,这些静态方法被称为创建表达式树的工厂方法,而属性则通过方法参数类设置。
  3. 手动创建**Expression<Func<Book,bool>>e1=(b)=>b.price>5;**的表达式树
/**  Expression<Func<Book,bool>>e1=(b)=>b.price>5; 查看生成的表达式树
var b = new ParameterExpression {
    Type = typeof(Book),
    IsByRef = false,
    Name = "b"
};
new Expression<Func<Book, bool>> {
    NodeType = ExpressionType.Lambda,
    Type = typeof(Func<Book, bool>),
    Parameters = new ReadOnlyCollection<ParameterExpression> {
        b
    },
    Body = new BinaryExpression {
        NodeType = ExpressionType.GreaterThan,
        Type = typeof(bool),
        Left = new MemberExpression {
            Type = typeof(int),
            Expression = b,
            Member = typeof(Book).GetProperty("Price")
        },
        Right = new ConstantExpression {
            Type = typeof(int),
            Value = 30
        }
    },
    ReturnType = typeof(bool)
}
*/


/***-------------------------- 手动创建表达式树---------------------------------- **/

//创建树叶的左边
ParameterExpression paramExprB = Expression.Parameter(typeof (Book)"b");
// 创建树叶的右边
ConstantExpression constExpr5 = Expression. Constant(6.0,typeof(double));
//访问Price节点
MemberExpression memExprPrice = Expression. MakeMemberAccess (paramExprB, typeof(Book).CetProperty("Price"));
//大于节点                                                              
BinaryExpression binExpGreaterThan = Expression. GreaterThan(memExprPrice,constExpr5);
//生成表达式                                                              
Expression<Func<Book,bool>> exprRoot = Expression. Lambda<Func<Book>, bool>>(binExpGreaterThan,paramExprB);

  1. 常用工厂方法:
    • Add 加法
    • AndAlso:短路与运算
    • ArrayAccess:数组元素访问
    • call:方法访问
    • Condition:三元条件运算符
    • Constant:常量表达式
    • Convert:类型转换
    • GreaterThan:大于运算符
    • GreaterThanOrEqual:大于或等于运算符
    • MakeBinary:创建二元运算
    • NotEqual:不等于运算
    • OrElse:短路或运算
    • Parameter:表达式参数

9.4 更简单的生成表达式树

  1. 可以用**ExpressionTreeToString的ToString("Factory methods","C#")**输出类似于工厂方法生成这个表达式树的代码。
  2. 输出的所有代码都是对于工厂方法的调用,不过调用工厂方法的时候都省略了Expression类。手动添加Expression或者用**using static System.Linq.Expressions.Expression;**
  3. 手动创建**Expression<Func<Book,bool>>e1=(b)=>b.price>5;**的表达式树
var b = Expression.Parameter(
    typeof(Book),
    "b"
);

var expre = Expression.Lambda<Func<Book,bool>>(
    Expression.GreaterThan(
       Expression. MakeMemberAccess(b,
            typeof(Book).GetProperty("Price")
        ),
        Expression.Constant(5.0)
    ),
    b
);
  1. 动态拼接表达式树
using ConsoleApp1;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection.Metadata;

Console.WriteLine("请输入筛选方式,1:大于,2:小于");
string? s=Console.ReadLine();

var b = Expression.Parameter(
    typeof(Book),
    "b"
);
var exprePrice = Expression.MakeMemberAccess(b,typeof(Book).GetProperty("Price"));

BinaryExpression binaryExpression;
var constant = Expression.Constant(5.0);

if (s=="1")
{
    binaryExpression=Expression.GreaterThan( exprePrice, constant);
}
else 
{
    binaryExpression=Expression.GreaterThan(exprePrice, constant);
}

var expre = Expression.Lambda<Func<Book,bool>>(binaryExpression ,b);

  1. 动态拼接表达式树2
static IEnumerable<Book> QueryBook(string propertyName,object value)
{
    using(BookDataContext ctx=new BookDataContext() )
    { 
        //获取传入参数的类型
        Type type =typeof(Book).GetProperty(propertyName).PropertyType;
        //传入参数book的参数名
        var b = Expression.Parameter(typeof(Book),"b");
        //要访问的对象
        MemberExpression memberExpression = Expression.MakeMemberAccess(b, typeof(Book).GetProperty(propertyName));
        //比较的常量
        ConstantExpression constant = Expression.Constant(System.Convert.ChangeType(value, type));
        //操作
        BinaryExpression binaryExpression;
        //生成表达式树
        Expression expression;
        
        if (type.IsPrimitive) //原始类型 
        {
            binaryExpression= Expression.Equal(memberExpression,constant);       
        }
        else 
        {
    
            binaryExpression= Expression.MakeBinary
            (
                ExpressionType.Equal,
                 memberExpression,
                 constant,
                 false,
                 typeof(string).GetMethod("op_Equality") //等式相等调用的方法
            );
        }
        expression=Expression.Lambda<Func<Book, bool>>(binaryExpression, b);
        return ctx.Books.Where(expression).ToList();
    }
}

9.5 动态设定select查询的列

  1. 需求:在运行时动态设定select查询出来的属性。
  2. 实现:
  • Select参数中传递一个数组: **Select(b=>new object[]){b.ld,b.Title});**
  • 把列对应的属性的访问表达式放到一个Expression数组中,然后使用Expression.NewArraylnit构建一个代表数组的NewArrayExpression表达式对象,然后就可以用这个NewArrayExpression对象来供Select调用来执行了。
  1. 实现代码:

static async IEnumerable<object[]> MyQuery<T>(params string[] propertyNames) where T : class
{
    //创建传入的参数
    var parameter =Expression.Parameter(typeof(T));
    // 存储最终要输出的列
    List <Expression> expressions = new List <Expression>();

    foreach (var property in propertyNames) 
    {
        //Expression.Convert() 创建一个类型转换的表达式树
        Expression expression = Expression.Convert
        (
               //要访问类型的属性
               Expression.MakeMemberAccess(parameter,typeof(T).GetProperty(property)),
               //要转换的类型
               typeof(object)
        );
       
        expressions.Add (expression);
    }

    Expression[] initArray= expressions.ToArray();
    // NewArrayInit创建一个表示创建一维数组并使用元素列表初始化该数组
    var newArrayExpression = Expression.NewArrayInit(typeof(object), initArray);

    var selectExpression = Expression.Lambda<Func<T, object[]>>(newArrayExpression,parameter);

   using (TestDbContext ctx = new TestDbContext()
   {
      ctx.Set<T> ().Select(selectExpr). ToArray();
   };
     
}
//调用 

var items=  MyQuery<Book> ("Id","Price");
foreach(var item in items)
{
   Console.WriteLine(item[0]+"  "+item[1]);              
}
          

9.6 实现动态查询

  1. 常规实现动态规划
static List<Book> QueryDynamic(string title,double?lowPrice,double?upPrice,int orderType) 
{
   using(TextContext ctx=new TextContext())
   {
        IQueryable<Book> books = ctx.Books;
       
        if(title!=null)
        {
         	books = books. Where(b=>b.Title==title);
        }
        if(lowPrice!=null)
        {
        	books = books.Where(b =>b.Price>=lowPrice);
        }
        if (upPrice != null)
        {
        	books = books.Where(b => b.Price <= upPrice);
        }
       switch(orderType)
       {
		case 1:
			books = books.OrderByDescending(b=>b.Price) ;break ;
		case 2:
			books = books.OrderBy(b =>b.Price);break;
       }
		return books.ToList();
               
   }

}
  1. System.Linq.Dynamic.Core 实现动态查询
  • nuget 安装:System.Linq.Dynamic.Core
  • 使用字符串格式的语法来进行数据操作**var query = db.Customers.Where("City == @0 and Orders.Count >= @1","London",10).OrderBy("CompanyName").Select("new(CompanyName as Name, Phone)");**

10. C# 新增语法

10.1 顶级语法(C#9)

  1. 直接在C#文件中直接编写入口方法的代码,不用类,不用Main。经典写法仍然支持。反编译发现最后生成的代码中,任然生成了一个类似于Main方法的函数。
  2. 同一个项目中只能有一个文件具有顶级语句。
  3. 顶级语句中可以直接使用await语法,也可以声明函数

10.2 全局Using(C#10)

  1. 将global修饰符添加到using前,这个命名空间就应用到整个项目,不用重复using。
  2. 通常创建一个专门用来编写全局using代码的C#文件
  3. 如果csproj中启用了**<lmplicitUsings>enable</ImplicitUsings>**,编译器会自动隐式增加对于System、System.Linq等常用命名空间的引入,不同各类型项目引入的命名空间也不一样。

10.3 Using 声明(C#8)

  1. 在实现了ldisposable/lAsyncDisposable接口的类型的变量声明前加上using,当代码执行离开变量的作用域时,对象就会被释放。
using SqlConnection conn = new SqlConnection("Data Source=.;InitialCatalog=db1;Integrated Security=True");
conn.Open();
using SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = "select * from T_Articles";
using SqIDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
  string title = reader.GetString(reader.GetOrdinal("Title"));
  Console.WriteLine(title);
}

10.4 命名空间声明(C#10)

namespace XXXX;
public class Test
{
    
}

10.5 可空类型(C#8)

  1. csproj中enable启用可空引用类型检查。
  2. 在引用类型后添加“?”修饰符来声明这个类型是可空的。对于没有添加“?”修饰符的引用类型的变量,如果编译器发现存在为这个变量赋值null的可能性的时候,编译器会给出警告信息。
  3. 如果程序员确认被访问的变量、成员确实不会出现为空的情况,也可以在访可可的艾里、以贝日l大加上!来抑制编译器的警告。
Student s1 = GetData();
Console.WriteLine(s1.Name.ToLower());
Console.WriteLine(s1.PhoneNumber!.ToLower());

10.6 Record 类型(C#9)

  1. C#中的==运算符默认是判断两个变量指向的是否是同一个对象,即使两个对象内容完全一样,也不相等。可以通过重写Equals方法、重写=运算符等来解决这个问题,不过需要开发人员编写非常多的额外代码。
  2. 在C#9.0中增加了记录 (record)类型的语法,编译器会为我们自动生成Equals、GetHashcode等方法。 **public record Person(string FirstName, string LastName);**
Person p1= new Person("Yang", "Zack");
Person p2 = new Person("Yang" ,"Zack");
Person p3 = new Person("Gates","Bill");
Console.WriteLine(p1);
Console.WriteLine(p1==p2);
Console.WriteLine(p1==p3);
Console.WriteLine(p1.FirstName);
  1. 编译器会根据Person类型中的属性定义,自动为Person类型生成包含全部属性的构造方法。注意,默认情况下,编译器会生成一个包含所有属性的构造方法,因此,我们编写new Person()、newPerson(“Yang”)这两种写法都是不可以的。也会生成ToString方法和Equals等方法。
  2. 通过反编译看背后原理。避免反编译器的优化,需罗把反编侔器生成的代码改成C#8.0的语法。结论: record就是晋通的一个类。
  3. record数据类型为我们提供了为所有属性赋值的构造方法,所有属性都是只读的,而且对象可以进行值相等性比较,并且提供了可读性强的ToString()返回值。在需要编写一些不可变类并且需要进行对象值比较的对象时候,record可以帮我们把代码的编写难度大大降低。
  4. 可以实现部分属性是只读的、而部分属性是可以读写。
internal record Student (int Id,string Name)
{
	public string NickName { get; set; }
}

internal record Cat
{
    public int Id { get; set; }
    public string Name { get; set;}
}
  1. 默认生成的构造方法的行为不能修改,我们可以为类型提供多个构造方法,然后其他构造方法通过this调用默认的构造方法。
internal record Student (int Id,string Name)
{
    public string? NickName { get; set; }
    public Student (int Id,string Name,string nickName):this(Id, Name)
    {
    	this.NickName = nickName;
    }
}
  1. 也推荐使用只读属性的类型。这样的所有属性都为只读的类型叫做“不可变类型",可以让程序逻辑简单减少并发访问、状态管理等的麻烦。
  2. 实现深复制
Person p1=new Person(1,"ssr");
var p4=p1 with {}; //创建了一个p1的副本,并不是同一个对象
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值