efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Tracking changes in json column in EF Core 8 and postgres

Open bartoszgrzeda opened this issue 1 year ago • 4 comments

i have a problem with tracking changes in json column in EF Core 8 and postgres 8. It seems like explicity setting entries as modified does not work

Models i am using:

public class DocumentReadModelEntity<T> where T : IDocumentReadModel
{
    public Guid Id { get; set; }
    public T Data { get; set; }

    public DocumentReadModelEntity()
    {
        
    }

    public DocumentReadModelEntity(T data)
    {
        Data = data;
        Id = data.Id;
    }
}
public sealed class SupplierReadModel : IDocumentReadModel
{
    public string? Description { get; set; }
    public string? Website { get; set; }
    public Guid? LogoFileId { get; set; }
    public bool IsActive { get; set; }
    public bool ShouldSendRemittanceEmail { get; set; }

    public List<SupplierBlockModel> Blocks { get; set; }
    public List<EstateBlockModel> Estates { get; set; }

    public Guid CompanyId { get; set; }
    public CompanyShortData? CompanyData { get; set; }

    public BankAccountModel? BankAccount { get; set; }
}

every complex property in SupplierReadModel (in this example they are: SupplierBlockModel, EstateBlockModel, CompanyShortData, BankAccountModel) inherits from

[Owned]
public abstract class ReadModelDataModel
{
}

For example (SupplierBlockModel):

public class SupplierBlockModel : ReadModelDataModel
{
    public Guid BlockId { get; set; }
    public BlockShortData? BlockData { get; set; }
}

Nested properties inherit as well

public class BlockShortData : ReadModelDataModel
{
    public string CustomId { get; set; }
    public string BlockName { get; set; }
    [JsonIgnore]
    public string FullName => $"[{CustomId}] {BlockName}";
}

Entity configuration looks like:

public static void ApplyDefaultConfiguration<T>(this EntityTypeBuilder<DocumentReadModelEntity<T>> builder) where T : class, IDocumentReadModel
{
    builder.ToTable(typeof(T).Name);

    builder.HasKey(x => x.Id);

    builder.OwnsOne(x => x.Data, x =>
    {
        x.ToJson();
    });
}

I am quering db context as:

 return await _entities.AsNoTracking()
     .Where(x => ids.Contains(x.Id))
     .Select(x => x.Data)
     .ToListAsync();

where _entities is _entities = _dbContext.Set<DocumentReadModelEntity<T>>(); in generic repository public class DocumentStore<T> : IDocumentStore<T> where T : class, IDocumentReadModel, new()

when i am updating SupplierReadModel and i try to update this entry with:

public void Update(T updated)
{
    var entity = new DocumentReadModelEntity<T>(updated);
    _entities.Entry(entity).State = EntityState.Modified;
}

and then

public async Task Save()
{
   await _dbContext!.SaveChangesAsync();
}

nothing is saved. additionaly when i look at the debug view of db context change tracker i can see

DocumentReadModelEntity<SupplierReadModel> {Id: 018d834f-75d9-48eb-8af1-543579976bc5} Modified
    Id: '018d834f-75d9-48eb-8af1-543579976bc5' PK
  Data: <not found>

what do i wrong?

bartoszgrzeda avatar Feb 13 '24 19:02 bartoszgrzeda

This issue is lacking enough information for us to be able to fully understand what is happening. Please attach a small, runnable project or post a small, runnable code listing that reproduces what you are seeing so that we can investigate.

ajcvickers avatar Feb 14 '24 09:02 ajcvickers

@bartoszgrzeda a simple console program rather than a collection of snippets is what we need in order to investigate.

roji avatar Feb 14 '24 12:02 roji

i created a console program. https://github.com/bartoszgrzeda/TrackingChanges in readme you have a scripts to run to seed database. connection string is hardcoded in DocumentStore class

expected behaviour is to have updated TestProperty value

bartoszgrzeda avatar Feb 17 '24 12:02 bartoszgrzeda

Note for EF team: the problem here is that the JSON document is used Id as its primary key, but EF is not picking up on this, so the resulting model is:

Model: 
  EntityType: DocumentReadModelEntity<TestReadModel>
    Properties: 
      Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Navigations: 
      Data (TestReadModel) ToDependent TestReadModel
    Keys: 
      Id PK
  EntityType: TestReadModel Owned
    Properties: 
      DocumentReadModelEntityId (no field, Guid) Shadow Required PK FK AfterSave:Throw
      Id (Guid) Required
      TestProperty (string) Required
    Keys: 
      DocumentReadModelEntityId PK
    Foreign keys: 
      TestReadModel {'DocumentReadModelEntityId'} -> DocumentReadModelEntity<TestReadModel> {'Id'} Unique Required Ownership Cascade ToDependent: Data

Notice the shadow PK/FK. This can be fixed with:

builder.OwnsOne(x => x.Data, x =>
{
    x.HasKey(e => e.Id);
    x.ToJson();
});

Which gives the expected model:

Model: 
  EntityType: DocumentReadModelEntity<TestReadModel>
    Properties: 
      Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Navigations: 
      Data (TestReadModel) ToDependent TestReadModel
    Keys: 
      Id PK
  EntityType: TestReadModel Owned
    Properties: 
      Id (Guid) Required PK FK AfterSave:Throw
      TestProperty (string) Required
    Keys: 
      Id PK
    Foreign keys: 
      TestReadModel {'Id'} -> DocumentReadModelEntity<TestReadModel> {'Id'} Unique Required Ownership Cascade ToDependent: Data

But then hits #29380.

ajcvickers avatar Feb 19 '24 13:02 ajcvickers

Closing this since #29380 is tracking the root cause.

ajcvickers avatar Mar 07 '24 07:03 ajcvickers