EF Core 8 Release Candidate 2: Smaller features in EF8

Arthur Vickers

The second release candidate of Entity Framework Core (EF Core) 8 is available on NuGet today!

Basic information

EF Core 8, or just EF8, is the successor to EF Core 7, and is scheduled for release in November 2023, at the same time as .NET 8.

EF8 requires .NET 8 and this RC 2 release should be used with the .NET 8 RC 2 SDK.

EF8 will align with .NET 8 as a long-term support (LTS) release. See the .NET support policy for more information.

New in EF8

In this post we’re going to take at a few of the smaller features included in EF8. Be sure to check out EF8 content from previous posts:

Sentinel values and database defaults

Databases allow columns to be configured to generate a default value if no value is provided when inserting a row. This can be represented in EF using HasDefaultValue for constants:

b.Property(e => e.Status).HasDefaultValue("Hidden");

Or HasDefaultValueSql for arbitrary SQL clauses:

b.Property(e => e.LeaseDate).HasDefaultValueSql("getutcdate()");

In order for EF to make use of this, it must determine when and when not to send a value for the column. By default, EF uses the CLR default as a sentinel for this. That is, when the value of Status or LeaseDate in the examples above are the CLR defaults for these types, then EF interprets that to mean that the property has not been set, and so does not send a value to the database. This works well for reference types–for example, if the string property Status is null, then EF doesn’t send null to the database, but rather does not include any value so that the database default ("Hidden") is used. Likewise, for the DateTime property LeaseDate, EF will not insert the CLR default value of 1/1/0001 12:00:00 AM, but will instead omit this value so that database default is used.

However, in some cases the CLR default value is a valid value to insert. EF8 handles this by allowing the sentinel value for a colum to change. For example, consider an integer column configured with a database default:

b.Property(e => e.Credits).HasDefaultValueSql(10);

In this case, we want the new entity to be inserted with the given number of credits, unless this is not specified, in which case 10 credits are assigned. However, this means that inserting a record with zero credits is not possible, since zero is the CLR default, and hence will cause EF to send no value. In EF8, this can be fixed by changing the sentinel for the property from zero (the CLR default) to -1:

b.Property(e => e.Credits).HasDefaultValueSql(10).HasSentinel(-1);

EF will now only use the database default if Credits is set to -1; a value of zero will be inserted like any other amount.

Check out the .NET Data Community Standup for a more in-dept discussion on this subject including examples showing how sentinels can help prevent bugs with enum and bool values.

Tip: using a nullable backing field

Another way to handle the same problem is to use a nullable backing field for the property. For example, instead of defining the Credits property as:

public class User
{
    public int Credits { get; set; }
}

It can be defined as:

public class User
{
    private int? _credits;

    public int Credits
    {
        get => _credits ?? 0;
        set => _credits = value;
    }
}

The backing field here will remain null unless the property setter is actually called. That is, the value of the backing field is a better indication of whether the property has been set or not than the CLR default of the property. This works out-of-the box with EF, since EF will use the backing field to read and write the property by default.

Better ExecuteUpdate and ExecuteDelete

SQL commands that perform updates and deletes, such as those generated by ExecuteUpdate and ExecuteDelete methods, must target a single database table. However, in EF7, ExecuteUpdate and ExecuteDelete did not support updates accessing multiple entity types even when the query ultimately affected a single table. EF8 removes this limitation. For example, consider a Customer entity type with CustomerInfo owned type:

[Table("Customers")]
public class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public required CustomerInfo CustomerInfo { get; set; }
}

[Owned]
public class CustomerInfo
{
    public string? Tag { get; set; }
}

Both of these entity types map to the Customers table. However, the following bulk update fails on EF7 because it uses both entity types:

await context.Customers
    .Where(e => e.Name == name)
    .ExecuteUpdateAsync(
        s => s.SetProperty(b => b.CustomerInfo.Tag, "Tagged")
            .SetProperty(b => b.Name, b => b.Name + "_Tagged"));

In EF8, this now translates to the following SQL when using Azure SQL:

UPDATE [c]
SET [c].[Name] = [c].[Name] + N'_Tagged',
    [c].[CustomerInfo_Tag] = N'Tagged'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0

Check out the .NET Data Community Standup for other examples using Union queries and TPT inheritance mapping.

Better use of IN queries

When the Contains operator is used with a subquery, EF Core now generates better queries using SQL IN instead of EXISTS; aside from producing more readable SQL, in some cases this can result in dramatically faster queries. For example, consider the following LINQ query:

var blogsWithPosts = await context.Blogs
    .Where(b => context.Posts.Select(p => p.BlogId).Contains(b.Id))
    .ToListAsync();

EF7 generates the following for PostgreSQL:

