From a322b26f2b3ba3f70945a0d464dc7356f52e2b39 Mon Sep 17 00:00:00 2001 From: guillaume-fr Date: Fri, 27 Jun 2014 09:36:12 +0200 Subject: [PATCH 1/3] Allows to merge on a custom entity key instead of primary key --- .../GraphDiff.Tests/Models/TestModels.cs | 4 +- .../Tests/OwnedCollectionBehaviours.cs | 730 +++++++++--------- GraphDiff/GraphDiff/DbContextExtensions.cs | 24 +- GraphDiff/GraphDiff/GraphDiff.csproj | 1 + GraphDiff/GraphDiff/Internal/ChangeTracker.cs | 476 ++++++------ GraphDiff/GraphDiff/Internal/EntityManager.cs | 345 +++++---- GraphDiff/GraphDiff/Internal/GraphDiffer.cs | 4 +- GraphDiff/GraphDiff/Internal/QueryLoader.cs | 2 +- GraphDiff/GraphDiff/KeysConfiguration.cs | 67 ++ 9 files changed, 915 insertions(+), 738 deletions(-) create mode 100644 GraphDiff/GraphDiff/KeysConfiguration.cs diff --git a/GraphDiff/GraphDiff.Tests/Models/TestModels.cs b/GraphDiff/GraphDiff.Tests/Models/TestModels.cs index 8988e6d..47468fc 100644 --- a/GraphDiff/GraphDiff.Tests/Models/TestModels.cs +++ b/GraphDiff/GraphDiff.Tests/Models/TestModels.cs @@ -13,7 +13,9 @@ namespace RefactorThis.GraphDiff.Tests.Models public class Entity { [Key] - public int Id { get; set; } + public int Id { get; set; } + + public Guid UniqueId { get; set; } [MaxLength(128)] public string Title { get; set; } diff --git a/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs b/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs index eb12535..b976ead 100644 --- a/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs +++ b/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs @@ -1,345 +1,385 @@ -using System.Collections.ObjectModel; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using RefactorThis.GraphDiff.Tests.Models; -using System.Linq; -using System.Data.Entity; -using System.Collections.Generic; - -namespace RefactorThis.GraphDiff.Tests.Tests -{ - [TestClass] - public class OwnedCollectionBehaviours : TestBase - { - [TestMethod] - public void ShouldUpdateItemInOwnedCollection() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel { Title = "Hello" } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - node1.OneToManyOwned.First().Title = "What's up"; - using (var context = new TestDbContext()) - { - // Setup mapping - context.UpdateGraph(node1, map => map - .OwnedCollection(p => p.OneToManyOwned)); - - context.SaveChanges(); - var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); - Assert.IsNotNull(node2); - var owned = node2.OneToManyOwned.First(); - Assert.IsTrue(owned.OneParent == node2 && owned.Title == "What's up"); - } - } - - [TestMethod] - public void ShouldUpdateItemInNestedOwnedCollection() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel - { - Title = "Hello", - OneToManyOneToManyOwned = new Collection - { - new OneToManyOneToManyOwnedModel {Title = "BeforeUpdate"} - } - } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - var oneToManyOneToManyOwned = node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); - var expectedId = oneToManyOneToManyOwned.Id; - const string expectedTitle = "AfterUpdate"; - oneToManyOneToManyOwned.Title = expectedTitle; - - using (var context = new TestDbContext()) - { - // Setup mapping - node1 = context.UpdateGraph(node1, map => map.OwnedCollection(p => p.OneToManyOwned, - with => with.OwnedCollection(p => p.OneToManyOneToManyOwned))); - context.SaveChanges(); - - Assert.AreEqual(1, node1.OneToManyOwned.Count); - Assert.AreEqual(1, node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Count); - - oneToManyOneToManyOwned = node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); - Assert.AreEqual(expectedId, oneToManyOneToManyOwned.Id); - Assert.AreEqual(expectedTitle, oneToManyOneToManyOwned.Title); - - var node1Reloaded = context.Nodes - .Include("OneToManyOwned.OneToManyOneToManyOwned") - .Single(n => n.Id == node1.Id); - - Assert.AreEqual(1, node1Reloaded.OneToManyOwned.Count); - Assert.AreEqual(1, node1Reloaded.OneToManyOwned.Single().OneToManyOneToManyOwned.Count); - - oneToManyOneToManyOwned = node1Reloaded.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); - Assert.AreEqual(expectedId, oneToManyOneToManyOwned.Id); - Assert.AreEqual(expectedTitle, oneToManyOneToManyOwned.Title); - } - } - - [TestMethod] - public void ShouldAddNewItemInOwnedCollection() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel { Title = "Hello" } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - var newModel = new OneToManyOwnedModel { Title = "Hi" }; - node1.OneToManyOwned.Add(newModel); - - using (var context = new TestDbContext()) - { - node1 = context.UpdateGraph(node1, map => map.OwnedCollection(p => p.OneToManyOwned)); - context.SaveChanges(); - - var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); - Assert.IsNotNull(node2); - Assert.AreEqual(2, node2.OneToManyOwned.Count); - - var ownedId = node1.OneToManyOwned.Skip(1).Select(o => o.Id).Single(); - var owned = context.OneToManyOwnedModels.Single(p => p.Id == ownedId); - Assert.IsTrue(owned.OneParent == node2 && owned.Title == "Hi"); - } - } - - [TestMethod] - public void ShouldAddNewItemInOwnedCollectionWithoutChangingRequiredAssociate() - { - var root = new RootEntity { RequiredAssociate = new RequiredAssociate(), Sources = new List() }; - var requiredAssociate = new RequiredAssociate(); - using (var context = new TestDbContext()) - { - context.RootEntities.Add(root); - context.RequiredAssociates.Add(requiredAssociate); - context.SaveChanges(); - } // Simulate detach - - var expectedAssociateId = requiredAssociate.Id; - var owned = new RootEntity { RequiredAssociate = requiredAssociate }; - root.Sources.Add(owned); - - using (var context = new TestDbContext()) - { - root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); - context.SaveChanges(); - - var ownedAfterSave = root.Sources.FirstOrDefault(); - Assert.IsNotNull(ownedAfterSave); - Assert.IsNotNull(ownedAfterSave.RequiredAssociate); - Assert.AreEqual(expectedAssociateId, ownedAfterSave.RequiredAssociate.Id); - - var ownedReloaded = context.RootEntities.Single(r => r.Id == ownedAfterSave.Id); - Assert.IsNotNull(ownedReloaded.RequiredAssociate); - Assert.AreEqual(expectedAssociateId, ownedReloaded.RequiredAssociate.Id); - } - } - - [TestMethod] - public void ShouldAddTwoNewOwnedItemsWithSharedRequiredAssociate() - { - var root = new RootEntity { RequiredAssociate = new RequiredAssociate(), Sources = new List() }; - var requiredAssociate = new RequiredAssociate(); - using (var context = new TestDbContext()) - { - context.RootEntities.Add(root); - context.RequiredAssociates.Add(requiredAssociate); - context.SaveChanges(); - } // Simulate detach - - var expectedAssociateId = requiredAssociate.Id; - var ownedOne = new RootEntity { RequiredAssociate = requiredAssociate }; - root.Sources.Add(ownedOne); - var ownedTwo = new RootEntity { RequiredAssociate = requiredAssociate }; - root.Sources.Add(ownedTwo); - - using (var context = new TestDbContext()) - { - root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); - context.SaveChanges(); - - Assert.IsTrue(root.Sources.All(s => s.RequiredAssociate.Id == expectedAssociateId)); - - var sourceIds = root.Sources.Select(s => s.Id).ToArray(); - var sourcesReloaded = context.RootEntities.Where(r => sourceIds.Contains(r.Id)).ToList(); - Assert.IsTrue(sourcesReloaded.All(s => s.RequiredAssociate != null && s.RequiredAssociate.Id == expectedAssociateId)); - } - } - - [TestMethod] - public void ShouldNotChangeRequiredAssociateEvenIfItIsUsedTwice() - { - var requiredAssociate = new RequiredAssociate(); - var root = new RootEntity { RequiredAssociate = requiredAssociate, Sources = new List() }; - using (var context = new TestDbContext()) - { - context.RootEntities.Add(root); - context.RequiredAssociates.Add(requiredAssociate); - context.SaveChanges(); - } // Simulate detach - - var expectedAssociateId = requiredAssociate.Id; - var owned = new RootEntity { RequiredAssociate = requiredAssociate }; - root.Sources.Add(owned); - - using (var context = new TestDbContext()) - { - root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); - context.SaveChanges(); - - var ownedAfterSave = root.Sources.FirstOrDefault(); - Assert.IsNotNull(ownedAfterSave); - Assert.IsNotNull(ownedAfterSave.RequiredAssociate); - Assert.AreEqual(expectedAssociateId, ownedAfterSave.RequiredAssociate.Id); - - var ownedReloaded = context.RootEntities.Single(r => r.Id == ownedAfterSave.Id); - Assert.IsNotNull(ownedReloaded.RequiredAssociate); - Assert.AreEqual(expectedAssociateId, ownedReloaded.RequiredAssociate.Id); - } - } - - [TestMethod] - public void ShouldRemoveItemsInOwnedCollection() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel { Title = "Hello" }, - new OneToManyOwnedModel { Title = "Hello2" }, - new OneToManyOwnedModel { Title = "Hello3" } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); - node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); - node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); - using (var context = new TestDbContext()) - { - // Setup mapping - context.UpdateGraph(node1, map => map - .OwnedCollection(p => p.OneToManyOwned)); - - context.SaveChanges(); - var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); - Assert.IsNotNull(node2); - Assert.IsTrue(node2.OneToManyOwned.Count == 0); - } - } - - [TestMethod] - public void ShouldRemoveItemsInOwnedCollectionWhenSetToNull() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel { Title = "Hello" }, - new OneToManyOwnedModel { Title = "Hello2" }, - new OneToManyOwnedModel { Title = "Hello3" } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - node1.OneToManyOwned = null; - using (var context = new TestDbContext()) - { - // Setup mapping - context.UpdateGraph(node1, map => map - .OwnedCollection(p => p.OneToManyOwned)); - - context.SaveChanges(); - var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); - Assert.IsNotNull(node2); - Assert.IsTrue(node2.OneToManyOwned.Count == 0); - } - } - - [TestMethod] - public void ShouldMergeTwoCollectionsAndDecideOnUpdatesDeletesAndAdds() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel { Title = "This" }, - new OneToManyOwnedModel { Title = "Is" }, - new OneToManyOwnedModel { Title = "A" }, - new OneToManyOwnedModel { Title = "Test" } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); - node1.OneToManyOwned.First().Title = "Hello"; - node1.OneToManyOwned.Add(new OneToManyOwnedModel { Title = "Finish" }); - using (var context = new TestDbContext()) - { - // Setup mapping - context.UpdateGraph(node1, map => map - .OwnedCollection(p => p.OneToManyOwned)); - - context.SaveChanges(); - var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); - Assert.IsNotNull(node2); - var list = node2.OneToManyOwned.ToList(); - Assert.IsTrue(list[0].Title == "Hello"); - Assert.IsTrue(list[1].Title == "A"); - Assert.IsTrue(list[2].Title == "Test"); - Assert.IsTrue(list[3].Title == "Finish"); - } - } - } -} +using System.Collections.ObjectModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using RefactorThis.GraphDiff.Tests.Models; +using System.Linq; +using System.Data.Entity; +using System.Collections.Generic; +using System; + +namespace RefactorThis.GraphDiff.Tests.Tests +{ + [TestClass] + public class OwnedCollectionBehaviours : TestBase + { + [TestMethod] + public void ShouldUpdateItemInOwnedCollection() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "Hello" } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + node1.OneToManyOwned.First().Title = "What's up"; + using (var context = new TestDbContext()) + { + // Setup mapping + context.UpdateGraph(node1, map => map + .OwnedCollection(p => p.OneToManyOwned)); + + context.SaveChanges(); + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + var owned = node2.OneToManyOwned.First(); + Assert.IsTrue(owned.OneParent == node2 && owned.Title == "What's up"); + } + } + + [TestMethod] + public void ShouldUpdateItemInNestedOwnedCollection() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel + { + Title = "Hello", + OneToManyOneToManyOwned = new Collection + { + new OneToManyOneToManyOwnedModel {Title = "BeforeUpdate"} + } + } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + var oneToManyOneToManyOwned = node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); + var expectedId = oneToManyOneToManyOwned.Id; + const string expectedTitle = "AfterUpdate"; + oneToManyOneToManyOwned.Title = expectedTitle; + + using (var context = new TestDbContext()) + { + // Setup mapping + node1 = context.UpdateGraph(node1, map => map.OwnedCollection(p => p.OneToManyOwned, + with => with.OwnedCollection(p => p.OneToManyOneToManyOwned))); + context.SaveChanges(); + + Assert.AreEqual(1, node1.OneToManyOwned.Count); + Assert.AreEqual(1, node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Count); + + oneToManyOneToManyOwned = node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); + Assert.AreEqual(expectedId, oneToManyOneToManyOwned.Id); + Assert.AreEqual(expectedTitle, oneToManyOneToManyOwned.Title); + + var node1Reloaded = context.Nodes + .Include("OneToManyOwned.OneToManyOneToManyOwned") + .Single(n => n.Id == node1.Id); + + Assert.AreEqual(1, node1Reloaded.OneToManyOwned.Count); + Assert.AreEqual(1, node1Reloaded.OneToManyOwned.Single().OneToManyOneToManyOwned.Count); + + oneToManyOneToManyOwned = node1Reloaded.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); + Assert.AreEqual(expectedId, oneToManyOneToManyOwned.Id); + Assert.AreEqual(expectedTitle, oneToManyOneToManyOwned.Title); + } + } + + [TestMethod] + public void ShouldAddNewItemInOwnedCollection() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "Hello" } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + var newModel = new OneToManyOwnedModel { Title = "Hi" }; + node1.OneToManyOwned.Add(newModel); + + using (var context = new TestDbContext()) + { + node1 = context.UpdateGraph(node1, map => map.OwnedCollection(p => p.OneToManyOwned)); + context.SaveChanges(); + + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + Assert.AreEqual(2, node2.OneToManyOwned.Count); + + var ownedId = node1.OneToManyOwned.Skip(1).Select(o => o.Id).Single(); + var owned = context.OneToManyOwnedModels.Single(p => p.Id == ownedId); + Assert.IsTrue(owned.OneParent == node2 && owned.Title == "Hi"); + } + } + + [TestMethod] + public void ShouldAddNewItemInOwnedCollectionWithoutChangingRequiredAssociate() + { + var root = new RootEntity { RequiredAssociate = new RequiredAssociate(), Sources = new List() }; + var requiredAssociate = new RequiredAssociate(); + using (var context = new TestDbContext()) + { + context.RootEntities.Add(root); + context.RequiredAssociates.Add(requiredAssociate); + context.SaveChanges(); + } // Simulate detach + + var expectedAssociateId = requiredAssociate.Id; + var owned = new RootEntity { RequiredAssociate = requiredAssociate }; + root.Sources.Add(owned); + + using (var context = new TestDbContext()) + { + root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); + context.SaveChanges(); + + var ownedAfterSave = root.Sources.FirstOrDefault(); + Assert.IsNotNull(ownedAfterSave); + Assert.IsNotNull(ownedAfterSave.RequiredAssociate); + Assert.AreEqual(expectedAssociateId, ownedAfterSave.RequiredAssociate.Id); + + var ownedReloaded = context.RootEntities.Single(r => r.Id == ownedAfterSave.Id); + Assert.IsNotNull(ownedReloaded.RequiredAssociate); + Assert.AreEqual(expectedAssociateId, ownedReloaded.RequiredAssociate.Id); + } + } + + [TestMethod] + public void ShouldAddTwoNewOwnedItemsWithSharedRequiredAssociate() + { + var root = new RootEntity { RequiredAssociate = new RequiredAssociate(), Sources = new List() }; + var requiredAssociate = new RequiredAssociate(); + using (var context = new TestDbContext()) + { + context.RootEntities.Add(root); + context.RequiredAssociates.Add(requiredAssociate); + context.SaveChanges(); + } // Simulate detach + + var expectedAssociateId = requiredAssociate.Id; + var ownedOne = new RootEntity { RequiredAssociate = requiredAssociate }; + root.Sources.Add(ownedOne); + var ownedTwo = new RootEntity { RequiredAssociate = requiredAssociate }; + root.Sources.Add(ownedTwo); + + using (var context = new TestDbContext()) + { + root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); + context.SaveChanges(); + + Assert.IsTrue(root.Sources.All(s => s.RequiredAssociate.Id == expectedAssociateId)); + + var sourceIds = root.Sources.Select(s => s.Id).ToArray(); + var sourcesReloaded = context.RootEntities.Where(r => sourceIds.Contains(r.Id)).ToList(); + Assert.IsTrue(sourcesReloaded.All(s => s.RequiredAssociate != null && s.RequiredAssociate.Id == expectedAssociateId)); + } + } + + [TestMethod] + public void ShouldNotChangeRequiredAssociateEvenIfItIsUsedTwice() + { + var requiredAssociate = new RequiredAssociate(); + var root = new RootEntity { RequiredAssociate = requiredAssociate, Sources = new List() }; + using (var context = new TestDbContext()) + { + context.RootEntities.Add(root); + context.RequiredAssociates.Add(requiredAssociate); + context.SaveChanges(); + } // Simulate detach + + var expectedAssociateId = requiredAssociate.Id; + var owned = new RootEntity { RequiredAssociate = requiredAssociate }; + root.Sources.Add(owned); + + using (var context = new TestDbContext()) + { + root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); + context.SaveChanges(); + + var ownedAfterSave = root.Sources.FirstOrDefault(); + Assert.IsNotNull(ownedAfterSave); + Assert.IsNotNull(ownedAfterSave.RequiredAssociate); + Assert.AreEqual(expectedAssociateId, ownedAfterSave.RequiredAssociate.Id); + + var ownedReloaded = context.RootEntities.Single(r => r.Id == ownedAfterSave.Id); + Assert.IsNotNull(ownedReloaded.RequiredAssociate); + Assert.AreEqual(expectedAssociateId, ownedReloaded.RequiredAssociate.Id); + } + } + + [TestMethod] + public void ShouldRemoveItemsInOwnedCollection() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "Hello" }, + new OneToManyOwnedModel { Title = "Hello2" }, + new OneToManyOwnedModel { Title = "Hello3" } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); + node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); + node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); + using (var context = new TestDbContext()) + { + // Setup mapping + context.UpdateGraph(node1, map => map + .OwnedCollection(p => p.OneToManyOwned)); + + context.SaveChanges(); + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + Assert.IsTrue(node2.OneToManyOwned.Count == 0); + } + } + + [TestMethod] + public void ShouldRemoveItemsInOwnedCollectionWhenSetToNull() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "Hello" }, + new OneToManyOwnedModel { Title = "Hello2" }, + new OneToManyOwnedModel { Title = "Hello3" } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + node1.OneToManyOwned = null; + using (var context = new TestDbContext()) + { + // Setup mapping + context.UpdateGraph(node1, map => map + .OwnedCollection(p => p.OneToManyOwned)); + + context.SaveChanges(); + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + Assert.IsTrue(node2.OneToManyOwned.Count == 0); + } + } + + [TestMethod] + public void ShouldMergeTwoCollectionsAndDecideOnUpdatesDeletesAndAdds() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "This" }, + new OneToManyOwnedModel { Title = "Is" }, + new OneToManyOwnedModel { Title = "A" }, + new OneToManyOwnedModel { Title = "Test" } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); + node1.OneToManyOwned.First().Title = "Hello"; + node1.OneToManyOwned.Add(new OneToManyOwnedModel { Title = "Finish" }); + using (var context = new TestDbContext()) + { + // Setup mapping + context.UpdateGraph(node1, map => map + .OwnedCollection(p => p.OneToManyOwned)); + + context.SaveChanges(); + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + var list = node2.OneToManyOwned.ToList(); + Assert.IsTrue(list[0].Title == "Hello"); + Assert.IsTrue(list[1].Title == "A"); + Assert.IsTrue(list[2].Title == "Test"); + Assert.IsTrue(list[3].Title == "Finish"); + } + } + + [TestMethod] + public void ShouldUpdateItemInOwnedCollectionWithCustomKey() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "Hello", UniqueId = new Guid("DA6B78FF-BB7F-4FA1-8659-F64AC6457D14") } + } + }; + + int originalOwnedId; + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + originalOwnedId = node1.OneToManyOwned.First().Id; + } // Simulate detach + + node1.OneToManyOwned.First().Title = "What's up"; + node1.OneToManyOwned.First().Id = 0; //We will try to update on Guid + + using (var context = new TestDbContext()) + { + // Setup mapping + context.UpdateGraph(node1, map => map + .OwnedCollection(p => p.OneToManyOwned), + keysConfiguration: new KeysConfiguration() + .ForEntity(e => e.UniqueId)); + + context.SaveChanges(); + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + var owned = node2.OneToManyOwned.First(); + Assert.IsTrue(owned.OneParent == node2 && owned.Title == "What's up" && owned.Id == originalOwnedId); + } + } + } +} diff --git a/GraphDiff/GraphDiff/DbContextExtensions.cs b/GraphDiff/GraphDiff/DbContextExtensions.cs index 423a806..e29fdfe 100644 --- a/GraphDiff/GraphDiff/DbContextExtensions.cs +++ b/GraphDiff/GraphDiff/DbContextExtensions.cs @@ -7,11 +7,8 @@ using RefactorThis.GraphDiff.Internal; using RefactorThis.GraphDiff.Internal.Caching; using RefactorThis.GraphDiff.Internal.Graph; -using RefactorThis.GraphDiff.Internal.GraphBuilders; using System; -using System.Collections.Generic; using System.Data.Entity; -using System.Linq; using System.Linq.Expressions; namespace RefactorThis.GraphDiff @@ -26,10 +23,11 @@ public static class DbContextExtensions /// The root entity. /// The mapping configuration to define the bounds of the graph /// Update configuration overrides + /// The mapping configuration to define properties to use as key. The primary key is used if no other configuration is given. /// The attached entity graph - public static T UpdateGraph(this DbContext context, T entity, Expression, object>> mapping, UpdateParams updateParams = null) where T : class, new() + public static T UpdateGraph(this DbContext context, T entity, Expression, object>> mapping, UpdateParams updateParams = null, KeysConfiguration keysConfiguration = null) where T : class, new() { - return UpdateGraph(context, entity, mapping, null, updateParams); + return UpdateGraph(context, entity, mapping, null, updateParams, keysConfiguration); } /// @@ -40,10 +38,11 @@ public static class DbContextExtensions /// The root entity. /// Pre-configured mappingScheme /// Update configuration overrides + /// The mapping configuration to define properties to use as key. The primary key is used if no other configuration is given. /// The attached entity graph - public static T UpdateGraph(this DbContext context, T entity, string mappingScheme, UpdateParams updateParams = null) where T : class, new() + public static T UpdateGraph(this DbContext context, T entity, string mappingScheme, UpdateParams updateParams = null, KeysConfiguration keysConfiguration = null) where T : class, new() { - return UpdateGraph(context, entity, null, mappingScheme, updateParams); + return UpdateGraph(context, entity, null, mappingScheme, updateParams, keysConfiguration); } /// @@ -53,10 +52,11 @@ public static class DbContextExtensions /// The database context to attach / detach. /// The root entity. /// Update configuration overrides + /// The mapping configuration to define properties to use as key. The primary key is used if no other configuration is given. /// The attached entity graph - public static T UpdateGraph(this DbContext context, T entity, UpdateParams updateParams = null) where T : class, new() + public static T UpdateGraph(this DbContext context, T entity, UpdateParams updateParams = null, KeysConfiguration keysConfiguration = null) where T : class, new() { - return UpdateGraph(context, entity, null, null, updateParams); + return UpdateGraph(context, entity, null, null, updateParams, keysConfiguration); } /// @@ -69,7 +69,7 @@ public static class DbContextExtensions /// The aggregate loaded from the database public static T LoadAggregate(this DbContext context, Func keyPredicate, QueryMode queryMode = QueryMode.SingleQuery) where T : class { - var entityManager = new EntityManager(context); + var entityManager = new EntityManager(context, new KeysConfiguration()); var graph = new AggregateRegister(new CacheProvider()).GetEntityGraph(); var queryLoader = new QueryLoader(context, entityManager); @@ -85,12 +85,12 @@ public static T LoadAggregate(this DbContext context, Func keyPredic // other methods are convenience wrappers around this. private static T UpdateGraph(this DbContext context, T entity, Expression, object>> mapping, - string mappingScheme, UpdateParams updateParams) where T : class, new() + string mappingScheme, UpdateParams updateParams, KeysConfiguration keysConfiguration) where T : class, new() { GraphNode root; GraphDiffer differ; - var entityManager = new EntityManager(context); + var entityManager = new EntityManager(context, keysConfiguration ?? new KeysConfiguration()); var queryLoader = new QueryLoader(context, entityManager); var register = new AggregateRegister(new CacheProvider()); diff --git a/GraphDiff/GraphDiff/GraphDiff.csproj b/GraphDiff/GraphDiff/GraphDiff.csproj index be0b99c..6222b4b 100644 --- a/GraphDiff/GraphDiff/GraphDiff.csproj +++ b/GraphDiff/GraphDiff/GraphDiff.csproj @@ -72,6 +72,7 @@ + diff --git a/GraphDiff/GraphDiff/Internal/ChangeTracker.cs b/GraphDiff/GraphDiff/Internal/ChangeTracker.cs index 8fe557a..7e03f19 100644 --- a/GraphDiff/GraphDiff/Internal/ChangeTracker.cs +++ b/GraphDiff/GraphDiff/Internal/ChangeTracker.cs @@ -1,231 +1,245 @@ -using System; -using System.Data.Entity; -using System.Data.Entity.Core.Metadata.Edm; -using System.Data.Entity.Core.Objects; -using System.Data.Entity.Infrastructure; -using System.Linq; -using System.Reflection; - -namespace RefactorThis.GraphDiff.Internal.Graph -{ - /// - /// Change tracker abstraction - /// - internal interface IChangeTracker - { - /// - /// Adds a new entity to the change tracker - /// - /// The new entity - void AddItem(object entity); - - /// - /// Updates the values of an existing tracked entity - /// - /// The old item - /// The new item values to apply - /// Perform a concurrency check when updating - void UpdateItem(object from, object to, bool doConcurrencyCheck = false); - - /// - /// Marks an entity as requiring removal from the database - /// - /// The entity to be removed - void RemoveItem(object entity); - - /// - /// Returns the current state of an entity (detached, attached, etc) - /// - EntityState GetItemState(object entity); - - /// - /// Attach the associated entity to the change tracker and reload the entity. - /// - object AttachAndReloadAssociatedEntity(object entity); - - /// - /// Ensure references back to the parent from the child are kept in sync - /// - void AttachCyclicNavigationProperty(object parent, object child); - - /// - /// Ensures all required navigation properties are attached - /// - void AttachRequiredNavigationProperties(object updating, object persisted); - } - - internal class ChangeTracker : IChangeTracker - { - private readonly DbContext _context; - private readonly IEntityManager _entityManager; - - private ObjectContext _objectContext - { - get { return ((IObjectContextAdapter)_context).ObjectContext; } - } - - public ChangeTracker(DbContext context, IEntityManager entityManager) - { - _entityManager = entityManager; - _context = context; - } - - public void AddItem(object item) - { - var type = ObjectContext.GetObjectType(item.GetType()); - _context.Set(type).Add(item); - } - - public EntityState GetItemState(object item) - { - return _context.Entry(item).State; - } - - public void UpdateItem(object from, object to, bool doConcurrencyCheck = false) - { - if (doConcurrencyCheck && _context.Entry(to).State != EntityState.Added) - { - EnsureConcurrency(from, to); - } - - _context.Entry(to).CurrentValues.SetValues(from); - } - - public void RemoveItem(object item) - { - var type = ObjectContext.GetObjectType(item.GetType()); - _context.Set(type).Remove(item); - } - - public void AttachCyclicNavigationProperty(object parent, object child) - { - if (parent == null || child == null) - { - return; - } - - var parentType = ObjectContext.GetObjectType(parent.GetType()); - var childType = ObjectContext.GetObjectType(child.GetType()); - - var navigationProperties = _entityManager.GetNavigationPropertiesForType(childType); - - var parentNavigationProperty = navigationProperties - .Where(navigation => navigation.TypeUsage.EdmType.Name == parentType.Name) - .Select(navigation => childType.GetProperty(navigation.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) - .FirstOrDefault(); - - if (parentNavigationProperty != null) - { - parentNavigationProperty.SetValue(child, parent, null); - } - } - - public object AttachAndReloadAssociatedEntity(object entity) - { - var localCopy = FindTrackedEntity(entity); - if (localCopy != null) - { - return localCopy; - } - - if (_context.Entry(entity).State == EntityState.Detached) - { - // TODO look into a possible better way of doing this, I don't particularly like it - // will add a key-only object to the change tracker. at the moment this is being reloaded, - // performing a db query which would impact performance - var entityType = ObjectContext.GetObjectType(entity.GetType()); - var instance = _entityManager.CreateEmptyEntityWithKey(entity); - - _context.Set(entityType).Attach(instance); - _context.Entry(instance).Reload(); - - AttachRequiredNavigationProperties(entity, instance); - return instance; - } - - if (GraphDiffConfiguration.ReloadAssociatedEntitiesWhenAttached) - { - _context.Entry(entity).Reload(); - } - - return entity; - } - - public void AttachRequiredNavigationProperties(object updating, object persisted) - { - var entityType = ObjectContext.GetObjectType(updating.GetType()); - foreach (var navigationProperty in _entityManager.GetRequiredNavigationPropertiesForType(updating.GetType())) - { - var navigationPropertyInfo = entityType.GetProperty(navigationProperty.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); - var associatedEntity = navigationPropertyInfo.GetValue(updating, null); - - if (associatedEntity != null) - { - // TODO this is performing a db query - look for alternative. - associatedEntity = FindEntityByKey(associatedEntity); - } - - navigationPropertyInfo.SetValue(persisted, associatedEntity, null); - } - } - - // Privates - - private void EnsureConcurrency(object entity1, object entity2) - { - // get concurrency properties of T - var entityType = ObjectContext.GetObjectType(entity1.GetType()); - var metadata = _objectContext.MetadataWorkspace; - - var objType = metadata.GetItems(DataSpace.OSpace).Single(p => p.FullName == entityType.FullName); - - // TODO need internal string, code smells bad.. any better way to do this? - var cTypeName = (string)objType.GetType() - .GetProperty("CSpaceTypeName", BindingFlags.Instance | BindingFlags.NonPublic) - .GetValue(objType, null); - - var conceptualType = metadata.GetItems(DataSpace.CSpace).Single(p => p.FullName == cTypeName); - var concurrencyProperties = conceptualType.Members - .Where(member => member.TypeUsage.Facets.Any(facet => facet.Name == "ConcurrencyMode" && (ConcurrencyMode)facet.Value == ConcurrencyMode.Fixed)) - .Select(member => entityType.GetProperty(member.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) - .ToList(); - - // Check if concurrency properties are equal - // TODO EF should do this automatically should it not? - foreach (var concurrencyProp in concurrencyProperties) - { - var type = concurrencyProp.PropertyType; - var obj1 = concurrencyProp.GetValue(entity1, null); - var obj2 = concurrencyProp.GetValue(entity2, null); - - // if is byte[] use array comparison, else equals(). - if ( - (obj1 == null || obj2 == null) || - (type == typeof(byte[]) && !((byte[])obj1).SequenceEqual((byte[])obj2)) || - (type != typeof(byte[]) && !obj1.Equals(obj2)) - ) - { - throw new DbUpdateConcurrencyException(String.Format("{0} failed optimistic concurrency", concurrencyProp.Name)); - } - } - } - - private object FindTrackedEntity(object entity) - { - var eType = ObjectContext.GetObjectType(entity.GetType()); - return _context.Set(eType) - .Local - .OfType() - .FirstOrDefault(local => _entityManager.AreKeysIdentical(local, entity)); - } - - private object FindEntityByKey(object associatedEntity) - { - var associatedEntityType = ObjectContext.GetObjectType(associatedEntity.GetType()); - var keyFields = _entityManager.GetPrimaryKeyFieldsFor(associatedEntityType); - var keys = keyFields.Select(key => key.GetValue(associatedEntity, null)).ToArray(); - return _context.Set(associatedEntityType).Find(keys); - } - - } -} +using System; +using System.Data.Entity; +using System.Data.Entity.Core.Metadata.Edm; +using System.Data.Entity.Core.Objects; +using System.Data.Entity.Infrastructure; +using System.Linq; +using System.Reflection; + +namespace RefactorThis.GraphDiff.Internal.Graph +{ + /// + /// Change tracker abstraction + /// + internal interface IChangeTracker + { + /// + /// Adds a new entity to the change tracker + /// + /// The new entity + void AddItem(object entity); + + /// + /// Updates the values of an existing tracked entity + /// + /// The old item + /// The new item values to apply + /// Perform a concurrency check when updating + void UpdateItem(object from, object to, bool doConcurrencyCheck = false); + + /// + /// Marks an entity as requiring removal from the database + /// + /// The entity to be removed + void RemoveItem(object entity); + + /// + /// Returns the current state of an entity (detached, attached, etc) + /// + EntityState GetItemState(object entity); + + /// + /// Attach the associated entity to the change tracker and reload the entity. + /// + object AttachAndReloadAssociatedEntity(object entity); + + /// + /// Ensure references back to the parent from the child are kept in sync + /// + void AttachCyclicNavigationProperty(object parent, object child); + + /// + /// Ensures all required navigation properties are attached + /// + void AttachRequiredNavigationProperties(object updating, object persisted); + } + + internal class ChangeTracker : IChangeTracker + { + private readonly DbContext _context; + private readonly IEntityManager _entityManager; + + private ObjectContext _objectContext + { + get { return ((IObjectContextAdapter)_context).ObjectContext; } + } + + public ChangeTracker(DbContext context, IEntityManager entityManager) + { + _entityManager = entityManager; + _context = context; + } + + public void AddItem(object item) + { + var type = ObjectContext.GetObjectType(item.GetType()); + _context.Set(type).Add(item); + } + + public EntityState GetItemState(object item) + { + return _context.Entry(item).State; + } + + public void UpdateItem(object from, object to, bool doConcurrencyCheck = false) + { + Type entityType = from.GetType(); + var toEntry = _context.Entry(to); + + if (doConcurrencyCheck && toEntry.State != EntityState.Added) + { + EnsureConcurrency(entityType, from, to); + } + + var metadata = _objectContext.MetadataWorkspace + .GetItems(DataSpace.OSpace) + .SingleOrDefault(p => p.FullName == entityType.FullName); + + // When a custom key is specified the primary key in the from object is ignored. + // We must set it to the actual value from database so it won't try to change the primary key + if (_entityManager.KeysConfiguration.HasConfigurationFor(entityType)) + { + // Copy inverted for primary key : from context entity to detached entity + _entityManager.CopyPrimaryKeyFields(entityType, from: to, to: from); + } + + _context.Entry(to).CurrentValues.SetValues(from); + } + + public void RemoveItem(object item) + { + var type = ObjectContext.GetObjectType(item.GetType()); + _context.Set(type).Remove(item); + } + + public void AttachCyclicNavigationProperty(object parent, object child) + { + if (parent == null || child == null) + { + return; + } + + var parentType = ObjectContext.GetObjectType(parent.GetType()); + var childType = ObjectContext.GetObjectType(child.GetType()); + + var navigationProperties = _entityManager.GetNavigationPropertiesForType(childType); + + var parentNavigationProperty = navigationProperties + .Where(navigation => navigation.TypeUsage.EdmType.Name == parentType.Name) + .Select(navigation => childType.GetProperty(navigation.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + .FirstOrDefault(); + + if (parentNavigationProperty != null) + { + parentNavigationProperty.SetValue(child, parent, null); + } + } + + public object AttachAndReloadAssociatedEntity(object entity) + { + var localCopy = FindTrackedEntity(entity); + if (localCopy != null) + { + return localCopy; + } + + if (_context.Entry(entity).State == EntityState.Detached) + { + // TODO look into a possible better way of doing this, I don't particularly like it + // will add a key-only object to the change tracker. at the moment this is being reloaded, + // performing a db query which would impact performance + var entityType = ObjectContext.GetObjectType(entity.GetType()); + var instance = _entityManager.CreateEmptyEntityWithKey(entity); + + _context.Set(entityType).Attach(instance); + _context.Entry(instance).Reload(); + + AttachRequiredNavigationProperties(entity, instance); + return instance; + } + + if (GraphDiffConfiguration.ReloadAssociatedEntitiesWhenAttached) + { + _context.Entry(entity).Reload(); + } + + return entity; + } + + public void AttachRequiredNavigationProperties(object updating, object persisted) + { + var entityType = ObjectContext.GetObjectType(updating.GetType()); + foreach (var navigationProperty in _entityManager.GetRequiredNavigationPropertiesForType(updating.GetType())) + { + var navigationPropertyInfo = entityType.GetProperty(navigationProperty.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + var associatedEntity = navigationPropertyInfo.GetValue(updating, null); + + if (associatedEntity != null) + { + // TODO this is performing a db query - look for alternative. + associatedEntity = FindEntityByKey(associatedEntity); + } + + navigationPropertyInfo.SetValue(persisted, associatedEntity, null); + } + } + + // Privates + + private void EnsureConcurrency(Type entityType, object entity1, object entity2) + { + // get concurrency properties of T + var metadata = _objectContext.MetadataWorkspace; + + var objType = metadata.GetItems(DataSpace.OSpace).Single(p => p.FullName == entityType.FullName); + + // TODO need internal string, code smells bad.. any better way to do this? + var cTypeName = (string)objType.GetType() + .GetProperty("CSpaceTypeName", BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(objType, null); + + var conceptualType = metadata.GetItems(DataSpace.CSpace).Single(p => p.FullName == cTypeName); + var concurrencyProperties = conceptualType.Members + .Where(member => member.TypeUsage.Facets.Any(facet => facet.Name == "ConcurrencyMode" && (ConcurrencyMode)facet.Value == ConcurrencyMode.Fixed)) + .Select(member => entityType.GetProperty(member.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + .ToList(); + + // Check if concurrency properties are equal + // TODO EF should do this automatically should it not? + foreach (var concurrencyProp in concurrencyProperties) + { + var type = concurrencyProp.PropertyType; + var obj1 = concurrencyProp.GetValue(entity1, null); + var obj2 = concurrencyProp.GetValue(entity2, null); + + // if is byte[] use array comparison, else equals(). + if ( + (obj1 == null || obj2 == null) || + (type == typeof(byte[]) && !((byte[])obj1).SequenceEqual((byte[])obj2)) || + (type != typeof(byte[]) && !obj1.Equals(obj2)) + ) + { + throw new DbUpdateConcurrencyException(String.Format("{0} failed optimistic concurrency", concurrencyProp.Name)); + } + } + } + + private object FindTrackedEntity(object entity) + { + var eType = ObjectContext.GetObjectType(entity.GetType()); + return _context.Set(eType) + .Local + .OfType() + .FirstOrDefault(local => _entityManager.AreKeysIdentical(local, entity)); + } + + private object FindEntityByKey(object associatedEntity) + { + var associatedEntityType = ObjectContext.GetObjectType(associatedEntity.GetType()); + var keyFields = _entityManager.GetKeyFieldsFor(associatedEntityType); + var keys = keyFields.Select(key => key.GetValue(associatedEntity, null)).ToArray(); + return _context.Set(associatedEntityType).Find(keys); + } + + } +} diff --git a/GraphDiff/GraphDiff/Internal/EntityManager.cs b/GraphDiff/GraphDiff/Internal/EntityManager.cs index 6f0bd6f..a7b616d 100644 --- a/GraphDiff/GraphDiff/Internal/EntityManager.cs +++ b/GraphDiff/GraphDiff/Internal/EntityManager.cs @@ -1,146 +1,199 @@ -using System; -using System.Collections.Generic; -using System.Data.Entity; -using System.Data.Entity.Core; -using System.Data.Entity.Core.Metadata.Edm; -using System.Data.Entity.Core.Objects; -using System.Data.Entity.Infrastructure; -using System.Linq; -using System.Reflection; - -namespace RefactorThis.GraphDiff.Internal -{ - /// - /// Entity creation, type & key management - /// - internal interface IEntityManager - { - /// - /// Creates the unique entity key for an entity - /// - EntityKey CreateEntityKey(object entity); - - /// - /// Creates an empty object of the same type and keys matching the entity provided - /// - object CreateEmptyEntityWithKey(object entity); - - /// - /// Returns true if the keys of entity1 and entity2 match. - /// - bool AreKeysIdentical(object entity1, object entity2); - - /// - /// Returns the primary key fields for a given entity type - /// - IEnumerable GetPrimaryKeyFieldsFor(Type entityType); - - /// - /// Retrieves the required navigation properties for the given type - /// - IEnumerable GetRequiredNavigationPropertiesForType(Type entityType); - - /// - /// Retrieves the navigation properties for the given type - /// - IEnumerable GetNavigationPropertiesForType(Type entityType); - } - - internal class EntityManager : IEntityManager - { - private readonly DbContext _context; - private ObjectContext _objectContext - { - get { return ((IObjectContextAdapter)_context).ObjectContext; } - } - - public EntityManager(DbContext context) - { - _context = context; - } - - public EntityKey CreateEntityKey(object entity) - { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } - - return _objectContext.CreateEntityKey(GetEntitySetName(entity.GetType()), entity); - } - - public bool AreKeysIdentical(object newValue, object dbValue) - { - if (newValue == null || dbValue == null) - { - return false; - } - - return CreateEntityKey(newValue) == CreateEntityKey(dbValue); - } - - public object CreateEmptyEntityWithKey(object entity) - { - var instance = Activator.CreateInstance(entity.GetType()); - CopyPrimaryKeyFields(entity, instance); - return instance; - } - - public IEnumerable GetPrimaryKeyFieldsFor(Type entityType) - { - var metadata = _objectContext.MetadataWorkspace - .GetItems(DataSpace.OSpace) - .SingleOrDefault(p => p.FullName == entityType.FullName); - - if (metadata == null) - { - throw new InvalidOperationException(String.Format("The type {0} is not known to the DbContext.", entityType.FullName)); - } - - return metadata.KeyMembers - .Select(k => entityType.GetProperty(k.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) - .ToList(); - } - - public IEnumerable GetRequiredNavigationPropertiesForType(Type entityType) - { - return GetNavigationPropertiesForType(ObjectContext.GetObjectType(entityType)) - .Where(navigationProperty => navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One); - } - - public IEnumerable GetNavigationPropertiesForType(Type entityType) - { - return _objectContext.MetadataWorkspace - .GetItems(DataSpace.OSpace) - .Single(p => p.FullName == entityType.FullName) - .NavigationProperties; - } - - private string GetEntitySetName(Type entityType) - { - Type type = entityType; - EntitySetBase set = null; - - while (set == null && type != null) - { - set = _objectContext.MetadataWorkspace - .GetEntityContainer(_objectContext.DefaultContainerName, DataSpace.CSpace) - .EntitySets - .FirstOrDefault(item => item.ElementType.Name.Equals(type.Name)); - - type = type.BaseType; - } - - return set != null ? set.Name : null; - } - - private void CopyPrimaryKeyFields(object from, object to) - { - var keyProperties = GetPrimaryKeyFieldsFor(from.GetType()); - foreach (var keyProperty in keyProperties) - { - keyProperty.SetValue(to, keyProperty.GetValue(from, null), null); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Data.Entity.Core; +using System.Data.Entity.Core.Metadata.Edm; +using System.Data.Entity.Core.Objects; +using System.Data.Entity.Infrastructure; +using System.Linq; +using System.Reflection; + +namespace RefactorThis.GraphDiff.Internal +{ + /// + /// Entity creation, type & key management + /// + internal interface IEntityManager + { + /// + /// Gets custom key mappins for entities + /// + KeysConfiguration KeysConfiguration { get; } + + /// + /// Creates the unique entity key for an entity + /// + EntityKey CreateEntityKey(object entity); + + /// + /// Creates an empty object of the same type and keys matching the entity provided + /// + object CreateEmptyEntityWithKey(object entity); + + /// + /// Returns true if the keys of entity1 and entity2 match. + /// + bool AreKeysIdentical(object entity1, object entity2); + + /// + /// Returns the key fields (using key configuration if available) for a given entity type + /// + IEnumerable GetKeyFieldsFor(Type entityType); + + /// + /// Copy primary key fields from an entity to another of the same type + /// + void CopyPrimaryKeyFields(Type entityType, object from, object to); + + /// + /// Retrieves the required navigation properties for the given type + /// + IEnumerable GetRequiredNavigationPropertiesForType(Type entityType); + + /// + /// Retrieves the navigation properties for the given type + /// + IEnumerable GetNavigationPropertiesForType(Type entityType); + } + + internal class EntityManager : IEntityManager + { + private readonly DbContext _context; + + private ObjectContext _objectContext + { + get { return ((IObjectContextAdapter)_context).ObjectContext; } + } + + public KeysConfiguration KeysConfiguration { get; private set; } + + public EntityManager(DbContext context, KeysConfiguration keysConfiguration) + { + if (context == null) + throw new ArgumentNullException("context"); + if (keysConfiguration == null) + throw new ArgumentNullException("keysConfiguration"); + + _context = context; + KeysConfiguration = keysConfiguration; + } + + public EntityKey CreateEntityKey(object entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); + } + + var entityType = entity.GetType(); + var entitySetName = GetEntitySetName(entityType); + if (KeysConfiguration.HasConfigurationFor(entityType)) + { + var keyMembers = GetKeyFieldsFor(entityType) + .Select(p => new EntityKeyMember(p.Name, p.GetValue(entity, null))); + return new EntityKey(_objectContext.DefaultContainerName + "." + entitySetName, keyMembers); + } + else + { + return _objectContext.CreateEntityKey(entitySetName, entity); + } + } + + public bool AreKeysIdentical(object newValue, object dbValue) + { + if (newValue == null || dbValue == null) + { + return false; + } + + return CreateEntityKey(newValue) == CreateEntityKey(dbValue); + } + + public object CreateEmptyEntityWithKey(object entity) + { + var entityType = entity.GetType(); + var instance = Activator.CreateInstance(entityType); + CopyKeyFields(entityType, entity, instance); + return instance; + } + + public IEnumerable GetKeyFieldsFor(Type entityType) + { + var keyColumns = KeysConfiguration.GetEntityKey(entityType); + if (keyColumns != null) + { + return keyColumns; + } + else + { + return GetPrimaryKeyFieldsFor(entityType); + } + } + + public IEnumerable GetPrimaryKeyFieldsFor(Type entityType) + { + var metadata = _objectContext.MetadataWorkspace + .GetItems(DataSpace.OSpace) + .SingleOrDefault(p => p.FullName == entityType.FullName); + + if (metadata == null) + { + throw new InvalidOperationException(String.Format("The type {0} is not known to the DbContext.", entityType.FullName)); + } + + return metadata.KeyMembers + .Select(k => entityType.GetProperty(k.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + .ToList(); + } + + public IEnumerable GetRequiredNavigationPropertiesForType(Type entityType) + { + return GetNavigationPropertiesForType(ObjectContext.GetObjectType(entityType)) + .Where(navigationProperty => navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One); + } + + public IEnumerable GetNavigationPropertiesForType(Type entityType) + { + return _objectContext.MetadataWorkspace + .GetItems(DataSpace.OSpace) + .Single(p => p.FullName == entityType.FullName) + .NavigationProperties; + } + + private string GetEntitySetName(Type entityType) + { + Type type = entityType; + EntitySetBase set = null; + + while (set == null && type != null) + { + set = _objectContext.MetadataWorkspace + .GetEntityContainer(_objectContext.DefaultContainerName, DataSpace.CSpace) + .EntitySets + .FirstOrDefault(item => item.ElementType.Name.Equals(type.Name)); + + type = type.BaseType; + } + + return set != null ? set.Name : null; + } + + private void CopyKeyFields(Type entityType, object from, object to) + { + var keyProperties = GetKeyFieldsFor(entityType); + foreach (var keyProperty in keyProperties) + { + keyProperty.SetValue(to, keyProperty.GetValue(from, null), null); + } + } + + public void CopyPrimaryKeyFields(Type entityType, object from, object to) + { + var keyProperties = GetPrimaryKeyFieldsFor(entityType); + foreach (var keyProperty in keyProperties) + { + keyProperty.SetValue(to, keyProperty.GetValue(from, null), null); + } + } + } +} diff --git a/GraphDiff/GraphDiff/Internal/GraphDiffer.cs b/GraphDiff/GraphDiff/Internal/GraphDiffer.cs index e176387..7a4264a 100644 --- a/GraphDiff/GraphDiff/Internal/GraphDiffer.cs +++ b/GraphDiff/GraphDiff/Internal/GraphDiffer.cs @@ -55,8 +55,8 @@ public T Merge(T updating, QueryMode queryMode = QueryMode.SingleQuery) throw new InvalidOperationException("GraphDiff supports detached entities only at this time. Please try AsNoTracking() or detach your entites before calling the UpdateGraph method"); } - // Perform recursive update - var entityManager = new EntityManager(_dbContext); + // Perform recursive update + var entityManager = new EntityManager(_dbContext, _entityManager.KeysConfiguration); var changeTracker = new ChangeTracker(_dbContext, entityManager); _root.Update(changeTracker, entityManager, persisted, updating); diff --git a/GraphDiff/GraphDiff/Internal/QueryLoader.cs b/GraphDiff/GraphDiff/Internal/QueryLoader.cs index db4112f..a95843d 100644 --- a/GraphDiff/GraphDiff/Internal/QueryLoader.cs +++ b/GraphDiff/GraphDiff/Internal/QueryLoader.cs @@ -68,7 +68,7 @@ public T LoadEntity(Func keyPredicate, List includeStrings, private Func CreateKeyPredicateExpression(IObjectContextAdapter context, T entity) { // get key properties of T - var keyProperties = _entityManager.GetPrimaryKeyFieldsFor(typeof(T)).ToList(); + var keyProperties = _entityManager.GetKeyFieldsFor(typeof(T)).ToList(); ParameterExpression parameter = Expression.Parameter(typeof(T)); Expression expression = CreateEqualsExpression(entity, keyProperties[0], parameter); diff --git a/GraphDiff/GraphDiff/KeysConfiguration.cs b/GraphDiff/GraphDiff/KeysConfiguration.cs new file mode 100644 index 0000000..a62de5f --- /dev/null +++ b/GraphDiff/GraphDiff/KeysConfiguration.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace RefactorThis.GraphDiff +{ + /// + /// Defines custom entity keys to use during merge instead of primary key + /// + public sealed class KeysConfiguration + { + private class PropertyInfoExpressionVisitor : ExpressionVisitor + { + public PropertyInfo PropertyInfo { get; private set; } + + protected override Expression VisitMember(MemberExpression node) + { + var pi = node.Member as PropertyInfo; + if (pi != null) + PropertyInfo = pi; + return base.VisitMember(node); + } + } + + private readonly Dictionary> _entityKeys = new Dictionary>(); + + /// + /// Defines a key configuration for an entity type. + /// Be careful about your key, you have to ensure uniqueness. + /// + /// Entity type + /// Path to entity key properties. Ensure that your key is unique. + /// Keys configuration to chain call + public KeysConfiguration ForEntity(params Expression>[] key) + { + if (_entityKeys.ContainsKey(typeof(T))) + throw new InvalidOperationException("A key configuration is already defined for entity type" + typeof(T).Name); + var propertyInfos = key.Select(e => GetPropertyInfo(e)); + _entityKeys.Add(typeof(T), propertyInfos.ToList()); + return this; + } + + private static PropertyInfo GetPropertyInfo(Expression> expression) + { + var visitor = new PropertyInfoExpressionVisitor(); + visitor.Visit(expression); + return visitor.PropertyInfo; + } + + internal IList GetEntityKey(Type entityType) + { + IList result; + if (_entityKeys.TryGetValue(entityType, out result)) + return result; + else + return null; + } + + internal bool HasConfigurationFor(Type entityType) + { + return _entityKeys.ContainsKey(entityType); + } + } +} From dbc98402bbcdb1f63b5fd8d43b5d0fce01ff6a72 Mon Sep 17 00:00:00 2001 From: guillaume-fr Date: Thu, 18 Sep 2014 16:01:23 +0200 Subject: [PATCH 2/3] Normalize sources to Unix line endings --- .../Tests/OwnedCollectionBehaviours.cs | 770 +++++++++--------- GraphDiff/GraphDiff/Internal/ChangeTracker.cs | 490 +++++------ GraphDiff/GraphDiff/Internal/EntityManager.cs | 398 ++++----- GraphDiff/GraphDiff/KeysConfiguration.cs | 134 +-- 4 files changed, 896 insertions(+), 896 deletions(-) diff --git a/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs b/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs index b976ead..8551e9f 100644 --- a/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs +++ b/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs @@ -1,385 +1,385 @@ -using System.Collections.ObjectModel; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using RefactorThis.GraphDiff.Tests.Models; -using System.Linq; -using System.Data.Entity; -using System.Collections.Generic; -using System; - -namespace RefactorThis.GraphDiff.Tests.Tests -{ - [TestClass] - public class OwnedCollectionBehaviours : TestBase - { - [TestMethod] - public void ShouldUpdateItemInOwnedCollection() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel { Title = "Hello" } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - node1.OneToManyOwned.First().Title = "What's up"; - using (var context = new TestDbContext()) - { - // Setup mapping - context.UpdateGraph(node1, map => map - .OwnedCollection(p => p.OneToManyOwned)); - - context.SaveChanges(); - var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); - Assert.IsNotNull(node2); - var owned = node2.OneToManyOwned.First(); - Assert.IsTrue(owned.OneParent == node2 && owned.Title == "What's up"); - } - } - - [TestMethod] - public void ShouldUpdateItemInNestedOwnedCollection() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel - { - Title = "Hello", - OneToManyOneToManyOwned = new Collection - { - new OneToManyOneToManyOwnedModel {Title = "BeforeUpdate"} - } - } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - var oneToManyOneToManyOwned = node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); - var expectedId = oneToManyOneToManyOwned.Id; - const string expectedTitle = "AfterUpdate"; - oneToManyOneToManyOwned.Title = expectedTitle; - - using (var context = new TestDbContext()) - { - // Setup mapping - node1 = context.UpdateGraph(node1, map => map.OwnedCollection(p => p.OneToManyOwned, - with => with.OwnedCollection(p => p.OneToManyOneToManyOwned))); - context.SaveChanges(); - - Assert.AreEqual(1, node1.OneToManyOwned.Count); - Assert.AreEqual(1, node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Count); - - oneToManyOneToManyOwned = node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); - Assert.AreEqual(expectedId, oneToManyOneToManyOwned.Id); - Assert.AreEqual(expectedTitle, oneToManyOneToManyOwned.Title); - - var node1Reloaded = context.Nodes - .Include("OneToManyOwned.OneToManyOneToManyOwned") - .Single(n => n.Id == node1.Id); - - Assert.AreEqual(1, node1Reloaded.OneToManyOwned.Count); - Assert.AreEqual(1, node1Reloaded.OneToManyOwned.Single().OneToManyOneToManyOwned.Count); - - oneToManyOneToManyOwned = node1Reloaded.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); - Assert.AreEqual(expectedId, oneToManyOneToManyOwned.Id); - Assert.AreEqual(expectedTitle, oneToManyOneToManyOwned.Title); - } - } - - [TestMethod] - public void ShouldAddNewItemInOwnedCollection() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel { Title = "Hello" } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - var newModel = new OneToManyOwnedModel { Title = "Hi" }; - node1.OneToManyOwned.Add(newModel); - - using (var context = new TestDbContext()) - { - node1 = context.UpdateGraph(node1, map => map.OwnedCollection(p => p.OneToManyOwned)); - context.SaveChanges(); - - var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); - Assert.IsNotNull(node2); - Assert.AreEqual(2, node2.OneToManyOwned.Count); - - var ownedId = node1.OneToManyOwned.Skip(1).Select(o => o.Id).Single(); - var owned = context.OneToManyOwnedModels.Single(p => p.Id == ownedId); - Assert.IsTrue(owned.OneParent == node2 && owned.Title == "Hi"); - } - } - - [TestMethod] - public void ShouldAddNewItemInOwnedCollectionWithoutChangingRequiredAssociate() - { - var root = new RootEntity { RequiredAssociate = new RequiredAssociate(), Sources = new List() }; - var requiredAssociate = new RequiredAssociate(); - using (var context = new TestDbContext()) - { - context.RootEntities.Add(root); - context.RequiredAssociates.Add(requiredAssociate); - context.SaveChanges(); - } // Simulate detach - - var expectedAssociateId = requiredAssociate.Id; - var owned = new RootEntity { RequiredAssociate = requiredAssociate }; - root.Sources.Add(owned); - - using (var context = new TestDbContext()) - { - root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); - context.SaveChanges(); - - var ownedAfterSave = root.Sources.FirstOrDefault(); - Assert.IsNotNull(ownedAfterSave); - Assert.IsNotNull(ownedAfterSave.RequiredAssociate); - Assert.AreEqual(expectedAssociateId, ownedAfterSave.RequiredAssociate.Id); - - var ownedReloaded = context.RootEntities.Single(r => r.Id == ownedAfterSave.Id); - Assert.IsNotNull(ownedReloaded.RequiredAssociate); - Assert.AreEqual(expectedAssociateId, ownedReloaded.RequiredAssociate.Id); - } - } - - [TestMethod] - public void ShouldAddTwoNewOwnedItemsWithSharedRequiredAssociate() - { - var root = new RootEntity { RequiredAssociate = new RequiredAssociate(), Sources = new List() }; - var requiredAssociate = new RequiredAssociate(); - using (var context = new TestDbContext()) - { - context.RootEntities.Add(root); - context.RequiredAssociates.Add(requiredAssociate); - context.SaveChanges(); - } // Simulate detach - - var expectedAssociateId = requiredAssociate.Id; - var ownedOne = new RootEntity { RequiredAssociate = requiredAssociate }; - root.Sources.Add(ownedOne); - var ownedTwo = new RootEntity { RequiredAssociate = requiredAssociate }; - root.Sources.Add(ownedTwo); - - using (var context = new TestDbContext()) - { - root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); - context.SaveChanges(); - - Assert.IsTrue(root.Sources.All(s => s.RequiredAssociate.Id == expectedAssociateId)); - - var sourceIds = root.Sources.Select(s => s.Id).ToArray(); - var sourcesReloaded = context.RootEntities.Where(r => sourceIds.Contains(r.Id)).ToList(); - Assert.IsTrue(sourcesReloaded.All(s => s.RequiredAssociate != null && s.RequiredAssociate.Id == expectedAssociateId)); - } - } - - [TestMethod] - public void ShouldNotChangeRequiredAssociateEvenIfItIsUsedTwice() - { - var requiredAssociate = new RequiredAssociate(); - var root = new RootEntity { RequiredAssociate = requiredAssociate, Sources = new List() }; - using (var context = new TestDbContext()) - { - context.RootEntities.Add(root); - context.RequiredAssociates.Add(requiredAssociate); - context.SaveChanges(); - } // Simulate detach - - var expectedAssociateId = requiredAssociate.Id; - var owned = new RootEntity { RequiredAssociate = requiredAssociate }; - root.Sources.Add(owned); - - using (var context = new TestDbContext()) - { - root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); - context.SaveChanges(); - - var ownedAfterSave = root.Sources.FirstOrDefault(); - Assert.IsNotNull(ownedAfterSave); - Assert.IsNotNull(ownedAfterSave.RequiredAssociate); - Assert.AreEqual(expectedAssociateId, ownedAfterSave.RequiredAssociate.Id); - - var ownedReloaded = context.RootEntities.Single(r => r.Id == ownedAfterSave.Id); - Assert.IsNotNull(ownedReloaded.RequiredAssociate); - Assert.AreEqual(expectedAssociateId, ownedReloaded.RequiredAssociate.Id); - } - } - - [TestMethod] - public void ShouldRemoveItemsInOwnedCollection() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel { Title = "Hello" }, - new OneToManyOwnedModel { Title = "Hello2" }, - new OneToManyOwnedModel { Title = "Hello3" } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); - node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); - node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); - using (var context = new TestDbContext()) - { - // Setup mapping - context.UpdateGraph(node1, map => map - .OwnedCollection(p => p.OneToManyOwned)); - - context.SaveChanges(); - var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); - Assert.IsNotNull(node2); - Assert.IsTrue(node2.OneToManyOwned.Count == 0); - } - } - - [TestMethod] - public void ShouldRemoveItemsInOwnedCollectionWhenSetToNull() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel { Title = "Hello" }, - new OneToManyOwnedModel { Title = "Hello2" }, - new OneToManyOwnedModel { Title = "Hello3" } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - node1.OneToManyOwned = null; - using (var context = new TestDbContext()) - { - // Setup mapping - context.UpdateGraph(node1, map => map - .OwnedCollection(p => p.OneToManyOwned)); - - context.SaveChanges(); - var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); - Assert.IsNotNull(node2); - Assert.IsTrue(node2.OneToManyOwned.Count == 0); - } - } - - [TestMethod] - public void ShouldMergeTwoCollectionsAndDecideOnUpdatesDeletesAndAdds() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel { Title = "This" }, - new OneToManyOwnedModel { Title = "Is" }, - new OneToManyOwnedModel { Title = "A" }, - new OneToManyOwnedModel { Title = "Test" } - } - }; - - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - } // Simulate detach - - node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); - node1.OneToManyOwned.First().Title = "Hello"; - node1.OneToManyOwned.Add(new OneToManyOwnedModel { Title = "Finish" }); - using (var context = new TestDbContext()) - { - // Setup mapping - context.UpdateGraph(node1, map => map - .OwnedCollection(p => p.OneToManyOwned)); - - context.SaveChanges(); - var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); - Assert.IsNotNull(node2); - var list = node2.OneToManyOwned.ToList(); - Assert.IsTrue(list[0].Title == "Hello"); - Assert.IsTrue(list[1].Title == "A"); - Assert.IsTrue(list[2].Title == "Test"); - Assert.IsTrue(list[3].Title == "Finish"); - } - } - - [TestMethod] - public void ShouldUpdateItemInOwnedCollectionWithCustomKey() - { - var node1 = new TestNode - { - Title = "New Node", - OneToManyOwned = new List - { - new OneToManyOwnedModel { Title = "Hello", UniqueId = new Guid("DA6B78FF-BB7F-4FA1-8659-F64AC6457D14") } - } - }; - - int originalOwnedId; - using (var context = new TestDbContext()) - { - context.Nodes.Add(node1); - context.SaveChanges(); - originalOwnedId = node1.OneToManyOwned.First().Id; - } // Simulate detach - - node1.OneToManyOwned.First().Title = "What's up"; - node1.OneToManyOwned.First().Id = 0; //We will try to update on Guid - - using (var context = new TestDbContext()) - { - // Setup mapping - context.UpdateGraph(node1, map => map - .OwnedCollection(p => p.OneToManyOwned), - keysConfiguration: new KeysConfiguration() - .ForEntity(e => e.UniqueId)); - - context.SaveChanges(); - var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); - Assert.IsNotNull(node2); - var owned = node2.OneToManyOwned.First(); - Assert.IsTrue(owned.OneParent == node2 && owned.Title == "What's up" && owned.Id == originalOwnedId); - } - } - } -} +using System.Collections.ObjectModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using RefactorThis.GraphDiff.Tests.Models; +using System.Linq; +using System.Data.Entity; +using System.Collections.Generic; +using System; + +namespace RefactorThis.GraphDiff.Tests.Tests +{ + [TestClass] + public class OwnedCollectionBehaviours : TestBase + { + [TestMethod] + public void ShouldUpdateItemInOwnedCollection() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "Hello" } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + node1.OneToManyOwned.First().Title = "What's up"; + using (var context = new TestDbContext()) + { + // Setup mapping + context.UpdateGraph(node1, map => map + .OwnedCollection(p => p.OneToManyOwned)); + + context.SaveChanges(); + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + var owned = node2.OneToManyOwned.First(); + Assert.IsTrue(owned.OneParent == node2 && owned.Title == "What's up"); + } + } + + [TestMethod] + public void ShouldUpdateItemInNestedOwnedCollection() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel + { + Title = "Hello", + OneToManyOneToManyOwned = new Collection + { + new OneToManyOneToManyOwnedModel {Title = "BeforeUpdate"} + } + } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + var oneToManyOneToManyOwned = node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); + var expectedId = oneToManyOneToManyOwned.Id; + const string expectedTitle = "AfterUpdate"; + oneToManyOneToManyOwned.Title = expectedTitle; + + using (var context = new TestDbContext()) + { + // Setup mapping + node1 = context.UpdateGraph(node1, map => map.OwnedCollection(p => p.OneToManyOwned, + with => with.OwnedCollection(p => p.OneToManyOneToManyOwned))); + context.SaveChanges(); + + Assert.AreEqual(1, node1.OneToManyOwned.Count); + Assert.AreEqual(1, node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Count); + + oneToManyOneToManyOwned = node1.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); + Assert.AreEqual(expectedId, oneToManyOneToManyOwned.Id); + Assert.AreEqual(expectedTitle, oneToManyOneToManyOwned.Title); + + var node1Reloaded = context.Nodes + .Include("OneToManyOwned.OneToManyOneToManyOwned") + .Single(n => n.Id == node1.Id); + + Assert.AreEqual(1, node1Reloaded.OneToManyOwned.Count); + Assert.AreEqual(1, node1Reloaded.OneToManyOwned.Single().OneToManyOneToManyOwned.Count); + + oneToManyOneToManyOwned = node1Reloaded.OneToManyOwned.Single().OneToManyOneToManyOwned.Single(); + Assert.AreEqual(expectedId, oneToManyOneToManyOwned.Id); + Assert.AreEqual(expectedTitle, oneToManyOneToManyOwned.Title); + } + } + + [TestMethod] + public void ShouldAddNewItemInOwnedCollection() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "Hello" } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + var newModel = new OneToManyOwnedModel { Title = "Hi" }; + node1.OneToManyOwned.Add(newModel); + + using (var context = new TestDbContext()) + { + node1 = context.UpdateGraph(node1, map => map.OwnedCollection(p => p.OneToManyOwned)); + context.SaveChanges(); + + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + Assert.AreEqual(2, node2.OneToManyOwned.Count); + + var ownedId = node1.OneToManyOwned.Skip(1).Select(o => o.Id).Single(); + var owned = context.OneToManyOwnedModels.Single(p => p.Id == ownedId); + Assert.IsTrue(owned.OneParent == node2 && owned.Title == "Hi"); + } + } + + [TestMethod] + public void ShouldAddNewItemInOwnedCollectionWithoutChangingRequiredAssociate() + { + var root = new RootEntity { RequiredAssociate = new RequiredAssociate(), Sources = new List() }; + var requiredAssociate = new RequiredAssociate(); + using (var context = new TestDbContext()) + { + context.RootEntities.Add(root); + context.RequiredAssociates.Add(requiredAssociate); + context.SaveChanges(); + } // Simulate detach + + var expectedAssociateId = requiredAssociate.Id; + var owned = new RootEntity { RequiredAssociate = requiredAssociate }; + root.Sources.Add(owned); + + using (var context = new TestDbContext()) + { + root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); + context.SaveChanges(); + + var ownedAfterSave = root.Sources.FirstOrDefault(); + Assert.IsNotNull(ownedAfterSave); + Assert.IsNotNull(ownedAfterSave.RequiredAssociate); + Assert.AreEqual(expectedAssociateId, ownedAfterSave.RequiredAssociate.Id); + + var ownedReloaded = context.RootEntities.Single(r => r.Id == ownedAfterSave.Id); + Assert.IsNotNull(ownedReloaded.RequiredAssociate); + Assert.AreEqual(expectedAssociateId, ownedReloaded.RequiredAssociate.Id); + } + } + + [TestMethod] + public void ShouldAddTwoNewOwnedItemsWithSharedRequiredAssociate() + { + var root = new RootEntity { RequiredAssociate = new RequiredAssociate(), Sources = new List() }; + var requiredAssociate = new RequiredAssociate(); + using (var context = new TestDbContext()) + { + context.RootEntities.Add(root); + context.RequiredAssociates.Add(requiredAssociate); + context.SaveChanges(); + } // Simulate detach + + var expectedAssociateId = requiredAssociate.Id; + var ownedOne = new RootEntity { RequiredAssociate = requiredAssociate }; + root.Sources.Add(ownedOne); + var ownedTwo = new RootEntity { RequiredAssociate = requiredAssociate }; + root.Sources.Add(ownedTwo); + + using (var context = new TestDbContext()) + { + root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); + context.SaveChanges(); + + Assert.IsTrue(root.Sources.All(s => s.RequiredAssociate.Id == expectedAssociateId)); + + var sourceIds = root.Sources.Select(s => s.Id).ToArray(); + var sourcesReloaded = context.RootEntities.Where(r => sourceIds.Contains(r.Id)).ToList(); + Assert.IsTrue(sourcesReloaded.All(s => s.RequiredAssociate != null && s.RequiredAssociate.Id == expectedAssociateId)); + } + } + + [TestMethod] + public void ShouldNotChangeRequiredAssociateEvenIfItIsUsedTwice() + { + var requiredAssociate = new RequiredAssociate(); + var root = new RootEntity { RequiredAssociate = requiredAssociate, Sources = new List() }; + using (var context = new TestDbContext()) + { + context.RootEntities.Add(root); + context.RequiredAssociates.Add(requiredAssociate); + context.SaveChanges(); + } // Simulate detach + + var expectedAssociateId = requiredAssociate.Id; + var owned = new RootEntity { RequiredAssociate = requiredAssociate }; + root.Sources.Add(owned); + + using (var context = new TestDbContext()) + { + root = context.UpdateGraph(root, map => map.OwnedCollection(r => r.Sources, with => with.AssociatedEntity(s => s.RequiredAssociate))); + context.SaveChanges(); + + var ownedAfterSave = root.Sources.FirstOrDefault(); + Assert.IsNotNull(ownedAfterSave); + Assert.IsNotNull(ownedAfterSave.RequiredAssociate); + Assert.AreEqual(expectedAssociateId, ownedAfterSave.RequiredAssociate.Id); + + var ownedReloaded = context.RootEntities.Single(r => r.Id == ownedAfterSave.Id); + Assert.IsNotNull(ownedReloaded.RequiredAssociate); + Assert.AreEqual(expectedAssociateId, ownedReloaded.RequiredAssociate.Id); + } + } + + [TestMethod] + public void ShouldRemoveItemsInOwnedCollection() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "Hello" }, + new OneToManyOwnedModel { Title = "Hello2" }, + new OneToManyOwnedModel { Title = "Hello3" } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); + node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); + node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); + using (var context = new TestDbContext()) + { + // Setup mapping + context.UpdateGraph(node1, map => map + .OwnedCollection(p => p.OneToManyOwned)); + + context.SaveChanges(); + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + Assert.IsTrue(node2.OneToManyOwned.Count == 0); + } + } + + [TestMethod] + public void ShouldRemoveItemsInOwnedCollectionWhenSetToNull() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "Hello" }, + new OneToManyOwnedModel { Title = "Hello2" }, + new OneToManyOwnedModel { Title = "Hello3" } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + node1.OneToManyOwned = null; + using (var context = new TestDbContext()) + { + // Setup mapping + context.UpdateGraph(node1, map => map + .OwnedCollection(p => p.OneToManyOwned)); + + context.SaveChanges(); + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + Assert.IsTrue(node2.OneToManyOwned.Count == 0); + } + } + + [TestMethod] + public void ShouldMergeTwoCollectionsAndDecideOnUpdatesDeletesAndAdds() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "This" }, + new OneToManyOwnedModel { Title = "Is" }, + new OneToManyOwnedModel { Title = "A" }, + new OneToManyOwnedModel { Title = "Test" } + } + }; + + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + } // Simulate detach + + node1.OneToManyOwned.Remove(node1.OneToManyOwned.First()); + node1.OneToManyOwned.First().Title = "Hello"; + node1.OneToManyOwned.Add(new OneToManyOwnedModel { Title = "Finish" }); + using (var context = new TestDbContext()) + { + // Setup mapping + context.UpdateGraph(node1, map => map + .OwnedCollection(p => p.OneToManyOwned)); + + context.SaveChanges(); + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + var list = node2.OneToManyOwned.ToList(); + Assert.IsTrue(list[0].Title == "Hello"); + Assert.IsTrue(list[1].Title == "A"); + Assert.IsTrue(list[2].Title == "Test"); + Assert.IsTrue(list[3].Title == "Finish"); + } + } + + [TestMethod] + public void ShouldUpdateItemInOwnedCollectionWithCustomKey() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "Hello", UniqueId = new Guid("DA6B78FF-BB7F-4FA1-8659-F64AC6457D14") } + } + }; + + int originalOwnedId; + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + originalOwnedId = node1.OneToManyOwned.First().Id; + } // Simulate detach + + node1.OneToManyOwned.First().Title = "What's up"; + node1.OneToManyOwned.First().Id = 0; //We will try to update on Guid + + using (var context = new TestDbContext()) + { + // Setup mapping + context.UpdateGraph(node1, map => map + .OwnedCollection(p => p.OneToManyOwned), + keysConfiguration: new KeysConfiguration() + .ForEntity(e => e.UniqueId)); + + context.SaveChanges(); + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + var owned = node2.OneToManyOwned.First(); + Assert.IsTrue(owned.OneParent == node2 && owned.Title == "What's up" && owned.Id == originalOwnedId); + } + } + } +} diff --git a/GraphDiff/GraphDiff/Internal/ChangeTracker.cs b/GraphDiff/GraphDiff/Internal/ChangeTracker.cs index 7e03f19..680b8db 100644 --- a/GraphDiff/GraphDiff/Internal/ChangeTracker.cs +++ b/GraphDiff/GraphDiff/Internal/ChangeTracker.cs @@ -1,245 +1,245 @@ -using System; -using System.Data.Entity; -using System.Data.Entity.Core.Metadata.Edm; -using System.Data.Entity.Core.Objects; -using System.Data.Entity.Infrastructure; -using System.Linq; -using System.Reflection; - -namespace RefactorThis.GraphDiff.Internal.Graph -{ - /// - /// Change tracker abstraction - /// - internal interface IChangeTracker - { - /// - /// Adds a new entity to the change tracker - /// - /// The new entity - void AddItem(object entity); - - /// - /// Updates the values of an existing tracked entity - /// - /// The old item - /// The new item values to apply - /// Perform a concurrency check when updating - void UpdateItem(object from, object to, bool doConcurrencyCheck = false); - - /// - /// Marks an entity as requiring removal from the database - /// - /// The entity to be removed - void RemoveItem(object entity); - - /// - /// Returns the current state of an entity (detached, attached, etc) - /// - EntityState GetItemState(object entity); - - /// - /// Attach the associated entity to the change tracker and reload the entity. - /// - object AttachAndReloadAssociatedEntity(object entity); - - /// - /// Ensure references back to the parent from the child are kept in sync - /// - void AttachCyclicNavigationProperty(object parent, object child); - - /// - /// Ensures all required navigation properties are attached - /// - void AttachRequiredNavigationProperties(object updating, object persisted); - } - - internal class ChangeTracker : IChangeTracker - { - private readonly DbContext _context; - private readonly IEntityManager _entityManager; - - private ObjectContext _objectContext - { - get { return ((IObjectContextAdapter)_context).ObjectContext; } - } - - public ChangeTracker(DbContext context, IEntityManager entityManager) - { - _entityManager = entityManager; - _context = context; - } - - public void AddItem(object item) - { - var type = ObjectContext.GetObjectType(item.GetType()); - _context.Set(type).Add(item); - } - - public EntityState GetItemState(object item) - { - return _context.Entry(item).State; - } - - public void UpdateItem(object from, object to, bool doConcurrencyCheck = false) - { - Type entityType = from.GetType(); - var toEntry = _context.Entry(to); - - if (doConcurrencyCheck && toEntry.State != EntityState.Added) - { - EnsureConcurrency(entityType, from, to); - } - - var metadata = _objectContext.MetadataWorkspace - .GetItems(DataSpace.OSpace) - .SingleOrDefault(p => p.FullName == entityType.FullName); - - // When a custom key is specified the primary key in the from object is ignored. - // We must set it to the actual value from database so it won't try to change the primary key - if (_entityManager.KeysConfiguration.HasConfigurationFor(entityType)) - { - // Copy inverted for primary key : from context entity to detached entity - _entityManager.CopyPrimaryKeyFields(entityType, from: to, to: from); - } - - _context.Entry(to).CurrentValues.SetValues(from); - } - - public void RemoveItem(object item) - { - var type = ObjectContext.GetObjectType(item.GetType()); - _context.Set(type).Remove(item); - } - - public void AttachCyclicNavigationProperty(object parent, object child) - { - if (parent == null || child == null) - { - return; - } - - var parentType = ObjectContext.GetObjectType(parent.GetType()); - var childType = ObjectContext.GetObjectType(child.GetType()); - - var navigationProperties = _entityManager.GetNavigationPropertiesForType(childType); - - var parentNavigationProperty = navigationProperties - .Where(navigation => navigation.TypeUsage.EdmType.Name == parentType.Name) - .Select(navigation => childType.GetProperty(navigation.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) - .FirstOrDefault(); - - if (parentNavigationProperty != null) - { - parentNavigationProperty.SetValue(child, parent, null); - } - } - - public object AttachAndReloadAssociatedEntity(object entity) - { - var localCopy = FindTrackedEntity(entity); - if (localCopy != null) - { - return localCopy; - } - - if (_context.Entry(entity).State == EntityState.Detached) - { - // TODO look into a possible better way of doing this, I don't particularly like it - // will add a key-only object to the change tracker. at the moment this is being reloaded, - // performing a db query which would impact performance - var entityType = ObjectContext.GetObjectType(entity.GetType()); - var instance = _entityManager.CreateEmptyEntityWithKey(entity); - - _context.Set(entityType).Attach(instance); - _context.Entry(instance).Reload(); - - AttachRequiredNavigationProperties(entity, instance); - return instance; - } - - if (GraphDiffConfiguration.ReloadAssociatedEntitiesWhenAttached) - { - _context.Entry(entity).Reload(); - } - - return entity; - } - - public void AttachRequiredNavigationProperties(object updating, object persisted) - { - var entityType = ObjectContext.GetObjectType(updating.GetType()); - foreach (var navigationProperty in _entityManager.GetRequiredNavigationPropertiesForType(updating.GetType())) - { - var navigationPropertyInfo = entityType.GetProperty(navigationProperty.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); - var associatedEntity = navigationPropertyInfo.GetValue(updating, null); - - if (associatedEntity != null) - { - // TODO this is performing a db query - look for alternative. - associatedEntity = FindEntityByKey(associatedEntity); - } - - navigationPropertyInfo.SetValue(persisted, associatedEntity, null); - } - } - - // Privates - - private void EnsureConcurrency(Type entityType, object entity1, object entity2) - { - // get concurrency properties of T - var metadata = _objectContext.MetadataWorkspace; - - var objType = metadata.GetItems(DataSpace.OSpace).Single(p => p.FullName == entityType.FullName); - - // TODO need internal string, code smells bad.. any better way to do this? - var cTypeName = (string)objType.GetType() - .GetProperty("CSpaceTypeName", BindingFlags.Instance | BindingFlags.NonPublic) - .GetValue(objType, null); - - var conceptualType = metadata.GetItems(DataSpace.CSpace).Single(p => p.FullName == cTypeName); - var concurrencyProperties = conceptualType.Members - .Where(member => member.TypeUsage.Facets.Any(facet => facet.Name == "ConcurrencyMode" && (ConcurrencyMode)facet.Value == ConcurrencyMode.Fixed)) - .Select(member => entityType.GetProperty(member.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) - .ToList(); - - // Check if concurrency properties are equal - // TODO EF should do this automatically should it not? - foreach (var concurrencyProp in concurrencyProperties) - { - var type = concurrencyProp.PropertyType; - var obj1 = concurrencyProp.GetValue(entity1, null); - var obj2 = concurrencyProp.GetValue(entity2, null); - - // if is byte[] use array comparison, else equals(). - if ( - (obj1 == null || obj2 == null) || - (type == typeof(byte[]) && !((byte[])obj1).SequenceEqual((byte[])obj2)) || - (type != typeof(byte[]) && !obj1.Equals(obj2)) - ) - { - throw new DbUpdateConcurrencyException(String.Format("{0} failed optimistic concurrency", concurrencyProp.Name)); - } - } - } - - private object FindTrackedEntity(object entity) - { - var eType = ObjectContext.GetObjectType(entity.GetType()); - return _context.Set(eType) - .Local - .OfType() - .FirstOrDefault(local => _entityManager.AreKeysIdentical(local, entity)); - } - - private object FindEntityByKey(object associatedEntity) - { - var associatedEntityType = ObjectContext.GetObjectType(associatedEntity.GetType()); - var keyFields = _entityManager.GetKeyFieldsFor(associatedEntityType); - var keys = keyFields.Select(key => key.GetValue(associatedEntity, null)).ToArray(); - return _context.Set(associatedEntityType).Find(keys); - } - - } -} +using System; +using System.Data.Entity; +using System.Data.Entity.Core.Metadata.Edm; +using System.Data.Entity.Core.Objects; +using System.Data.Entity.Infrastructure; +using System.Linq; +using System.Reflection; + +namespace RefactorThis.GraphDiff.Internal.Graph +{ + /// + /// Change tracker abstraction + /// + internal interface IChangeTracker + { + /// + /// Adds a new entity to the change tracker + /// + /// The new entity + void AddItem(object entity); + + /// + /// Updates the values of an existing tracked entity + /// + /// The old item + /// The new item values to apply + /// Perform a concurrency check when updating + void UpdateItem(object from, object to, bool doConcurrencyCheck = false); + + /// + /// Marks an entity as requiring removal from the database + /// + /// The entity to be removed + void RemoveItem(object entity); + + /// + /// Returns the current state of an entity (detached, attached, etc) + /// + EntityState GetItemState(object entity); + + /// + /// Attach the associated entity to the change tracker and reload the entity. + /// + object AttachAndReloadAssociatedEntity(object entity); + + /// + /// Ensure references back to the parent from the child are kept in sync + /// + void AttachCyclicNavigationProperty(object parent, object child); + + /// + /// Ensures all required navigation properties are attached + /// + void AttachRequiredNavigationProperties(object updating, object persisted); + } + + internal class ChangeTracker : IChangeTracker + { + private readonly DbContext _context; + private readonly IEntityManager _entityManager; + + private ObjectContext _objectContext + { + get { return ((IObjectContextAdapter)_context).ObjectContext; } + } + + public ChangeTracker(DbContext context, IEntityManager entityManager) + { + _entityManager = entityManager; + _context = context; + } + + public void AddItem(object item) + { + var type = ObjectContext.GetObjectType(item.GetType()); + _context.Set(type).Add(item); + } + + public EntityState GetItemState(object item) + { + return _context.Entry(item).State; + } + + public void UpdateItem(object from, object to, bool doConcurrencyCheck = false) + { + Type entityType = from.GetType(); + var toEntry = _context.Entry(to); + + if (doConcurrencyCheck && toEntry.State != EntityState.Added) + { + EnsureConcurrency(entityType, from, to); + } + + var metadata = _objectContext.MetadataWorkspace + .GetItems(DataSpace.OSpace) + .SingleOrDefault(p => p.FullName == entityType.FullName); + + // When a custom key is specified the primary key in the from object is ignored. + // We must set it to the actual value from database so it won't try to change the primary key + if (_entityManager.KeysConfiguration.HasConfigurationFor(entityType)) + { + // Copy inverted for primary key : from context entity to detached entity + _entityManager.CopyPrimaryKeyFields(entityType, from: to, to: from); + } + + _context.Entry(to).CurrentValues.SetValues(from); + } + + public void RemoveItem(object item) + { + var type = ObjectContext.GetObjectType(item.GetType()); + _context.Set(type).Remove(item); + } + + public void AttachCyclicNavigationProperty(object parent, object child) + { + if (parent == null || child == null) + { + return; + } + + var parentType = ObjectContext.GetObjectType(parent.GetType()); + var childType = ObjectContext.GetObjectType(child.GetType()); + + var navigationProperties = _entityManager.GetNavigationPropertiesForType(childType); + + var parentNavigationProperty = navigationProperties + .Where(navigation => navigation.TypeUsage.EdmType.Name == parentType.Name) + .Select(navigation => childType.GetProperty(navigation.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + .FirstOrDefault(); + + if (parentNavigationProperty != null) + { + parentNavigationProperty.SetValue(child, parent, null); + } + } + + public object AttachAndReloadAssociatedEntity(object entity) + { + var localCopy = FindTrackedEntity(entity); + if (localCopy != null) + { + return localCopy; + } + + if (_context.Entry(entity).State == EntityState.Detached) + { + // TODO look into a possible better way of doing this, I don't particularly like it + // will add a key-only object to the change tracker. at the moment this is being reloaded, + // performing a db query which would impact performance + var entityType = ObjectContext.GetObjectType(entity.GetType()); + var instance = _entityManager.CreateEmptyEntityWithKey(entity); + + _context.Set(entityType).Attach(instance); + _context.Entry(instance).Reload(); + + AttachRequiredNavigationProperties(entity, instance); + return instance; + } + + if (GraphDiffConfiguration.ReloadAssociatedEntitiesWhenAttached) + { + _context.Entry(entity).Reload(); + } + + return entity; + } + + public void AttachRequiredNavigationProperties(object updating, object persisted) + { + var entityType = ObjectContext.GetObjectType(updating.GetType()); + foreach (var navigationProperty in _entityManager.GetRequiredNavigationPropertiesForType(updating.GetType())) + { + var navigationPropertyInfo = entityType.GetProperty(navigationProperty.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + var associatedEntity = navigationPropertyInfo.GetValue(updating, null); + + if (associatedEntity != null) + { + // TODO this is performing a db query - look for alternative. + associatedEntity = FindEntityByKey(associatedEntity); + } + + navigationPropertyInfo.SetValue(persisted, associatedEntity, null); + } + } + + // Privates + + private void EnsureConcurrency(Type entityType, object entity1, object entity2) + { + // get concurrency properties of T + var metadata = _objectContext.MetadataWorkspace; + + var objType = metadata.GetItems(DataSpace.OSpace).Single(p => p.FullName == entityType.FullName); + + // TODO need internal string, code smells bad.. any better way to do this? + var cTypeName = (string)objType.GetType() + .GetProperty("CSpaceTypeName", BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(objType, null); + + var conceptualType = metadata.GetItems(DataSpace.CSpace).Single(p => p.FullName == cTypeName); + var concurrencyProperties = conceptualType.Members + .Where(member => member.TypeUsage.Facets.Any(facet => facet.Name == "ConcurrencyMode" && (ConcurrencyMode)facet.Value == ConcurrencyMode.Fixed)) + .Select(member => entityType.GetProperty(member.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + .ToList(); + + // Check if concurrency properties are equal + // TODO EF should do this automatically should it not? + foreach (var concurrencyProp in concurrencyProperties) + { + var type = concurrencyProp.PropertyType; + var obj1 = concurrencyProp.GetValue(entity1, null); + var obj2 = concurrencyProp.GetValue(entity2, null); + + // if is byte[] use array comparison, else equals(). + if ( + (obj1 == null || obj2 == null) || + (type == typeof(byte[]) && !((byte[])obj1).SequenceEqual((byte[])obj2)) || + (type != typeof(byte[]) && !obj1.Equals(obj2)) + ) + { + throw new DbUpdateConcurrencyException(String.Format("{0} failed optimistic concurrency", concurrencyProp.Name)); + } + } + } + + private object FindTrackedEntity(object entity) + { + var eType = ObjectContext.GetObjectType(entity.GetType()); + return _context.Set(eType) + .Local + .OfType() + .FirstOrDefault(local => _entityManager.AreKeysIdentical(local, entity)); + } + + private object FindEntityByKey(object associatedEntity) + { + var associatedEntityType = ObjectContext.GetObjectType(associatedEntity.GetType()); + var keyFields = _entityManager.GetKeyFieldsFor(associatedEntityType); + var keys = keyFields.Select(key => key.GetValue(associatedEntity, null)).ToArray(); + return _context.Set(associatedEntityType).Find(keys); + } + + } +} diff --git a/GraphDiff/GraphDiff/Internal/EntityManager.cs b/GraphDiff/GraphDiff/Internal/EntityManager.cs index a7b616d..e11dff6 100644 --- a/GraphDiff/GraphDiff/Internal/EntityManager.cs +++ b/GraphDiff/GraphDiff/Internal/EntityManager.cs @@ -1,199 +1,199 @@ -using System; -using System.Collections.Generic; -using System.Data.Entity; -using System.Data.Entity.Core; -using System.Data.Entity.Core.Metadata.Edm; -using System.Data.Entity.Core.Objects; -using System.Data.Entity.Infrastructure; -using System.Linq; -using System.Reflection; - -namespace RefactorThis.GraphDiff.Internal -{ - /// - /// Entity creation, type & key management - /// - internal interface IEntityManager - { - /// - /// Gets custom key mappins for entities - /// - KeysConfiguration KeysConfiguration { get; } - - /// - /// Creates the unique entity key for an entity - /// - EntityKey CreateEntityKey(object entity); - - /// - /// Creates an empty object of the same type and keys matching the entity provided - /// - object CreateEmptyEntityWithKey(object entity); - - /// - /// Returns true if the keys of entity1 and entity2 match. - /// - bool AreKeysIdentical(object entity1, object entity2); - - /// - /// Returns the key fields (using key configuration if available) for a given entity type - /// - IEnumerable GetKeyFieldsFor(Type entityType); - - /// - /// Copy primary key fields from an entity to another of the same type - /// - void CopyPrimaryKeyFields(Type entityType, object from, object to); - - /// - /// Retrieves the required navigation properties for the given type - /// - IEnumerable GetRequiredNavigationPropertiesForType(Type entityType); - - /// - /// Retrieves the navigation properties for the given type - /// - IEnumerable GetNavigationPropertiesForType(Type entityType); - } - - internal class EntityManager : IEntityManager - { - private readonly DbContext _context; - - private ObjectContext _objectContext - { - get { return ((IObjectContextAdapter)_context).ObjectContext; } - } - - public KeysConfiguration KeysConfiguration { get; private set; } - - public EntityManager(DbContext context, KeysConfiguration keysConfiguration) - { - if (context == null) - throw new ArgumentNullException("context"); - if (keysConfiguration == null) - throw new ArgumentNullException("keysConfiguration"); - - _context = context; - KeysConfiguration = keysConfiguration; - } - - public EntityKey CreateEntityKey(object entity) - { - if (entity == null) - { - throw new ArgumentNullException("entity"); - } - - var entityType = entity.GetType(); - var entitySetName = GetEntitySetName(entityType); - if (KeysConfiguration.HasConfigurationFor(entityType)) - { - var keyMembers = GetKeyFieldsFor(entityType) - .Select(p => new EntityKeyMember(p.Name, p.GetValue(entity, null))); - return new EntityKey(_objectContext.DefaultContainerName + "." + entitySetName, keyMembers); - } - else - { - return _objectContext.CreateEntityKey(entitySetName, entity); - } - } - - public bool AreKeysIdentical(object newValue, object dbValue) - { - if (newValue == null || dbValue == null) - { - return false; - } - - return CreateEntityKey(newValue) == CreateEntityKey(dbValue); - } - - public object CreateEmptyEntityWithKey(object entity) - { - var entityType = entity.GetType(); - var instance = Activator.CreateInstance(entityType); - CopyKeyFields(entityType, entity, instance); - return instance; - } - - public IEnumerable GetKeyFieldsFor(Type entityType) - { - var keyColumns = KeysConfiguration.GetEntityKey(entityType); - if (keyColumns != null) - { - return keyColumns; - } - else - { - return GetPrimaryKeyFieldsFor(entityType); - } - } - - public IEnumerable GetPrimaryKeyFieldsFor(Type entityType) - { - var metadata = _objectContext.MetadataWorkspace - .GetItems(DataSpace.OSpace) - .SingleOrDefault(p => p.FullName == entityType.FullName); - - if (metadata == null) - { - throw new InvalidOperationException(String.Format("The type {0} is not known to the DbContext.", entityType.FullName)); - } - - return metadata.KeyMembers - .Select(k => entityType.GetProperty(k.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) - .ToList(); - } - - public IEnumerable GetRequiredNavigationPropertiesForType(Type entityType) - { - return GetNavigationPropertiesForType(ObjectContext.GetObjectType(entityType)) - .Where(navigationProperty => navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One); - } - - public IEnumerable GetNavigationPropertiesForType(Type entityType) - { - return _objectContext.MetadataWorkspace - .GetItems(DataSpace.OSpace) - .Single(p => p.FullName == entityType.FullName) - .NavigationProperties; - } - - private string GetEntitySetName(Type entityType) - { - Type type = entityType; - EntitySetBase set = null; - - while (set == null && type != null) - { - set = _objectContext.MetadataWorkspace - .GetEntityContainer(_objectContext.DefaultContainerName, DataSpace.CSpace) - .EntitySets - .FirstOrDefault(item => item.ElementType.Name.Equals(type.Name)); - - type = type.BaseType; - } - - return set != null ? set.Name : null; - } - - private void CopyKeyFields(Type entityType, object from, object to) - { - var keyProperties = GetKeyFieldsFor(entityType); - foreach (var keyProperty in keyProperties) - { - keyProperty.SetValue(to, keyProperty.GetValue(from, null), null); - } - } - - public void CopyPrimaryKeyFields(Type entityType, object from, object to) - { - var keyProperties = GetPrimaryKeyFieldsFor(entityType); - foreach (var keyProperty in keyProperties) - { - keyProperty.SetValue(to, keyProperty.GetValue(from, null), null); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Data.Entity.Core; +using System.Data.Entity.Core.Metadata.Edm; +using System.Data.Entity.Core.Objects; +using System.Data.Entity.Infrastructure; +using System.Linq; +using System.Reflection; + +namespace RefactorThis.GraphDiff.Internal +{ + /// + /// Entity creation, type & key management + /// + internal interface IEntityManager + { + /// + /// Gets custom key mappins for entities + /// + KeysConfiguration KeysConfiguration { get; } + + /// + /// Creates the unique entity key for an entity + /// + EntityKey CreateEntityKey(object entity); + + /// + /// Creates an empty object of the same type and keys matching the entity provided + /// + object CreateEmptyEntityWithKey(object entity); + + /// + /// Returns true if the keys of entity1 and entity2 match. + /// + bool AreKeysIdentical(object entity1, object entity2); + + /// + /// Returns the key fields (using key configuration if available) for a given entity type + /// + IEnumerable GetKeyFieldsFor(Type entityType); + + /// + /// Copy primary key fields from an entity to another of the same type + /// + void CopyPrimaryKeyFields(Type entityType, object from, object to); + + /// + /// Retrieves the required navigation properties for the given type + /// + IEnumerable GetRequiredNavigationPropertiesForType(Type entityType); + + /// + /// Retrieves the navigation properties for the given type + /// + IEnumerable GetNavigationPropertiesForType(Type entityType); + } + + internal class EntityManager : IEntityManager + { + private readonly DbContext _context; + + private ObjectContext _objectContext + { + get { return ((IObjectContextAdapter)_context).ObjectContext; } + } + + public KeysConfiguration KeysConfiguration { get; private set; } + + public EntityManager(DbContext context, KeysConfiguration keysConfiguration) + { + if (context == null) + throw new ArgumentNullException("context"); + if (keysConfiguration == null) + throw new ArgumentNullException("keysConfiguration"); + + _context = context; + KeysConfiguration = keysConfiguration; + } + + public EntityKey CreateEntityKey(object entity) + { + if (entity == null) + { + throw new ArgumentNullException("entity"); + } + + var entityType = entity.GetType(); + var entitySetName = GetEntitySetName(entityType); + if (KeysConfiguration.HasConfigurationFor(entityType)) + { + var keyMembers = GetKeyFieldsFor(entityType) + .Select(p => new EntityKeyMember(p.Name, p.GetValue(entity, null))); + return new EntityKey(_objectContext.DefaultContainerName + "." + entitySetName, keyMembers); + } + else + { + return _objectContext.CreateEntityKey(entitySetName, entity); + } + } + + public bool AreKeysIdentical(object newValue, object dbValue) + { + if (newValue == null || dbValue == null) + { + return false; + } + + return CreateEntityKey(newValue) == CreateEntityKey(dbValue); + } + + public object CreateEmptyEntityWithKey(object entity) + { + var entityType = entity.GetType(); + var instance = Activator.CreateInstance(entityType); + CopyKeyFields(entityType, entity, instance); + return instance; + } + + public IEnumerable GetKeyFieldsFor(Type entityType) + { + var keyColumns = KeysConfiguration.GetEntityKey(entityType); + if (keyColumns != null) + { + return keyColumns; + } + else + { + return GetPrimaryKeyFieldsFor(entityType); + } + } + + public IEnumerable GetPrimaryKeyFieldsFor(Type entityType) + { + var metadata = _objectContext.MetadataWorkspace + .GetItems(DataSpace.OSpace) + .SingleOrDefault(p => p.FullName == entityType.FullName); + + if (metadata == null) + { + throw new InvalidOperationException(String.Format("The type {0} is not known to the DbContext.", entityType.FullName)); + } + + return metadata.KeyMembers + .Select(k => entityType.GetProperty(k.Name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)) + .ToList(); + } + + public IEnumerable GetRequiredNavigationPropertiesForType(Type entityType) + { + return GetNavigationPropertiesForType(ObjectContext.GetObjectType(entityType)) + .Where(navigationProperty => navigationProperty.ToEndMember.RelationshipMultiplicity == RelationshipMultiplicity.One); + } + + public IEnumerable GetNavigationPropertiesForType(Type entityType) + { + return _objectContext.MetadataWorkspace + .GetItems(DataSpace.OSpace) + .Single(p => p.FullName == entityType.FullName) + .NavigationProperties; + } + + private string GetEntitySetName(Type entityType) + { + Type type = entityType; + EntitySetBase set = null; + + while (set == null && type != null) + { + set = _objectContext.MetadataWorkspace + .GetEntityContainer(_objectContext.DefaultContainerName, DataSpace.CSpace) + .EntitySets + .FirstOrDefault(item => item.ElementType.Name.Equals(type.Name)); + + type = type.BaseType; + } + + return set != null ? set.Name : null; + } + + private void CopyKeyFields(Type entityType, object from, object to) + { + var keyProperties = GetKeyFieldsFor(entityType); + foreach (var keyProperty in keyProperties) + { + keyProperty.SetValue(to, keyProperty.GetValue(from, null), null); + } + } + + public void CopyPrimaryKeyFields(Type entityType, object from, object to) + { + var keyProperties = GetPrimaryKeyFieldsFor(entityType); + foreach (var keyProperty in keyProperties) + { + keyProperty.SetValue(to, keyProperty.GetValue(from, null), null); + } + } + } +} diff --git a/GraphDiff/GraphDiff/KeysConfiguration.cs b/GraphDiff/GraphDiff/KeysConfiguration.cs index a62de5f..d2c1bfe 100644 --- a/GraphDiff/GraphDiff/KeysConfiguration.cs +++ b/GraphDiff/GraphDiff/KeysConfiguration.cs @@ -1,67 +1,67 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; - -namespace RefactorThis.GraphDiff -{ - /// - /// Defines custom entity keys to use during merge instead of primary key - /// - public sealed class KeysConfiguration - { - private class PropertyInfoExpressionVisitor : ExpressionVisitor - { - public PropertyInfo PropertyInfo { get; private set; } - - protected override Expression VisitMember(MemberExpression node) - { - var pi = node.Member as PropertyInfo; - if (pi != null) - PropertyInfo = pi; - return base.VisitMember(node); - } - } - - private readonly Dictionary> _entityKeys = new Dictionary>(); - - /// - /// Defines a key configuration for an entity type. - /// Be careful about your key, you have to ensure uniqueness. - /// - /// Entity type - /// Path to entity key properties. Ensure that your key is unique. - /// Keys configuration to chain call - public KeysConfiguration ForEntity(params Expression>[] key) - { - if (_entityKeys.ContainsKey(typeof(T))) - throw new InvalidOperationException("A key configuration is already defined for entity type" + typeof(T).Name); - var propertyInfos = key.Select(e => GetPropertyInfo(e)); - _entityKeys.Add(typeof(T), propertyInfos.ToList()); - return this; - } - - private static PropertyInfo GetPropertyInfo(Expression> expression) - { - var visitor = new PropertyInfoExpressionVisitor(); - visitor.Visit(expression); - return visitor.PropertyInfo; - } - - internal IList GetEntityKey(Type entityType) - { - IList result; - if (_entityKeys.TryGetValue(entityType, out result)) - return result; - else - return null; - } - - internal bool HasConfigurationFor(Type entityType) - { - return _entityKeys.ContainsKey(entityType); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace RefactorThis.GraphDiff +{ + /// + /// Defines custom entity keys to use during merge instead of primary key + /// + public sealed class KeysConfiguration + { + private class PropertyInfoExpressionVisitor : ExpressionVisitor + { + public PropertyInfo PropertyInfo { get; private set; } + + protected override Expression VisitMember(MemberExpression node) + { + var pi = node.Member as PropertyInfo; + if (pi != null) + PropertyInfo = pi; + return base.VisitMember(node); + } + } + + private readonly Dictionary> _entityKeys = new Dictionary>(); + + /// + /// Defines a key configuration for an entity type. + /// Be careful about your key, you have to ensure uniqueness. + /// + /// Entity type + /// Path to entity key properties. Ensure that your key is unique. + /// Keys configuration to chain call + public KeysConfiguration ForEntity(params Expression>[] key) + { + if (_entityKeys.ContainsKey(typeof(T))) + throw new InvalidOperationException("A key configuration is already defined for entity type" + typeof(T).Name); + var propertyInfos = key.Select(e => GetPropertyInfo(e)); + _entityKeys.Add(typeof(T), propertyInfos.ToList()); + return this; + } + + private static PropertyInfo GetPropertyInfo(Expression> expression) + { + var visitor = new PropertyInfoExpressionVisitor(); + visitor.Visit(expression); + return visitor.PropertyInfo; + } + + internal IList GetEntityKey(Type entityType) + { + IList result; + if (_entityKeys.TryGetValue(entityType, out result)) + return result; + else + return null; + } + + internal bool HasConfigurationFor(Type entityType) + { + return _entityKeys.ContainsKey(entityType); + } + } +} From 402eab14ee8d31d9444d470e8504f9cae965af6c Mon Sep 17 00:00:00 2001 From: guillaume-fr Date: Fri, 19 Sep 2014 15:28:34 +0200 Subject: [PATCH 3/3] Merge on custom key --- .../GraphDiff.Tests/Models/TestModels.cs | 4 +- .../Tests/OwnedCollectionBehaviours.cs | 40 ++++++++++ GraphDiff/GraphDiff/DbContextExtensions.cs | 24 +++--- GraphDiff/GraphDiff/GraphDiff.csproj | 1 + GraphDiff/GraphDiff/Internal/ChangeTracker.cs | 24 ++++-- GraphDiff/GraphDiff/Internal/EntityManager.cs | 75 ++++++++++++++++--- GraphDiff/GraphDiff/Internal/GraphDiffer.cs | 4 +- GraphDiff/GraphDiff/Internal/QueryLoader.cs | 2 +- GraphDiff/GraphDiff/KeysConfiguration.cs | 67 +++++++++++++++++ 9 files changed, 209 insertions(+), 32 deletions(-) create mode 100644 GraphDiff/GraphDiff/KeysConfiguration.cs diff --git a/GraphDiff/GraphDiff.Tests/Models/TestModels.cs b/GraphDiff/GraphDiff.Tests/Models/TestModels.cs index 8988e6d..47468fc 100644 --- a/GraphDiff/GraphDiff.Tests/Models/TestModels.cs +++ b/GraphDiff/GraphDiff.Tests/Models/TestModels.cs @@ -13,7 +13,9 @@ namespace RefactorThis.GraphDiff.Tests.Models public class Entity { [Key] - public int Id { get; set; } + public int Id { get; set; } + + public Guid UniqueId { get; set; } [MaxLength(128)] public string Title { get; set; } diff --git a/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs b/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs index eb12535..8551e9f 100644 --- a/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs +++ b/GraphDiff/GraphDiff.Tests/Tests/OwnedCollectionBehaviours.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Data.Entity; using System.Collections.Generic; +using System; namespace RefactorThis.GraphDiff.Tests.Tests { @@ -341,5 +342,44 @@ public void ShouldMergeTwoCollectionsAndDecideOnUpdatesDeletesAndAdds() Assert.IsTrue(list[3].Title == "Finish"); } } + + [TestMethod] + public void ShouldUpdateItemInOwnedCollectionWithCustomKey() + { + var node1 = new TestNode + { + Title = "New Node", + OneToManyOwned = new List + { + new OneToManyOwnedModel { Title = "Hello", UniqueId = new Guid("DA6B78FF-BB7F-4FA1-8659-F64AC6457D14") } + } + }; + + int originalOwnedId; + using (var context = new TestDbContext()) + { + context.Nodes.Add(node1); + context.SaveChanges(); + originalOwnedId = node1.OneToManyOwned.First().Id; + } // Simulate detach + + node1.OneToManyOwned.First().Title = "What's up"; + node1.OneToManyOwned.First().Id = 0; //We will try to update on Guid + + using (var context = new TestDbContext()) + { + // Setup mapping + context.UpdateGraph(node1, map => map + .OwnedCollection(p => p.OneToManyOwned), + keysConfiguration: new KeysConfiguration() + .ForEntity(e => e.UniqueId)); + + context.SaveChanges(); + var node2 = context.Nodes.Include(p => p.OneToManyOwned).Single(p => p.Id == node1.Id); + Assert.IsNotNull(node2); + var owned = node2.OneToManyOwned.First(); + Assert.IsTrue(owned.OneParent == node2 && owned.Title == "What's up" && owned.Id == originalOwnedId); + } + } } } diff --git a/GraphDiff/GraphDiff/DbContextExtensions.cs b/GraphDiff/GraphDiff/DbContextExtensions.cs index 423a806..e29fdfe 100644 --- a/GraphDiff/GraphDiff/DbContextExtensions.cs +++ b/GraphDiff/GraphDiff/DbContextExtensions.cs @@ -7,11 +7,8 @@ using RefactorThis.GraphDiff.Internal; using RefactorThis.GraphDiff.Internal.Caching; using RefactorThis.GraphDiff.Internal.Graph; -using RefactorThis.GraphDiff.Internal.GraphBuilders; using System; -using System.Collections.Generic; using System.Data.Entity; -using System.Linq; using System.Linq.Expressions; namespace RefactorThis.GraphDiff @@ -26,10 +23,11 @@ public static class DbContextExtensions /// The root entity. /// The mapping configuration to define the bounds of the graph /// Update configuration overrides + /// The mapping configuration to define properties to use as key. The primary key is used if no other configuration is given. /// The attached entity graph - public static T UpdateGraph(this DbContext context, T entity, Expression, object>> mapping, UpdateParams updateParams = null) where T : class, new() + public static T UpdateGraph(this DbContext context, T entity, Expression, object>> mapping, UpdateParams updateParams = null, KeysConfiguration keysConfiguration = null) where T : class, new() { - return UpdateGraph(context, entity, mapping, null, updateParams); + return UpdateGraph(context, entity, mapping, null, updateParams, keysConfiguration); } /// @@ -40,10 +38,11 @@ public static class DbContextExtensions /// The root entity. /// Pre-configured mappingScheme /// Update configuration overrides + /// The mapping configuration to define properties to use as key. The primary key is used if no other configuration is given. /// The attached entity graph - public static T UpdateGraph(this DbContext context, T entity, string mappingScheme, UpdateParams updateParams = null) where T : class, new() + public static T UpdateGraph(this DbContext context, T entity, string mappingScheme, UpdateParams updateParams = null, KeysConfiguration keysConfiguration = null) where T : class, new() { - return UpdateGraph(context, entity, null, mappingScheme, updateParams); + return UpdateGraph(context, entity, null, mappingScheme, updateParams, keysConfiguration); } /// @@ -53,10 +52,11 @@ public static class DbContextExtensions /// The database context to attach / detach. /// The root entity. /// Update configuration overrides + /// The mapping configuration to define properties to use as key. The primary key is used if no other configuration is given. /// The attached entity graph - public static T UpdateGraph(this DbContext context, T entity, UpdateParams updateParams = null) where T : class, new() + public static T UpdateGraph(this DbContext context, T entity, UpdateParams updateParams = null, KeysConfiguration keysConfiguration = null) where T : class, new() { - return UpdateGraph(context, entity, null, null, updateParams); + return UpdateGraph(context, entity, null, null, updateParams, keysConfiguration); } /// @@ -69,7 +69,7 @@ public static class DbContextExtensions /// The aggregate loaded from the database public static T LoadAggregate(this DbContext context, Func keyPredicate, QueryMode queryMode = QueryMode.SingleQuery) where T : class { - var entityManager = new EntityManager(context); + var entityManager = new EntityManager(context, new KeysConfiguration()); var graph = new AggregateRegister(new CacheProvider()).GetEntityGraph(); var queryLoader = new QueryLoader(context, entityManager); @@ -85,12 +85,12 @@ public static T LoadAggregate(this DbContext context, Func keyPredic // other methods are convenience wrappers around this. private static T UpdateGraph(this DbContext context, T entity, Expression, object>> mapping, - string mappingScheme, UpdateParams updateParams) where T : class, new() + string mappingScheme, UpdateParams updateParams, KeysConfiguration keysConfiguration) where T : class, new() { GraphNode root; GraphDiffer differ; - var entityManager = new EntityManager(context); + var entityManager = new EntityManager(context, keysConfiguration ?? new KeysConfiguration()); var queryLoader = new QueryLoader(context, entityManager); var register = new AggregateRegister(new CacheProvider()); diff --git a/GraphDiff/GraphDiff/GraphDiff.csproj b/GraphDiff/GraphDiff/GraphDiff.csproj index be0b99c..6222b4b 100644 --- a/GraphDiff/GraphDiff/GraphDiff.csproj +++ b/GraphDiff/GraphDiff/GraphDiff.csproj @@ -72,6 +72,7 @@ + diff --git a/GraphDiff/GraphDiff/Internal/ChangeTracker.cs b/GraphDiff/GraphDiff/Internal/ChangeTracker.cs index 8fe557a..680b8db 100644 --- a/GraphDiff/GraphDiff/Internal/ChangeTracker.cs +++ b/GraphDiff/GraphDiff/Internal/ChangeTracker.cs @@ -83,9 +83,24 @@ public EntityState GetItemState(object item) public void UpdateItem(object from, object to, bool doConcurrencyCheck = false) { - if (doConcurrencyCheck && _context.Entry(to).State != EntityState.Added) + Type entityType = from.GetType(); + var toEntry = _context.Entry(to); + + if (doConcurrencyCheck && toEntry.State != EntityState.Added) + { + EnsureConcurrency(entityType, from, to); + } + + var metadata = _objectContext.MetadataWorkspace + .GetItems(DataSpace.OSpace) + .SingleOrDefault(p => p.FullName == entityType.FullName); + + // When a custom key is specified the primary key in the from object is ignored. + // We must set it to the actual value from database so it won't try to change the primary key + if (_entityManager.KeysConfiguration.HasConfigurationFor(entityType)) { - EnsureConcurrency(from, to); + // Copy inverted for primary key : from context entity to detached entity + _entityManager.CopyPrimaryKeyFields(entityType, from: to, to: from); } _context.Entry(to).CurrentValues.SetValues(from); @@ -171,10 +186,9 @@ public void AttachRequiredNavigationProperties(object updating, object persisted // Privates - private void EnsureConcurrency(object entity1, object entity2) + private void EnsureConcurrency(Type entityType, object entity1, object entity2) { // get concurrency properties of T - var entityType = ObjectContext.GetObjectType(entity1.GetType()); var metadata = _objectContext.MetadataWorkspace; var objType = metadata.GetItems(DataSpace.OSpace).Single(p => p.FullName == entityType.FullName); @@ -222,7 +236,7 @@ private object FindTrackedEntity(object entity) private object FindEntityByKey(object associatedEntity) { var associatedEntityType = ObjectContext.GetObjectType(associatedEntity.GetType()); - var keyFields = _entityManager.GetPrimaryKeyFieldsFor(associatedEntityType); + var keyFields = _entityManager.GetKeyFieldsFor(associatedEntityType); var keys = keyFields.Select(key => key.GetValue(associatedEntity, null)).ToArray(); return _context.Set(associatedEntityType).Find(keys); } diff --git a/GraphDiff/GraphDiff/Internal/EntityManager.cs b/GraphDiff/GraphDiff/Internal/EntityManager.cs index 6f0bd6f..e11dff6 100644 --- a/GraphDiff/GraphDiff/Internal/EntityManager.cs +++ b/GraphDiff/GraphDiff/Internal/EntityManager.cs @@ -15,6 +15,11 @@ namespace RefactorThis.GraphDiff.Internal /// internal interface IEntityManager { + /// + /// Gets custom key mappins for entities + /// + KeysConfiguration KeysConfiguration { get; } + /// /// Creates the unique entity key for an entity /// @@ -31,9 +36,14 @@ internal interface IEntityManager bool AreKeysIdentical(object entity1, object entity2); /// - /// Returns the primary key fields for a given entity type + /// Returns the key fields (using key configuration if available) for a given entity type + /// + IEnumerable GetKeyFieldsFor(Type entityType); + + /// + /// Copy primary key fields from an entity to another of the same type /// - IEnumerable GetPrimaryKeyFieldsFor(Type entityType); + void CopyPrimaryKeyFields(Type entityType, object from, object to); /// /// Retrieves the required navigation properties for the given type @@ -45,18 +55,27 @@ internal interface IEntityManager /// IEnumerable GetNavigationPropertiesForType(Type entityType); } - + internal class EntityManager : IEntityManager { private readonly DbContext _context; + private ObjectContext _objectContext { get { return ((IObjectContextAdapter)_context).ObjectContext; } } - public EntityManager(DbContext context) + public KeysConfiguration KeysConfiguration { get; private set; } + + public EntityManager(DbContext context, KeysConfiguration keysConfiguration) { + if (context == null) + throw new ArgumentNullException("context"); + if (keysConfiguration == null) + throw new ArgumentNullException("keysConfiguration"); + _context = context; + KeysConfiguration = keysConfiguration; } public EntityKey CreateEntityKey(object entity) @@ -66,7 +85,18 @@ public EntityKey CreateEntityKey(object entity) throw new ArgumentNullException("entity"); } - return _objectContext.CreateEntityKey(GetEntitySetName(entity.GetType()), entity); + var entityType = entity.GetType(); + var entitySetName = GetEntitySetName(entityType); + if (KeysConfiguration.HasConfigurationFor(entityType)) + { + var keyMembers = GetKeyFieldsFor(entityType) + .Select(p => new EntityKeyMember(p.Name, p.GetValue(entity, null))); + return new EntityKey(_objectContext.DefaultContainerName + "." + entitySetName, keyMembers); + } + else + { + return _objectContext.CreateEntityKey(entitySetName, entity); + } } public bool AreKeysIdentical(object newValue, object dbValue) @@ -81,16 +111,30 @@ public bool AreKeysIdentical(object newValue, object dbValue) public object CreateEmptyEntityWithKey(object entity) { - var instance = Activator.CreateInstance(entity.GetType()); - CopyPrimaryKeyFields(entity, instance); + var entityType = entity.GetType(); + var instance = Activator.CreateInstance(entityType); + CopyKeyFields(entityType, entity, instance); return instance; } + public IEnumerable GetKeyFieldsFor(Type entityType) + { + var keyColumns = KeysConfiguration.GetEntityKey(entityType); + if (keyColumns != null) + { + return keyColumns; + } + else + { + return GetPrimaryKeyFieldsFor(entityType); + } + } + public IEnumerable GetPrimaryKeyFieldsFor(Type entityType) { var metadata = _objectContext.MetadataWorkspace - .GetItems(DataSpace.OSpace) - .SingleOrDefault(p => p.FullName == entityType.FullName); + .GetItems(DataSpace.OSpace) + .SingleOrDefault(p => p.FullName == entityType.FullName); if (metadata == null) { @@ -134,9 +178,18 @@ private string GetEntitySetName(Type entityType) return set != null ? set.Name : null; } - private void CopyPrimaryKeyFields(object from, object to) + private void CopyKeyFields(Type entityType, object from, object to) + { + var keyProperties = GetKeyFieldsFor(entityType); + foreach (var keyProperty in keyProperties) + { + keyProperty.SetValue(to, keyProperty.GetValue(from, null), null); + } + } + + public void CopyPrimaryKeyFields(Type entityType, object from, object to) { - var keyProperties = GetPrimaryKeyFieldsFor(from.GetType()); + var keyProperties = GetPrimaryKeyFieldsFor(entityType); foreach (var keyProperty in keyProperties) { keyProperty.SetValue(to, keyProperty.GetValue(from, null), null); diff --git a/GraphDiff/GraphDiff/Internal/GraphDiffer.cs b/GraphDiff/GraphDiff/Internal/GraphDiffer.cs index e176387..7a4264a 100644 --- a/GraphDiff/GraphDiff/Internal/GraphDiffer.cs +++ b/GraphDiff/GraphDiff/Internal/GraphDiffer.cs @@ -55,8 +55,8 @@ public T Merge(T updating, QueryMode queryMode = QueryMode.SingleQuery) throw new InvalidOperationException("GraphDiff supports detached entities only at this time. Please try AsNoTracking() or detach your entites before calling the UpdateGraph method"); } - // Perform recursive update - var entityManager = new EntityManager(_dbContext); + // Perform recursive update + var entityManager = new EntityManager(_dbContext, _entityManager.KeysConfiguration); var changeTracker = new ChangeTracker(_dbContext, entityManager); _root.Update(changeTracker, entityManager, persisted, updating); diff --git a/GraphDiff/GraphDiff/Internal/QueryLoader.cs b/GraphDiff/GraphDiff/Internal/QueryLoader.cs index db4112f..a95843d 100644 --- a/GraphDiff/GraphDiff/Internal/QueryLoader.cs +++ b/GraphDiff/GraphDiff/Internal/QueryLoader.cs @@ -68,7 +68,7 @@ public T LoadEntity(Func keyPredicate, List includeStrings, private Func CreateKeyPredicateExpression(IObjectContextAdapter context, T entity) { // get key properties of T - var keyProperties = _entityManager.GetPrimaryKeyFieldsFor(typeof(T)).ToList(); + var keyProperties = _entityManager.GetKeyFieldsFor(typeof(T)).ToList(); ParameterExpression parameter = Expression.Parameter(typeof(T)); Expression expression = CreateEqualsExpression(entity, keyProperties[0], parameter); diff --git a/GraphDiff/GraphDiff/KeysConfiguration.cs b/GraphDiff/GraphDiff/KeysConfiguration.cs new file mode 100644 index 0000000..d2c1bfe --- /dev/null +++ b/GraphDiff/GraphDiff/KeysConfiguration.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; + +namespace RefactorThis.GraphDiff +{ + /// + /// Defines custom entity keys to use during merge instead of primary key + /// + public sealed class KeysConfiguration + { + private class PropertyInfoExpressionVisitor : ExpressionVisitor + { + public PropertyInfo PropertyInfo { get; private set; } + + protected override Expression VisitMember(MemberExpression node) + { + var pi = node.Member as PropertyInfo; + if (pi != null) + PropertyInfo = pi; + return base.VisitMember(node); + } + } + + private readonly Dictionary> _entityKeys = new Dictionary>(); + + /// + /// Defines a key configuration for an entity type. + /// Be careful about your key, you have to ensure uniqueness. + /// + /// Entity type + /// Path to entity key properties. Ensure that your key is unique. + /// Keys configuration to chain call + public KeysConfiguration ForEntity(params Expression>[] key) + { + if (_entityKeys.ContainsKey(typeof(T))) + throw new InvalidOperationException("A key configuration is already defined for entity type" + typeof(T).Name); + var propertyInfos = key.Select(e => GetPropertyInfo(e)); + _entityKeys.Add(typeof(T), propertyInfos.ToList()); + return this; + } + + private static PropertyInfo GetPropertyInfo(Expression> expression) + { + var visitor = new PropertyInfoExpressionVisitor(); + visitor.Visit(expression); + return visitor.PropertyInfo; + } + + internal IList GetEntityKey(Type entityType) + { + IList result; + if (_entityKeys.TryGetValue(entityType, out result)) + return result; + else + return null; + } + + internal bool HasConfigurationFor(Type entityType) + { + return _entityKeys.ContainsKey(entityType); + } + } +}