diff --git a/src/EFCore.Relational/Query/Internal/RelationalJsonUtilities.cs b/src/EFCore.Relational/Query/Internal/RelationalJsonUtilities.cs index 88162c03f9f..fa48fa15bcb 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalJsonUtilities.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalJsonUtilities.cs @@ -89,8 +89,10 @@ void WriteJsonObject(Utf8JsonWriter writer, IComplexType complexType, object? ob Check.DebugAssert(jsonPropertyName is not null); writer.WritePropertyName(jsonPropertyName); + var jsonValueReaderWriter = property.GetJsonValueReaderWriter() ?? property.GetTypeMapping().JsonValueReaderWriter; + var propertyValue = property.GetGetter().GetClrValue(objectValue); - if (propertyValue is null) + if (propertyValue is null && jsonValueReaderWriter?.HandlesNullWrites != true) { if (!property.IsNullable) { @@ -101,7 +103,6 @@ void WriteJsonObject(Utf8JsonWriter writer, IComplexType complexType, object? ob } else { - var jsonValueReaderWriter = property.GetJsonValueReaderWriter() ?? property.GetTypeMapping().JsonValueReaderWriter; Check.DebugAssert(jsonValueReaderWriter is not null, "Missing JsonValueReaderWriter on JSON property"); jsonValueReaderWriter.ToJson(writer, propertyValue); } diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index 7f453145d6a..6f8bac21dd8 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -3188,6 +3188,56 @@ private Expression CreateReadJsonPropertyValueExpression( resultExpression = Convert(resultExpression, property.ClrType); } + var converter = property.GetTypeMapping().Converter; + Expression nullExpression; + if (converter?.ConvertsNulls == true) + { + var typeMappingExpression = Call( + Convert( + _parentVisitor.Dependencies.LiftableConstantFactory.CreateLiftableConstant( + property, + LiftableConstantExpressionHelpers.BuildMemberAccessLambdaForProperty(property), + property.Name + "Property", + typeof(IPropertyBase)), + typeof(IReadOnlyProperty)), + PropertyGetTypeMappingMethod); + + var converterExpression = (Expression)Property(typeMappingExpression, nameof(CoreTypeMapping.Converter)); + + var converterType = converter.GetType(); + var typedConverterType = converterType.GetGenericTypeImplementations(typeof(ValueConverter<,>)).FirstOrDefault(); + if (typedConverterType != null) + { + if (converterExpression.Type != converter.GetType()) + { + converterExpression = Convert(converterExpression, converter.GetType()); + } + + nullExpression = Invoke( + Property( + converterExpression, + nameof(ValueConverter.ConvertFromProviderTyped)), + Default(converter.ProviderClrType)); + } + else + { + nullExpression = Invoke( + Property( + converterExpression, + nameof(ValueConverter.ConvertFromProvider)), + Default(typeof(object))); + } + + if (nullExpression.Type != property.ClrType) + { + nullExpression = Convert(nullExpression, property.ClrType); + } + } + else + { + nullExpression = Default(property.ClrType); + } + resultExpression = Condition( Equal( Property( @@ -3196,7 +3246,7 @@ private Expression CreateReadJsonPropertyValueExpression( Utf8JsonReaderManagerCurrentReaderField), Utf8JsonReaderTokenTypeProperty), Constant(JsonTokenType.Null)), - Default(property.ClrType), + nullExpression, resultExpression); } diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index bd6c225501c..a9019737e93 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -969,9 +969,9 @@ private void WriteJsonObject( #pragma warning disable EF1001 // Internal EF Core API usage. writer.WritePropertyName(jsonPropertyName); - if (propertyValue is not null) + var jsonValueReaderWriter = property.GetJsonValueReaderWriter() ?? property.GetTypeMapping().JsonValueReaderWriter; + if (propertyValue is not null || jsonValueReaderWriter?.HandlesNullWrites == true) { - var jsonValueReaderWriter = property.GetJsonValueReaderWriter() ?? property.GetTypeMapping().JsonValueReaderWriter; Check.DebugAssert(jsonValueReaderWriter is not null, "Missing JsonValueReaderWriter on JSON property"); jsonValueReaderWriter.ToJson(writer, propertyValue); } diff --git a/src/EFCore/Storage/Json/JsonConvertedValueReaderWriter.cs b/src/EFCore/Storage/Json/JsonConvertedValueReaderWriter.cs index 8c137b0e3df..74a36927a6e 100644 --- a/src/EFCore/Storage/Json/JsonConvertedValueReaderWriter.cs +++ b/src/EFCore/Storage/Json/JsonConvertedValueReaderWriter.cs @@ -32,13 +32,25 @@ public JsonConvertedValueReaderWriter( _converter = converter; } + /// + public override bool HandlesNullWrites => _converter.ConvertsNulls; + /// public override TModel FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null) => (TModel)_converter.ConvertFromProvider(_providerReaderWriter.FromJsonTyped(ref manager, existingObject))!; /// public override void ToJsonTyped(Utf8JsonWriter writer, TModel value) - => _providerReaderWriter.ToJson(writer, (TProvider)_converter.ConvertToProvider(value)!); + { + var convertedValue = _converter.ConvertToProvider(value); + if (convertedValue == null && !_providerReaderWriter.HandlesNullWrites) + { + writer.WriteNullValue(); + return; + } + + _providerReaderWriter.ToJson(writer, convertedValue); + } JsonValueReaderWriter ICompositeJsonValueReaderWriter.InnerReaderWriter => _providerReaderWriter; diff --git a/src/EFCore/Storage/Json/JsonValueReaderWriter.cs b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs index f61f541659d..94daf881dcb 100644 --- a/src/EFCore/Storage/Json/JsonValueReaderWriter.cs +++ b/src/EFCore/Storage/Json/JsonValueReaderWriter.cs @@ -21,6 +21,15 @@ internal JsonValueReaderWriter() { } + /// + /// If , then the nulls will be passed to the writer's method. Otherwise null + /// values will always be written as . + /// + /// + /// The default is . + /// + public virtual bool HandlesNullWrites { get; } = false; + /// /// Reads the value from a UTF8 JSON stream or buffer. /// @@ -48,7 +57,7 @@ internal JsonValueReaderWriter() /// /// The into which the value should be written. /// The value to write. - public abstract void ToJson(Utf8JsonWriter writer, object value); + public abstract void ToJson(Utf8JsonWriter writer, object? value); /// /// The type of the value being read/written. diff --git a/src/EFCore/Storage/Json/JsonValueReaderWriter`.cs b/src/EFCore/Storage/Json/JsonValueReaderWriter`.cs index f6b67545f7e..1c9735fb8d2 100644 --- a/src/EFCore/Storage/Json/JsonValueReaderWriter`.cs +++ b/src/EFCore/Storage/Json/JsonValueReaderWriter`.cs @@ -15,8 +15,15 @@ public sealed override object FromJson(ref Utf8JsonReaderManager manager, object => FromJsonTyped(ref manager, existingObject)!; /// - public sealed override void ToJson(Utf8JsonWriter writer, object value) - => ToJsonTyped(writer, (TValue)value!); + public sealed override void ToJson(Utf8JsonWriter writer, object? value) + { + if (value == null && !HandlesNullWrites) + { + throw new ArgumentNullException(nameof(value)); + } + + ToJsonTyped(writer, (TValue)value!); + } /// public sealed override Type ValueType diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs index e62be31c9f2..6ea02f68f6c 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs @@ -693,6 +693,74 @@ public class JsonNestedType #endregion HasJsonPropertyName + #region Value converter equality null scalar + + [ConditionalFact] + public virtual async Task Value_converter_equality_null_scalar() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: m => m.Entity().ComplexProperty(e => e.Json, b => + { + b.ToJson(); + + b.Property(j => j.IntToString).HasConversion(new Context37983_StringToIntConverter()); + }), + seed: context => + { + context.Set().Add(new Context37983.Entity + { + Json = new Context37983.JsonComplexType + { + IntToString = null, + } + }); + + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + TestSqlLoggerFactory.Clear(); + + var complexType = new Context37983.JsonComplexType + { + IntToString = null, + }; + + Assert.Equal(1, await context.Set().CountAsync(e => e.Json == complexType)); + } + + protected class Context37983(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + public class Entity + { + public int Id { get; set; } + public JsonComplexType Json { get; set; } + } + + public class JsonComplexType + { + public int? IntToString { get; set; } + } + } + + protected class Context37983_StringToIntConverter : ValueConverter + { + public Context37983_StringToIntConverter() + : base( + v => v == null ? "" : v.ToString(), + v => int.Parse(v)) + { + } + + public override bool ConvertsNulls => true; + } + + #endregion + protected TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; diff --git a/test/EFCore.Specification.Tests/ValueConvertersEndToEndTestBase.cs b/test/EFCore.Specification.Tests/ValueConvertersEndToEndTestBase.cs index 7cfb4bffdcf..d4429b82171 100644 --- a/test/EFCore.Specification.Tests/ValueConvertersEndToEndTestBase.cs +++ b/test/EFCore.Specification.Tests/ValueConvertersEndToEndTestBase.cs @@ -168,9 +168,10 @@ public virtual async Task Can_insert_and_read_back_with_conversions(int[] valueO { var entity = new ConvertingEntity(); + Add(context, entity); + SetPropertyValues(context, entity, valueOrder[0], -1); - context.Add(entity); await context.SaveChangesAsync(); id = entity.Id; @@ -178,28 +179,39 @@ public virtual async Task Can_insert_and_read_back_with_conversions(int[] valueO using (var context = CreateContext()) { - SetPropertyValues(context, await context.Set().SingleAsync(e => e.Id == id), valueOrder[1], valueOrder[0]); + SetPropertyValues(context, await GetAsync(context, id), valueOrder[1], valueOrder[0]); await context.SaveChangesAsync(); } using (var context = CreateContext()) { - SetPropertyValues(context, await context.Set().SingleAsync(e => e.Id == id), valueOrder[2], valueOrder[1]); + SetPropertyValues(context, await GetAsync(context, id), valueOrder[2], valueOrder[1]); await context.SaveChangesAsync(); } using (var context = CreateContext()) { - SetPropertyValues(context, await context.Set().SingleAsync(e => e.Id == id), valueOrder[3], valueOrder[2]); + SetPropertyValues(context, await GetAsync(context, id), valueOrder[3], valueOrder[2]); await context.SaveChangesAsync(); } } - private static void SetPropertyValues(DbContext context, ConvertingEntity entity, int valueIndex, int previousValueIndex) + protected virtual void Add(DbContext context, ConvertingEntity entity) + => context.Add(entity); + + protected virtual Task GetAsync(DbContext context, Guid id) + => context.Set().SingleAsync(e => e.Id == id); + + protected virtual PropertyEntry Property(DbContext context, ConvertingEntity entity, IProperty property) + => context.Entry(entity).Property(property); + + protected virtual ITypeBase FindType(DbContext context) + => context.Model.FindEntityType( + typeof(ConvertingEntity))!; + + private void SetPropertyValues(DbContext context, ConvertingEntity entity, int valueIndex, int previousValueIndex) { - var entry = context.Entry(entity); - foreach (var property in context.Model.FindEntityType( - entity.GetType())!.GetProperties().Where(p => !p.IsPrimaryKey() && !p.IsShadowProperty())) + foreach (var property in FindType(context).GetProperties().Where(p => !p.IsPrimaryKey() && !p.IsShadowProperty())) { var testValues = (property.ClrType == typeof(string) ? StringTestValues[property.GetValueConverter()!.ProviderClrType.UnwrapNullableType()] @@ -211,7 +223,7 @@ private static void SetPropertyValues(DbContext context, ConvertingEntity entity testValues[3] = null; } - var propertyEntry = entry.Property(property); + var propertyEntry = Property(context, entity, property); if (previousValueIndex >= 0 && property.FindAnnotation("Relational:DefaultValue") == null) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs index 123a5a2228e..a2af824e218 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs @@ -680,6 +680,20 @@ public override async Task Entity_splitting_with_owned_json() SELECT TOP(2) [m].[Id], [m].[PropertyInMainTable], [o].[PropertyInOtherTable], [m].[Json] FROM [MyEntity] AS [m] INNER JOIN [OtherTable] AS [o] ON [m].[Id] = [o].[Id] +"""); + } + + public override async Task Value_converter_equality_null_scalar() + { + await base.Value_converter_equality_null_scalar(); + + AssertSql( + """ +@entity_equality_complexType='{"IntToString":"\u003Cnull\u003E"}' (Size = 34) + +SELECT COUNT(*) +FROM [Entities] AS [e] +WHERE [e].[Json] = @entity_equality_complexType """); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs index a8a747d71b8..61c307ab008 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateJsonTypeSqlServerTest.cs @@ -2497,9 +2497,9 @@ public override async Task Add_and_update_nested_optional_primitive_collection(b var parameterSize = value switch { - true => "1558", - false => "1555", - _ => "1557" + true => "1560", + false => "1557", + _ => "1559" }; var updateParameter = value switch @@ -2531,7 +2531,7 @@ public override async Task Add_and_update_nested_optional_primitive_collection(b AssertSql( @"@p0='[{""TestBoolean"":false,""TestBooleanCollection"":[],""TestByte"":0,""TestByteArray"":null,""TestByteCollection"":null,""TestCharacter"":""\u0000"",""TestCharacterCollection"":" + characterCollection - + @",""TestDateOnly"":""0001-01-01"",""TestDateOnlyCollection"":[],""TestDateTime"":""0001-01-01T00:00:00"",""TestDateTimeCollection"":[],""TestDateTimeOffset"":""0001-01-01T00:00:00+00:00"",""TestDateTimeOffsetCollection"":[],""TestDecimal"":0,""TestDecimalCollection"":[],""TestDefaultString"":null,""TestDefaultStringCollection"":[],""TestDouble"":0,""TestDoubleCollection"":[],""TestEnum"":0,""TestEnumCollection"":[],""TestEnumWithIntConverter"":0,""TestEnumWithIntConverterCollection"":[],""TestGuid"":""00000000-0000-0000-0000-000000000000"",""TestGuidCollection"":[],""TestInt16"":0,""TestInt16Collection"":[],""TestInt32"":0,""TestInt32Collection"":[],""TestInt64"":0,""TestInt64Collection"":[],""TestMaxLengthString"":null,""TestMaxLengthStringCollection"":[],""TestNullableEnum"":null,""TestNullableEnumCollection"":[],""TestNullableEnumWithConverterThatHandlesNulls"":null,""TestNullableEnumWithConverterThatHandlesNullsCollection"":[],""TestNullableEnumWithIntConverter"":null,""TestNullableEnumWithIntConverterCollection"":[],""TestNullableInt32"":null,""TestNullableInt32Collection"":[],""TestSignedByte"":0,""TestSignedByteCollection"":[],""TestSingle"":0,""TestSingleCollection"":[],""TestTimeOnly"":""00:00:00.0000000"",""TestTimeOnlyCollection"":[],""TestTimeSpan"":""0:00:00"",""TestTimeSpanCollection"":[],""TestUnsignedInt16"":0,""TestUnsignedInt16Collection"":[],""TestUnsignedInt32"":0,""TestUnsignedInt32Collection"":[],""TestUnsignedInt64"":0,""TestUnsignedInt64Collection"":[]}]' (Nullable = false) (Size = " + + @",""TestDateOnly"":""0001-01-01"",""TestDateOnlyCollection"":[],""TestDateTime"":""0001-01-01T00:00:00"",""TestDateTimeCollection"":[],""TestDateTimeOffset"":""0001-01-01T00:00:00+00:00"",""TestDateTimeOffsetCollection"":[],""TestDecimal"":0,""TestDecimalCollection"":[],""TestDefaultString"":null,""TestDefaultStringCollection"":[],""TestDouble"":0,""TestDoubleCollection"":[],""TestEnum"":0,""TestEnumCollection"":[],""TestEnumWithIntConverter"":0,""TestEnumWithIntConverterCollection"":[],""TestGuid"":""00000000-0000-0000-0000-000000000000"",""TestGuidCollection"":[],""TestInt16"":0,""TestInt16Collection"":[],""TestInt32"":0,""TestInt32Collection"":[],""TestInt64"":0,""TestInt64Collection"":[],""TestMaxLengthString"":null,""TestMaxLengthStringCollection"":[],""TestNullableEnum"":null,""TestNullableEnumCollection"":[],""TestNullableEnumWithConverterThatHandlesNulls"":""Null"",""TestNullableEnumWithConverterThatHandlesNullsCollection"":[],""TestNullableEnumWithIntConverter"":null,""TestNullableEnumWithIntConverterCollection"":[],""TestNullableInt32"":null,""TestNullableInt32Collection"":[],""TestSignedByte"":0,""TestSignedByteCollection"":[],""TestSingle"":0,""TestSingleCollection"":[],""TestTimeOnly"":""00:00:00.0000000"",""TestTimeOnlyCollection"":[],""TestTimeSpan"":""0:00:00"",""TestTimeSpanCollection"":[],""TestUnsignedInt16"":0,""TestUnsignedInt16Collection"":[],""TestUnsignedInt32"":0,""TestUnsignedInt32Collection"":[],""TestUnsignedInt64"":0,""TestUnsignedInt64Collection"":[]}]' (Nullable = false) (Size = " + parameterSize + @") @p1='7624' diff --git a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs index 8d6856d6194..504aad29e14 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Update/JsonUpdateSqlServerTest.cs @@ -2445,9 +2445,9 @@ public override async Task Add_and_update_nested_optional_primitive_collection(b var parameterSize = value switch { - true => "1558", - false => "1555", - _ => "1557" + true => "1560", + false => "1557", + _ => "1559" }; var updateParameterSize = value switch @@ -2466,7 +2466,7 @@ public override async Task Add_and_update_nested_optional_primitive_collection(b var p0 = @"@p0='[{""TestBoolean"":false,""TestBooleanCollection"":[],""TestByte"":0,""TestByteArray"":null,""TestByteCollection"":null,""TestCharacter"":""\u0000"",""TestCharacterCollection"":" + characterCollection -+ @",""TestDateOnly"":""0001-01-01"",""TestDateOnlyCollection"":[],""TestDateTime"":""0001-01-01T00:00:00"",""TestDateTimeCollection"":[],""TestDateTimeOffset"":""0001-01-01T00:00:00+00:00"",""TestDateTimeOffsetCollection"":[],""TestDecimal"":0,""TestDecimalCollection"":[],""TestDefaultString"":null,""TestDefaultStringCollection"":[],""TestDouble"":0,""TestDoubleCollection"":[],""TestEnum"":0,""TestEnumCollection"":[],""TestEnumWithIntConverter"":0,""TestEnumWithIntConverterCollection"":[],""TestGuid"":""00000000-0000-0000-0000-000000000000"",""TestGuidCollection"":[],""TestInt16"":0,""TestInt16Collection"":[],""TestInt32"":0,""TestInt32Collection"":[],""TestInt64"":0,""TestInt64Collection"":[],""TestMaxLengthString"":null,""TestMaxLengthStringCollection"":[],""TestNullableEnum"":null,""TestNullableEnumCollection"":[],""TestNullableEnumWithConverterThatHandlesNulls"":null,""TestNullableEnumWithConverterThatHandlesNullsCollection"":[],""TestNullableEnumWithIntConverter"":null,""TestNullableEnumWithIntConverterCollection"":[],""TestNullableInt32"":null,""TestNullableInt32Collection"":[],""TestSignedByte"":0,""TestSignedByteCollection"":[],""TestSingle"":0,""TestSingleCollection"":[],""TestTimeOnly"":""00:00:00.0000000"",""TestTimeOnlyCollection"":[],""TestTimeSpan"":""0:00:00"",""TestTimeSpanCollection"":[],""TestUnsignedInt16"":0,""TestUnsignedInt16Collection"":[],""TestUnsignedInt32"":0,""TestUnsignedInt32Collection"":[],""TestUnsignedInt64"":0,""TestUnsignedInt64Collection"":[]}]' (Nullable = false) (Size = " ++ @",""TestDateOnly"":""0001-01-01"",""TestDateOnlyCollection"":[],""TestDateTime"":""0001-01-01T00:00:00"",""TestDateTimeCollection"":[],""TestDateTimeOffset"":""0001-01-01T00:00:00+00:00"",""TestDateTimeOffsetCollection"":[],""TestDecimal"":0,""TestDecimalCollection"":[],""TestDefaultString"":null,""TestDefaultStringCollection"":[],""TestDouble"":0,""TestDoubleCollection"":[],""TestEnum"":0,""TestEnumCollection"":[],""TestEnumWithIntConverter"":0,""TestEnumWithIntConverterCollection"":[],""TestGuid"":""00000000-0000-0000-0000-000000000000"",""TestGuidCollection"":[],""TestInt16"":0,""TestInt16Collection"":[],""TestInt32"":0,""TestInt32Collection"":[],""TestInt64"":0,""TestInt64Collection"":[],""TestMaxLengthString"":null,""TestMaxLengthStringCollection"":[],""TestNullableEnum"":null,""TestNullableEnumCollection"":[],""TestNullableEnumWithConverterThatHandlesNulls"":""Null"",""TestNullableEnumWithConverterThatHandlesNullsCollection"":[],""TestNullableEnumWithIntConverter"":null,""TestNullableEnumWithIntConverterCollection"":[],""TestNullableInt32"":null,""TestNullableInt32Collection"":[],""TestSignedByte"":0,""TestSignedByteCollection"":[],""TestSingle"":0,""TestSingleCollection"":[],""TestTimeOnly"":""00:00:00.0000000"",""TestTimeOnlyCollection"":[],""TestTimeSpan"":""0:00:00"",""TestTimeSpanCollection"":[],""TestUnsignedInt16"":0,""TestUnsignedInt16Collection"":[],""TestUnsignedInt32"":0,""TestUnsignedInt32Collection"":[],""TestUnsignedInt64"":0,""TestUnsignedInt64Collection"":[]}]' (Nullable = false) (Size = " + parameterSize + @")"; diff --git a/test/EFCore.SqlServer.FunctionalTests/ValueConvertersEndToEndSqlServerJsonTest.cs b/test/EFCore.SqlServer.FunctionalTests/ValueConvertersEndToEndSqlServerJsonTest.cs new file mode 100644 index 00000000000..0682a962c3a --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/ValueConvertersEndToEndSqlServerJsonTest.cs @@ -0,0 +1,264 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public class ValueConvertersEndToEndSqlServerJsonTest(ValueConvertersEndToEndSqlServerJsonTest.ValueConvertersEndToEndSqlServerJsonFixture fixture) + : ValueConvertersEndToEndTestBase(fixture) +{ + protected override void Add(DbContext context, ConvertingEntity entity) + { + var root = new RootEntity + { + Id = Guid.NewGuid(), + ConvertingEntity = entity + }; + entity.Id = root.Id; + context.Add(root); + } + + protected override async Task GetAsync(DbContext context, Guid id) + => (await context.Set() + .Where(e => e.Id == id) + .SingleAsync()).ConvertingEntity; + + protected override PropertyEntry Property(DbContext context, ConvertingEntity entity, IProperty property) + => context.ChangeTracker.Entries().Single(x => x.Entity.ConvertingEntity == entity).Property(property); + + protected override ITypeBase FindType(DbContext context) + => context.Model.GetEntityTypes().Single(x => x.ClrType == typeof(RootEntity)).GetComplexProperties().Single().ComplexType; + + public class ValueConvertersEndToEndSqlServerJsonFixture : ValueConvertersEndToEndFixtureBase + { + protected override string StoreName => nameof(ValueConvertersEndToEndSqlServerJsonFixture); + + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity().ComplexProperty( + e => e.ConvertingEntity, b => + { + b.ToJson(); + b.Ignore(x => x.Id); + b.Property(e => e.BoolAsChar).HasConversion(new BoolToTwoValuesConverter('O', 'N')); + b.Property(e => e.BoolAsNullableChar).HasConversion(new BoolToTwoValuesConverter('O', 'N')); + b.Property(e => e.NullableBoolAsChar).HasConversion(new BoolToTwoValuesConverter('O', 'N')); + b.Property(e => e.NullableBoolAsNullableChar).HasConversion(new BoolToTwoValuesConverter('O', 'N')); + + b.Property(e => e.BoolAsString).HasConversion(new BoolToStringConverter("Non", "Oui")); + b.Property(e => e.BoolAsNullableString).HasConversion( + new BoolToTwoValuesConverter("Non", "Oui", mappingHints: new ConverterMappingHints(size: 3))); + b.Property(e => e.NullableBoolAsString).HasConversion(new BoolToStringConverter("Non", "Oui")); + b.Property(e => e.NullableBoolAsNullableString).HasConversion( + new BoolToTwoValuesConverter("Non", "Oui", mappingHints: new ConverterMappingHints(size: 3))); + + b.Property(e => e.BoolAsInt).HasConversion(new BoolToZeroOneConverter()); + b.Property(e => e.BoolAsNullableInt).HasConversion(new BoolToZeroOneConverter()); + b.Property(e => e.NullableBoolAsInt).HasConversion(new BoolToZeroOneConverter()); + b.Property(e => e.NullableBoolAsNullableInt).HasConversion(new BoolToZeroOneConverter()); + + b.Property(e => e.IntAsLong).HasConversion(new CastingConverter()); + b.Property(e => e.IntAsNullableLong).HasConversion(new CastingConverter()); + b.Property(e => e.NullableIntAsLong).HasConversion(new CastingConverter()); + b.Property(e => e.NullableIntAsNullableLong).HasConversion(new CastingConverter()); + + b.Property(e => e.BytesAsString).HasConversion( + (ValueConverter)new BytesToStringConverter(), new ArrayStructuralComparer()); + b.Property(e => e.BytesAsNullableString).HasConversion( + (ValueConverter)new BytesToStringConverter(), new ArrayStructuralComparer()); + b.Property(e => e.NullableBytesAsString).HasConversion( + new BytesToStringConverter(), new ArrayStructuralComparer()); + b.Property(e => e.NullableBytesAsNullableString).HasConversion( + new BytesToStringConverter(), new ArrayStructuralComparer()); + + b.Property(e => e.CharAsString).HasConversion(new CharToStringConverter()); + b.Property(e => e.NullableCharAsString).HasConversion(new CharToStringConverter()); + b.Property(e => e.CharAsNullableString).HasConversion(new CharToStringConverter()); + b.Property(e => e.NullableCharAsNullableString).HasConversion(new CharToStringConverter()); + + b.Property(e => e.DateTimeOffsetToBinary).HasConversion(new DateTimeOffsetToBinaryConverter()); + b.Property(e => e.DateTimeOffsetToNullableBinary).HasConversion(new DateTimeOffsetToBinaryConverter()); + b.Property(e => e.NullableDateTimeOffsetToBinary).HasConversion(new DateTimeOffsetToBinaryConverter()); + b.Property(e => e.NullableDateTimeOffsetToNullableBinary).HasConversion(new DateTimeOffsetToBinaryConverter()); + + b.Property(e => e.DateTimeOffsetToString).HasConversion(new DateTimeOffsetToStringConverter()); + b.Property(e => e.DateTimeOffsetToNullableString).HasConversion(new DateTimeOffsetToStringConverter()); + b.Property(e => e.NullableDateTimeOffsetToString).HasConversion(new DateTimeOffsetToStringConverter()); + b.Property(e => e.NullableDateTimeOffsetToNullableString).HasConversion(new DateTimeOffsetToStringConverter()); + + b.Property(e => e.DateTimeToBinary).HasConversion(new DateTimeToBinaryConverter()); + b.Property(e => e.DateTimeToNullableBinary).HasConversion(new DateTimeToBinaryConverter()); + b.Property(e => e.NullableDateTimeToBinary).HasConversion(new DateTimeToBinaryConverter()); + b.Property(e => e.NullableDateTimeToNullableBinary).HasConversion(new DateTimeToBinaryConverter()); + + b.Property(e => e.DateTimeToString).HasConversion(new DateTimeToStringConverter()); + b.Property(e => e.DateTimeToNullableString).HasConversion(new DateTimeToStringConverter()); + b.Property(e => e.NullableDateTimeToString).HasConversion(new DateTimeToStringConverter()); + b.Property(e => e.NullableDateTimeToNullableString).HasConversion(new DateTimeToStringConverter()); + + b.Property(e => e.DateOnlyToString).HasConversion(new DateOnlyToStringConverter()); + b.Property(e => e.DateOnlyToNullableString).HasConversion(new DateOnlyToStringConverter()); + b.Property(e => e.NullableDateOnlyToString).HasConversion(new DateOnlyToStringConverter()); + b.Property(e => e.NullableDateOnlyToNullableString).HasConversion(new DateOnlyToStringConverter()); + + b.Property(e => e.EnumToString).HasConversion(new EnumToStringConverter()); + b.Property(e => e.EnumToNullableString).HasConversion(new EnumToStringConverter()); + b.Property(e => e.NullableEnumToString).HasConversion(new EnumToStringConverter()); + b.Property(e => e.NullableEnumToNullableString).HasConversion(new EnumToStringConverter()); + + b.Property(e => e.EnumToNumber).HasConversion(new EnumToNumberConverter()); + b.Property(e => e.EnumToNullableNumber).HasConversion(new EnumToNumberConverter()); + b.Property(e => e.NullableEnumToNumber).HasConversion(new EnumToNumberConverter()); + b.Property(e => e.NullableEnumToNullableNumber).HasConversion(new EnumToNumberConverter()); + + b.Property(e => e.GuidToString).HasConversion(new GuidToStringConverter()); + b.Property(e => e.GuidToNullableString).HasConversion(new GuidToStringConverter()); + b.Property(e => e.NullableGuidToString).HasConversion(new GuidToStringConverter()); + b.Property(e => e.NullableGuidToNullableString).HasConversion(new GuidToStringConverter()); + + b.Property(e => e.GuidToBytes).HasConversion(new GuidToBytesConverter()); + b.Property(e => e.GuidToNullableBytes).HasConversion(new GuidToBytesConverter()); + b.Property(e => e.NullableGuidToBytes).HasConversion(new GuidToBytesConverter()); + b.Property(e => e.NullableGuidToNullableBytes).HasConversion(new GuidToBytesConverter()); + + b.Property(e => e.IPAddressToString).HasConversion((ValueConverter)new IPAddressToStringConverter()); + b.Property(e => e.IPAddressToNullableString).HasConversion((ValueConverter)new IPAddressToStringConverter()); + b.Property(e => e.NullableIPAddressToString).HasConversion(new IPAddressToStringConverter()); + b.Property(e => e.NullableIPAddressToNullableString).HasConversion(new IPAddressToStringConverter()); + + b.Property(e => e.IPAddressToBytes).HasConversion((ValueConverter)new IPAddressToBytesConverter()); + b.Property(e => e.IPAddressToNullableBytes).HasConversion((ValueConverter)new IPAddressToBytesConverter()); + b.Property(e => e.NullableIPAddressToBytes).HasConversion(new IPAddressToBytesConverter()); + b.Property(e => e.NullableIPAddressToNullableBytes).HasConversion(new IPAddressToBytesConverter()); + + b.Property(e => e.PhysicalAddressToString).HasConversion((ValueConverter)new PhysicalAddressToStringConverter()); + b.Property(e => e.PhysicalAddressToNullableString) + .HasConversion((ValueConverter)new PhysicalAddressToStringConverter()); + b.Property(e => e.NullablePhysicalAddressToString).HasConversion(new PhysicalAddressToStringConverter()); + b.Property(e => e.NullablePhysicalAddressToNullableString).HasConversion(new PhysicalAddressToStringConverter()); + + b.Property(e => e.PhysicalAddressToBytes).HasConversion((ValueConverter)new PhysicalAddressToBytesConverter()); + b.Property(e => e.PhysicalAddressToNullableBytes) + .HasConversion((ValueConverter)new PhysicalAddressToBytesConverter()); + b.Property(e => e.NullablePhysicalAddressToBytes).HasConversion(new PhysicalAddressToBytesConverter()); + b.Property(e => e.NullablePhysicalAddressToNullableBytes).HasConversion(new PhysicalAddressToBytesConverter()); + + b.Property(e => e.NumberToString).HasConversion(new NumberToStringConverter()); + b.Property(e => e.NumberToNullableString).HasConversion(new NumberToStringConverter()); + b.Property(e => e.NullableNumberToString).HasConversion(new NumberToStringConverter()); + b.Property(e => e.NullableNumberToNullableString).HasConversion(new NumberToStringConverter()); + + b.Property(e => e.NumberToBytes).HasConversion(new NumberToBytesConverter()); + b.Property(e => e.NumberToNullableBytes).HasConversion(new NumberToBytesConverter()); + b.Property(e => e.NullableNumberToBytes).HasConversion(new NumberToBytesConverter()); + b.Property(e => e.NullableNumberToNullableBytes).HasConversion(new NumberToBytesConverter()); + + b.Property(e => e.StringToBool).HasConversion(new StringToBoolConverter()); + b.Property(e => e.StringToNullableBool).HasConversion(new StringToBoolConverter()); + b.Property(e => e.NullableStringToBool).HasConversion((ValueConverter)new StringToBoolConverter()); + b.Property(e => e.NullableStringToNullableBool).HasConversion((ValueConverter)new StringToBoolConverter()); + + b.Property(e => e.StringToBytes).HasConversion((ValueConverter)new StringToBytesConverter(Encoding.UTF32)); + b.Property(e => e.StringToNullableBytes).HasConversion((ValueConverter)new StringToBytesConverter(Encoding.UTF32)); + b.Property(e => e.NullableStringToBytes).HasConversion(new StringToBytesConverter(Encoding.UTF32)); + b.Property(e => e.NullableStringToNullableBytes).HasConversion(new StringToBytesConverter(Encoding.UTF32)); + + b.Property(e => e.StringToChar).HasConversion(new StringToCharConverter()); + b.Property(e => e.StringToNullableChar).HasConversion(new StringToCharConverter()); + b.Property(e => e.NullableStringToChar).HasConversion((ValueConverter)new StringToCharConverter()); + b.Property(e => e.NullableStringToNullableChar).HasConversion((ValueConverter)new StringToCharConverter()); + + b.Property(e => e.StringToDateTime).HasConversion(new StringToDateTimeConverter()); + b.Property(e => e.StringToNullableDateTime).HasConversion(new StringToDateTimeConverter()); + b.Property(e => e.NullableStringToDateTime).HasConversion((ValueConverter)new StringToDateTimeConverter()); + b.Property(e => e.NullableStringToNullableDateTime).HasConversion((ValueConverter)new StringToDateTimeConverter()); + + b.Property(e => e.StringToDateTimeOffset).HasConversion(new StringToDateTimeOffsetConverter()); + b.Property(e => e.StringToNullableDateTimeOffset).HasConversion(new StringToDateTimeOffsetConverter()); + b.Property(e => e.NullableStringToDateTimeOffset) + .HasConversion((ValueConverter)new StringToDateTimeOffsetConverter()); + b.Property(e => e.NullableStringToNullableDateTimeOffset) + .HasConversion((ValueConverter)new StringToDateTimeOffsetConverter()); + + b.Property(e => e.StringToEnum).HasConversion(new StringToEnumConverter()); + b.Property(e => e.StringToNullableEnum).HasConversion(new StringToEnumConverter()); + b.Property(e => e.NullableStringToEnum).HasConversion((ValueConverter)new StringToEnumConverter()); + b.Property(e => e.NullableStringToNullableEnum) + .HasConversion((ValueConverter)new StringToEnumConverter()); + + b.Property(e => e.StringToGuid).HasConversion(new StringToGuidConverter()); + b.Property(e => e.StringToNullableGuid).HasConversion(new StringToGuidConverter()); + b.Property(e => e.NullableStringToGuid).HasConversion((ValueConverter)new StringToGuidConverter()); + b.Property(e => e.NullableStringToNullableGuid).HasConversion((ValueConverter)new StringToGuidConverter()); + + b.Property(e => e.StringToNumber).HasConversion(new StringToNumberConverter()); + b.Property(e => e.StringToNullableNumber).HasConversion(new StringToNumberConverter()); + b.Property(e => e.NullableStringToNumber).HasConversion((ValueConverter)new StringToNumberConverter()); + b.Property(e => e.NullableStringToNullableNumber) + .HasConversion((ValueConverter)new StringToNumberConverter()); + + b.Property(e => e.StringToTimeSpan).HasConversion(new StringToTimeSpanConverter()); + b.Property(e => e.StringToNullableTimeSpan).HasConversion(new StringToTimeSpanConverter()); + b.Property(e => e.NullableStringToTimeSpan).HasConversion((ValueConverter)new StringToTimeSpanConverter()); + b.Property(e => e.NullableStringToNullableTimeSpan).HasConversion((ValueConverter)new StringToTimeSpanConverter()); + + b.Property(e => e.TimeSpanToTicks).HasConversion(new TimeSpanToTicksConverter()); + b.Property(e => e.TimeSpanToNullableTicks).HasConversion(new TimeSpanToTicksConverter()); + b.Property(e => e.NullableTimeSpanToTicks).HasConversion(new TimeSpanToTicksConverter()); + b.Property(e => e.NullableTimeSpanToNullableTicks).HasConversion(new TimeSpanToTicksConverter()); + + b.Property(e => e.TimeSpanToString).HasConversion(new TimeSpanToStringConverter()); + b.Property(e => e.TimeSpanToNullableString).HasConversion(new TimeSpanToStringConverter()); + b.Property(e => e.NullableTimeSpanToString).HasConversion(new TimeSpanToStringConverter()); + b.Property(e => e.NullableTimeSpanToNullableString).HasConversion(new TimeSpanToStringConverter()); + + b.Property(e => e.UriToString).HasConversion((ValueConverter)new UriToStringConverter()); + b.Property(e => e.UriToNullableString).HasConversion((ValueConverter)new UriToStringConverter()); + b.Property(e => e.NullableUriToString).HasConversion(new UriToStringConverter()); + b.Property(e => e.NullableUriToNullableString).HasConversion(new UriToStringConverter()); + + b.Property(e => e.NonNullIntToNullString).HasConversion(new NonNullIntToNullStringConverter()); + b.Property(e => e.NonNullIntToNonNullString).HasConversion(new NonNullIntToNonNullStringConverter()); + b.Property(e => e.NullIntToNullString).HasConversion(new NullIntToNullStringConverter()).IsRequired(false); + b.Property(e => e.NullIntToNonNullString).HasConversion(new NullIntToNonNullStringConverter()).IsRequired(false); + + b.Property(e => e.NullStringToNonNullString).HasConversion(new NullStringToNonNullStringConverter()).IsRequired(); + b.Property(e => e.NonNullStringToNullString).HasConversion(new NonNullStringToNullStringConverter()) + .IsRequired(false); + + b.Property(e => e.NullableListOfInt).HasConversion( + (ValueConverter?)new ListOfIntToJsonConverter(), new ListOfIntComparer()); + + b.Property(e => e.ListOfInt).HasConversion( + new ListOfIntToJsonConverter(), new ListOfIntComparer()); + + b.Property(e => e.NullableEnumerableOfInt).HasConversion( + (ValueConverter?)new EnumerableOfIntToJsonConverter(), new EnumerableOfIntComparer()); + + b.Property(e => e.EnumerableOfInt).HasConversion( + new EnumerableOfIntToJsonConverter(), new EnumerableOfIntComparer()); + }); + + var complexType = modelBuilder.Model.GetEntityTypes().Single(x => x.ClrType == typeof(RootEntity)).GetComplexProperties().Single().ComplexType; + foreach (var property in complexType.GetProperties()) + { + if (property.GetValueConverter() is null) + { + Assert.Fail("All properties should have a value converter configured"); + } + } + } + } + + protected class RootEntity + { + public Guid Id { get; set; } + + public ConvertingEntity ConvertingEntity { get; set; } = null!; + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Update/JsonUpdateSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Update/JsonUpdateSqliteTest.cs index 3fb88eaccc1..bece74e6b91 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Update/JsonUpdateSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Update/JsonUpdateSqliteTest.cs @@ -2295,9 +2295,9 @@ public override async Task Add_and_update_nested_optional_primitive_collection(b var parameterSize = value switch { - true => "1562", - false => "1559", - _ => "1561" + true => "1564", + false => "1561", + _ => "1563" }; var updateParameter = value switch @@ -2310,7 +2310,7 @@ public override async Task Add_and_update_nested_optional_primitive_collection(b AssertSql( @"@p0='[{""TestBoolean"":false,""TestBooleanCollection"":[],""TestByte"":0,""TestByteArray"":null,""TestByteCollection"":null,""TestCharacter"":""\u0000"",""TestCharacterCollection"":" + characterCollection - + @",""TestDateOnly"":""0001-01-01"",""TestDateOnlyCollection"":[],""TestDateTime"":""0001-01-01 00:00:00"",""TestDateTimeCollection"":[],""TestDateTimeOffset"":""0001-01-01 00:00:00+00:00"",""TestDateTimeOffsetCollection"":[],""TestDecimal"":""0.0"",""TestDecimalCollection"":[],""TestDefaultString"":null,""TestDefaultStringCollection"":[],""TestDouble"":0,""TestDoubleCollection"":[],""TestEnum"":0,""TestEnumCollection"":[],""TestEnumWithIntConverter"":0,""TestEnumWithIntConverterCollection"":[],""TestGuid"":""00000000-0000-0000-0000-000000000000"",""TestGuidCollection"":[],""TestInt16"":0,""TestInt16Collection"":[],""TestInt32"":0,""TestInt32Collection"":[],""TestInt64"":0,""TestInt64Collection"":[],""TestMaxLengthString"":null,""TestMaxLengthStringCollection"":[],""TestNullableEnum"":null,""TestNullableEnumCollection"":[],""TestNullableEnumWithConverterThatHandlesNulls"":null,""TestNullableEnumWithConverterThatHandlesNullsCollection"":[],""TestNullableEnumWithIntConverter"":null,""TestNullableEnumWithIntConverterCollection"":[],""TestNullableInt32"":null,""TestNullableInt32Collection"":[],""TestSignedByte"":0,""TestSignedByteCollection"":[],""TestSingle"":0,""TestSingleCollection"":[],""TestTimeOnly"":""00:00:00.0000000"",""TestTimeOnlyCollection"":[],""TestTimeSpan"":""0:00:00"",""TestTimeSpanCollection"":[],""TestUnsignedInt16"":0,""TestUnsignedInt16Collection"":[],""TestUnsignedInt32"":0,""TestUnsignedInt32Collection"":[],""TestUnsignedInt64"":0,""TestUnsignedInt64Collection"":[]}]' (Nullable = false) (Size = " + + @",""TestDateOnly"":""0001-01-01"",""TestDateOnlyCollection"":[],""TestDateTime"":""0001-01-01 00:00:00"",""TestDateTimeCollection"":[],""TestDateTimeOffset"":""0001-01-01 00:00:00+00:00"",""TestDateTimeOffsetCollection"":[],""TestDecimal"":""0.0"",""TestDecimalCollection"":[],""TestDefaultString"":null,""TestDefaultStringCollection"":[],""TestDouble"":0,""TestDoubleCollection"":[],""TestEnum"":0,""TestEnumCollection"":[],""TestEnumWithIntConverter"":0,""TestEnumWithIntConverterCollection"":[],""TestGuid"":""00000000-0000-0000-0000-000000000000"",""TestGuidCollection"":[],""TestInt16"":0,""TestInt16Collection"":[],""TestInt32"":0,""TestInt32Collection"":[],""TestInt64"":0,""TestInt64Collection"":[],""TestMaxLengthString"":null,""TestMaxLengthStringCollection"":[],""TestNullableEnum"":null,""TestNullableEnumCollection"":[],""TestNullableEnumWithConverterThatHandlesNulls"":""Null"",""TestNullableEnumWithConverterThatHandlesNullsCollection"":[],""TestNullableEnumWithIntConverter"":null,""TestNullableEnumWithIntConverterCollection"":[],""TestNullableInt32"":null,""TestNullableInt32Collection"":[],""TestSignedByte"":0,""TestSignedByteCollection"":[],""TestSingle"":0,""TestSingleCollection"":[],""TestTimeOnly"":""00:00:00.0000000"",""TestTimeOnlyCollection"":[],""TestTimeSpan"":""0:00:00"",""TestTimeSpanCollection"":[],""TestUnsignedInt16"":0,""TestUnsignedInt16Collection"":[],""TestUnsignedInt32"":0,""TestUnsignedInt32Collection"":[],""TestUnsignedInt64"":0,""TestUnsignedInt64Collection"":[]}]' (Nullable = false) (Size = " + parameterSize + @") @p1='7624'