From 845977f86cc2a5c29336ea0291f40f678c8bcf65 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Tue, 30 Aug 2022 20:59:54 +0100 Subject: [PATCH] Updates to What's New for JSON columns and ExecuteUpdate/ExecuteDelete --- .../core/what-is-new/ef-core-7.0/whatsnew.md | 104 +++++++++++++++++- .../NewInEFCore7/BlogsContext.cs | 2 +- .../NewInEFCore7/ExecuteDeleteSample.cs | 4 +- .../NewInEFCore7/ExecuteUpdateSample.cs | 27 +++++ .../NewInEFCore7/JsonColumnsSample.cs | 35 +++++- .../NewInEFCore7/NewInEFCore7.csproj | 10 +- 6 files changed, 170 insertions(+), 12 deletions(-) diff --git a/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md b/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md index 41f9c6ba2d..186f8cce8f 100644 --- a/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md +++ b/entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md @@ -2,7 +2,7 @@ title: What's New in EF Core 7.0 description: Overview of new features in EF Core 7.0 author: ajcvickers -ms.date: 08/24/2022 +ms.date: 08/30/2022 uid: core/what-is-new/ef-core-7 --- @@ -283,6 +283,9 @@ This aggregate type contains several nested types and collections. Calls to `Own --> [!code-csharp[PostMetadataConfig](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=PostMetadataConfig)] +> [!TIP] +> `ToJson` is only needed on the aggregate root to map the entire aggregate to a JSON document. + With this mapping, EF7 can create and query into a complex JSON document like this: ```json @@ -447,6 +450,94 @@ WHERE CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int) > 3000 > [!NOTE] > More complex queries involving JSON collections require `jsonpath` support. Vote for [Support jsonpath querying](https://github.com/dotnet/efcore/issues/28616) if this is something you are interested in. +> [!TIP] +> Consider creating indexes to improve query performance in JSON documents. For example, see [Index Json data](/sql/relational-databases/json/index-json-data) when using SQL Server. + +### Updating JSON columns + +[`SaveChanges` and `SaveChangesAsync`](xref:core/saving/basic) work in the normal way to make updates a JSON column. For extensive changes, the entire document will be updated. For example, replacing most of the `Contact` document for an author: + + +[!code-csharp[UpdateDocument](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=UpdateDocument)] + +In this case, the entire new document is passed as a parameter to the `Update` command: + +```text +info: 8/30/2022 20:21:24.392 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (2ms) [Parameters=[@p0='{"Phone":"01632 88346","Address":{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"2 Riverside"}}' (Nullable = false) (Size = 114), @p1='2'], CommandType='Text', CommandTimeout='30'] +``` + +```sql + SET IMPLICIT_TRANSACTIONS OFF; + SET NOCOUNT ON; + UPDATE [Authors] SET [Contact] = @p0 + OUTPUT 1 + WHERE [Id] = @p1; +``` + +However, if only a sub-document is changed, then EF Core will use a "JSON_MODIFY" command to update only the sub-document. For example, changing the `Address` inside a `Contact` document: + + +[!code-csharp[UpdateSubDocument](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=UpdateSubDocument)] + +Generates the following SQL: + +```text +info: 8/30/2022 20:53:01.669 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30'] + SELECT TOP(2) [a].[Id], [a].[Name], JSON_QUERY([a].[Contact],'$') + FROM [Authors] AS [a] + WHERE [a].[Name] LIKE N'Brice%' +``` + +```sql +info: 8/30/2022 20:53:01.676 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (2ms) [Parameters=[@p0='{"City":"Trimbridge","Country":"UK","Postcode":"TB1 5ZS","Street":"4 Riverside"}' (Nullable = false) (Size = 80), @p1='5'], CommandType='Text', CommandTimeout='30'] + SET IMPLICIT_TRANSACTIONS OFF; + SET NOCOUNT ON; + UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address', JSON_QUERY(@p0)) + OUTPUT 1 + WHERE [Id] = @p1; +``` + +Finally, if only a single property is changed, then EF Core will again use a "JSON_MODIFY" command, this time to patch only the changed property value. For example: + + +[!code-csharp[UpdateProperty](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=UpdateProperty)] + +Generates the following SQL: + +```text +info: 8/30/2022 20:24:04.677 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) + Executed DbCommand (2ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30'] +``` + +```sql +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]')) +OUTPUT 1 +WHERE [Id] = @p1; +``` + ## ExecuteUpdate and ExecuteDelete (Bulk updates) By default, EF Core [tracks changes to entities](xref:core/change-tracking/index), and then [sends updates to the database](xref:core/saving/index) when one of the `SaveChanges` methods is called. Changes are only sent for properties and relationships that have actually changed. Also, the tracked entities remain in sync with the changes sent to the database. This mechanism is an efficient and convenient way to send general-purpose inserts, updates, and deletes to the database. These changes are also batched to reduce the number of database round-trips. @@ -640,14 +731,19 @@ The statement has been terminated. To fix this, we must first either delete the posts, or sever the relationship between each post and its author by setting `AuthorId` foreign key property to null. For example, using the delete option: [!code-csharp[DeleteAllAuthors](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteAllAuthors)] +> [!TIP] +> can be used to tag `ExecuteDelete` or `ExecuteUpdate` in the same way as it tags normal queries. + This results in two separate commands; the first to delete the dependents: ```sql +-- Deleting posts... + DELETE FROM [p] FROM [Posts] AS [p] ``` @@ -655,6 +751,8 @@ FROM [Posts] AS [p] And the second to delete the principals: ```sql +-- Deleting authors... + DELETE FROM [a] FROM [Authors] AS [a] ``` diff --git a/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs b/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs index d5816fef3a..c30385ff0d 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs @@ -77,7 +77,7 @@ public Author(string name) #region ContactDetailsAggregate public class ContactDetails { - public Address Address { get; init; } = null!; + public Address Address { get; set; } = null!; public string? Phone { get; set; } } diff --git a/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs b/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs index 929220d68a..3b4141440e 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs @@ -161,8 +161,8 @@ private static async Task DeleteAllAuthors() context.LoggingEnabled = true; #region DeleteAllAuthors - await context.Posts.ExecuteDeleteAsync(); - await context.Authors.ExecuteDeleteAsync(); + await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync(); + await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync(); #endregion context.LoggingEnabled = false; diff --git a/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs b/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs index c223428cc6..68598a221a 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs @@ -56,6 +56,9 @@ private static async Task ExecuteUpdateTest() await UpdateTagsOnOldPosts(); + // https://github.com/dotnet/efcore/issues/28921 (EF.Default doesn't work for value types) + // await ResetPostPublishedOnToDefault(); + Console.WriteLine(); } @@ -164,4 +167,28 @@ await context.Tags $"Tags after update: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}"); Console.WriteLine(); } + + private static async Task ResetPostPublishedOnToDefault() + where TContext : BlogsContext, new() + { + await using var context = new TContext(); + await context.Database.BeginTransactionAsync(); + + Console.WriteLine("Reset PublishedOn on posts to its default value"); + Console.WriteLine( + $"Posts before update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "' " + e.PublishedOn.Date).ToListAsync())}"); + Console.WriteLine(); + + context.LoggingEnabled = true; + await context.Set() + .ExecuteUpdateAsync( + setPropertyCalls => setPropertyCalls + .SetProperty(post => post.PublishedOn, post => EF.Default())); + context.LoggingEnabled = false; + + Console.WriteLine(); + Console.WriteLine( + $"Posts after update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "' " + e.PublishedOn.Date).ToListAsync())}"); + Console.WriteLine(); + } } diff --git a/samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs b/samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs index d584cc9ac1..c0e62e2fd2 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs +++ b/samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs @@ -121,12 +121,45 @@ private static async Task JsonColumnsTest() context.ChangeTracker.Clear(); + Console.WriteLine("Updating a 'Contact' JSON document..."); + Console.WriteLine(); + + #region UpdateDocument + var jeremy = await context.Authors.SingleAsync(author => author.Name.StartsWith("Jeremy")); + + jeremy.Contact = new() { Address = new("2 Riverside", "Trimbridge", "TB1 5ZS", "UK"), Phone = "01632 88346" }; + + await context.SaveChangesAsync(); + #endregion + + context.ChangeTracker.Clear(); + + Console.WriteLine("Updating an 'Address' inside the 'Contact' JSON document..."); + Console.WriteLine(); + + #region UpdateSubDocument + var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice")); + + brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK"); + + await context.SaveChangesAsync(); + #endregion + + context.ChangeTracker.Clear(); + + Console.WriteLine(); + Console.WriteLine($"Updating only 'Country' in a 'Contact' JSON document..."); + Console.WriteLine(); + + #region UpdateProperty var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur")); - arthur.Contact.Phone = "01632 22345"; arthur.Contact.Address.Country = "United Kingdom"; await context.SaveChangesAsync(); + #endregion + + Console.WriteLine(); context.ChangeTracker.Clear(); diff --git a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj index 25ba37699b..00b3a745c7 100644 --- a/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj +++ b/samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj @@ -9,11 +9,11 @@ - - - - - + + + + +