Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Working with Disconnected Graphs #5092

Closed
deenairn opened this issue Apr 16, 2016 · 4 comments
Closed

Working with Disconnected Graphs #5092

deenairn opened this issue Apr 16, 2016 · 4 comments
Assignees

Comments

@deenairn
Copy link

deenairn commented Apr 16, 2016

It would be nice if the Entity Framework Core 1.0 API providing a better level of support for the disconnected graph scenario (i.e. where changes are made on a web client and the results posted back to the server where the context is not aware of the changes to the state of the world). The current API provides two methods, one called Attach which by default adds the whole tree in an "Unchanged" state and one called Update which by default adds the whole tree in a "Modified" state. As I understand it - the intention for the first is that the user builds up the state of the world manually by setting the status of entities manually, and the second marks everything within the graph as modified updating or adding any missing entries.

However, to my mind, this misses the case where the disconnected client has removed entities. In the case where you have a Parent and some Children, and one of those children are removed (but not assigned any other parent) - is ignored because it is not a part of the graph. Therefore, when the client removes a child from the parent - it is left to the developer to work this change out.

If such a scenario could be avoided by a further API method (Merge?), or perhaps by changing Update to include this scenario - this would be fantastic. There would be cost to this as it would involve a trip to the database and comparison to be made to the current DB state compared to the state of the tree. GraphDiff supports this by labelling properties as either Associated or Owned to work out the state of a graph.

The (very basic) code below shows a simple example where a child has been removed and the DbContext Update method did not do quite what I had expected it would do.

    class Program
    {
        public class Parent
        {
            public long Id { get; set; }
            public string Name { get; set; }
            public ICollection<Child> Children { get; set; } 
        }

        public class Child
        {
            public long Id { get; set; }
            public string Name { get; set; }
        }

        public class ExampleDataContext : DbContext
        {
            public DbSet<Parent> Parents { get; set; }
            public DbSet<Child> Childs { get; set; }

            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<Parent>()
                    .HasMany(x => x.Children)
                    .WithOne()
                    .IsRequired();
            }

            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                optionsBuilder.UseSqlite(new SqliteConnection(@"FileName=test.db"));
            }
        }

        static void Main(string[] args)
        {
            // Initial DB state
            var parent1 = new Parent
            {
                Name = "Parent1",
                Children = new List<Child>
                {
                    new Child
                    {
                        Name = "Parent1.Child1"
                    },
                    new Child
                    {
                        Name = "Parent1.Child2"
                    }
                }
            };

            // Add to DB to create current state of the world
            using (var context1 = new ExampleDataContext())
            {
                context1.Database.EnsureDeleted();

                context1.Database.EnsureCreated();

                context1.Add(parent1);

                context1.SaveChanges();
            }

            // Spoof a client's changes by adding via a different context
            parent1.Name = "Parent1.NewName";
            parent1.Children.Remove(parent1.Children.Last());
            parent1.Children.First().Name = "Parent1.Child.NewName";
            parent1.Children.Add(new Child {Name = "Parent1.AddedChild1"});

            using (var newDataContext = new ExampleDataContext())
            {
                newDataContext.Update(parent1);
                newDataContext.SaveChanges();
            }

            // Check the state of the DB with another context to find out current state of the world
            using (var checkDataContext = new ExampleDataContext())
            {
                var check = checkDataContext.Parents.Include(x => x.Children).Single(x => x.Id == parent1.Id);

                Debug.Assert(check.Children.Count == 2); // FAIL
            }
        }
    }
@PowerMogli
Copy link

PowerMogli commented May 3, 2016

We have a similar problem, when we try to add a new child to parent. When calling DbContext.Update(entity) we get a DbUpdateConcurrencyException with message "Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded." Is this behavior intended or a bug?

Simple code below in a txt-file:
Simple_Code_Sample.txt

@divega
Copy link
Contributor

divega commented May 23, 2016

The current API provides two methods, one called Attach which by default adds the whole tree in an "Unchanged" state and one called Update which by default adds the whole tree in a "Modified" state. As I understand it - the intention for the first is that the user builds up the state of the world manually by setting the status of entities manually, and the second marks everything within the graph as modified updating or adding any missing entries.

EF Core 1.0 actually includes a new method on the ChangeTracker class called TrackGraph() which can be used to handle disconnected graphs. TrackGraph() facilitates visiting a graph of objects and affecting the change tracking state maintained in the DbContext for each object. The documentation for on how to do this isn't finished but #5424 (comment) contains a small usage example.

TrackGraph() doesn't assume any default rules for deciding the state of an object and is better understood as a building block for creating application specific code that handles disconnected graphs.

We agree that there would be value in including more of a "turnkey" API in EF Core for handling disconnected graphs, but this is one of those things that are often easier to handle at the application level by leveraging knowledge specific to each model. Handling disconnected graphs in an automatic way for any application would poise additional challenges and design choices which we haven't tackled yet. A few examples:

  • What is the original state of each entity, where and how to store it? Should some component of EF Core or the application take a snapshot of the original state of each object before the graph is modified or should the current state of the database at the time the modified graph is returned treated as the original state? What is the impact to concurrency control if we pick the latter?
  • As @deenairn mentioned, how to decide which entities are deleted? This could be handled by serializing a list of deleted entities alongside the graph, or through reachability if we knew for sure that an entity that is no longer reachable in the graph has to be deleted. For the latter, we could leverage knowledge of the aggregate boundaries (which could be fully defined in the model as described in Take advantage of ownership to enable aggregate behaviors in model #1985 or defined in a piecemeal basis by discriminating between ownership and mere association relationships, i.e. the approach GraphDiff takes), or we could use the serializable equivalent of a DbContext to provide a "spine" which guarantees all non-deleted objects are reachable. What to do when these additional information is missing?

@divega divega removed this from the 1.0.0 milestone May 23, 2016
@divega
Copy link
Contributor

divega commented May 23, 2016

For triage: Clearing up milestone and labels to bring this issue back to triage. Originally my plan was to respond to customers with some additional details and close this issue a duplicate of a backlog issue issue, but I haven't been able to find an existing issue for a turnkey solution for disconnected graphs.

@rowanmiller
Copy link
Contributor

Opened #5536 to track a turn-key solution

@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants