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
15 changes: 15 additions & 0 deletions src/EFCore/ChangeTracking/Internal/ChangeDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,21 @@ public virtual bool DetectComplexPropertyChange(InternalEntryBase entry, IComple

if ((currentValue is null) != (originalValue is null))
{
// Set the discriminator value for the complex type when transitioning from null to non-null or vice versa.
// The discriminator is a shadow property whose value needs to be updated to reflect the new state.
var discriminatorProperty = complexProperty.ComplexType.FindDiscriminatorProperty();
if (discriminatorProperty != null)
{
if (currentValue is not null)
{
entry[discriminatorProperty] = complexProperty.ComplexType.GetDiscriminatorValue();
}
else if (discriminatorProperty.IsShadowProperty())
{
entry[discriminatorProperty] = discriminatorProperty.ClrType.GetDefaultValue();
}
}

// If it changed from null to non-null or from non-null to null, mark all inner properties as modified
// to ensure the entity is detected as modified and the complex type properties are persisted
foreach (var innerProperty in complexProperty.ComplexType.GetFlattenedProperties())
Expand Down
18 changes: 18 additions & 0 deletions src/EFCore/Metadata/Internal/InternalComplexTypeBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,24 @@ public virtual bool CanSetServiceOnlyConstructorBinding(
=> configurationSource.Overrides(Metadata.GetServiceOnlyConstructorBindingConfigurationSource())
|| Metadata.ServiceOnlyConstructorBinding == constructorBinding;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
protected override InternalPropertyBuilder? GetOrCreateDiscriminatorProperty(
Type? type,
string? name,
MemberInfo? memberInfo,
ConfigurationSource configurationSource)
{
var builder = base.GetOrCreateDiscriminatorProperty(type, name, memberInfo, configurationSource);
builder?.AfterSave(PropertySaveBehavior.Save, ConfigurationSource.Convention);

return builder;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
4 changes: 3 additions & 1 deletion src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2145,7 +2145,9 @@ bool PreserveConvertNode(Expression expression)
if (visited != expression)
{
parameterName = QueryFilterPrefix
+ (RemoveConvert(expression) is MemberExpression { Member.Name: var memberName } ? ("__" + memberName) : "__p");
+ (RemoveConvert(expression) is MemberExpression { Member.Name: var memberName }
? "__" + SanitizeCompilerGeneratedName(memberName)
: "__p");
isContextAccessor = true;

// Context accessors (query filters accessing the context) never get constantized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ SELECT VALUE c
FROM root c
WHERE (c["AllOptionalsComplexType"] = null)
OFFSET 0 LIMIT 2
""",
//
"""
SELECT VALUE c
FROM root c
""");
}

Expand Down Expand Up @@ -103,6 +108,115 @@ OFFSET 0 LIMIT 2
""");
}

public override async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip()
{
await base.Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip();

AssertSql(
"""
SELECT VALUE c
FROM root c
OFFSET 0 LIMIT 2
""",
//
"""
SELECT VALUE c
FROM root c
OFFSET 0 LIMIT 2
""");
}

public override async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip()
{
await base.Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip();

AssertSql(
"""
SELECT VALUE c
FROM root c
OFFSET 0 LIMIT 2
""",
//
"""
SELECT VALUE c
FROM root c
OFFSET 0 LIMIT 2
""");
}

public override async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip()
{
await base.Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip();

AssertSql(
"""
SELECT VALUE c
FROM root c
OFFSET 0 LIMIT 2
""",
//
"""
SELECT VALUE c
FROM root c
OFFSET 0 LIMIT 2
""");
}

public override async Task Nullable_complex_type_with_discriminator_set_to_different_value()
{
await base.Nullable_complex_type_with_discriminator_set_to_different_value();
}

public override async Task Nullable_complex_type_with_discriminator_set_to_null()
{
// On Cosmos, setting the discriminator shadow property to null doesn't affect materialization
// because the complex property's data is still present in the JSON document.
var contextFactory = await InitializeNonSharedTest<Context38119>();

Guid entityId;
await using (var context = contextFactory.CreateDbContext())
{
var entity = new Context38119.EntityType
{
Id = Guid.NewGuid(),
Prop = new Context38119.OptionalComplexProperty { OptionalValue = true }
};
context.Add(entity);
entityId = entity.Id;

var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator");
Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue);
discriminatorEntry.CurrentValue = null;
await context.SaveChangesAsync();
}

await using (var context = contextFactory.CreateDbContext())
{
var entity = await context.Set<Context38119.EntityType>().SingleAsync(e => e.Id == entityId);
Assert.NotNull(entity.Prop);
Assert.True(entity.Prop.OptionalValue);
}

}

public override async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip()
{
await base.Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip();

AssertSql(
"""
SELECT VALUE c
FROM root c
OFFSET 0 LIMIT 2
""",
//
"""
SELECT VALUE c
FROM root c
OFFSET 0 LIMIT 2
""");
}

protected override DbContextOptionsBuilder AddNonSharedOptions(DbContextOptionsBuilder builder)
=> base.AddNonSharedOptions(builder)
.ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
Expand Down
Loading
Loading