Skip to content

TPH inheritance with owned JSON type causes NullReferenceException when replacing derived entity with same key #36019

@MaikelOrisha

Description

@MaikelOrisha

Bug description

We have a navigation property with a list of inherited types, mapped using table-per-hierarchy (TPH). One derived type (LocaleValueAttribute) includes an owned JSON type (LocaleValue). When we try to remove an instance of that type and add a different derived type (StringAttribute) with the same composite key, we run into a NullReferenceException.

I initially placed a comment in #34274, but I now believe it's a separate issue. That's why I created a new ticket.

Your code

using Microsoft.EntityFrameworkCore;

await using var context = new ReproDbContext();

await context.Database.MigrateAsync();

var item = context.Items.Add(new Item
{
    Name = "Product 1",
    Attributes = [
        new LocaleValueAttribute()
        {
            Key = "TextValue",
            Value = new LocaleValue()
            {
                Entries = [
                    new LocaleValueEntry()
                    {
                        Locale = "en-US",
                        Value = "Hello"
                    }
                ]
            }
        }
    ]
});

await context.SaveChangesAsync();

item.Entity.Attributes.RemoveAll(attr => attr.Key == "TextValue");

item.Entity.Attributes.Add(new StringAttribute
{
    Key = "TextValue",
    Value = "World"
});

await context.SaveChangesAsync(); // Throws the NullReferenceException

// Db context
public class ReproDbContext : DbContext
{
    
    public DbSet<Item> Items { get; set; }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=efcore-bug-repro;Trusted_Connection=True;MultipleActiveResultSets=true;");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Item>()
            .HasKey(x => x.Id);

        modelBuilder.Entity<ItemAttribute>(b =>
        {
            b.HasKey(x => new { x.ItemId, x.Key });
            b.HasDiscriminator<string>("Discriminator")
                .HasValue<StringAttribute>("string")
                .HasValue<LocaleValueAttribute>("locale-value");
        });
        
        modelBuilder.Entity<StringAttribute>(b =>
        {
            b.Property(x => x.Value).HasColumnName("StringValue");
        });
        
        modelBuilder.Entity<LocaleValueAttribute>(b =>
        {
            b.OwnsOne(x => x.Value, bc =>
            {
                bc.ToJson("LocaleValue");
                bc.OwnsMany(x => x.Entries, v =>
                {
                    v.Property(x => x.Locale).IsRequired();
                    v.Property(x => x.Value);
                });
            });
        });
    }
}

// Models
public class Item
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public List<ItemAttribute> Attributes { get; set; } = [];
}

public abstract class ItemAttribute
{
    public int ItemId { get; set; }
    public required string Key { get; set; }
}

public class StringAttribute : ItemAttribute
{
    public string Value { get; set; } = null!;
}

public class LocaleValueAttribute : ItemAttribute
{
    public LocaleValue Value { get; set; } = null!;
}

public class LocaleValue
{
    public List<LocaleValueEntry> Entries { get; set; } = [];
}

public class LocaleValueEntry
{
    public required string Locale { get; set; }
    public required string Value { get; set; }
}

Stack traces

System.NullReferenceException: Object reference not set to an instance of an object.
   at Microsoft.EntityFrameworkCore.Update.ModificationCommand.<GenerateColumnModifications>g__FindJsonPartialUpdateInfo|41_2(IUpdateEntry entry, List`1 processedEntries)
   at Microsoft.EntityFrameworkCore.Update.ModificationCommand.<GenerateColumnModifications>g__HandleJson|41_4(List`1 columnModifications, <>c__DisplayClass41_0&)
   at Microsoft.EntityFrameworkCore.Update.ModificationCommand.GenerateColumnModifications()
   at Microsoft.EntityFrameworkCore.Update.ModificationCommand.<>c.<get_ColumnModifications>b__33_0(ModificationCommand command)
   at Microsoft.EntityFrameworkCore.Internal.NonCapturingLazyInitializer.EnsureInitialized[TParam,TValue](TValue& target, TParam param, Func`2 valueFactory)
   at Microsoft.EntityFrameworkCore.Update.ModificationCommand.get_ColumnModifications()
   at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.CreateCommandBatches(IEnumerable`1 commandSet, Boolean moreCommandSets, Boolean assertColumnModification, ParameterNameGenerator parameterNameGenerator)+MoveNext()
   at Microsoft.EntityFrameworkCore.Update.Internal.CommandBatchPreparer.BatchCommands(IList`1 entries, IUpdateAdapter updateAdapter)+MoveNext()
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
   at Program.<Main>$(String[] args) in C:\Work\Experimental\ConsoleApp2\ConsoleApp2\Program.cs:line 40
   at Program.<Main>$(String[] args) in C:\Work\Experimental\ConsoleApp2\ConsoleApp2\Program.cs:line 40
   at Program.<Main>(String[] args)

Verbose output


EF Core version

9.0.4

Database provider

Microsoft.EntityFrameworkCore.SqlServer

Target framework

.NET 9.0

Operating system

No response

IDE

No response

Metadata

Metadata

Assignees

Type

No fields configured for Bug.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions