Skip to content

Commit f227811

Browse files
committed
Updates to What's New for JSON columns and ExecuteUpdate/ExecuteDelete
1 parent dc24b31 commit f227811

File tree

6 files changed

+171
-12
lines changed

6 files changed

+171
-12
lines changed

entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md

+102-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: What's New in EF Core 7.0
33
description: Overview of new features in EF Core 7.0
44
author: ajcvickers
5-
ms.date: 08/24/2022
5+
ms.date: 08/30/2022
66
uid: core/what-is-new/ef-core-7
77
---
88

@@ -283,6 +283,9 @@ This aggregate type contains several nested types and collections. Calls to `Own
283283
-->
284284
[!code-csharp[PostMetadataConfig](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=PostMetadataConfig)]
285285

286+
> [!TIP]
287+
> `ToJson` is only needed on the aggregate root to map the entire aggregate to a JSON document.
288+
286289
With this mapping, EF7 can create and query into a complex JSON document like this:
287290

288291
```json
@@ -447,6 +450,95 @@ WHERE CAST(JSON_VALUE([p].[Metadata],'$.Views') AS int) > 3000
447450
> [!NOTE]
448451
> 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.
449452
453+
> [!TIP]
454+
> 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.
455+
456+
### Updating JSON columns
457+
458+
[`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:
459+
460+
<!--
461+
var jeremy = await context.Authors.SingleAsync(author => author.Name.StartsWith("Jeremy"));
462+
463+
jeremy.Contact = new() { Address = new("2 Riverside", "Trimbridge", "TB1 5ZS", "UK"), Phone = "01632 88346" };
464+
465+
await context.SaveChangesAsync();
466+
-->
467+
[!code-csharp[UpdateDocument](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=UpdateDocument)]
468+
469+
In this case, the entire new document is passed as a parameter to the `Update` command:
470+
471+
```text
472+
info: 8/30/2022 20:21:24.392 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
473+
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']
474+
```
475+
476+
```sql
477+
SET IMPLICIT_TRANSACTIONS OFF;
478+
SET NOCOUNT ON;
479+
UPDATE [Authors] SET [Contact] = @p0
480+
OUTPUT 1
481+
WHERE [Id] = @p1;
482+
```
483+
484+
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:
485+
486+
<!--
487+
var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));
488+
489+
brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");
490+
491+
await context.SaveChangesAsync();
492+
-->
493+
[!code-csharp[UpdateSubDocument](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=UpdateSubDocument)]
494+
495+
Generates the following SQL:
496+
497+
```text
498+
info: 8/30/2022 20:53:01.669 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
499+
Executed DbCommand (2ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
500+
SELECT TOP(2) [a].[Id], [a].[Name], JSON_QUERY([a].[Contact],'$')
501+
FROM [Authors] AS [a]
502+
WHERE [a].[Name] LIKE N'Brice%'
503+
```
504+
505+
```sql
506+
info: 8/30/2022 20:53:01.676 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
507+
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']
508+
SET IMPLICIT_TRANSACTIONS OFF;
509+
SET NOCOUNT ON;
510+
UPDATE [Authors] SET [Contact] = JSON_MODIFY([Contact], 'strict $.Address', JSON_QUERY(@p0))
511+
OUTPUT 1
512+
WHERE [Id] = @p1;
513+
```
514+
515+
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:
516+
517+
<!--
518+
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
519+
520+
arthur.Contact.Address.Country = "United Kingdom";
521+
522+
await context.SaveChangesAsync();
523+
-->
524+
[!code-csharp[UpdateProperty](../../../../samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs?name=UpdateProperty)]
525+
526+
Generates the following SQL:
527+
528+
```text
529+
info: 8/30/2022 20:24:04.677 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
530+
Executed DbCommand (2ms) [Parameters=[@p0='["United Kingdom"]' (Nullable = false) (Size = 18), @p1='4'], CommandType='Text', CommandTimeout='30']
531+
```
532+
533+
```sql
534+
SET IMPLICIT_TRANSACTIONS OFF;
535+
SET NOCOUNT ON;
536+
UPDATE [Authors] SET [Contact] = JSON_MODIFY(
537+
[Contact], 'strict $.Address.Country', JSON_VALUE(@p0, '$[0]'))
538+
OUTPUT 1
539+
WHERE [Id] = @p1;
540+
```
541+
450542
## ExecuteUpdate and ExecuteDelete (Bulk updates)
451543

452544
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,21 +732,28 @@ The statement has been terminated.
640732
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:
641733

642734
<!--
643-
await context.Posts.ExecuteDeleteAsync();
644-
await context.Authors.ExecuteDeleteAsync();
735+
await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
736+
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();
645737
-->
646738
[!code-csharp[DeleteAllAuthors](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteAllAuthors)]
647739

740+
> [!TIP]
741+
> `TagWith` can be used to tag `ExecuteDelete` or `ExecuteUpdate` in the same way as it tags normal queries.
742+
648743
This results in two separate commands; the first to delete the dependents:
649744

650745
```sql
746+
-- Deleting posts...
747+
651748
DELETE FROM [p]
652749
FROM [Posts] AS [p]
653750
```
654751

655752
And the second to delete the principals:
656753

657754
```sql
755+
-- Deleting authors...
756+
658757
DELETE FROM [a]
659758
FROM [Authors] AS [a]
660759
```

samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public Author(string name)
7777
#region ContactDetailsAggregate
7878
public class ContactDetails
7979
{
80-
public Address Address { get; init; } = null!;
80+
public Address Address { get; set; } = null!;
8181
public string? Phone { get; set; }
8282
}
8383

samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ private static async Task DeleteAllAuthors<TContext>()
161161
context.LoggingEnabled = true;
162162

163163
#region DeleteAllAuthors
164-
await context.Posts.ExecuteDeleteAsync();
165-
await context.Authors.ExecuteDeleteAsync();
164+
await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
165+
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();
166166
#endregion
167167

168168
context.LoggingEnabled = false;

samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs

+27
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ private static async Task ExecuteUpdateTest<TContext>()
5656

5757
await UpdateTagsOnOldPosts<TContext>();
5858

59+
// https://github.com/dotnet/efcore/issues/28921 (EF.Default doesn't work for value types)
60+
// await ResetPostPublishedOnToDefault<TContext>();
61+
5962
Console.WriteLine();
6063
}
6164

@@ -164,4 +167,28 @@ await context.Tags
164167
$"Tags after update: {string.Join(", ", await context.Tags.AsNoTracking().Select(e => "'" + e.Text + "'").ToListAsync())}");
165168
Console.WriteLine();
166169
}
170+
171+
private static async Task ResetPostPublishedOnToDefault<TContext>()
172+
where TContext : BlogsContext, new()
173+
{
174+
await using var context = new TContext();
175+
await context.Database.BeginTransactionAsync();
176+
177+
Console.WriteLine("Reset PublishedOn on posts to its default value");
178+
Console.WriteLine(
179+
$"Posts before update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "' " + e.PublishedOn.Date).ToListAsync())}");
180+
Console.WriteLine();
181+
182+
context.LoggingEnabled = true;
183+
await context.Set<Post>()
184+
.ExecuteUpdateAsync(
185+
setPropertyCalls => setPropertyCalls
186+
.SetProperty(post => post.PublishedOn, post => EF.Default<DateTime>()));
187+
context.LoggingEnabled = false;
188+
189+
Console.WriteLine();
190+
Console.WriteLine(
191+
$"Posts after update: {string.Join(", ", await context.Posts.AsNoTracking().Select(e => "'..." + e.Title.Substring(e.Title.Length - 12) + "' " + e.PublishedOn.Date).ToListAsync())}");
192+
Console.WriteLine();
193+
}
167194
}

samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs

+34-1
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,45 @@ private static async Task JsonColumnsTest<TContext>()
121121

122122
context.ChangeTracker.Clear();
123123

124+
Console.WriteLine("Updating a 'Contact' JSON document...");
125+
Console.WriteLine();
126+
127+
#region UpdateDocument
128+
var jeremy = await context.Authors.SingleAsync(author => author.Name.StartsWith("Jeremy"));
129+
130+
jeremy.Contact = new() { Address = new("2 Riverside", "Trimbridge", "TB1 5ZS", "UK"), Phone = "01632 88346" };
131+
132+
await context.SaveChangesAsync();
133+
#endregion
134+
135+
context.ChangeTracker.Clear();
136+
137+
Console.WriteLine("Updating an 'Address' inside the 'Contact' JSON document...");
138+
Console.WriteLine();
139+
140+
#region UpdateSubDocument
141+
var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));
142+
143+
brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");
144+
145+
await context.SaveChangesAsync();
146+
#endregion
147+
148+
context.ChangeTracker.Clear();
149+
150+
Console.WriteLine();
151+
Console.WriteLine($"Updating only 'Country' in a 'Contact' JSON document...");
152+
Console.WriteLine();
153+
154+
#region UpdateProperty
124155
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));
125156

126-
arthur.Contact.Phone = "01632 22345";
127157
arthur.Contact.Address.Country = "United Kingdom";
128158

129159
await context.SaveChangesAsync();
160+
#endregion
161+
162+
Console.WriteLine();
130163

131164
context.ChangeTracker.Clear();
132165

samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj

+5-5
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12-
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-rc.2.22424.11" />
13-
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.2.22424.11" />
14-
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0-rc.2.22424.11" />
15-
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="7.0.0-rc.2.22424.11" />
16-
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0-rc.2.22424.11" />
12+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-rc.2.22429.6" />
13+
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.2.22429.6" />
14+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0-rc.2.22429.6" />
15+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="7.0.0-rc.2.22429.6" />
16+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0-rc.2.22429.6" />
1717
</ItemGroup>
1818

1919
<ItemGroup>

0 commit comments

Comments
 (0)