Telerik blogs

With the recent arrival of .NET 7, new features are also available for EF Core in the latest version 7. Check out this blog post for the main news with practical examples.

.NET 7 was officially released recently and brought significant improvements to several areas of .NET such as servers and runtime, minimal APIs, Blazor and others. What also received improvements was EF Core.

In this blog post, we will check the main news for Microsoft ORM and how we can use it in practice.

Getting Started with EF Core

If you are new to ASP.NET Core development, I suggest reading this article .NET Basics: ORM (Object Relational Mapping) which explains what an ORM is and how EF Core (Entity Framework Core) works.

💡 All examples shown in this article can be accessed in this repository: source code.

The New ExecuteUpdate and ExecuteDelete Methods

Prior to the release of EF Core 7, the most common way to save changes to the database was via the SaveChanges extension method, which by default tracks changes to entities and sends updates to the database.

Changes made by SaveChanges are mapped and sent only to properties and relations that have changed. In addition, controlled entities are synchronized with changes sent to the database.

This process is an efficient way to send any type of change to the database—after all, in addition to the functions mentioned above, they are also grouped in batches to reduce the number of accesses to the database.

Despite being efficient, it is not always necessary to track changes as is done in the traditional SaveChanges method. So EF7 brings two new methods that perform changes and deletions of records without loading the entities in memory, which results in performance gains through changes and more efficient exclusions.

However, it is important to consider that:

  • Changes must be specified explicitly as they are not automatically detected by EF Core.
  • Tracked entities will not be kept in sync with the database.
  • Additional commands may need to be sent in the correct order to not violate database restrictions, such as deleting foreign keys.

Therefore, the new ExecuteUpdate and ExecuteDelete methods do not replace SaveChanges—they just bring more possibilities when working with databases.

Example of Using the ExecuteUpdate Method

The ExecuteUpdate and ExecuteUpdateAsync methods are very similar to the ExecuteDelete method. However, to perform an update, it is necessary to inform which properties must be updated and how this will be done, which can be through one or more calls to the SetProperty method.

Below is an example of using the ExecuteUpdate method:

   await db.Posts
    .Where(p => p.Id == "3fa85f64-5717-4562-b3fc-2c963f66afa6")
    .ExecuteUpdateAsync(s => s
    .SetProperty(b => b.AuthorName, "John Smith")
    .SetProperty(b => b.Title, b =>  "EF7 is here!")
    .SetProperty(b => b.Text, b =>  "	")
    .SetProperty(b => b.LastUpdateDate, "2022-30-11 17:29:46.5028235"));

Note that to execute the update, the parameter that indicates which records should receive the change is passed to the Where extension method, which in this case is the id of the record. Then the ExecuteUpdateAsync method is called together with the SetProperty method that defines which properties of the database will receive the change and what value it will receive.

The following SQL is executed:

update Posts
set AuthorName = 'John Smith', 
Title = 'EF7 is here!', 
Text = 'Lorem ipsum dolor sit amet...', 
LastUpdateDate = '2022-30-11 17:29:46.5028235'
where Id = '88A7FC2B-55B5-4002-AE75-A125C90E54BD'

Example of Using the ExecuteDelete Method

The ExecuteDelete or ExecuteDeleteAsync methods immediately delete all entities from the database that are within the parameters informed to the DbSet.

The example below shows how the ExecuteDeleteAsync method can be used, both in the condition of a single record and in the condition of several.

Deleting a single record:

await db.Posts.Where(p => p.Id == "3fa85f64-5717-4562-b3fc-2c963f66afa6").ExecuteDeleteAsync();

The following SQL is executed:

delete from Posts where Id = '3fa85f64-5717-4562-b3fc-2c963f66afa6'

Deleting multiple records:

await db.Posts.Where(p => p?.Text.Contains(".NET")).ExecuteDeleteAsync();

The following SQL is executed:

delete from Posts where Text like '%.NET%'

JSON Columns

Using JSON columns in relational databases can be a great idea, because through them it is possible to filter and classify data through document elements, which allows relational databases to acquire characteristics of NoSQL databases, making a kind of hybrid between the two technologies.

EF7 has native support for JSON columns, which allows mapping .NET types to JSON documents. This also includes LINQ queries that can be used in aggregations and will be converted to the appropriate query constructs for JSON, and is also possible to update and save changes to JSON documents.

An important detail is that this mapping is currently possible only for SQL SERVER but support for SQLite is already planned for future versions of EF. PostgreSQL and Pomelo MySQL databases already support JSON.

Mapping JSON Columns

Below is an example of converting a .NET type to JSON using the ContactDetails class.

public class Author
{
    public Guid Id { get; set; }
    public string? Name { get; set; }
    public ContactDetails? Contact { get; set; }
}

public class ContactDetails
{
    public Address Address { get; set; } = null!;
    public string? Phone { get; set; }
}

public class Address
{
    public Address(string street, string city, string postcode, string country)
    {
        Street = street;
        City = city;
        Postcode = postcode;
        Country = country;
    }

    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
    public string Country { get; set; }
}

Note that the ContactDetails class is an object of the Author class, so when EF7 does the conversion, it will be transformed into a string of data in JSON format.

