EF Core 8 RC 2: Smaller features in EF8
source link: https://devblogs.microsoft.com/dotnet/announcing-ef8-rc2/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
EF Core 8 RC 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.
- Watch our YouTube playlist of previous shows
- Visit the .NET Community Standup page to preview upcoming shows
- Submit your ideas for a guest, product, demo, or other content to cover
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.
Helpful Links
The following links are provided for easy reference and access.
- EF Core Community Standup Playlist: aka.ms/efstandups
- Main documentation: aka.ms/efdocs
- What’s New in EF Core 8: aka.ms/ef8-new
- What’s New in EF Core 7: aka.ms/ef7-new
- Issues and feature requests for EF Core: github.com/dotnet/efcore/issues
- Entity Framework Roadmap: aka.ms/efroadmap
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK