Entity framework not detecting jsonb properties changes in c#
Asked Answered
F

2

6

I am using Entity Framework Core with npgsql postgresql for Entity Framework Core. and i'm working with .net core 3

My question is, when i try to update a MyTableRelated element from the MyTableClass and saving the context to the database, no changes are detected.

For example, lets suppose we have the following classes:

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

    [Column(TypeName = "jsonb")]
    public virtual List<MyTableRelated> Data { get; set; }
}

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

    public string prop1 { get; set; }

    public string prop2 { get; set; }
}

and some code like this (this is not actual code, its just to get the ideia):

var context = dbContext;

var newMyTableClass = new MyTableClass() {
    Id = 1;
};

var newMyTableRelated = new MyTableRelated(){
    Id=1;

    prop1 = "";

    prop2 = "";
}

newMyTableClass.Data.Add(newMyTableRelated);

context.SaveChanges();

This works, and the entry is saved on the database.

Now somewhere on the application, i want to access that entry and change values on Data:

var context = dbContext;

var updateMyTableClass = context.MyTableClass.FirstOrDefault(x => x.Id == 1);

var tableRelated = updateMyTableClass.Data.FirstOrDefault(y => y.Id == 1);

tableRelated.prop1 = "prop1";

tableRelated.prop2 = "prop2";

context.SaveChanges();

I would suppose this would change values on database, like it does for other types of properties. But nothing happens.

A solution i found, was using this:

var entry = context.Entry(updateMyTableClass);
if (entry.State == EntityState.Unchanged)
{
   entry.State = EntityState.Modified;
}

This is more of a temporary solution for that case.

How can we then make the EF automatically detect changes on jsonb properties?

Someone pointed to me that i should look at coase grained lock. https://www.martinfowler.com/eaaCatalog/coarseGrainedLock.html

How can something like that be implemented?

Fredrick answered 26/5, 2020 at 11:33 Comment(0)
S
5

Automatic change detection would mean that EF Core would take a snapshot of the JSON document when it loads the property (duplicating the entire tree), and then do a complete structural comparison of the original and current tree whenever SaveChanges is called. As this can be very heavy perf-wise, it is not done by default.

However, if you wish to do so, you can create a value comparer to implement precisely this - see the EF docs on how to do that. I've opened an issue on the Npgsql provider repo in case someone wishes to contribute this.

For perf reasons, I'd recommend manually flagging properties when they change, similar to what you have done. Note that you're marking the entire entity instance as changed - so all properties will be saved. You can use the following to only mark the JSON property:

ctx.Entry(entry).Property(e => e.SomeJsonProperty).IsModified = true;
Sangsanger answered 28/5, 2020 at 10:28 Comment(0)
S
1

Looks like this issue still exists even now. I use a lighter-weight update method to update specific entities, and it's really just calling context.Entry(entity).DetectChanges() on attached entities. I know this works for certain when you replace the entire JSON object with a new one, but it only occasionally seems to work whenever I'm updating a single property on a preexisting JSON object.

Anyway, I solved for it in my case by adding a Clone() method to use for the snapshot in the value comparer. You can implement this however it makes sense for your entity. For me, I'm simply using JsonConvert.Seralize/Deserialize as I only use JSON types with very simple payloads. Then, I called this method on any applicable JSON properties in my model builder:

    private static PropertyBuilder<T> IsJson<T>(this PropertyBuilder<T> builder, bool canSearch = true) where T : class
    {
        builder.HasColumnType(canSearch ? "jsonb" : "json")
            .Metadata
            .SetValueComparer(
                new ValueComparer<T>(
                    (a, b) => (a == null && b == null) || (a != null && b != null && a.GetHashCode() == b.GetHashCode()),
                    x => x == null ? -1 : x.GetHashCode(),
                    a => a == null ? null : a.Clone()));
        return builder;
    }

example usage:

builder.Entity<MyTable>().Property(x => x.MyJsonObject).IsJson()

Note that you might want to use another condition for the equality check in the value comparer. For my cases, it works well as I've implemented hash code overrides on these models.

Sebbie answered 15/6, 2023 at 14:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.