SELECT b."Id", b."Name"
      FROM "Blogs" AS b
      WHERE EXISTS (
          SELECT 1
          FROM "Posts" AS p
          WHERE p."BlogId" = b."Id")

Since the subquery references the external Blogs table (via b."Id"), this is a correlated subquery, meaning that the Posts subquery must be executed for each row in the Blogs table. In EF8, the following SQL is generated instead:

SELECT b."Id", b."Name"
      FROM "Blogs" AS b
      WHERE b."Id" IN (
          SELECT p."BlogId"
          FROM "Posts" AS p
      )

Since the subquery no longer references Blogs, it can be evaluated once, yielding massive performance improvements on most database systems. However, some database systems, most notably SQL Server, the database is able to optimize the first query to the second query so that the performance is the same.

Check out the .NET Data Community Standup for discussion and additional examples of IN translations.

Numeric rowversions for SQL Azure/SQL Server

SQL Server automatic optimistic concurrency is handled using rowversion columns. A rowversion is an 8-byte opaque value passed between database, client, and server. By default, SqlClient exposes rowversion types as byte[], despite mutable reference types being a bad match for rowversion semantics. In EF8, it is easy instead map rowversion columns to long or ulong properties. For example:

modelBuilder.Entity<Blog>()
    .Property(e => e.RowVersion)
    .HasConversion<byte[]>()
    .IsRowVersion();

Parentheses elimination

Generating readable SQL is an important goal for EF Core. In EF8, the generated SQL is more readable through automatic elimination of unneeded parenthesis. For example, the following LINQ query:

await ctx.Customers  
    .Where(c => c.Id * 3 + 2 > 0 && c.FirstName != null || c.LastName != null)  
    .ToListAsync();  

Translates to the following Azure SQL when using EF7:

SELECT [c].[Id], [c].[City], [c].[FirstName], [c].[LastName], [c].[Street]
FROM [Customers] AS [c]
WHERE ((([c].[Id] * 3) + 2) > 0 AND ([c].[FirstName] IS NOT NULL)) OR ([c].[LastName] IS NOT NULL)

Which has been improved to the following when using EF8:

SELECT [c].[Id], [c].[City], [c].[FirstName], [c].[LastName], [c].[Street]
FROM [Customers] AS [c]
WHERE ([c].[Id] * 3 + 2 > 0 AND [c].[FirstName] IS NOT NULL) OR [c].[LastName] IS NOT NULL

Everything in EF8

Overall, EF8 RC 2 contains all the major feature features we intend to ship in EF8, although further tweaks and bug fixes are coming for GA. These features include:

How to get EF8 RC 2

EF8 is distributed exclusively as a set of NuGet packages. For example, to add the SQL Server provider to your project, you can use the following command using the dotnet tool:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 8.0.0-rc.2.23480.1

Installing the EF8 Command Line Interface (CLI)

The dotnet-ef tool must be installed before executing EF8 Core migration or scaffolding commands.

To install the tool globally, use:

dotnet tool install --global dotnet-ef --version 8.0.0-rc.2.23480.1

If you already have the tool installed, you can upgrade it with the following command:

dotnet tool update --global dotnet-ef --version 8.0.0-rc.2.23480.1

The .NET Data Community Standup

The .NET data access team is now live streaming every other Wednesday at 10am Pacific Time, 1pm Eastern Time, or 18:00 UTC. Join the stream learn and ask questions about many .NET Data related topics.

Documentation and Feedback

The starting point for all EF Core documentation is docs.microsoft.com/ef/. Please file issues found and any other feedback on the dotnet/efcore GitHub repo.

The following links are provided for easy reference and access.

6 comments

Discussion is closed. Login to edit/delete existing comments.

  • Daniel Smith 3

    You guys have done a great job with EF8 – it’s looking like a really solid release. I especially love all the little changes that make life easier.

    Any news on the “EF Core Database First in Visual Studio” part of the plan? We’re fortunate to have Eric’s Power Tools, which is great, but I’d still like for there to be official VS tooling from Microsoft to handle database first scenarios.

    • Brice LambsonMicrosoft employee 1

      We’re continuing to work with the Visual Studio Web Tooling team on that feature. No ETA yet on when it will ship.

  • Hamed 1

    My favorite; Parentheses elimination

    • Stilgar Naib 3

      I don’t know why this is listed under smaller features. It might be the biggest improvement to my daily use of EF

  • Stilgar Naib 2

    Is there an issue for supporting null checks in EF LINQ via the “is null” and “is not null” pattern matching syntax? My Google Fu is not strong enough to find one and I wonder if it exist or I should submit it

    • Brice LambsonMicrosoft employee 2

      EF Core is limited by what the C# compiler supports in LINQ expressions. The dotnet/csharplang#2545 issue tracks expanding what is allowed inside LINQ expressions.

Feedback usabilla icon