Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to What's New for JSON columns and ExecuteUpdate/ExecuteDelete #4006

Merged
merged 1 commit into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 102 additions & 3 deletions entity-framework/core/what-is-new/ef-core-7.0/whatsnew.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -447,6 +450,95 @@ 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:

<!--
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();
-->
[!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:

<!--
var brice = await context.Authors.SingleAsync(author => author.Name.StartsWith("Brice"));

brice.Contact.Address = new("4 Riverside", "Trimbridge", "TB1 5ZS", "UK");

await context.SaveChangesAsync();
-->
[!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:

<!--
var arthur = await context.Authors.SingleAsync(author => author.Name.StartsWith("Arthur"));

arthur.Contact.Address.Country = "United Kingdom";

await context.SaveChangesAsync();
-->
[!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.
Expand Down Expand Up @@ -640,21 +732,28 @@ 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:

<!--
await context.Posts.ExecuteDeleteAsync();
await context.Authors.ExecuteDeleteAsync();
await context.Posts.TagWith("Deleting posts...").ExecuteDeleteAsync();
await context.Authors.TagWith("Deleting authors...").ExecuteDeleteAsync();
-->
[!code-csharp[DeleteAllAuthors](../../../../samples/core/Miscellaneous/NewInEFCore7/ExecuteDeleteSample.cs?name=DeleteAllAuthors)]

> [!TIP]
> `TagWith` 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]
```

And the second to delete the principals:

```sql
-- Deleting authors...

DELETE FROM [a]
FROM [Authors] AS [a]
```
Expand Down
2 changes: 1 addition & 1 deletion samples/core/Miscellaneous/NewInEFCore7/BlogsContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ private static async Task DeleteAllAuthors<TContext>()
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;
Expand Down
27 changes: 27 additions & 0 deletions samples/core/Miscellaneous/NewInEFCore7/ExecuteUpdateSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ private static async Task ExecuteUpdateTest<TContext>()

await UpdateTagsOnOldPosts<TContext>();

// https://github.com/dotnet/efcore/issues/28921 (EF.Default doesn't work for value types)
// await ResetPostPublishedOnToDefault<TContext>();

Console.WriteLine();
}

Expand Down Expand Up @@ -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<TContext>()
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<Post>()
.ExecuteUpdateAsync(
setPropertyCalls => setPropertyCalls
.SetProperty(post => post.PublishedOn, post => EF.Default<DateTime>()));
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();
}
}
35 changes: 34 additions & 1 deletion samples/core/Miscellaneous/NewInEFCore7/JsonColumnsSample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,45 @@ private static async Task JsonColumnsTest<TContext>()

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();

Expand Down
10 changes: 5 additions & 5 deletions samples/core/Miscellaneous/NewInEFCore7/NewInEFCore7.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-rc.2.22424.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.2.22424.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0-rc.2.22424.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="7.0.0-rc.2.22424.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0-rc.2.22424.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0-rc.2.22429.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0-rc.2.22429.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0-rc.2.22429.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuite" Version="7.0.0-rc.2.22429.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0-rc.2.22429.6" />
</ItemGroup>

<ItemGroup>
Expand Down