What's New in EF Core 5

What's New in EF Core 5

EF Core 5.0 is currently in development, and here is the list of all the interesting changes introduced so far in each preview.

Preview 1

Simple logging

The simple logging feature adds functionality similar to Database.Log in EF6 by providing a simple way to get logs from EF Core without the need to configure any kind of external logging framework.

Simple way to get generated SQL

EF Core 5.0 introduces the ToQueryString extension method, which will return the SQL that EF Core will generate when executing a LINQ query.

Use a C# attribute to indicate that an entity has no key

An entity type can now be configured as having no key using the new KeylessAttribute.
1
[Keyless]
2
public class Address
3
{
4
public string Street { get; set; }
5
public string City { get; set; }
6
public int Zip { get; set; }
7
}
Copied!

Connection or connection string can be changed on initialized DbContext

    It is now easier to create a DbContext instance without any connection or connection string.
    The connection or connection string can now be mutated on the context instance.
    This feature allows the same context instance to dynamically connect to different databases.

Change-tracking proxies

    EF Core can now generate runtime proxies that automatically implement INotifyPropertyChanging and INotifyPropertyChanged.
    These then report value changes on entity properties directly to EF Core, avoiding the need to scan for changes.
    However, proxies come with their own set of limitations, so they are not for everyone.

Enhanced debug views

    Debug views are an easy way to look at the internals of EF Core when debugging issues. A debug view for the Model was implemented some time ago. For EF Core 5.0, we have made the model view easier to read and added a new debug view for tracked entities in the state manager.

Improved handling of database null semantics

    Relational databases typically treat NULL as an unknown value and therefore not equal to any other NULL.
    While C# treats null as a defined value, which compares equal to any other null.
    EF Core by default translates queries so that they use C# null semantics. EF Core 5.0 greatly improves the efficiency of these translations.

Indexer properties

EF Core 5.0 supports the mapping of C# indexer properties. These properties allow entities to act as property bags where columns are mapped to named properties in the bag.

Generation of check constraints for enum mappings

EF Core 5.0 migrations can now generate CHECK constraints for enum property mappings.
1
MyEnumColumn VARCHAR(10) NOT NULL CHECK (MyEnumColumn IN ('Useful', 'Useless', 'Unknown'))
Copied!

IsRelational

A new IsRelational method has been added in addition to the existing IsSqlServer, IsSqlite and IsInMemory. This method can be used to test if the DbContext is using any relational database provider.
1
protected override void OnModelCreating(ModelBuilder modelBuilder)
2
{
3
if (Database.IsRelational())
4
{
5
// Do relational-specific model configuration.
6
}
7
}
Copied!

Cosmos optimistic concurrency with ETags

The Azure Cosmos DB database provider now supports optimistic concurrency using ETags. Use the model builder in OnModelCreating to configure an ETag.
1
builder.Entity<Customer>().Property(c => c.ETag).IsEtagConcurrency();
Copied!
The SaveChanges will then throw a DbUpdateConcurrencyException on a concurrency conflict, which can be handled to implement retries, etc.

Query translations for more DateTime constructs

Queries containing new DateTime construction is now translated.
Also, the following SQL Server functions are now mapped:
    DateDiffWeek
    DateFromParts
For example:
1
var count = context.Orders.Count(c => date > EF.Functions.DateFromParts(DateTime.Now.Year, 12, 25));
Copied!

Query translations for more byte array constructs

Queries using Contains, Length, SequenceEqual, etc. on byte[] properties are now translated to SQL.

Query translation for Reverse

Queries using Reverse are now translated.
1
context.Employees.OrderBy(e => e.EmployeeID).Reverse()
Copied!

Query translation for bitwise operators

Queries using bit-wise operators are now translated in more cases.
1
context.Orders.Where(o => ~o.OrderID == negatedId)
Copied!

Query translation for strings on Cosmos

Queries that use the string methods such as, Contains, StartsWith, and EndsWith are now translated when using the Azure Cosmos DB provider.

Preview 2

Use a C# attribute to specify a property backing field

A C# attribute can now be used to specify the backing field for a property. This attribute allows EF Core to still write to and read from the backing field as would normally happen, even when the backing field cannot be found automatically.
1
public class Blog
2
{
3
private string _mainTitle;
4
​
5
public int Id { get; set; }
6
​
7
[BackingField(nameof(_mainTitle))]
8
public string Title
9
{
10
get => _mainTitle;
11
set => _mainTitle = value;
12
}
13
}
Copied!

Complete discriminator mapping

