本文最后更新于:2024年11月2日 下午
.NET Core 核心基础组件 依赖注入 Microsoft.Extension.DependencyInjection
什么是依赖注入 我们把负责提供对象的注册和获取功能的框架叫做“容器”,注册到容器中的对象叫做“服务”(service
)。 依赖注入框架时根据服务的类型来获取服务的,因此在获取服务的时候必须指定获取什么类型的服务。依赖注入框架中注册服务的时候可以分别指定服务类型和实现类型,两者可能相同,也可能不同。比如:services.AddScoped<IUserDAO, UserDAO>();
,IUserDAO
就是服务类型,UserDAO
就是实现类型,所以推荐面向接口编程,这样代码就依赖于服务接口,而不是实现类。 现在有一个User
类,运行在一个框架中,创建这个User
类的对象之后,框架会自动为其中的Conn
属性赋值一个合适的对象,这种框架自动创建(Conn
)对象的动作就叫做“注入”(injection
)。 个人觉得需要结合例子才好明白。 详细代码示例请参考书本 P58~P60。
什么是IoC(控制反转) 控制反转的目的就是把“创建和组装对象”操作的控制权从业务逻辑的代码中转移到框架中,这样业务代码中只要说明“我需要某个类型的对象”,框架就会帮我们创建这个对象。
依赖注入框架中的生命周期
瞬态(transient
):每次被请求的时候都会创建一个新对象。
范围(scoped
):在给定的范围内,多次请求共享同一个服务对象,服务每次被请求的时候都会返回同一个对象;在不同的范围内,服务每次被请求的时候会返回不同的对象。
单例(singleton
):全局共享同一个服务对象。
不同的服务之间可能有依赖关系,比如 A 服务中有一个 B 服务的属性,那么被依赖的 B 的生命周期不能比 A 的生命周期短,否则会出现 B 失效了,A 还可用的问题。
如何在这三种生命周期中进行选择?请参考书本 P55。
配置系統 Microsoft.Extension.Configuration
手动读取配置 Microsoft.Extension.Configuration.Json
我们需要把config.json
文件设置为生成项目的时候自动被复制到生成目录:右击config.json
,选择【属性】,将【复制到输出目录】修改为“如果较新则复制”
代码参考书本 P61
使用选项方式读取配置 Microsoft.Extension.Options
& Microsoft.Extension.Configuration.Binder
可以从文件、命令行或是环境变量中读取配置文件 需要创建一个类用于获取注入的选项值。但是声明接受选项注入的对象不能直接使用该类,而是需要使用IOptions<T>
、IOptionsMonitor<T>
、IOptionsSnapShot<T>
等泛型接口类型,因为它们可以帮我们处理容器生命周期、配置刷新等。
IOptions<T>
:在配置改变后,无法读到新的值,必须重启程序才可以读到新的值
IOptionsMonitor<T>
:在配置改变后,可以读到新的值
IOptionsSnapShot<T>
:在配置改变后,可以读到新的值,且在同一个范围内会保持一致性
日志 .Net Core 提供的日志 Microsoft.Extension.Logging
第三方文件日志提供程序 建议使用NLog
集中式日志 Exceptionless
EF Core Getting Start 通常的开发环节都是先创建好数据库,再根据Schema
来创建实体类,这种开发模式叫做“数据驱动开发”;但是使用EF Core
进行开发的话,可以使用“模型驱动开发”的开发模式,即:先创建实体类,通过EF Core
来帮助我们创建数据库表(这种操作叫“迁移(Migration)”)。
安装相关包 Microsoft.EntityFrameworkCore.SqlServer
,Microsoft.EntityFrameworkCore.Tools
创建实体类 1 2 3 4 5 6 7 8 public class Book { public long Id { get ; set ; } public string Title { get ; set ; } public DateTime PubTime { get ; set ; } public double Price { get ; set ; } public string AuthorName { get ; set ; } }
创建实体类的配置类 用于配置实体类和数据库表的对应关系
1 2 3 4 5 6 7 8 9 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(); } }
通过接口类型IEntityTypeConfiguration
的泛型参数类指定这个类要对哪个实体类进行配置,然后在Configure
方法中对实体类和数据库表的关系做详细的配置。若没有配置各个属性在数据库中的列名和数据类型,EF Core 将会默认把属性的名字作为列名,并且根据属性的类型来推断数据库表中各列的数据类型。
创建上下文(Context)类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class TestDbContext : DbContext { public DbSet<Book> Books { get ; set ; } protected override void OnConfiguring (DbContextOptionsBuilder optionsBuilder ) { string connStr = "Server=.;Database=demo1;Trusted_Connection=True" ; optionsBuilder.UseSqlServer(connStr); } protected override void OnModelCreating (ModelBuilder modelBuilder ) { base .OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(this .GetType().Assembly); } }
OnConfiguration
方法用于对程序要连接的数据库进行配置。
进行迁移 在【程序包管理器控制台】中执行命令:Add-Mirgration InitialCreate
,InitialCreate
是本次迁移的名字。 然后执行:Update-database
命令编译并且执行数据库迁移代码。
若遇到错误:Microsoft.Data.SqlClient.SqlException (0x80131904): A connection was successfully established with the server, but then an error occurred during the login process.
,可以尝试在数据库连接字串中加入TrustServerCertificate=true
或是encrypy=false
如果命令执行失败,提示Build failed.
,不妨看看输出窗口中的信息。 如果创建出来的迁移代码中发现并没有生成对应代码,不妨检查一下是否缺失了配置,如:DbContext
中是否加入了对应类的DbSet
属性。
EF Core 基本操作 要对数据进行操作,其实就是对Context
中的各个DbSet
属性进行操作。
插入数据 1 2 3 4 using TestDbContext ctx = new TestDbContext();var b1 = new Book { AuthorName = "XYZ" , Title = "Test" , Price = 59.8 , PubTime = DateTime.Now }; ctx.Books.Add(b1);await ctx.SaveChangesAsync();
由于DbContext
实现了IDisposable
接口,因此需要使用using
语句确保资源的释放。
使用 LINQ 查询数据 DbSet
实现了IEnumerable<T>
接口,所以可以直接使用LINQ
来查询数据
1 2 3 4 5 IEnumerable<T> books2 = ctx.Books.Where(b => b.Price > 80 );foreach (Book b in books2) { ... }
修改和删除数据 要修改数据,直接对DbSet
中的实体类对象进行修改,最后用SaveChangesAsync()
方法即可同步修改至数据库。 要删除数据,同样也是对DbSet
中的实体类对象进行删除,将要删除的对象作为参数传入Remove()
方法即可,例如:ctx.Remove(b)
/ctx.Books.Remove(b)
。
EF Core 的实体类配置 EF Core
采用了“约定大于配置”的设计原则,下面是几条主要规则:
数据库表名采用上下文类中对应的DbSet
的属性名
数据库表列的名字采用实体类属性的名字,列的数据类型采用和实体类属性类型兼容的类型。
数据库表列的可空性取决于对应实体类属性的可空性。
名字为Id
的属性为主键,如果主键为short
、int
或long
类型,则主键默认采用自动增长类型的列。
在使用Guid
类型的主键时,一定不要把主键设置为聚集索引,否则在数据量大的时候将会导致非常糟糕的数据插入性能!
Data Annotation Data Annotation(数据注释)指的是使用 .NET 提供的Attribute
来对实体类、属性等进行标注的方式来实现实体类配置,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [Table("T_Authors" ) ]public class Author { [Key ] public long Id { get ; set ; } [MaxLength(20) ] [Required ] public string Name { get ; set ; } [MaxLength(50) ] public string Country { get ; set ; } [MaxLength(50) ] public string Email { get ; set ; } public ICollection<BookAuthor> BookAuthors { get ; set ; } }
Fluent API 编写专门对应的、实现了IEntityTypeConfiguration
接口的Config
类来配置实体类,就是Fluent API
的一种体现方式,比如上面的BookEntityConfig
类。其另一种体现方式是,在Context
类中使用ModelBuilder
类的对象来对实体类进行配置,例如:
1 2 3 4 5 protected override void OnModelCreating (ModelBuilder modelBuilder ) { modelBuilder.Entity<Book>() .Ignore(b => b.PubTime) }
DataAnnotation 和 Fluent API 是可以一起使用的,但后者的优先级比前者高
仔细看就会发现,其实在继承了DbContext
的上下文类中使用 Fluent API ,和在实现了IEntityTypeConfiguration
接口的配置类中进行配置后再在继承了DbContext
的上下文类中使用modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
是等效的,不过后者会直接加载所有配置类。 下面是一些 Fluent API 的基本配置:
表与实体类映射: modelBuilder.Entity<Book>().ToTable("T_Books");
视图与实体类映射: modelBuilder.Entity<Book>().ToView("BookView");
排除属性映射: modelBuilder.Entity<Book>().Ignore(b => b.PubTime);
数据库表列名: modelBuilder.Entity<Book>().Property(b => b.PubTime).HasColumnName("CreateTime");
列数据类型: modelBuilder.Entity<Book>().Property(b => b.PubTime).HasColumnType("Varchar(200)");
主键: modelBuilder.Entity<BookAuthors>().HasKey(ba => new { ba.BookId, ba.AuthorId });
索引: modelBuilder.Entity<Person>().HasIndex(p => new { p.FirstName, p.LastName }).HasDatabaseName("IX_PERSON_1");
主键的选择
普通自增:直接使用自增long
类型作为主键
Guid
算法:直接使用Guid
类型作为主键
自增 + Guid
算法:使用自增long
类型作为物理主键,Guid
类型作为逻辑主键。在数据库表结构的设计上,把long
类型自增列设置为主键,但是在和其他表关联及对外通信上,都使用Guid
列。
Hi/Lo 算法:EF Core 支持使用 Hi/Lo 算法来优化自增列的性能。
数据库迁移 控制台命令 除常用的Add-migration
和Update-datebase
外,还有一些(可能是常用的)有用命令:
Remove-migration
: 删除最后一次的迁移脚本
Script-Migration
: 根据迁移代码来生成 SQL 脚本
Scaffold-DbContext
: 根据数据库表来生成实体类
查看 EF Core 生成的 SQL 语句 将日志输出到控制台 在DbContext
类的OnConfiguring
方法中加入语句:optionsBuilder.LogTo(Console.WriteLine);
即可。 如果只想输出 SQL 语句,使用下面的代码:
1 2 3 4 optionsBuilder .LogTo(Console.WriteLine, new [] { DbLoggerCategory.Database.Command.Name },Microsoft.Extensions.Logging.LogLevel.Information) .EnableSensitiveDataLogging() .EnableDetailedErrors();
关系配置 一对一、一对多、多对多 EF Core 中实体类之间关系的配置采用如下的模式:HasXXX(...).WithYYY(...);
,代表“A 实体类对象有 XXX 个 B 实体类对象,B 实体类对象有 YYY 个 A 实体类对象”。XXX 和 YYY 有One
和Many
两个可选值。 在配置实体类之间关系映射的时候,不仅需要在Config
类中编写HasXXX(...).WithYYY(...);
,还需要在Entity
类中设置其对应的其他实体类的对象(或对象集合)。
一对多 Entity
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Article { public long Id { get ; set ; } public string Title { get ; set ; } = string .Empty; public string Content { get ; set ; } = string .Empty; public List<Comment> Comments { get ; set ; } = new List<Comment>(); }public class Comment { public long Id { get ; set ; } public string Message { get ; set ; } = string .Empty; public Article Article { get ; set ; } = null !; }
Config
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 internal class ArticleConfig : IEntityTypeConfiguration <Article > { public void Configure (EntityTypeBuilder<Article> builder ) { builder.ToTable("T_Articles" ); builder.HasKey(a => a.Id); builder.Property(a => a.Title).IsRequired().IsUnicode().HasMaxLength(50 ); builder.Property(a => a.Content).IsRequired().IsUnicode().HasMaxLength(2000 ); } }internal class CommentConfig : IEntityTypeConfiguration <Comment > { public void Configure (EntityTypeBuilder<Comment> builder ) { builder.ToTable("T_Comments" ); builder.HasKey(c => c.Id); builder.Property(c => c.Message).IsRequired().IsUnicode().HasMaxLength(200 ); builder.HasOne(c => c.Article).WithMany(a => a.Comments).IsRequired(); } }
插入数据:
1 2 3 4 5 6 7 8 9 Article a1 = new Article(); a1.Title = "EF Core" ; a1.Content = "EF Core 是一个轻量级、可扩展、跨平台的对象关系映射 (ORM) 框架,支持 .NET Core。" ; Comment c1 = new Comment { Message = "EF Core 是一个轻量级的 ORM 框架。" }; Comment c2 = new Comment { Message = "EF Core 支持 .NET Core。" }; a1.Comments.Add(c1); a1.Comments.Add(c2); ctx.Articles.Add(a1);await ctx.SaveChangesAsync();
一对一 Entity
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class Order { public long Id { get ; set ; } public string Name { get ; set ; } = string .Empty; public string Address { get ; set ; } = string .Empty; public Delivery? Delivery { get ; set ; } }public class Delivery { public long Id { get ; set ; } public string CompanyName { get ; set ; } = string .Empty; public string Number { get ; set ; } = string .Empty; public Order Order { get ; set ; } = null !; public long OrderId { get ; set ; } }
Config
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 internal class OrderConfig : IEntityTypeConfiguration <Order > { public void Configure (EntityTypeBuilder<Order> builder ) { builder.ToTable("T_Orders" ); builder.HasKey(o => o.Id); builder.Property(o => o.Name).IsRequired().IsUnicode().HasMaxLength(50 ); builder.Property(o => o.Address).IsRequired().IsUnicode().HasMaxLength(200 ); builder.HasOne(o => o.Delivery).WithOne(d => d.Order).HasForeignKey<Delivery>(d => d.OrderId); } }internal class DeliveryConfig : IEntityTypeConfiguration <Delivery > { public void Configure (EntityTypeBuilder<Delivery> builder ) { builder.ToTable("T_Deliveries" ); builder.HasKey(d => d.Id); builder.Property(d => d.CompanyName).IsRequired().IsUnicode().HasMaxLength(50 ); builder.Property(d => d.Number).IsRequired().IsUnicode().HasMaxLength(50 ); } }
插入数据:
1 2 3 4 5 Order order = new Order { Name = "订单1" , Address = "地址1" }; Delivery delivery = new Delivery { CompanyName = "顺丰" , Number = "SF123456" }; order.Delivery = delivery; ctx.Orders.Add(order);await ctx.SaveChangesAsync();
多对多 Entity
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Student { public long Id { get ; set ; } public string Name { get ; set ; } = string .Empty; public List<Teacher> Teachers { get ; set ; } = new List<Teacher>(); }public class Teacher { public long Id { get ; set ; } public string Name { get ; set ; } = string .Empty; public List<Student> Students { get ; set ; } = new List<Student>(); }
Config
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 internal class TeacherConfig : IEntityTypeConfiguration <Teacher > { public void Configure (EntityTypeBuilder<Teacher> builder ) { builder.ToTable("T_Teachers" ); builder.HasKey(t => t.Id); builder.Property(t => t.Name).IsRequired().IsUnicode().HasMaxLength(50 ); builder.HasMany(t => t.Students).WithMany(s => s.Teachers); } }internal class StudentConfig : IEntityTypeConfiguration <Student > { public void Configure (EntityTypeBuilder<Student> builder ) { builder.ToTable("T_Students" ); builder.HasKey(s => s.Id); builder.Property(s => s.Name).IsRequired().IsUnicode().HasMaxLength(50 ); } }
插入数据:
1 2 3 4 5 6 7 8 9 10 11 Student s1 = new Student { Name = "张三" }; Student s2 = new Student { Name = "李四" }; Student s3 = new Student { Name = "王五" }; Teacher t1 = new Teacher { Name = "王老师" }; Teacher t2 = new Teacher { Name = "李老师" }; t1.Students.Add(s1); t1.Students.Add(s2); t2.Students.Add(s2); t2.Students.Add(s3); ctx.AddRange(t1, t2);await ctx.SaveChangesAsync();
关联数据的获取 如果需要进行关联查询(join),可以使用Include
方法,比如:查询 Id 为 1 的 Teacher 所拥有的 Student,就可以像下面这么写:
1 2 3 4 5 ctx.Teachers.Include(t => t.Students).Where(t => t.Id == 1 ).ToList().ForEach(t => { Console.WriteLine($"{t.Name} has students: " ); t.Students.ForEach(s => Console.WriteLine(s.Name)); });
单向导航属性 在上面列举的三个例子,它们的实体类中都存在其映射的另一个实体类的对象或对象集合属性,能够很方便地获取另一个实体类的信息,这种称为双向导航。 而单项导航其实就是:在存在映射关系的两个实体类中,只在其中一个类里配置另一个实体类的属性;并且,对应的Config
类中不向WithMany()
方法传参。下面是一个例子Entity
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class User { public long Id { get ; set ; } public string Name { get ; set ; } = null !; }public class Leave { public long Id { get ; set ; } public User Requester { get ; set ; } = null !; public User? Approver { get ; set ; } public string Remarks { get ; set ; } = string .Empty; public DateTime From { get ; set ; } public DateTime To { get ; set ; } public int Status { get ; set ; } }
Config
类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 internal class UserConfig : IEntityTypeConfiguration <User > { public void Configure (Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder<User> builder ) { builder.ToTable("T_Users" ); builder.HasKey(u => u.Id); builder.Property(u => u.Name).IsUnicode().IsRequired().HasMaxLength(50 ); } }internal class LeaveConfig : IEntityTypeConfiguration <Leave > { public void Configure (Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder<Leave> builder ) { builder.ToTable("T_Leaves" ); builder.HasOne<User>(l => l.Requester).WithMany().IsRequired(); builder.HasOne<User>(l => l.Approver).WithMany().IsRequired(false ); builder.Property(l => l.Remarks).IsUnicode().IsRequired().HasMaxLength(200 ); } }
数据的插入及查询:
1 2 3 4 5 6 7 8 9 10 User u1 = new User() { Name = "Maple" }; Leave l1 = new Leave() { Requester = u1, Remarks = "Sick" , From = DateTime.Now, To = DateTime.Now.AddDays(1 ), Status = 0 }; ctx.Leaves.Add(l1);await ctx.SaveChangesAsync(); User u = await ctx.Users.SingleAsync(u => u.Name == "Maple" );foreach (var l in ctx.Leaves.Where(l => l.Requester == u)) { Console.WriteLine(l.Remarks); }
希望本文章能够帮到您~