Summary of new functions of EF Core 6 (2)

Summary of new functions of EF Core 6 (2)

Following the previous one, this one will bring you ten more new features in EF Core 6, including improvements to value converters, scaffoldings, and DbContext.

最后更新 6/2/2022 9:51 PM
liamwang 精致码农
预计阅读 10 分钟
分类
EF Core
标签
.NET C# EF Core ORM

上一篇 之后,这一篇将给大家带来另外十个 EF Core 6 中的新功能特性,包括值转换器、脚手架和 DbContext 的改进等。

1 HasConversion supports value converters

在 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 Simplify the configuration of many-to-many relationships

Starting with EF Core 6.0, you can configure a connection entity in a many-to-many relationship without any additional configuration. In addition, you can configure a connection entity without explicitly specifying the left-right relationship.

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 Improvement of the many-to-many relationship of scaffolding

EF Core 6.0 improves the scaffolding of existing databases. It detects connection tables and generates many-to-many mappings for them.

The following example database:

Through CLI:

dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=EFCore6Many2Many" Microsoft.EntityFrameworkCore.SqlServer --context ExampleContext --output-dir Models

来自生成的 DbContextOnModelCreating

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 Scaffold generates nullable reference types

EF Core 6.0 improves the scaffolding of existing databases. When nullable reference types (NRT) are enabled in a project, EF Core automatically uses NRT to build DbContext and entity types.

As shown in the example table:

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [Description] nvarchar(max) NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id])
)

Models will be generated:

public partial class Post
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Desciption { get; set; }
}

5 Scaffold Generation Database Comments

EF Core 6.0 associates database comments with code comments.

Database examples:

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];

Generated model:

/// <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 Register 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 pool without dependency injection

在 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 Enumeration

在 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-value converter allows conversion of null values

In EF Core 6.0, the value converter allows the conversion of null values. This is useful when you have an enumeration of unknown values and it represents a nullable string of characters in the table.

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 Clearly set temporary values

In EF Core 6.0, you can explicitly set temporary values for entities before they are tracked. When a value is marked as a temporary value, EF will not reset it as before.

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");
}

End of 11

本文所有代码示例都可以在我的 GitHub 中找到:https://github.com/okyrylchuk/dotnet6_features/tree/main/EF%20Core%206#miscellaneous-enhancements

Original: blog.okyrylchuk.dev/entity-framework-core-6-features-part-2

Author: Oleg Kyrylchuk

Translation: Exquisite Code Farmer

Keep Exploring

延伸阅读

更多文章