EF Core uses a discriminator column for TPH mapping of an inheritance hierarchy.
    Some performance enhancements are possible as long as EF Core knows all possible values for the discriminator.
    EF Core 5.0 now implements these enhancements.
For example, previous versions of EF Core would always generate this SQL for a query returning all types in a hierarchy.
1
SELECT [a].[Id], [a].[Discriminator], [a].[Name]
2
FROM [Animal] AS [a]
3
WHERE [a].[Discriminator] IN (N'Animal', N'Cat', N'Dog', N'Human')
Copied!
EF Core 5.0 will now generate the following when a complete discriminator mapping is configured
1
SELECT [a].[Id], [a].[Discriminator], [a].[Name]
2
FROM [Animal] AS [a]
Copied!
It will be the default behavior starting with preview 3.

Performance improvements in Microsoft.Data.Sqlite

The following two performances improvements are made for SQLite:
    Retrieving binary and string data with GetBytes, GetChars, and GetTextReader is now more efficient by making use of SqliteBlob and streams.
    The initialization of SqliteConnection is now lazy.
These improvements are in the ADO.NET Microsoft.Data.Sqlite provider and hence also improve performance outside of EF Core.

Preview 3

Filtered Include

The Include method now supports filtering of the entities included.
1
var blogs = context.Blogs
2
.Include(e => e.Posts.Where(p => p.Title.Contains("Cheese")))
3
.ToList();
Copied!
This query will return blogs together with each associated post, but only when the post title contains "Cheese".
Skip and Take can also be used to reduce the number of included entities.
1
var blogs = context.Blogs
2
.Include(e => e.Posts.OrderByDescending(post => post.Title).Take(5)))
3
.ToList();
Copied!
This query will return blogs with at most five posts included on each blog.

New ModelBuilder API for navigation properties

Navigation properties are primarily configured when defining relationships. However, the new Navigation method can be used in cases where navigation properties need an additional configuration. For example, to set a backing field for the navigation when the field would not be found by convention.
1
modelBuilder.Entity<Blog>().Navigation(e => e.Posts).HasField("_myposts");
Copied!
Note that the Navigation API does not replace relationship configuration. Instead, it allows additional configuration of navigation properties in already discovered or defined relationships.

New command-line parameters for namespaces and connection strings

Migrations and scaffolding now allow namespaces to be specified on the command line. For example, to reverse engineer a database putting the context and model classes in different namespaces.
1
dotnet ef dbcontext scaffold "connection string" Microsoft.EntityFrameworkCore.SqlServer --context-namespace "My.Context" --namespace "My.Model"
Copied!
Also, a connection string can now be passed to the database-update command.
1
dotnet ef database update --connection "connection string"
Copied!
Equivalent parameters have also been added to the PowerShell commands used in the VS Package Manager Console.

EnableDetailedErrors has returned

For performance reasons, EF doesn't do additional null-checks when reading values from the database. This can result in exceptions that are hard to root-cause when an unexpected null is encountered.
Using EnableDetailedErrors will add extra null checking to queries such that, for a small performance overhead, these errors are easier to trace back to a root cause.
1
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
2
=> optionsBuilder
3
.EnableDetailedErrors()
4
.EnableSensitiveDataLogging() // Often also useful with EnableDetailedErrors
5
.UseSqlServer(Your.SqlServerConnectionString);
Copied!

Cosmos partition keys

The partition key to use for a given query can now be specified in the query.
1
await context.Set<Customer>()
2
.WithPartitionKey(myPartitionKey)
3
.FirstAsync();
Copied!

Support for the SQL Server DATALENGTH function

This can be accessed using the new EF.Functions.DataLength method.
1
var count = context.Orders.Count(c => 100 < EF.Functions.DataLength(c.OrderDate));
Copied!

Preview 4

Configure database precision/scale in model

Precision and scale for a property can now be specified using the model builder.
1
modelBuilder
2
.Entity<Blog>()
3
.Property(b => b.Numeric)
4
.HasPrecision(16, 4);
Copied!
Precision and scale can still be set via the full database type, such as "decimal(16,4)".

Specify SQL Server index fill factor

The fill factor can now be specified when creating an index on SQL Server. For example.
1
modelBuilder
2
.Entity<Customer>()
3
.HasIndex(e => e.Name)
4
.HasFillFactor(90);
Copied!

Preview 5

Database collations

