asp.net core EFCore 属性配置与DbContext
前言
Entity Framework (EF) Core 是轻量化、可扩展、开源和跨平台版的常用 Entity Framework 数据访问技术。用于程序中的class类和数据库中的表互相之间建立映射关系。在学习过程中,EFCore中的属性配置显的尤为重要它是学习好asp.net core的基础是配置数据库表结构的重要基石。本篇内容为学习与整理微软文档(MicrosoftDoc)笔者认为重要部分具体内容参见: Microsoft Build
EF Core 可用作对象关系映射程序 (O/RM),这可以实现以下两点:
- 使 .NET 开发人员能够使用 .NET 对象处理数据库。
- 无需再像通常那样编写大部分数据访问代码。
EF Core 支持多个数据库引擎,可通过名为数据库提供程序的插件库访问许多不同的数据库。
EF Core 提供程序通常跨次要版本工作,但不跨主要版本工作。 例如,针对 EF Core 2.1 发布的提供程序应用于 EF Core 2.2,但不适用于 EF Core 3.0。
使用 Entity Framework Core (EF Core) 时的典型工作单元包括:
1、创建 DbContext 实例
根据上下文跟踪实体实例。 实体将在以下情况下被跟踪
- 正在从查询返回
- 正在添加或附加到上下文
- 根据需要对所跟踪的实体进行更改以实现业务规则
- 调用 SaveChanges 或 SaveChangesAsync。 EF Core 检测所做的更改,并将这些更改写入数据库。
2、 释放 DbContext 实例
3、重要
- 使用后释放 DbContext 非常重要。 这可确保释放所有非托管资源,并注销任何事件或其他挂钩,以防止在实例保持引用时出现内存泄漏。
- DbContext 不是线程安全的。 不要在线程之间共享上下文。 请确保在继续使用上下文实例之前,等待所有异步调用。 EF Core
- 代码引发的 InvalidOperationException 可以使上下文进入不可恢复的状态。此类异常指示程序错误,并且不旨在从其中恢复。
一、 ASP.NET Core 依赖关系注入中的 DbContext
可以使用 Startup.cs 的 ConfigureServices 方法中的 AddDbContext 将 EF Core 添加到此配置。 例如:
C#
。
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));
}
此示例将名为 ApplicationDbContext 的 DbContext 子类注册为 ASP.NET Core 应用程序服务提供程序(也称为依赖关系注入容器)中的作用域服务。 上下文配置为使用 SQL Server 数据库提供程序,并将从 ASP.NET Core 配置读取连接字符串。 在 ConfigureServices 中的何处调用 AddDbContext 通常不重要。
ApplicationDbContext 类必须公开具有 DbContextOptions << ApplicationDbContext> >参数的公共构造函数。 这是将 AddDbContext 的上下文配置传递到 DbContext 的方式。 例如:
C#
。
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
然后,ApplicationDbContext 可以通过构造函数注入在 ASP.NET Core 控制器或其他服务中使用。 例如:
c#
。
public class MyController
{
private readonly ApplicationDbContext _context;
public MyController(ApplicationDbContext context)
{
_context = context;
}
}
最终结果是为每个请求创建一个 ApplicationDbContext 实例,并传递给控制器,以在请求结束后释放前执行工作单元。
二、DbContext 初始化与创建并配置模型
2.1、 DbContext 初始化
可以按照常规的 .NET 方式构造 DbContext 实例,例如,使用 C# 中的 new。 可以通过重写 OnConfiguring 方法或通过将选项传递给构造函数来执行配置。 例如:
- 重写 OnConfiguring 方法
C#
。
public class ApplicationDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
}
}
通过此模式,还可以轻松地通过 DbContext 构造函数传递配置(如连接字符串)。 例如:
- DbContext 构造函数传递配置
C#
。
public class ApplicationDbContext : DbContext
{
private readonly string _connectionString;
public ApplicationDbContext(string connectionString)
{
_connectionString = connectionString;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_connectionString);
}
}
或者,可以使用 DbContextOptionsBuilder 创建 DbContextOptions 对象,然后将该对象传递到 DbContext 构造函数。 这使得为依赖关系注入配置的 DbContext 也能显式构造。 例如,使用上述为 ASP.NET Core 的 Web 应用定义的 ApplicationDbContext 时:
ApplicationDbContext C#
。
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
2.2、创建并配置模型
Entity Framework Core 使用一组约定来根据实体类的形状生成模型。 可指定其他配置以补充和/或替代约定的内容。
模型配置主要分为两种方法:
- FluentAPI
- 数据注解
2.2.1使用 fluent API 配置模型
可在派生上下文中替代 OnModelCreating 方法,并使用 ModelBuilder API 来配置模型。 此配置方法最为有效,并可在不修改实体类的情况下指定配置。 Fluent API 配置具有最高优先级,并将替代约定和数据注释。
- Fluent API 配置
C#
。
using Microsoft.EntityFrameworkCore;
namespace EFModeling.EntityProperties.FluentAPI.Required;
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
#region Required
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
#endregion
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
2.2.2分组配置
如果你有多个表需要配置。为了减小 OnModelCreating 方法的大小,可以将实体类型的所有配置提取到实现 IEntityTypeConfiguration 的单独类中。
- IEntityTypeConfiguration接口
C#
。
public class BlogEntityTypeConfiguration : IEntityTypeConfiguration<Blog>
{
public void Configure(EntityTypeBuilder<Blog> builder)
{
builder
.Property(b => b.Url)
.IsRequired();
}
}
然后,只需从 OnModelCreating 调用 Configure 方法。
- OnModelCreating 调用 Configure 方法
C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
new BlogEntityTypeConfiguration().Configure(modelBuilder.Entity<Blog>());
}
也可以在给定程序集中应用实现 IEntityTypeConfiguration 的类型中指定的所有配置。
- IEntityTypeConfiguration 的类型中指定的所有配置
C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
//modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
modelBuilder.ApplyConfigurationsFromAssembly(typeof(BlogEntityTypeConfiguration).Assembly);
}
应用配置的顺序是不确定的,因此仅当顺序不重要时才应使用此方法。
2.2.3使用数据注释来配置模型
也可将特性(称为数据注释)应用于类和属性。 数据注释会替代约定,但会被 Fluent API 配置替代。
- 数据注释来配置模型
C#
。
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace EFModeling.EntityProperties.DataAnnotations.Annotations;
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
}
[Table("Blogs")]
public class Blog
{
public int BlogId { get; set; }
[Required]
public string Url { get; set; }
}
三、实体类型
在上下文中包含一种类型的 DbSet 意味着它包含在 EF Core 的模型中;我们通常将此类类型称为实体。 EF Core 可以从/向数据库中读取和写入实体实例,如果使用的是关系数据库,EF Core 可以通过迁移为实体创建表。
按照约定,上下文的 DbSet 属性中公开的类型作为实体包含在模型中。 还包括在 OnModelCreating 方法中指定的实体类型,以及通过递归探索其他发现的实体类型的导航属性找到的任何类型。
下面的代码示例中包含了所有类型:
- 包含 Blog,因为它在上下文的 DbSet 属性中公开。
- 包含 Post,因为它是通过 Blog.Posts 导航属性发现的。
- 包含 AuditEntry因为它是 OnModelCreating 中指定的。
Fluent APIC#
。
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AuditEntry>();
}
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public Blog Blog { get; set; }
}
public class AuditEntry
{
public int AuditEntryId { get; set; }
public string Username { get; set; }
public string Action { get; set; }
}
四、实体属性
模型中的每个实体类型都有一组属性,EF Core 将从数据库中读取和写入这些属性。 如果使用的是关系数据库,实体属性将映射到表列。
4.1、列名
按照约定,使用关系数据库时,实体属性将映射到与属性同名的表列。如果希望配置具有不同名称的列,可以按以下代码片段进行操作:
Fluent API C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Id)
.HasColumnName("id");//数据库表字段为英文小写
}
4.2、列数据类型
使用关系数据库时,数据库提供程序会根据属性的 .NET 类型选择数据类型。 它还会考虑其他元数据,例如配置的最大长度、属性是否是主键的一部分等等。
例如,SQL Server 将 DateTime 属性映射到 datetime2(7) 列,将 string 属性映射到 nvarchar(max) 列(或对于用作键的属性,映射到 nvarchar(450))。
还可以配置列以指定列的确切数据类型。 例如,以下代码将 Url 配置为非 unicode 字符串,其最大长度为 200,并将 Rating 配置为十进制,其精度为 5,小数位数为 2:
Fluent API C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>(
eb =>
{
eb.Property(b => b.Url).HasColumnType("varchar(200)");
eb.Property(b => b.Rating).HasColumnType("decimal(5, 2)");
});
}
4.3、最大长度
配置最大长度可向数据库提供程序提供有关为给定属性选择适当列数据类型的提示。 最大长度仅适用于数组数据类型,如 string 和 byte[]。
在下面的示例中,将最大长度配置为 500 将导致在 SQL Server 上创建 nvarchar(500) 类型的列:
下面展示一些 内联代码片
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.HasMaxLength(500);
}
4.3、精度和小数位数
某些关系数据类型支持精度和小数位数 Facet,它们用于控制可以存储哪些值,以及列需要多少存储。 哪些数据类型支持精度和小数位数取决于数据库,但在大多数数据库中,decimal 和 DateTime 类型支持这些 Facet。 对于 decimal 属性,精度用于定义表示列将包含的任何值所需的最大位数,小数位数用于定义所需的最大小数位数。 对于 DateTime 属性,精度用于定义表示秒的小数部分所需的最大位数,不使用小数位数。
在以下示例中,将 Score 属性配置为精度为 14 和小数位数为 2 将导致在 SQL Server 上创建 decimal(14,2) 类型的列,将 LastUpdated 属性配置为精度为 3 将导致创建 datetime2(3) 类型的列:
C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Score)
.HasPrecision(14, 2);
modelBuilder.Entity<Blog>()
.Property(b => b.LastUpdated)
.HasPrecision(3);
}
EF Core 5.0 中引入了用于配置精度和小数位数的 Fluent API。如果不先定义精度,则永远不会定义小数位数,因此用于定义小数位数的 Fluent API 为 HasPrecision(precision, scale)。
4.4、Unicode
在某些关系数据库中,存在不同的类型来表示 Unicode 和非 Unicode 文本数据。 例如,在 SQL Server 中,nvarchar(x)用于表示 UTF-16 中的 Unicode 数据,而varchar(x)用于表示非 Unicode 数据 (,但请参阅有关 UTF-8 支持) SQL Server说明。 对于不支持此概念的数据库,配置此概念将不起作用。
默认情况下,文本属性配置为 Unicode。 可以将列配置为非 Unicode,如下所示:
C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>()
.Property(b => b.Isbn)
.IsUnicode(false);
}
4.4必需和可选属性
如果属性包含 null 是有效的,则该属性被视为可选属性。 如果 null 不是要分配给属性的有效值,则它被视为必需属性。映射到关系数据库架构时,必需属性创建为不可为 null 的列,可选属性创建为可为 null 的列。
按照约定,其 .NET 类型可包含 null 的属性将配置为可选属性,而 .NET 类型不能包含 null 的属性将配置为必需属性。例如,将具有 .NET 值类型的所有属性 (int decimal bool、) 等等配置为必需,并且所有具有可为 null 的 .NET值类型 (int?decimal?等bool?属性都配置为可选。
C# 8 引入了一项名为可为 null 引用类型 (NRT) 的新功能,该功能允许对引用类型进行批注,指示引用类型能否包含 null。此功能默认处于禁用状态,它会通过以下方式影响 EF Core的行为:
- 如果禁用了可为 null 的引用类型(默认设置),则具有 .NET 引用类型的所有属性都按约定配置为可选属性(例如string)。
- 如果启用了可为 null 的引用类型,则基于属性的 .NET 类型的 C# 为 Null 性来配置属性:string? 将配置为可选属性,但 string 将配置为必需属性。
- 以下示例演示一个具有必需和可选属性的实体类型,分别说明了可为 null的引用功能在禁用(默认设置)时和启用时的情况:
禁用了可为 null 的引用类型C#(默认设置)
。
public class CustomerWithoutNullableReferenceTypes
{
public int Id { get; set; }
[Required] // 根据需要配置所需的数据注释
public string FirstName { get; set; }
[Required]
public string LastName { get; set; } // 根据需要配置所需的数据注释
public string MiddleName { get; set; } // 可选的按照惯例
}
启用了可为 null 的引用类型 C#可为 null 引用类型 (NRT)
。
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; } // Required by convention
public string LastName { get; set; } // Required by convention
public string? MiddleName { get; set; } // Optional by convention
//注意以下构造函数绑定的使用,它避免了对未初始化的非空属性的编译警告。
public Customer(string firstName, string lastName, string? middleName = null)
{
FirstName = firstName;
LastName = lastName;
MiddleName = middleName;
}
}
五、OnModelCreating配置
5.1、显式配置
按约定为可选属性的属性可以配置为必需属性,如下所示:
C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
5.2、默认值
在关系数据库中,可以为列配置默认值;如果插入的行没有该列的值,则将使用默认值。
可以在属性上配置默认值: C"
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Rating)
.HasDefaultValue(3);
}
还可以指定用于计算默认值的 SQL 片段: C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Created)
.HasDefaultValueSql("getdate()");
}
5.3、计算列
在大多数关系数据库中,可以将列配置为在数据库中计算其值,并且通常使用引用其他列的表达式:
C#
。
modelBuilder.Entity<Person>()
.Property(p => p.DisplayName)
.HasComputedColumnSql("[LastName] + ', ' + [FirstName]");
以上命令将创建一个虚拟计算列,每次从数据库中提取时都会计算其值。 你也可以将计算列指定为存储(有时称为持久化)计算列,这意味着系统会在每次更新行时计算该列,并将其与常规列一起存储在磁盘上:
- EF Core 5.0 中添加了对创建存储计算列的支持。
C#
。
modelBuilder.Entity<Person>()
.Property(p => p.NameLength)
.HasComputedColumnSql("LEN([LastName]) + LEN([FirstName])", stored: true);
5.4、主键
- 按照约定,如果应用程序未提供值,则将类型为 short、int、long 或 Guid 的非复合主键设置为针对插入的实体生成值。
数据库提供程序通常负责必要的配置;例如,SQL Server 中的数字主键会自动设置为 IDENTITY 列。 - 根据约定,名为 Id 或 Id 的属性将被配置为实体的主键。
可将单个属性配置为实体的主键,如下所示:
下面展示一些C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasKey(c => c.LicensePlate);
}
还可将多个属性配置为实体的键 - 这称为组合键。 组合键只能使用 Fluent API 进行配置;约定永远不会设置组合键,并且不能使用数据注释来配置组合键。
下面展示一些 内联代码片
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>()
.HasKey(c => new { c.State, c.LicensePlate });
}
5.5、主键名称
根据约定,在关系数据库上,主键使用名称 PK_ 进行创建。 可按如下方式配置主键约束的名称:
主键名称 "HasName"C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasKey(b => b.BlogId)
.HasName("PrimaryKey_BlogId");
}
-
虽然 EF Core 支持使用任何基元类型的属性作为主键(包括 string、Guid、byte[]
等),但并非所有数据库都支持所有类型作为键。 在某些情况下,键值可以自动转换为支持的类型,否则应手动指定转换。 -
向上下文添加新实体时,键属性必须始终具有非默认值,但某些类型将由数据库生成。 在这种情况下,当添加实体以用于跟踪时,EF将尝试生成一个临时值。 调用 SaveChanges 后,临时值将替换为数据库生成的值。
-
如果键属性的值由数据库生成,并且在添加实体时指定了非默认值,则 EF 将假定该实体已存在于数据库中,并尝试更新它,而不是插入新的实体。
若要避免这种情况,请禁用值生成。
六、值生成
6.1、 显式配置值生成
EF Core 会自动为主键设置值生成 - 但我们可能希望对非键属性执行相同的操作。 你可以将任何属性配置为针对插入的实体生成其值,如下所示:
- 使用ValueGeneratedOnAdd
C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Inserted)
.ValueGeneratedOnAdd();
}
同样,可以将属性配置为在添加或更新时生成其值:
- 使用ValueGeneratedOnAddOrUpdate
C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.LastUpdated)
.ValueGeneratedOnAddOrUpdate();
}
警告
- 与默认值或计算列不同,我们没有指定值的生成方式;这取决于所使用的数据库提供程序。
数据库提供程序可能会自动为某些属性类型设置值生成,但其他属性类型可能需要你手动设置值的生成方式。 - 例如,在 SQL Server 上,如果 GUID 属性配置为在添加时生成值,提供程序会自动在客户端执行值生成,并使用算法生成最佳顺序GUID 值。 但是,在 ValueGeneratedOnAdd DateTime 属性上指定 将不起作用 。
- 同样,配置为在添加或更新时生成值并标记为并发标记的 byte[] 属性将设置为 rowversion 数据类型,以便在数据库中自动生成值。但是,指定 ValueGeneratedOnAdd 不起作用。
- 根据所使用的数据库提供程序,值可能由 EF 在客户端生成或在数据库中生成。 如果值是由数据库生成的,那么 EF可能会在你将实体添加到上下文时分配一个临时值;在 SaveChanges() 期间,此临时值将替换为数据库生成的值。
6.2、日期/时间值生成
一个常见的请求是让数据库列包含第一次插入列的日期/时间(添加时生成的值)或上次更新列的日期/时间(添加或更新时生成的值)。 由于可通过各种策略执行此操作,因此 EF Core 提供程序通常不会为日期/时间列自动设置值生成 - 你必须自行配置。
6.2.1、创建时间戳
若要将日期/时间列配置为包含行的创建时间戳,通常需要使用适当的 SQL 函数来配置默认值。 例如,在 SQL Server 上,你可以使用以下命令:
创建时间戳 C#
。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Created)
.HasDefaultValueSql("getdate()");
}
请确保选择适当的函数,因为可能存在多个函数(例如 GETDATE() 与 GETUTCDATE())。
6.2.1、更新时间戳
尽管存储计算列看起来非常适合管理上次更新时间戳,但数据库通常不允许在计算列中指定诸如 GETDATE() 之类的函数。 作为替代方法,你可以设置一个数据库触发器来达到同样的效果:
更新时间戳 C#
。
CREATE TRIGGER [dbo].[Blogs_UPDATE] ON [dbo].[Blogs]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF ((SELECT TRIGGER_NESTLEVEL()) > 1) RETURN;
DECLARE @Id INT
SELECT @Id = INSERTED.BlogId
FROM INSERTED
UPDATE dbo.Blogs
SET LastUpdated = GETDATE()
WHERE BlogId = @Id
END
6.3、替代值生成
尽管为属性配置了值生成,但在许多情况下,你仍然可以为其显式指定一个值。 此操作能否真正起作用取决于已配置的特定值生成机制;虽然你可以指定显式值而不是使用列的默认值,但不能对计算列执行相同的操作。
若要使用显式值替代值生成,只需将属性设置为该属性类型的 CLR 默认值(string 为 null,int 为 0,Guid 为 Guid.Empty,等等)以外的任意值。
6.3.1、将显式值插入 IDENTITY 列中
默认情况下,尝试将显式值插入 SQL Server IDENTITY 会失败,因为SQL Server 不允许将显式值插入 IDENTITY 列中。 为此,必须在调用 SaveChanges() 之前手动启用 IDENTITY_INSERT,如下所示:
启用 IDENTITY_INSERT C#
。
using (var context = new ExplicitIdentityValuesContext())
{
context.Blogs.Add(new Blog { BlogId = 100, Url = "http://blog1.somesite.com" });
context.Blogs.Add(new Blog { BlogId = 101, Url = "http://blog2.somesite.com" });
context.Database.OpenConnection();
try
{
//启用 IDENTITY_INSERT 开
context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT dbo.Blogs ON");
context.SaveChanges();
//启用 IDENTITY_INSERT 关
context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT dbo.Blogs OFF");
}
finally
{
context.Database.CloseConnection();
}
}