Question
In EF Core 8, replacing a TPH entity in a collection by removing the old instance and adding a new instance with the same ID generates an UPDATE that includes the Discriminator column. In EF Core 9, the Discriminator is no longer included in the UPDATE, so the type change is silently lost.
I'm not sure whether the EF 8 behaviour was correct (and this is a regression), or whether EF 9 is correctly rejecting a pattern that was never meant to be supported. I found two issues that seem to be related to this issue and may suggest that this is valid use case:
Output of the provided example
SQL generated by EF Core 8
UPDATE "Commands" SET "Discriminator" = @p0, "Label" = @p1
WHERE "Id" = @p2
RETURNING 1;
SQL generated by EF Core 9
UPDATE "Commands" SET "DayPlanId" = @p0, "Label" = @p1
WHERE "Id" = @p2
RETURNING 1;
The Discriminator column is absent from the EF 9 UPDATE.
Your code
using Microsoft.EntityFrameworkCore;
using var initCtx = new AppDbContext();
initCtx.Database.EnsureDeleted();
initCtx.Database.EnsureCreated();
// 1. Seed a CommandA
var dayPlan = new DayPlan { Name = "Day 1" };
var original = new CommandA { Label = "Original A" };
dayPlan.Commands.Add(original);
initCtx.DayPlans.Add(dayPlan);
initCtx.SaveChanges();
Console.WriteLine($"Seeded: Id={original.Id}, Type={original.GetType().Name}");
PrintDb(initCtx);
// 2. Replace CommandA with CommandB
using var ctx = new AppDbContext();
var loadedPlan = ctx.DayPlans.Include(d => d.Commands).First();
var oldCmd = loadedPlan.Commands.First();
var newCmd = new CommandB { Label = "replaced B" };
// Replace CommandA with CommandB => preserve ID
loadedPlan.Commands.Remove(oldCmd);
newCmd.Id = oldCmd.Id;
loadedPlan.Commands.Add(newCmd);
Console.WriteLine($"Before SaveChanges:");
foreach (var entry in initCtx.ChangeTracker.Entries<Command>())
{
Console.WriteLine($" [{entry.State}] {entry.Entity.GetType().Name} Id={entry.Entity.Id}");
}
Console.WriteLine($"Replacing commands:");
ctx.SaveChanges();
Console.WriteLine($"After SaveChanges:");
PrintDb(ctx);
// Load from DB via new instance of the context
Console.WriteLine($"Fresh context:");
using var freshCtx = new AppDbContext();
PrintDb(freshCtx);
static void PrintDb(AppDbContext ctx)
{
ctx.ChangeTracker.Clear();
var rows = ctx.DayPlans.Include(d => d.Commands).First().Commands;
foreach (var c in rows)
{
Console.WriteLine($" DB row: Id={c.Id}, Type={c.GetType().Name}, Label={c.Label}");
}
}
// -----------------------------------------------------------------------
// Model
// -----------------------------------------------------------------------
public class DayPlan
{
public int Id { get; set; }
public string Name { get; set; } = "";
public ICollection<Command> Commands { get; set; } = new List<Command>();
}
public abstract class Command
{
public int Id { get; set; }
public string Label { get; set; } = "";
public int DayPlanId { get; set; }
}
public class CommandA : Command { }
public class CommandB : Command { }
// -----------------------------------------------------------------------
// DbContext — SQLite in-memory for zero setup
// -----------------------------------------------------------------------
public class AppDbContext : DbContext
{
public DbSet<DayPlan> DayPlans => Set<DayPlan>();
public DbSet<Command> Commands => Set<Command>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Command>()
.HasDiscriminator<string>("Discriminator")
.HasValue<CommandA>("CommandA")
.HasValue<CommandB>("CommandB");
}
protected override void OnConfiguring(DbContextOptionsBuilder o) =>
o.UseSqlite("Data Source=repro.db")
.LogTo(msg =>
{
// Only print SQL statements, skip noise
if (msg.Contains("Executed DbCommand"))
{
Console.WriteLine(msg);
}
}, Microsoft.Extensions.Logging.LogLevel.Information);
}
Stack traces
Verbose output
EF Core version
9.0.0
Database provider
Microsoft.EntityFrameworkCore.Sqlite
Target framework
No response
Operating system
No response
IDE
No response
Question
In EF Core 8, replacing a TPH entity in a collection by removing the old instance and adding a new instance with the same ID generates an
UPDATEthat includes theDiscriminatorcolumn. In EF Core 9, theDiscriminatoris no longer included in theUPDATE, so the type change is silently lost.I'm not sure whether the EF 8 behaviour was correct (and this is a regression), or whether EF 9 is correctly rejecting a pattern that was never meant to be supported. I found two issues that seem to be related to this issue and may suggest that this is valid use case:
UPDATEwith a changed discriminator in a similar scenario, implying it was working and accepted behaviour.Output of the provided example
SQL generated by EF Core 8
SQL generated by EF Core 9
The
Discriminatorcolumn is absent from the EF 9UPDATE.Your code
Stack traces
Verbose output
EF Core version
9.0.0
Database provider
Microsoft.EntityFrameworkCore.Sqlite
Target framework
No response
Operating system
No response
IDE
No response