Skip to content

Commit ea319f3

Browse files
CopilotAndriySvyryd
authored andcommitted
Fix owned entities with default values not saved in TPH with shared columns
Fixes #37525
1 parent 2f5c7f3 commit ea319f3

5 files changed

Lines changed: 164 additions & 29 deletions

File tree

src/EFCore.Relational/Update/ModificationCommand.cs

Lines changed: 70 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ void HandleColumn(
407407
{
408408
// Note that for stored procedures we always need to send all parameters, regardless of whether the property
409409
// actually changed.
410-
writeValue = columnPropagator?.TryPropagate(columnMapping, entry)
410+
writeValue = !columnPropagator?.TryPropagate(columnMapping, entry)
411411
?? (entry.EntityState == EntityState.Added
412412
|| entry.EntityState == EntityState.Deleted
413413
|| ColumnModification.IsModified(entry, property)
@@ -1217,16 +1217,24 @@ public void RecordValue(IColumnMapping mapping, IUpdateEntry entry)
12171217

12181218
break;
12191219
case EntityState.Added:
1220+
var currentProviderValue = Update.ColumnModification.GetCurrentProviderValue(entry, property);
1221+
if (_originalValueInitialized
1222+
&& mapping.Column.ProviderValueComparer.Equals(_originalValue, currentProviderValue))
1223+
{
1224+
// This Added entry has the same value as the original (pre-modification) value.
1225+
// It will receive the new value via TryPropagate, so don't let it reset _currentValue or _write.
1226+
break;
1227+
}
1228+
12201229
if (_currentValue == null
12211230
|| !property.GetValueComparer().Equals(
12221231
Update.ColumnModification.GetCurrentValue(entry, property),
12231232
property.Sentinel))
12241233
{
1225-
_currentValue = Update.ColumnModification.GetCurrentProviderValue(entry, property);
1234+
_currentValue = currentProviderValue;
12261235
}
12271236

1228-
_write = !_originalValueInitialized
1229-
|| !mapping.Column.ProviderValueComparer.Equals(_originalValue, _currentValue);
1237+
_write = true;
12301238

12311239
break;
12321240
case EntityState.Deleted:
@@ -1243,40 +1251,73 @@ public void RecordValue(IColumnMapping mapping, IUpdateEntry entry)
12431251
}
12441252
}
12451253

1254+
/// <summary>
1255+
/// Attempts to propagate the recorded value from other enties in this command to the specified entry.
1256+
/// </summary>
1257+
/// <param name="mapping">The column mapping for which the value may be propagated.</param>
1258+
/// <param name="entry">The entry that may receive the propagated value.</param>
1259+
/// <returns>
1260+
/// <see langword="true" /> if no value was recorded (column should not be written), or if the value was
1261+
/// successfully propagated to <paramref name="entry" />.
1262+
/// <see langword="false" /> if a value was recorded for this column (i.e. the column should be written)
1263+
/// but propagation to <paramref name="entry" /> was skipped because the entry is the source of the write;
1264+
/// </returns>
12461265
public bool TryPropagate(IColumnMappingBase mapping, IUpdateEntry entry)
12471266
{
1267+
if (!_write)
1268+
{
1269+
return true;
1270+
}
1271+
12481272
var property = mapping.Property;
1249-
if (_write
1250-
&& (entry.EntityState == EntityState.Unchanged
1251-
|| (entry.EntityState == EntityState.Modified && !Update.ColumnModification.IsModified(entry, property))
1252-
|| (entry.EntityState == EntityState.Added
1253-
&& ((!_originalValueInitialized
1254-
&& property.GetValueComparer().Equals(
1255-
Update.ColumnModification.GetCurrentValue(entry, property),
1256-
property.Sentinel))
1257-
|| (_originalValueInitialized
1258-
&& mapping.Column.ProviderValueComparer.Equals(
1259-
Update.ColumnModification.GetCurrentProviderValue(entry, property),
1260-
_originalValue))))))
1273+
var shouldPropagate = entry.EntityState switch
12611274
{
1262-
if ((property.GetAfterSaveBehavior() == PropertySaveBehavior.Save
1263-
|| entry.EntityState == EntityState.Added)
1264-
&& property.ValueGenerated != ValueGenerated.Never)
1265-
{
1266-
var value = _currentValue;
1267-
var converter = property.GetTypeMapping().Converter;
1268-
if (converter != null)
1269-
{
1270-
value = converter.ConvertFromProvider(value);
1271-
}
1275+
EntityState.Unchanged => true,
1276+
EntityState.Modified => !Update.ColumnModification.IsModified(entry, property),
1277+
EntityState.Added => ShouldPropagateForAddedEntry(mapping, entry, property),
1278+
_ => false
1279+
};
1280+
1281+
if (!shouldPropagate)
1282+
{
1283+
return false;
1284+
}
12721285

1273-
Update.ColumnModification.SetStoreGeneratedValue(entry, property, value);
1286+
if ((property.GetAfterSaveBehavior() == PropertySaveBehavior.Save
1287+
|| entry.EntityState == EntityState.Added)
1288+
&& property.ValueGenerated != ValueGenerated.Never)
1289+
{
1290+
var value = _currentValue;
1291+
var converter = property.GetTypeMapping().Converter;
1292+
if (converter != null)
1293+
{
1294+
value = converter.ConvertFromProvider(value);
12741295
}
12751296

1276-
return false;
1297+
Update.ColumnModification.SetStoreGeneratedValue(entry, property, value);
12771298
}
12781299

1279-
return _write;
1300+
return true;
1301+
1302+
bool ShouldPropagateForAddedEntry(IColumnMappingBase mapping, IUpdateEntry entry, IProperty property)
1303+
{
1304+
if (!_originalValueInitialized)
1305+
{
1306+
// All entries are in the Added state, this entry still has the sentinel (default) value
1307+
// and the propagated value is different — accept propagation
1308+
return property.GetValueComparer().Equals(
1309+
Update.ColumnModification.GetCurrentValue(entry, property),
1310+
property.Sentinel)
1311+
&& !mapping.Column.ProviderValueComparer.Equals(
1312+
_currentValue,
1313+
Update.ColumnModification.GetCurrentProviderValue(entry, property));
1314+
}
1315+
1316+
// Entry's current value matches the original — it's stale, accept the new propagated value
1317+
return mapping.Column.ProviderValueComparer.Equals(
1318+
Update.ColumnModification.GetCurrentProviderValue(entry, property),
1319+
_originalValue);
1320+
}
12801321
}
12811322
}
12821323
}

