Following the previous article, this one will bring you ten more new features in EF Core 6, including value converters, scaffolding, and enhancements to DbContext.
1 HasConversion supports value converters
In EF Core 6.0, the generic overload of the HasConversion method allows specifying 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 Simplified configuration for many-to-many relationships
Starting with EF Core 6.0, you can configure a join entity in a many-to-many relationship without any additional configuration. Also, you can configure a join entity without explicitly specifying the left and right relationships.
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 improvements for many-to-many relationships
EF Core 6.0 improves scaffolding from an existing database. It can detect join tables and generate many-to-many mappings for them.
Example database:

Via CLI:
dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=EFCore6Many2Many" Microsoft.EntityFrameworkCore.SqlServer --context ExampleContext --output-dir Models
From the generated DbContext's 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 Scaffolding generates nullable reference types
EF Core 6.0 improves scaffolding from an existing database. When nullable reference types (NRT) are enabled in the project, EF Core automatically constructs DbContext and entity types with NRT.
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])
)
Generates the model:
public partial class Post
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? Desciption { get; set; }
}
5 Scaffolding generates database comments
EF Core 6.0 associates database comments with code comments.
Database example:
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 registers DbContext
In EF Core 5.0, you could register a factory to manually create DbContext instances. Starting with EF Core 6.0, you can use AddDbContextFactory to register DbContext. This allows you to inject both the factory and the DbContext as needed.
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 is now public. The pool is created with DbContextOptions, which will be used to create DbContext 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, a new enum CommandSource has been added to the CommandEventData type, provided to diagnostic sources and interceptors. This enum value indicates which part of EF created the command.
Using CommandSource in a 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. This is useful when you have an enum with an unknown value and it represents a nullable string column 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
}
But be aware, there are pitfalls. See details at the link: https://docs.microsoft.com/en-us/ef/core/what-is-new/ef-core-6.0/whatsnew#allow-value-converters-to-convert-nulls
10 Explicitly setting temporary values
In EF Core 6.0, you can explicitly set temporary values on entities before they are tracked. When a value is marked as temporary, EF will not reset it as it did 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");
}
11 Conclusion
All code examples from this article can be found in my GitHub: https://github.com/okyrylchuk/dotnet6_features/tree/main/EF%20Core%206#miscellaneous-enhancements
Original: https://blog.okyrylchuk.dev/entity-framework-core-6-features-part-2
Author: Oleg Kyrylchuk
Translation: Fine coder