Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating EF entities based on deep JSON data

I have a data structure which looks something like this: foo 1:* bar 1:* baz

It could look something like this when passed to the client:

{
    id: 1,
    bar: [{
            id: 1,
            baz: []
        },
        {
            id: 2,
            baz: [{
                id: 1
            }]
        }
    ]
}

In my UI, this is represented by a tree structure, where the user can update/add/remove items on all levels.

My question is, when the user has made modifications and I'm sending the altered data back to the server, how should I perform the EF database update? My initial thought was to implement dirty tracking on the client side, and make use of the dirty flag on the server in order to know what to update. Or maybe EF can be smart enough to do an incremental update itself?

like image 410
filur Avatar asked Aug 18 '17 11:08

filur


1 Answers

Unfortunately EF provides little if no help for such scenario.

The change tracker works well in connected scenarios, but working with disconnected entities has been totally left out for the develpers using the EF. The provided context methods for manipulating entity state can handle simplified scenarios with primitive data, but does not play well with related data.

The general way to handle it is to load the existing data (icluding related) from the database, then detect and apply the add/updates/deletes yourself. But accounting for all related data (navigation property) types (one-to-many (owned), many-to-one (associated), many-to-many etc), plus the hard way to work with EF6 metadata makes the generic solution extremely hard.

The only attempt to address the issue generically AFAIK is the GraphDiff package. Applying the modifications with that package in your scenario is simple as that:

using RefactorThis.GraphDiff;

IEnumerable<Foo> foos = ...;
using (var db = new YourDbContext())
{
    foreach (var foo in foos)
    {
        db.UpdateGraph(foo, fooMap =>
            fooMap.OwnedCollection(f => f.Bars, barMap =>
                barMap.OwnedCollection(b => b.Bazs)));
    }
    db.SaveChanges();
}

See Introducing GraphDiff for Entity Framework Code First - Allowing automated updates of a graph of detached entities for more info about the problem and how the package is addressing the different aspects of it.

The drawback is that the package is no more supported by the author, and also there is no support for EF Core in case you decide to port from EF6 (working with disconnected entities in EF Core has some improvements, but still doesn't offer a general out of the box solution).

But implementing correctly the update manually even for specific model is a real pain. Just for comparison, the most condensed equivalent of the above UpdateGraph method for 3 simple entities having only primitive and collection type navigation properties will look something like this:

db.Configuration.AutoDetectChangesEnabled = false;
var fooIds = foos.Where(f => f.Id != 0).Select(f => f.Id).ToList();
var oldFoos = db.Foos
    .Where(f => fooIds.Contains(f.Id))
    .Include(f => f.Bars.Select(b => b.Bazs))
    .ToDictionary(f => f.Id);
foreach (var foo in foos)
{
    Foo dbFoo;
    if (!oldFoos.TryGetValue(foo.Id, out dbFoo))
    {
        dbFoo = db.Foos.Create();
        dbFoo.Bars = new List<Bar>();
        db.Foos.Add(dbFoo);
    }
    db.Entry(dbFoo).CurrentValues.SetValues(foo);
    var oldBars = dbFoo.Bars.ToDictionary(b => b.Id);
    foreach (var bar in foo.Bars)
    {
        Bar dbBar;
        if (!oldBars.TryGetValue(bar.Id, out dbBar))
        {
            dbBar = db.Bars.Create();
            dbBar.Bazs = new List<Baz>();
            db.Bars.Add(dbBar);
            dbFoo.Bars.Add(dbBar);
        }
        else
        {
            oldBars.Remove(bar.Id);
        }
        db.Entry(dbBar).CurrentValues.SetValues(bar);
        var oldBazs = dbBar.Bazs.ToDictionary(b => b.Id);
        foreach (var baz in bar.Bazs)
        {
            Baz dbBaz;
            if (!oldBazs.TryGetValue(baz.Id, out dbBaz))
            {
                dbBaz = db.Bazs.Create();
                db.Bazs.Add(dbBaz);
                dbBar.Bazs.Add(dbBaz);
            }
            else
            {
                oldBazs.Remove(baz.Id);
            }
            db.Entry(dbBaz).CurrentValues.SetValues(baz);
        }
        db.Bazs.RemoveRange(oldBazs.Values);
    }
    db.Bars.RemoveRange(oldBars.Values);
}
db.Configuration.AutoDetectChangesEnabled = true;
like image 51
Ivan Stoev Avatar answered Oct 23 '22 08:10

Ivan Stoev