The default collation for a database can now be specified in the EF model. It will flow through to generated migrations to set the collation when the database is created.
1
modelBuilder.UseCollation("German_PhoneBook_CI_AS");
Copied!
When you create migrations then it generates the following to create the database on SQL Server.
1
CREATE DATABASE [Test]
2
COLLATE German_PhoneBook_CI_AS;
Copied!
You can also specify the collation to use for specific database columns.
1
modelBuilder
2
.Entity<User>()
3
.Property(e => e.Name)
4
.UseCollation("German_PhoneBook_CI_AS");
Copied!
For those not using migrations, collations are now reverse-engineered from the database when scaffolding a DbContext.
Finally, the EF.Functions.Collate() allows for ad-hoc queries using different collations.
1
context.Users.Single(e => EF.Functions.Collate(e.Name, "French_CI_AS") == "Jean-Michel Jarre");
Copied!
This will generate the following query for SQL Server.
1
SELECT TOP(2) [u].[Id], [u].[Name]
2
FROM [Users] AS [u]
3
WHERE [u].[Name] COLLATE French_CI_AS = N'Jean-Michel Jarre'
Copied!
The ad-hoc collations should be used with care as they can negatively impact database performance.

Flow arguments into IDesignTimeDbContextFactory

Arguments now flow from the command line into the CreateDbContext method of IDesignTimeDbContextFactory. For example, to indicate this is a dev build, a custom argument (e.g. dev) can be passed on the command line.
1
dotnet ef migrations add two --verbose --dev
Copied!
This argument will then flow into the factory, where it can be used to control how the context is created and initialized.
1
public class MyDbContextFactory : IDesignTimeDbContextFactory<SomeDbContext>
2
{
3
public SomeDbContext CreateDbContext(string[] args)
4
=> new SomeDbContext(args.Contains("--dev"));
5
}
Copied!

No-tracking queries with identity resolution

No-tracking queries can now be configured to perform identity resolution. For example, the following query will create a new Blog instance for each Post, even if each Blog has the same primary key.
1
context.Posts.AsNoTracking().Include(e => e.Blog).ToList();
Copied!
However, at the expense of usually being slightly slower and always using more memory, this query can be changed to ensure only a single Blog instance is created.
1
context.Posts.AsNoTracking().PerformIdentityResolution().Include(e => e.Blog).ToList();
Copied!
It is only useful for no-tracking queries since all tracking queries already exhibit this behavior. Also, following the API review, the PerformIdentityResolution the syntax will be changed.

Stored Computed columns

Most databases allow computed column values to be stored after computation.
    The computed column is calculated only once on the update, instead of each time its value is retrieved it takes up disk space.
    This also allows the column to be indexed for some databases.
EF Core 5.0 allows computed columns to be configured as stored.
1
modelBuilder
2
.Entity<User>()
3
.Property(e => e.SomethingComputed)
4
.HasComputedColumnSql("my sql", stored: true);
Copied!
SQLite computed columns
EF Core now supports computed columns in SQLite databases.

Preview 6

Starting with EF Core 3.0, EF Core always generates a single SQL query for each LINQ query.
    It ensures consistency of the data returned within the constraints of the transaction mode in use.
    However, it can become very slow when the query uses Include or a projection to bring back multiple related collections.
EF Core 5.0 now allows a single LINQ query including related collections to be split into multiple SQL queries.
    It can significantly improve performance but can result in inconsistency in the results returned if the data changes between the two queries.
    The serializable or snapshot transactions can be used to mitigate this and achieve consistency with split queries, but that may bring other performance costs and behavioral differences.
