Skip to content

Updating JSON column when query tracking is disabled throws #33862

@vladislav-karamfilov

Description

@vladislav-karamfilov

File a bug

Setting a new value to a JSON-mapped column property (a non-collection or a collection one) when query tracking is disabled fails with InvalidOperationException. Enabling query tracking with optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll) fixes the issue but it is not an option for the project I'm working on.

PS All is working well when we have primitive collections like List<string> in the entity instead of JSON object(s).

Include your code

using Microsoft.EntityFrameworkCore;

await EnsureThingsInDbAsync();

// These method calls throw!!!
await UpdateThingAsync(isManualUpdate: false, updateSinglePart: false);
await UpdateThingAsync(isManualUpdate: false, updateSinglePart: true);

// These method calls doesn't throw but the update is not performed (the value is not updated in DB)!!!
await UpdateThingAsync(isManualUpdate: true, updateSinglePart: false);
await UpdateThingAsync(isManualUpdate: true, updateSinglePart: true);

static async Task UpdateThingAsync(bool isManualUpdate, bool updateSinglePart)
{
    using var db = new AppDbContext();
    var thing = await db.Things.FirstOrDefaultAsync();

    if (updateSinglePart)
    {
        thing!.SinglePart = new Part { Name = "updated single part " + Random.Shared.Next() };
    }
    else
    {
        thing!.Parts = [new Part { Name = "updated part " + Random.Shared.Next() }];
    }

    if (isManualUpdate)
    {
        var entry = db.Entry(thing);
        if (entry.State == EntityState.Detached)
        {
            db.Things.Attach(thing);
        }

        entry.State = EntityState.Modified;
    }
    else
    {
        db.Things.Update(thing);
    }

    await db.SaveChangesAsync();
}

static async Task EnsureThingsInDbAsync()
{
    using var db = new AppDbContext();
    await db.Database.EnsureCreatedAsync();

    if (await db.Things.AnyAsync())
    {
        return;
    }

    db.Things.Add(new Thing
    {
        Parts = [new Part { Name = "part " + Random.Shared.Next() }],
        SinglePart = new Part { Name = "single part " + Random.Shared.Next() },
    });

    await db.SaveChangesAsync();
}

public class AppDbContext : DbContext
{
    public DbSet<Thing> Things { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
        => modelBuilder.Entity<Thing>()
            .OwnsOne(x => x.SinglePart, b => b.ToJson())
            .OwnsMany(x => x.Parts, b => b.ToJson());

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);

        optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=ef-json-cols");

        optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTrackingWithIdentityResolution);
        // optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); // Doesn't work too!!!
    }
}

public class Thing
{
    public int Id { get; set; }

    public List<Part> Parts { get; set; } = [];

    public Part? SinglePart { get; set; }
}

public class Part
{
    public required string Name { get; set; }
}

Include stack traces

Unhandled exception. System.InvalidOperationException: The value of shadow key property 'Thing.Parts#Part (Part).Id' is unknown when attempting to save changes. This is because shadow property values cannot be preserved when the entity is not being tracked. Consider adding the property to the entity's .NET type. See https://aka.ms/efcore-docs-owned-collections for more information.
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.<PrepareToSave>g__CheckForUnknownKey|111_0(IProperty property, <>c__DisplayClass111_0&)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.PrepareToSave()
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.GetEntriesToSave(Boolean cascadeChanges)
   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>$>g__UpdateThingAsync|0_0(Boolean isManualUpdate, Boolean updateSinglePart) in C:\Users\Vladislav\source\repos\ConsoleApp9\Program.cs:line 42
   at Program.<Main>$(String[] args) in C:\Users\Vladislav\source\repos\ConsoleApp9\Program.cs:line 8
   at Program.<Main>(String[] args)

Include provider and version information

EF Core version: 8.0.6
Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 8.0
Operating system: Windows 11
IDE: Visual Studio 2022 17.10.1

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions