繼 上一篇 之後,這一篇將給大家帶來另外十個 EF Core 6 中的新功能特性,包括值轉換器、脚手架和 DbContext 的改進等。
1 HasConversion 支援值轉換器
在 EF Core 6.0 中,HasConversion 方法的泛型重載方法可以指定內建或自訂的值轉換器。
public class ExampleContext : DbContext
{
public DbSet<Person> People { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Person>()
.Property(p => p.Address)
.HasConversion<AddressConverter>();
}
}
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string Country { get; set; }
public string Street { get; set; }
public string ZipCode { get; set; }
}
public class AddressConverter : ValueConverter<Address, string>
{
public AddressConverter()
: base(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Address>(v, (JsonSerializerOptions)null))
{
}
}
2 簡化多對多關係的設定
從 EF Core 6.0 開始,你可以在多對多的關係中設定一個連接實體,而無需任何額外的設定。另外,你可以設定一個連接實體,而不需要明確指定左右關係。
public class BloggingContext : DbContext
{
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<PostTag> PostTags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(t => t.Posts)
.UsingEntity<PostTag>();
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=EFCore6Many2Many;Trusted_Connection=True;");
}
public class Post
{
public int Id { get; set; }
public string Name { get; set; }
public List<Tag> Tags { get; set; } = new List<Tag>();
}
public class Tag
{
public int Id { get; set; }
public string Text { get; set; }
public List<Post> Posts { get; set; } = new List<Post>();
}
public class PostTag
{
public int PostId { get; set; }
public int TagId { get; set; }
public DateTime AddedDate { get; set; }
}
3 脚手架多對多關係的改進
EF Core 6.0 改進了現有資料庫的脚手架。它可以偵測到連接表並為其產生多對多的對應。
如下範例資料庫:

透過 CLI:
dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=EFCore6Many2Many" Microsoft.EntityFrameworkCore.SqlServer --context ExampleContext --output-dir Models
來自產生的 DbContext 的 OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>(entity =>
{
entity.HasMany(d => d.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, object>>(
"PostTag",
l => l.HasOne<Tag>().WithMany().HasForeignKey("TagId"),
r => r.HasOne<Post>().WithMany().HasForeignKey("PostId"),
j =>
{
j.HasKey("PostId", "TagId")
j.ToTable("PostTags")
j.HasIndex(new[] { "TagId" }, "IX_PostTags_TagId");
});
});
OnModelCreatingPartial(modelBuilder);
}
4 脚手架產生可為 Null 參考型別
EF Core 6.0 改進了現有資料庫的脚手架。當專案中啟用了可為 Null 參考型別(NRT),EF Core 會自動用 NRT 建構 DbContext 和實體型別。
如實例表:
CREATE TABLE [Posts] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[Description] nvarchar(max) NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([Id])
)
會產生模型:
public partial class Post
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? Desciption { get; set; }
}
5 脚手架產生資料庫註解
EF Core 6.0 將資料庫註解與程式碼註解關聯起來。
資料庫例子:
CREATE TABLE [Posts] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[Description] nvarchar(max) NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]));
EXEC sp_addextendedproperty
@name = N'MS_Description', @value = 'The post table',
@level0type = N'Schema', @level0name = dbo,
@level1type = N'Table', @level1name = Posts
EXEC sp_addextendedproperty
@name = N'MS_Description', @value = 'The post identifier',
@level0type = N'Schema', @level0name = dbo,
@level1type = N'Table', @level1name = Posts,
@level2type = N'Column', @level2name = [Id];
EXEC sp_addextendedproperty
@name = N'MS_Description', @value = 'The post name',
@level0type = N'Schema', @level0name = dbo,
@level1type = N'Table', @level1name = Posts,
@level2type = N'Column', @level2name = [Name];
EXEC sp_addextendedproperty
@name = N'MS_Description', @value = 'The description name',
@level0type = N'Schema', @level0name = dbo,
@level1type = N'Table', @level1name = Posts,
@level2type = N'Column', @level2name = [Description];
產生的模型:
/// <summary>
/// The post table
/// </summary>
public partial class Post
{
/// <summary>
/// The post identifier
/// </summary>
public int Id { get; set; }
/// <summary>
/// The post name
/// </summary>
public string Name { get; set; }
/// <summary>
/// The description name
/// </summary>
public string Description { get; set; }
}
6 AddDbContextFactory 註冊 DbContext
在 EF Core 5.0 中,你可以註冊一個工廠來手動建立 DbContext 實例。從 EF Core 6.0 開始,可用 AddDbContextFactory 註冊 DbContext。所以你可以根據你的需要同時注入工廠和 DbContext。
var serviceProvider = new ServiceCollection()
.AddDbContextFactory<ExampleContext>(builder =>
builder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database = EFCore6Playground"))
.BuildServiceProvider();
var factory = serviceProvider.GetService<IDbContextFactory<ExampleContext>>();
using (var context = factory.CreateDbContext())
{
// Contexts obtained from the factory must be explicitly disposed
}
using (var scope = serviceProvider.CreateScope())
{
var context = scope.ServiceProvider.GetService<ExampleContext>();
// Context is disposed when the scope is disposed
}
class ExampleContext : DbContext
{ }
7 無依賴性注入的 DbContext 池
在 EF Core 6.0 中,你可以使用沒有依賴注入的 DbContext 池。PooledDbContextFactory 型別已經定義為 public 了。池是用 DbContextOptions 建立的,它將被用來建立 DbContext 實例。
var options = new DbContextOptionsBuilder<ExampleContext>()
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6Playground")
.Options;
var factory = new PooledDbContextFactory<ExampleContext>(options);
using var context1 = factory.CreateDbContext();
Console.WriteLine($"Created DbContext with ID {context1.ContextId}");
// Output: Created DbContext with ID e49db9b7-a0b0-4b54-8d0d-2cbd6c4cece7:1
using var context2 = factory.CreateDbContext();
Console.WriteLine($"Created DbContext with ID {context2.ContextId}");
// Output: Created DbContext with ID b5a35bcb-270d-40f1-b668-5f76da1f35ad:1
class ExampleContext : DbContext
{
public ExampleContext(DbContextOptions<ExampleContext> options)
: base(options)
{
}
}
8 CommandSource 列舉
在 EF Core 6.0 中,新的列舉 CommandSource 已經被加入到 CommandEventData 型別中,提供給診斷來源和攔截器。這個列舉值表明了 EF 的哪個部分建立了這個命令。
在 Db 命令攔截器中使用 CommandSource:
class ExampleInterceptor : DbCommandInterceptor
{
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command,
CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
if (eventData.CommandSource == CommandSource.SaveChanges)
{
Console.WriteLine($"Saving changes for {eventData.Context.GetType().Name}:");
Console.WriteLine();
Console.WriteLine(command.CommandText);
}
if (eventData.CommandSource == CommandSource.FromSqlQuery)
{
Console.WriteLine($"From Sql query for {eventData.Context.GetType().Name}:");
Console.WriteLine();
Console.WriteLine(command.CommandText);
}
return result;
}
}
DbContext:
class ExampleContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6CommandSource")
.AddInterceptors(new ExampleInterceptor());
}
class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
Program:
using var context = new ExampleContext();
context.Products.Add(new Product { Name = "Laptop", Price = 1000 });
context.SaveChanges();
var product = context.Products
.FromSqlRaw("SELECT * FROM dbo.Products")
.ToList();
/* Output:
Saving changes for ExampleContext:
SET NOCOUNT ON;
INSERT INTO[Products] ([Name], [Price])
VALUES(@p0, @p1);
SELECT[Id]
FROM[Products]
WHERE @@ROWCOUNT = 1 AND[Id] = scope_identity();
From Sql query for ExampleContext:
SELECT* FROM dbo.Products
*/
9 值轉換器允許轉換 Null 值
在 EF Core 6.0 中,值轉換器允許轉換 Null 值。當你有一個未知值的列舉,並且它在表中表示一個可為 Null 的字串資料行時,這很有用。
public class ExampleContext : DbContext
{
public DbSet<Dog> Dogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Dog>()
.Property(c => c.Breed)
.HasConversion<BreedConverter>();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.EnableSensitiveDataLogging()
.LogTo(Console.WriteLine)
.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=EFCore6ValueConverterAllowsNulls;");
}
}
public enum Breed
{
Unknown,
Beagle,
Bulldog
}
public class Dog
{
public int Id { get; set; }
public string Name { get; set; }
public Breed? Breed { get; set; }
}
public class BreedConverter : ValueConverter<Breed, string>
{
#pragma warning disable EF1001
public BreedConverter()
: base(
v => v == Breed.Unknown ? null : v.ToString(),
v => v == null ? Breed.Unknown : Enum.Parse<Breed>(v),
convertsNulls: true)
{
}
#pragma warning restore EF1001
}
但要注意,這裡面有陷阱。詳情請見連結:https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-6.0/whatsnew#allow-value-converters-to-convert-nulls
10 明確設定暫存值
在 EF Core 6.0 中,你可以在實體被追蹤之前明確地給它們設定暫存值。當值被標記為暫存值時,EF 將不會像以前那樣重設它。
using var context = new ExampleContext();
Blog blog = new Blog { Id = -5 };
context.Add(blog).Property(p => p.Id).IsTemporary = true;
var post1 = new Post { Id = -1 };
var post1IdEntry = context.Add(post1).Property(e => e.Id).IsTemporary = true;
post1.BlogId = blog.Id;
var post2 = new Post();
var post2IdEntry = context.Add(post2).Property(e => e.Id).IsTemporary = true;
post2.BlogId = blog.Id;
Console.WriteLine($"Blog explicitly set temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 explicitly set temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 generated temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");
// Output:
// Blog explicitly set temporary ID = -5
// Post 1 explicitly set temporary ID = -1 and FK to Blog = -5
// Post 2 generated temporary ID = -2147482647 and FK to Blog = -5
class Blog
{
public int Id { get; set; }
}
class Post
{
public int Id { get; set; }
public int BlogId { get; set; }
}
class ExampleContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCore6TempValues");
}
11 結尾
本文所有程式碼範例都可以在我的 GitHub 中找到:https://github.com/okyrylchuk/dotnet6_features/tree/main/EF%20Core%206#miscellaneous-enhancements
原文:https://blog.okyrylchuk.dev/entity-framework-core-6-features-part-2
作者:Oleg Kyrylchuk
翻譯:精緻碼農