From dc87c70771694578d403fb770bc77a125ad17f8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 May 2025 23:52:31 +0000 Subject: [PATCH 1/3] Initial plan for issue From cb71c3429927d5d274779e13e38f1fab556d878e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 May 2025 00:02:47 +0000 Subject: [PATCH 2/3] Fix double escaping of Chinese characters in JSON columns during updates Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/SqlServerOwnedJsonTypeMapping.cs | 5 ++- .../Update/JsonUpdateTestBase.cs | 34 ++++++++++++++++ .../Update/JsonUpdateSqlServerTest.cs | 40 +++++++++++++++++++ 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerOwnedJsonTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerOwnedJsonTypeMapping.cs index 801ac044755..ae0b4034853 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerOwnedJsonTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerOwnedJsonTypeMapping.cs @@ -105,7 +105,10 @@ protected virtual string EscapeSqlLiteral(string literal) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - => $"'{EscapeSqlLiteral(JsonSerializer.Serialize(value))}'"; + { + var jsonString = value is string str ? str : JsonSerializer.Serialize(value); + return $"'{EscapeSqlLiteral(jsonString)}'"; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs index 90f5e03f5d4..d36a773550b 100644 --- a/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Update/JsonUpdateTestBase.cs @@ -3633,6 +3633,40 @@ public virtual Task Add_and_update_nested_optional_primitive_collection(bool? va } }); + [ConditionalFact] + public virtual Task Edit_single_property_with_chinese_characters() + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, + UseTransaction, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(x => x.Id == 1); + entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething = "测试1"; + + ClearLog(); + await context.SaveChangesAsync(); + }, + async context => + { + var result = await context.Set().SingleAsync(x => x.Id == 1); + Assert.Equal("测试1", result.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething); + }, + async context => + { + var query = await context.JsonEntitiesBasic.ToListAsync(); + var entity = query.Single(x => x.Id == 1); + entity.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething = "测试2"; + + ClearLog(); + await context.SaveChangesAsync(); + }, + async context => + { + var result = await context.Set().SingleAsync(x => x.Id == 1); + Assert.Equal("测试2", result.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething); + }); + public void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) => facade.UseTransaction(transaction.GetDbTransaction()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs index 42fee62b579..e8394557470 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs @@ -2535,6 +2535,46 @@ public override Task Edit_single_property_collection_of_collection_of_nullable_i public override Task Edit_single_property_collection_of_collection_of_single() => Assert.ThrowsAsync(base.Edit_single_property_collection_of_collection_of_single); + public override async Task Edit_single_property_with_chinese_characters() + { + await base.Edit_single_property_with_chinese_characters(); + + AssertSql( + """ +@p0='{"Name":"Root","Number":7,"OwnedCollectionBranch":[],"OwnedReferenceBranch":{"Date":"2101-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":10.1,"Id":10,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_r_c1"},{"SomethingSomething":"e1_r_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"测试1"}}}' (Nullable = false) (Size = 386) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = @p0 +OUTPUT 1 +WHERE [Id] = @p1; +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 1 +""", + // + """ +@p0='{"Name":"Root","Number":7,"OwnedCollectionBranch":[],"OwnedReferenceBranch":{"Date":"2101-01-01T00:00:00","Enum":2,"Enums":[-1,-1,2],"Fraction":10.1,"Id":10,"NullableEnum":-1,"NullableEnums":[null,-1,2],"OwnedCollectionLeaf":[{"SomethingSomething":"e1_r_r_c1"},{"SomethingSomething":"e1_r_r_c2"}],"OwnedReferenceLeaf":{"SomethingSomething":"测试2"}}}' (Nullable = false) (Size = 386) +@p1='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [JsonEntitiesBasic] SET [OwnedReferenceRoot] = @p0 +OUTPUT 1 +WHERE [Id] = @p1; +""", + // + """ +SELECT TOP(2) [j].[Id], [j].[EntityBasicId], [j].[Name], [j].[OwnedCollectionRoot], [j].[OwnedReferenceRoot] +FROM [JsonEntitiesBasic] AS [j] +WHERE [j].[Id] = 1 +"""); + } + protected override void ClearLog() => Fixture.TestSqlLoggerFactory.Clear(); From 4fedef575432c84b29bff5cccbc48bdb12de9feb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 May 2025 01:20:03 +0000 Subject: [PATCH 3/3] Implement proper fix for Chinese character escaping in JSON using AppContext switch Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Internal/SqlServerOwnedJsonTypeMapping.cs | 5 +---- src/EFCore/Storage/Json/JsonStringReaderWriter.cs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerOwnedJsonTypeMapping.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerOwnedJsonTypeMapping.cs index ae0b4034853..801ac044755 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerOwnedJsonTypeMapping.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerOwnedJsonTypeMapping.cs @@ -105,10 +105,7 @@ protected virtual string EscapeSqlLiteral(string literal) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override string GenerateNonNullSqlLiteral(object value) - { - var jsonString = value is string str ? str : JsonSerializer.Serialize(value); - return $"'{EscapeSqlLiteral(jsonString)}'"; - } + => $"'{EscapeSqlLiteral(JsonSerializer.Serialize(value))}'"; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Storage/Json/JsonStringReaderWriter.cs b/src/EFCore/Storage/Json/JsonStringReaderWriter.cs index 471cb8dd351..e954bc780e4 100644 --- a/src/EFCore/Storage/Json/JsonStringReaderWriter.cs +++ b/src/EFCore/Storage/Json/JsonStringReaderWriter.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.Encodings.Web; using System.Text.Json; namespace Microsoft.EntityFrameworkCore.Storage.Json; @@ -12,6 +13,9 @@ public sealed class JsonStringReaderWriter : JsonValueReaderWriter { private static readonly PropertyInfo InstanceProperty = typeof(JsonStringReaderWriter).GetProperty(nameof(Instance))!; + private static readonly bool UseOldBehavior32152 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue32152", out var enabled32152) && enabled32152; + /// /// The singleton instance of this stateless reader/writer. /// @@ -27,7 +31,16 @@ public override string FromJsonTyped(ref Utf8JsonReaderManager manager, object? /// public override void ToJsonTyped(Utf8JsonWriter writer, string value) - => writer.WriteStringValue(value); + { + if (UseOldBehavior32152) + { + writer.WriteStringValue(value); + } + else + { + writer.WriteStringValue(JsonEncodedText.Encode(value, JavaScriptEncoder.UnsafeRelaxedJsonEscaping)); + } + } /// public override Expression ConstructorExpression