diff --git a/Source/Csla.test/GraphMerge/BranchUniqueIdentities.cs b/Source/Csla.test/GraphMerge/BranchUniqueIdentities.cs new file mode 100644 index 0000000000..85a52040ec --- /dev/null +++ b/Source/Csla.test/GraphMerge/BranchUniqueIdentities.cs @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Marimer LLC. All rights reserved. +// Website: https://cslanet.com +// +// no summary +//----------------------------------------------------------------------- + +namespace Csla.Test.GraphMerge +{ + internal class BranchUniqueIdentities : BusinessBase + { + public static readonly PropertyInfo IdProperty = RegisterProperty(nameof(Id)); + public Guid Id + { + get => GetProperty(IdProperty); + private set => SetProperty(IdProperty, value); + } + + public static readonly PropertyInfo LeafsProperty = RegisterProperty(nameof(Leafs)); + public LeafsUniqueIdentities Leafs + { + get => GetProperty(LeafsProperty); + private set => SetProperty(LeafsProperty, value); + } + + [FetchChild] + private async void Create([Inject] IChildDataPortal leafsPortal) + { + using (BypassPropertyChecks) + { + Id = Guid.NewGuid(); + + Leafs = await leafsPortal.FetchChildAsync(); + } + } + + [InsertChild] + private async Task Insert() + { + await FieldManager.UpdateChildrenAsync(); + } + + [UpdateChild] + private void Update() + { + FieldManager.UpdateChildren(); + } + } +} \ No newline at end of file diff --git a/Source/Csla.test/GraphMerge/GraphMergerTests.cs b/Source/Csla.test/GraphMerge/GraphMergerTests.cs index 3132abc192..01c65c731a 100644 --- a/Source/Csla.test/GraphMerge/GraphMergerTests.cs +++ b/Source/Csla.test/GraphMerge/GraphMergerTests.cs @@ -8,6 +8,7 @@ using Csla.Core; using Csla.TestHelpers; +using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -17,6 +18,8 @@ namespace Csla.Test.GraphMerge public class GraphMergerTests { private static TestDIContext _testDIContext; + private ApplicationContext _applicationContext; + private GraphMerger _systemUnderTest; [ClassInitialize] public static void ClassInitialize(TestContext context) @@ -28,6 +31,8 @@ public static void ClassInitialize(TestContext context) public void Initialize() { TestResults.Reinitialise(); + _applicationContext = _testDIContext.CreateTestApplicationContext(); + _systemUnderTest = new GraphMerger(_applicationContext); } [TestMethod] @@ -363,5 +368,18 @@ public void MergeChildList() Assert.IsTrue(ReferenceEquals(target.ChildList, target.ChildList[1].Parent), "parent ref"); } + [TestMethod] + public async Task MergeChildsAtDepth2Correctly() + { + var root = await _testDIContext.CreateDataPortal().FetchAsync(); + + root.Branch.Leafs.AddNew().LeafId = 1337; + + var allLeafIds = root.Branch.Leafs.Select(l => l.LeafId).ToList(); + + await root.SaveAndMergeAsync(); + + root.Branch.Leafs.Select(l => l.LeafId).Should().ContainInOrder(allLeafIds); + } } } diff --git a/Source/Csla.test/GraphMerge/IdentityTests.cs b/Source/Csla.test/GraphMerge/IdentityTests.cs index 6da2a1a25a..4e13d82421 100644 --- a/Source/Csla.test/GraphMerge/IdentityTests.cs +++ b/Source/Csla.test/GraphMerge/IdentityTests.cs @@ -8,6 +8,7 @@ using Csla.Core; using Csla.TestHelpers; +using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -137,5 +138,16 @@ public void IdentityInitializedDynamicBindingListBase() var obj = dataPortal.Create(); Assert.IsTrue(((IBusinessObject)obj).Identity >= 0); } + + [TestMethod] + public async Task Identity_WhenAddingANewListItemAfterFetchTheIdentityWithinTheListMustBeUnique() + { + var root = await _testDIContext.CreateDataPortal().FetchAsync(); + + var newItem = root.Branch.Leafs.AddNew(); + newItem.LeafId = 1337; + + root.Branch.Leafs.Should().OnlyHaveUniqueItems(l => ((IBusinessObject)l).Identity); + } } } diff --git a/Source/Csla.test/GraphMerge/LeafUniqueIdentities.cs b/Source/Csla.test/GraphMerge/LeafUniqueIdentities.cs new file mode 100644 index 0000000000..bd2241654c --- /dev/null +++ b/Source/Csla.test/GraphMerge/LeafUniqueIdentities.cs @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Marimer LLC. All rights reserved. +// Website: https://cslanet.com +// +// no summary +//----------------------------------------------------------------------- + +namespace Csla.Test.GraphMerge +{ + internal class LeafUniqueIdentities : BusinessBase + { + public static readonly PropertyInfo LeafIdProperty = RegisterProperty(nameof(LeafId)); + public int LeafId + { + get => GetProperty(LeafIdProperty); + set => SetProperty(LeafIdProperty, value); + } + + [Create] + [CreateChild] + private async Task Create(int leafId) + { + using (BypassPropertyChecks) + { + LeafId = leafId; + } + + await BusinessRules.CheckRulesAsync(); + } + + [InsertChild] + private void Insert() { } + + [FetchChild] + private void Fetch(int id) + { + using (BypassPropertyChecks) + { + LeafId = id; + } + } + } +} \ No newline at end of file diff --git a/Source/Csla.test/GraphMerge/LeafsUniqueIdentities.cs b/Source/Csla.test/GraphMerge/LeafsUniqueIdentities.cs new file mode 100644 index 0000000000..a9682e4a86 --- /dev/null +++ b/Source/Csla.test/GraphMerge/LeafsUniqueIdentities.cs @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Marimer LLC. All rights reserved. +// Website: https://cslanet.com +// +// no summary +//----------------------------------------------------------------------- + +namespace Csla.Test.GraphMerge +{ + internal class LeafsUniqueIdentities : BusinessListBase + { + [FetchChild] + private async void Fetch([Inject] IChildDataPortal childDataPortal) + { + using (LoadListMode) + { + foreach (var id in Enumerable.Range(1, 5)) + { + Add(await childDataPortal.FetchChildAsync(id)); + } + } + } + } +} diff --git a/Source/Csla.test/GraphMerge/RootUniqueIdentities.cs b/Source/Csla.test/GraphMerge/RootUniqueIdentities.cs new file mode 100644 index 0000000000..6a46e164bb --- /dev/null +++ b/Source/Csla.test/GraphMerge/RootUniqueIdentities.cs @@ -0,0 +1,50 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Marimer LLC. All rights reserved. +// Website: https://cslanet.com +// +// no summary +//----------------------------------------------------------------------- + +namespace Csla.Test.GraphMerge +{ + internal class RootUniqueIdentities : BusinessBase + { + public static readonly PropertyInfo IdProperty = RegisterProperty(nameof(Id)); + public Guid Id + { + get => GetProperty(IdProperty); + private set => SetProperty(IdProperty, value); + } + + public static readonly PropertyInfo BranchProperty = RegisterProperty(nameof(Branch)); + public BranchUniqueIdentities Branch + { + get => GetProperty(BranchProperty); + private set => SetProperty(BranchProperty, value); + } + + [Fetch] + private async void Create([Inject] IChildDataPortal portalBranch) + { + using (BypassPropertyChecks) + { + Id = Guid.NewGuid(); + + Branch = await portalBranch.FetchChildAsync(); + } + } + + [Insert] + private async Task Insert() + { + await FieldManager.UpdateChildrenAsync(); + } + + [Update] + private async Task Update() + { + await FieldManager.UpdateChildrenAsync(); + } + } +} diff --git a/Source/Csla/BusinessBindingListBase.cs b/Source/Csla/BusinessBindingListBase.cs index c58b96ee59..bd599e10fb 100644 --- a/Source/Csla/BusinessBindingListBase.cs +++ b/Source/Csla/BusinessBindingListBase.cs @@ -338,6 +338,8 @@ protected override void InsertItem(int index, C item) { if (item.IsChild) { + IdentityManager.EnsureNextIdentityValueIsUnique(this, this); + // set parent reference item.SetParent(this); // ensure child uses same context as parent diff --git a/Source/Csla/BusinessListBase.cs b/Source/Csla/BusinessListBase.cs index 87021e2e49..8b5a815609 100644 --- a/Source/Csla/BusinessListBase.cs +++ b/Source/Csla/BusinessListBase.cs @@ -342,6 +342,8 @@ protected override void InsertItem(int index, C item) { if (item.IsChild) { + IdentityManager.EnsureNextIdentityValueIsUnique(this, this); + // set parent reference item.SetParent(this); // ensure child uses same context as parent diff --git a/Source/Csla/Core/IdentityManager.cs b/Source/Csla/Core/IdentityManager.cs index 0b9df2085e..b63f49226d 100644 --- a/Source/Csla/Core/IdentityManager.cs +++ b/Source/Csla/Core/IdentityManager.cs @@ -45,5 +45,23 @@ public int GetNextIdentity(int? current = null) } return result; } + + /// + /// Ensures that the internal value of is greater than the greatest within the given collection. + /// That ensures that new object get a unique identity within the collection. + /// + /// Item type of the list + /// + /// + internal static void EnsureNextIdentityValueIsUnique(IParent parent, IReadOnlyCollection items) where T : IBusinessObject + { + // No items means we do not have to worry about any identity duplicates + if (items.Count == 0) + { + return; + } + + _ = parent.GetNextIdentity(items.Max(c => c.Identity)); + } } } \ No newline at end of file diff --git a/Source/Csla/DynamicBindingListBase.cs b/Source/Csla/DynamicBindingListBase.cs index 080a573b61..3f4c155b1f 100644 --- a/Source/Csla/DynamicBindingListBase.cs +++ b/Source/Csla/DynamicBindingListBase.cs @@ -283,6 +283,7 @@ protected override object AddNewCore() /// Item to insert. protected override void InsertItem(int index, T item) { + IdentityManager.EnsureNextIdentityValueIsUnique(this, this); item.SetParent(this); base.InsertItem(index, item); } diff --git a/Source/Csla/DynamicListBase.cs b/Source/Csla/DynamicListBase.cs index b2ec4e1dc6..d2ae926a1c 100644 --- a/Source/Csla/DynamicListBase.cs +++ b/Source/Csla/DynamicListBase.cs @@ -316,6 +316,7 @@ protected override T AddNewCore() /// Item to insert. protected override void InsertItem(int index, T item) { + IdentityManager.EnsureNextIdentityValueIsUnique(this, this); item.SetParent(this); // ensure child uses same context as parent if (item is IUseApplicationContext iuac)