For the conversion to take place, it is enough that in the class that implements the DbContext class, the OnModelCreating method is added—and in it, the configuration that will declare which table columns will receive the JSON document, which is done through the ToJson extension method. You can see this in the code below:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Author>().OwnsOne(
            author => author.Contact, ownedNavigationBuilder =>
            {
                ownedNavigationBuilder.ToJson();
                ownedNavigationBuilder.OwnsOne(contactDetails => contactDetails.Address);
            });
    }

See in the image below how the record was after being recorded in the database:

JSON SQL Column

Queries on JSON Columns

Querying JSON columns is very simple—just use LINQ, like other types of aggregation.

Note in the example below where the value of a property (city) is filtered within the JSON.

app.MapGet("/auhtor/getAuhtorsByCity/{city}", async (AuthorDb db, string city) =>
{
    var authorsByCity = await db.Authors.Where(author => author.Contact.Address.City == city).ToListAsync();

    if (!authorsByCity.Any())
        return Results.NotFound();

    return Results.Ok(authorsByCity);
})
.WithName("GetAuhtorsByCity")
.WithOpenApi();

The above code results in the following SQL SERVER query. Note that the JSON_VALUE expression is being used to extract the JSON value from the Contact column.

SELECT [a].[Id], [a].[Name], JSON_QUERY([a].[Contact],'$')
FROM [Authors] AS [a]
WHERE CAST(JSON_VALUE([a].[Contact],'$.Address.City') AS nvarchar(max)) = N'Juneau'

Updating JSON Columns

To update JSON columns, SaveChanges and SaveChangesAsync methods can be used. For extensive changes, the entire document will be updated, as seen in the example below:


    var authorExists = await db.Authors.Where(author => author.Id == id).FirstOrDefaultAsync();

    authorExists.Contact = new() { Address = new("912 Park Allen", "Ket chikan", "99901", "USA"), Phone = "(907) 247-9199" };

    await db.SaveChangesAsync();

In this way, the entire object to be updated is passed as a parameter and the following SQL code is executed:

Executed DbCommand (4ms) [Parameters=[@p0='{"address": {"street": "912 Park Alles", "city": "Ket chikan", "postcode": "99901", "country": "USA"}, "phone": "(907) 247-9199"}' (Size = 119), @p1='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
UPDATE [Authors] SET [Contact] = @p0
OUTPUT 1
WHERE [Id] = @p1;

It is also possible to update a single JSON property. The following C# code:

    var authorExists = await db.Authors.Where(author => author.Id == id).FirstOrDefaultAsync();

    authorExists.Contact.Address.Street = "1523 Stellar Dr";

    await db.SaveChangesAsync();

… runs the following SQL code:

Executed DbCommand (4ms) [Parameters=[@p0='["1523 Stellar Dr"]' (Size = 19), @p1='?' (DbType = Guid)], CommandType='Text', CommandTimeout='30']
SET IMPLICIT_TRANSACTIONS OFF;
SET NOCOUNT ON;
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Street', JSON_VALUE(@p0, '$[0]'))
OUTPUT 1
WHERE [Id] = @p1;

Faster SaveChanges Method

The SaveChanges and SaveChangesAsync methods have been improved in EF7—their performance can be up to four times faster than in EF Core 6.0!

Most of this performance gain is due to performing fewer round trips to the database and faster SQL generation.

A detail that deserves our attention is the value of the SaveChanges method, as it can perform transactions and complex mappings through EF Core functions.

Following are some of the advantages of using SaveChanges:

  • Batch multiple inserts, updates and delete commands to reduce database round trips.
  • Find out if an explicit transaction is required or not.
  • Determine what order to insert, update and delete entities so that database constraints are not violated.
  • Ensure that database-generated values are efficiently returned and reseeded to entities.
  • Automatically define foreign key values using the generated values for primary keys.
  • Detect concurrency conflicts.
  • Detect different database systems and ensure that the correct and efficient commands are sent to each one.

New Query Options

EF7 brought new features to queries like GroupBy as the final operator. The example below shows how grouping can be done using the GroupBy extension method:

    var groupByAuthor = db.Posts.GroupBy(p => p.AuthorName).ToList();

This query results in the following SQL code:

SELECT [p].[Id], [Title].[Text], [p].[AuthorName], [p].[CreationDate],[p].[LastUpdateDate]
FROM [Posts] AS [p]
ORDER BY [b].[AuthorName]

Note that this type of GroupBy is not directly converted to SQL, so EF Core does grouping on the returned results.

Conclusion

This article showed some of the main features available in the recently released version 7.0 of EF Core. In addition to these, there are still other improvements that can be checked in full in the Microsoft documentation: EF Core 7.0—What’s new.

Undoubtedly, EF Core is a formidable tool for working with databases in .NET, and now it’s even more complete in this new version.


assis-zang-bio
About the Author

Assis Zang

Assis Zang is a software developer from Brazil, developing in the .NET platform since 2017. In his free time, he enjoys playing video games and reading good books. You can follow him at: LinkedIn and Github.

Related Posts

Comments

Comments are disabled in preview mode.