test/EFCore.Relational.Specification.Tests/Update/UpdatesRelationalTestBase.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,30 @@ public virtual Task Update_non_indexed_values()
260260
});
261261
}
262262

263+
[ConditionalTheory, InlineData(false), InlineData(true)] // Issue #37525
264+
public virtual async Task Can_save_owned_entity_with_default_values_in_TPH_with_shared_columns(bool async)
265+
=> await ExecuteWithStrategyInTransactionAsync(
266+
async context =>
267+
{
268+
var entity = new CrunchyNougat { Name = "Test" };
269+
context.Add(entity);
270+
_ = async ? await context.SaveChangesAsync() : context.SaveChanges();
271+
},
272+
async context =>
273+
{
274+
var entity = await context.Set<CrunchyNougat>().SingleAsync();
275+
Assert.Null(entity.Filling);
276+
entity.Filling = new NougatFilling();
277+
_ = async ? await context.SaveChangesAsync() : context.SaveChanges();
278+
},
279+
async context =>
280+
{
281+
var entity = await context.Set<CrunchyNougat>().SingleAsync();
282+
Assert.NotNull(entity.Filling);
283+
Assert.Equal(NougatFillingKind.Unknown, entity.Filling.Kind);
284+
Assert.False(entity.Filling.IsFresh);
285+
});
286+
263287
[ConditionalFact]
264288
public abstract void Identifiers_are_generated_correctly();
265289

@@ -301,6 +325,23 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
301325
.HasColumnName("ZipCode");
302326
});
303327

328+
modelBuilder.Entity<CrunchyNougat>(b =>
329+
{
330+
b.OwnsOne(e => e.Filling, ob =>
331+
{
332+
ob.Property(o => o.Kind).HasColumnName("FillingKind");
333+
ob.Property(o => o.IsFresh).HasColumnName("FillingIsFresh");
334+
});
335+
});
336+
modelBuilder.Entity<SoftNougat>(b =>
337+
{
338+
b.OwnsOne(e => e.Filling, ob =>
339+
{
340+
ob.Property(o => o.Kind).HasColumnName("FillingKind");
341+
ob.Property(o => o.IsFresh).HasColumnName("FillingIsFresh");
342+
});
343+
});
344+
304345
modelBuilder
305346
.Entity<
306347
LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectlyDetails
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.EntityFrameworkCore.TestModels.UpdatesModel;
5+
6+
#nullable disable
7+
8+
public abstract class Nougat
9+
{
10+
public int Id { get; set; }
11+
public string Name { get; set; }
12+
}
13+
14+
public class CrunchyNougat : Nougat
15+
{
16+
public NougatFilling Filling { get; set; }
17+
}
18+
19+
public class SoftNougat : Nougat
20+
{
21+
public NougatFilling Filling { get; set; }
22+
}
23+
24+
public class NougatFilling
25+
{
26+
public NougatFillingKind Kind { get; set; }
27+
public bool IsFresh { get; set; }
28+
}
29+
30+
public enum NougatFillingKind
31+
{
32+
Unknown = 0,
33+
Peanut = 1,
34+
Almond = 2,
35+
}

test/EFCore.Specification.Tests/TestModels/UpdatesModel/UpdatesContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public class UpdatesContext(DbContextOptions options) : PoolableDbContext(option
1515
public DbSet<ProductTableWithView> ProductTable { get; set; } = null!;
1616
public DbSet<ProductTableView> ProductTableView { get; set; } = null!;
1717
public DbSet<Rodney> Trotters { get; set; } = null!;
18+
public DbSet<CrunchyNougat> CrunchyNougats { get; set; } = null!;
19+
public DbSet<SoftNougat> SoftNougats { get; set; } = null!;
1820

1921
public static Task SeedAsync(UpdatesContext context)
2022
{

test/EFCore.Specification.Tests/Update/UpdatesTestBase.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
10421042
modelBuilder.Entity<LiftObscurer>().HasOne<Lift>().WithOne(x => x.Obscurer).HasForeignKey<LiftObscurer>(e => e.LiftId);
10431043
modelBuilder.Entity<LiftBag>();
10441044
modelBuilder.Entity<LiftPaper>();
1045+
1046+
modelBuilder.Entity<Nougat>();
1047+
modelBuilder.Entity<CrunchyNougat>(b =>
1048+
{
1049+
b.OwnsOne(e => e.Filling, ob =>
1050+
{
1051+
ob.Property(o => o.Kind).HasConversion<string>();
1052+
});
1053+
});
1054+
modelBuilder.Entity<SoftNougat>(b =>
1055+
{
1056+
b.OwnsOne(e => e.Filling, ob =>
1057+
{
1058+
ob.Property(o => o.Kind).HasConversion<string>();
1059+
});
1060+
});
10451061
}
10461062

10471063
protected override Task SeedAsync(UpdatesContext context)

0 commit comments

Comments
 (0)