Split queries with Include
For example, consider a query that pulls in two levels of related collections using Includemethod.
1
var artists = context.Artists
2
.Include(e => e.Albums).ThenInclude(e => e.Tags)
3
.ToList();
Copied!
By default, EF Core will generate the following SQL when using the SQLite provider.
1
SELECT "a"."Id", "a"."Name", "t0"."Id", "t0"."ArtistId", "t0"."Title", "t0"."Id0", "t0"."AlbumId", "t0"."Name"
2
FROM "Artists" AS "a"
3
LEFT JOIN (
4
SELECT "a0"."Id", "a0"."ArtistId", "a0"."Title", "t"."Id" AS "Id0", "t"."AlbumId", "t"."Name"
5
FROM "Album" AS "a0"
6
LEFT JOIN "Tag" AS "t" ON "a0"."Id" = "t"."AlbumId"
7
) AS "t0" ON "a"."Id" = "t0"."ArtistId"
8
ORDER BY "a"."Id", "t0"."Id", "t0"."Id0"
Copied!
The new AsSplitQuery API can be used to change this behavior.
1
var artists = context.Artists
2
.AsSplitQuery()
3
.Include(e => e.Albums).ThenInclude(e => e.Tags)
4
.ToList();
Copied!
The AsSplitQuery is available for all relational database providers and can be used anywhere in the query, just like AsNoTracking. EF Core will now generate the following three SQL queries.
1
SELECT "a"."Id", "a"."Name"
2
FROM "Artists" AS "a"
3
ORDER BY "a"."Id"
4
​
5
SELECT "a0"."Id", "a0"."ArtistId", "a0"."Title", "a"."Id"
6
FROM "Artists" AS "a"
7
INNER JOIN "Album" AS "a0" ON "a"."Id" = "a0"."ArtistId"
8
ORDER BY "a"."Id", "a0"."Id"
9
​
10
SELECT "t"."Id", "t"."AlbumId", "t"."Name", "a"."Id", "a0"."Id"
11
FROM "Artists" AS "a"
12
INNER JOIN "Album" AS "a0" ON "a"."Id" = "a0"."ArtistId"
13
INNER JOIN "Tag" AS "t" ON "a0"."Id" = "t"."AlbumId"
14
ORDER BY "a"."Id", "a0"."Id"
Copied!
All operations on the query root are supported including OrderBy, Skip, Take, Join, FirstOrDefault and similar single result selecting operations.
The filtered Includes with OrderBy, Skip, Take are not supported in preview 6, but are available in the daily builds and will be included in preview 7.
Split queries with collection projections
The AsSplitQuery method can also be used when collections are loaded in projections.
1
context.Artists
2
.AsSplitQuery()
3
.Select(e => new
4
{
5
Artist = e,
6
Albums = e.Albums,
7
}).ToList();
Copied!
The above LINQ query generates the following two SQL queries when using the SQLite provider
1
SELECT "a"."Id", "a"."Name"
2
FROM "Artists" AS "a"
3
ORDER BY "a"."Id"
4
​
5
SELECT "a0"."Id", "a0"."ArtistId", "a0"."Title", "a"."Id"
6
FROM "Artists" AS "a"
7
INNER JOIN "Album" AS "a0" ON "a"."Id" = "a0"."ArtistId"
8
ORDER BY "a"."Id"
Copied!
Only materialization of the collection is supported. Any composition after e.Albums in the above case won't result in a split query.

IndexAttribute

The new IndexAttribute can be placed on an entity type to specify an index for a single column.
1
[Index(nameof(FullName), IsUnique = true)]
2
public class User
3
{
4
public int Id { get; set; }
5
​
6
[MaxLength(128)]
7
public string FullName { get; set; }
8
}
Copied!
For SQL Server, Migrations will then generate the following SQL.
1
CREATE UNIQUE INDEX [IX_Users_FullName]
2
ON [Users] ([FullName])
3
WHERE [FullName] IS NOT NULL;
Copied!
IndexAttribute can also be used to specify an index spanning multiple columns.
1
[Index(nameof(FirstName), nameof(LastName), IsUnique = true)]
2
public class User
3
{
4
public int Id { get; set; }
5
​
6
[MaxLength(64)]
7
public string FirstName { get; set; }
8
​
9
[MaxLength(64)]
10
public string LastName { get; set; }
11
}
Copied!
For SQL Server, the result is as shown below.
1
CREATE UNIQUE INDEX [IX_Users_FirstName_LastName]
2
ON [Users] ([FirstName], [LastName])
3
WHERE [FirstName] IS NOT NULL AND [LastName] IS NOT NULL;
Copied!

Improved query translation exceptions

We are continuing to improve the exception messages generated when query translation fails. For example, this query uses the IsSignedunmapped property.
1
var artists = context.Artists.Where(e => e.IsSigned).ToList();
Copied!
EF Core will throw the following exception indicating that translation failed because IsSigned is not mapped.
1
Unhandled exception. System.InvalidOperationException: The LINQ expression 'DbSet<Artist>()
2
.Where(a => a.IsSigned)' could not be translated. Additional information: Translation of member 'IsSigned' on entity type 'Artist' failed. Possibly the specified member is not mapped. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See <https://go.microsoft.com/fwlink/?linkid=2101038> for more information.
Copied!
Similarly, better exception messages are now generated when attempting to translate string comparisons with culture-dependent semantics. For example, the following query attempts to use StringComparison.CurrentCulture.
1
var artists = context.Artists
2
.Where(e => e.Name.Equals("The Unicorns", StringComparison.CurrentCulture))
3
.ToList();
Copied!
EF Core will now throw the following exception.
1
Unhandled exception. System.InvalidOperationException: The LINQ expression 'DbSet<Artist>()
2
.Where(a => a.Name.Equals(
3
value: "The Unicorns",
4
comparisonType: CurrentCulture))' could not be translated. Additional information: Translation of 'string.Equals' method which takes 'StringComparison' argument is not supported. See <https://go.microsoft.com/fwlink/?linkid=2129535> for more information. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See <https://go.microsoft.com/fwlink/?linkid=2101038> for more information.
Copied!

