Entity Framework Core 6 features - Part 2

Entity Framework Core 6 features - Part 2

Next ten new features in EF Core 6. In this article, you can find improvements for value converters, scaffolding, and DbContext.

Β·

8 min read

It's a continuation of blog series about EF Core 6 features. Link to Part 1.

1. HasConversion Supports Value Converters

In EF Core, the generic overloads of the HasConversion methods used the generic parameter to specify the type to convert to. In EF Core 6.0, the generic type can also specify built-in or custom value converters.

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. Many-to-many Without Additional Configuration

Starting with EF Core 6.0, you can configure a join entity in a many-to-many relationship with any additional configuration. Also, you can configure a join entity without needing to specify the left and right relationships explicitly.

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. Scaffolding Many-to-many Improvements

EF Core 6.0 improves scaffolding from an existing database. It detects join tables and generates many-to-many mappings for them. For join table, the configuration will be scaffolded, without a class.

Having example database: diagram.png

Scaffold using Package Manager Console:

Scaffold-DbContext "Server=(localdb)\mssqllocaldb;Database=EFCore6Many2Many;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -Context ExampleContext -OutputDir Models

Scaffold using CLI:

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

OnModelCreating from generated DbContext:

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. Scaffolding Nullable Reference Types

EF Core 6.0 improves scaffolding from an existing database. When nullable reference types (NRTs) are enabled in the project, EF Core automatically scaffolds DbContext and entity types with NRT.

Having 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])
)

The generated model:

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

5. Scaffolding Database Comments

EF Core 6.0 scaffolds database comments to code comments.

Having example database:

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

The 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 Registers DbContext

In EF Core 5.0, you could register a factory for creating DbContext instances manually. Starting with EF Core 6.0, AddDbContextFactory also registers DbContext. So you can inject both the factory and DbContext depending on your needs.

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 Pooling Without Dependency Injection

In EF Core 6.0, you can use DbContext pooling without dependency injection. The PooledDbContextFactory type has been made public. The pool is created with an instance of DbContextOptions that will be used to create context instances.

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 Enum

In EF Core 6.0, the new enum CommandSource has been added to the CommandEventData type, supplied to diagnostics sources and interceptors. The enum value indicates which part of EF creates the command.

Using CommandSource in the Db command interceptor:

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 Converters Allow Converting Nulls

In EF Core 6.0, value converters allow converting nulls. It's useful when you have an enum with an unknown value, and it is represented as a nullable string column in a 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
}

But watch out, there are pitfalls with it. Details by the link.

10. Set Temporary Values Explicitly

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

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

Wrapping Up

All code samples you can find on my GitHub.

Did you find this article valuable?

Support Oleg Kyrylchuk by becoming a sponsor. Any amount is appreciated!

Β