Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3188,6 +3188,56 @@ private Expression CreateReadJsonPropertyValueExpression(
resultExpression = Convert(resultExpression, property.ClrType);
}

var converter = property.GetTypeMapping().Converter;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is copied over from above for CreateGetValueExpression

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<object, object>.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(
Expand All @@ -3196,7 +3246,7 @@ private Expression CreateReadJsonPropertyValueExpression(
Utf8JsonReaderManagerCurrentReaderField),
Utf8JsonReaderTokenTypeProperty),
Constant(JsonTokenType.Null)),
Default(property.ClrType),
nullExpression,
resultExpression);
}

Expand Down
4 changes: 2 additions & 2 deletions src/EFCore.Relational/Update/ModificationCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
14 changes: 13 additions & 1 deletion src/EFCore/Storage/Json/JsonConvertedValueReaderWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,25 @@ public JsonConvertedValueReaderWriter(
_converter = converter;
}

/// <inheritdoc />
public override bool HandlesNullWrites => _converter.ConvertsNulls;

/// <inheritdoc />
public override TModel FromJsonTyped(ref Utf8JsonReaderManager manager, object? existingObject = null)
=> (TModel)_converter.ConvertFromProvider(_providerReaderWriter.FromJsonTyped(ref manager, existingObject))!;

/// <inheritdoc />
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;
Expand Down
11 changes: 10 additions & 1 deletion src/EFCore/Storage/Json/JsonValueReaderWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ internal JsonValueReaderWriter()
{
}

/// <summary>
/// If <see langword="true" />, then the nulls will be passed to the writer's <see cref="ToJson"/> method. Otherwise null
/// values will always be written as <see langword="null"/>.
/// </summary>
/// <remarks>
/// The default is <see langword="false" />.
/// </remarks>
public virtual bool HandlesNullWrites { get; } = false;

/// <summary>
/// Reads the value from a UTF8 JSON stream or buffer.
/// </summary>
Expand Down Expand Up @@ -48,7 +57,7 @@ internal JsonValueReaderWriter()
/// </summary>
/// <param name="writer">The <see cref="Utf8JsonWriter" /> into which the value should be written.</param>
/// <param name="value">The value to write.</param>
public abstract void ToJson(Utf8JsonWriter writer, object value);
public abstract void ToJson(Utf8JsonWriter writer, object? value);

/// <summary>
/// The type of the value being read/written.
Expand Down
11 changes: 9 additions & 2 deletions src/EFCore/Storage/Json/JsonValueReaderWriter`.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,15 @@ public sealed override object FromJson(ref Utf8JsonReaderManager manager, object
=> FromJsonTyped(ref manager, existingObject)!;

/// <inheritdoc />
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!);
}

/// <inheritdoc />
public sealed override Type ValueType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Context37983>(
onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings),
onModelCreating: m => m.Entity<Context37983.Entity>().ComplexProperty(e => e.Json, b =>
{
b.ToJson();

b.Property(j => j.IntToString).HasConversion(new Context37983_StringToIntConverter());
}),
seed: context =>
{
context.Set<Context37983.Entity>().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<Context37983.Entity>().CountAsync(e => e.Json == complexType));
}

protected class Context37983(DbContextOptions options) : DbContext(options)
{
public DbSet<Entity> 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<int?, string>
{
public Context37983_StringToIntConverter()
: base(
v => v == null ? "<null>" : v.ToString(),
v => int.Parse(v))
{
}

public override bool ConvertsNulls => true;
}
Comment thread
JoasE marked this conversation as resolved.

#endregion

protected TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,38 +168,50 @@ 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;
}

using (var context = CreateContext())
{
SetPropertyValues(context, await context.Set<ConvertingEntity>().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<ConvertingEntity>().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<ConvertingEntity>().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<ConvertingEntity> GetAsync(DbContext context, Guid id)
=> context.Set<ConvertingEntity>().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()]
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
""");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
Loading
Loading