Specify transaction ID

EF Core exposes a transaction ID for the correlation of transactions across calls.
    This ID is typically set by EF Core when a transaction is started.
    If the application starts the transaction instead, then this feature allows the application to explicitly set the transaction ID so it is correlated correctly everywhere it is used.
1
using (context.Database.UseTransaction(myTransaction, myId))
2
{
3
...
4
}
Copied!

IPAddress mapping

The standard .NET IPAddress class is now automatically mapped to a string column for databases that do not already have native support. For example, consider mapping this entity type.
1
public class Host
2
{
3
public int Id { get; set; }
4
public IPAddress Address { get; set; }
5
}
Copied!
On SQL Server, the migration will create the following table.
1
CREATE TABLE [Host] (
2
[Id] int NOT NULL,
3
[Address] nvarchar(45) NULL,
4
CONSTRAINT [PK_Host] PRIMARY KEY ([Id]));
Copied!
Entities can then be added in the normal way.
1
context.AddRange(
2
new Host { Address = IPAddress.Parse("127.0.0.1")},
3
new Host { Address = IPAddress.Parse("0000:0000:0000:0000:0000:0000:0000:0001")});
Copied!
And the resulting SQL will insert the normalized IPv4 or IPv6 address.
1
Executed DbCommand (14ms) [Parameters=[@p0='1', @p1='127.0.0.1' (Size = 45), @p2='2', @p3='::1' (Size = 45)], CommandType='Text', CommandTimeout='30']
2
SET NOCOUNT ON;
3
INSERT INTO [Host] ([Id], [Address])
4
VALUES (@p0, @p1), (@p2, @p3);
Copied!

Exclude OnConfiguring when scaffolding

When a DbContext is scaffolded from an existing database, EF Core by default creates an OnConfiguring overload with a connection string so that the context is immediately usable. However, this is not useful if you already have a partial class with OnConfiguring, or if you are configuring the context some other way.
To address this, the scaffolding commands can now be instructed to omit the generation of OnConfiguring.
1
dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook" Microsoft.EntityFrameworkCore.SqlServer --no-onconfiguring
Copied!
Or in the Package Manager Console.
1
Scaffold-DbContext 'Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook' Microsoft.EntityFrameworkCore.SqlServer -NoOnConfiguring
Copied!

Translations for FirstOrDefault on strings

The FirstOrDefault and similar operators for characters in strings are now translated in a LINQ query.
1
context.Customers.Where(c => c.ContactName.FirstOrDefault() == 'A').ToList();
Copied!
It will be translated to the following SQL when using SQL Server.
1
SELECT [c].[Id], [c].[ContactName]
2
FROM [Customer] AS [c]
3
WHERE SUBSTRING([c].[ContactName], 1, 1) = N'A'
Copied!

Simplify case blocks

EF Core now generates better queries with CASE blocks. Let's consider the following LINQ query.
1
context.Weapons
2
.OrderBy(w => w.Name.CompareTo("Marcus' Lancer") == 0)
3
.ThenBy(w => w.Id)
Copied!
Previously, the above LINQ would be translated to the following query on SQL Server.
1
SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId]
2
FROM [Weapons] AS [w]
3
ORDER BY CASE
4
WHEN (CASE
5
WHEN [w].[Name] = N'Marcus'' Lancer' THEN 0
6
WHEN [w].[Name] > N'Marcus'' Lancer' THEN 1
7
WHEN [w].[Name] < N'Marcus'' Lancer' THEN -1
8
END = 0) AND CASE
9
WHEN [w].[Name] = N'Marcus'' Lancer' THEN 0
10
WHEN [w].[Name] > N'Marcus'' Lancer' THEN 1
11
WHEN [w].[Name] < N'Marcus'' Lancer' THEN -1
12
END IS NOT NULL THEN CAST(1 AS bit)
13
ELSE CAST(0 AS bit)
14
END, [w].[Id]");