diff --git a/src/abstractions/extensions/IBackedModelExtensions.cs b/src/abstractions/extensions/IBackedModelExtensions.cs new file mode 100644 index 00000000..39ce3aa9 --- /dev/null +++ b/src/abstractions/extensions/IBackedModelExtensions.cs @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Kiota.Abstractions.Store; + +namespace Microsoft.Kiota.Abstractions.Extensions +{ + /// + /// Extension methods for instances. + /// + public static class IBackedModelExtensions + { + /// + /// Sets all fields recursively to "modified" so they will be sent in the next serialization. + /// This is useful to allow the model object to be reused to send to a POST or PUT call. + /// Do not use if you are using a sparse PATCH. + /// + public static void MakeSendable(this IBackedModel kiotaModelObject) => kiotaModelObject.BackingStore.MakeSendable(); + } +} diff --git a/src/abstractions/store/IBackingStore.cs b/src/abstractions/store/IBackingStore.cs index 487658cc..46760681 100644 --- a/src/abstractions/store/IBackingStore.cs +++ b/src/abstractions/store/IBackingStore.cs @@ -56,5 +56,11 @@ public interface IBackingStore bool InitializationCompleted { get; set; } /// Whether to return only values that have changed since the initialization of the object when calling the Get and Enumerate methods. bool ReturnOnlyChangedValues { get; set; } + /// + /// Sets all fields recursively to "modified" so they will be sent in the next serialization. + /// This is useful to allow the model object to be reused to send to a POST or PUT call. + /// Do not use if you are using a sparse PATCH. + /// + void MakeSendable(); } } diff --git a/src/abstractions/store/InMemoryBackingStore.cs b/src/abstractions/store/InMemoryBackingStore.cs index d8453388..e5d0ad0d 100644 --- a/src/abstractions/store/InMemoryBackingStore.cs +++ b/src/abstractions/store/InMemoryBackingStore.cs @@ -220,6 +220,38 @@ public bool InitializationCompleted } } + /// + /// Sets all fields recursively to "modified" so they will be sent in the next serialization. + /// This is useful to allow the model object to be reused to send to a POST or PUT call. + /// Do not use if you are using a sparse PATCH. + /// + public void MakeSendable() + { + ReturnOnlyChangedValues = false; + + foreach(var entry in store) + { + store[entry.Key] = Tuple.Create(true, entry.Value.Item2); + + if(entry.Value.Item2 is Tuple collectionTuple) + { + foreach(var collectionItem in collectionTuple.Item1) + { + if(collectionItem is not IBackedModel backedModel) + { + break; + } + + backedModel.BackingStore.MakeSendable(); + } + } + else if(entry.Value.Item2 is IBackedModel backedModel) + { + backedModel.BackingStore.MakeSendable(); + } + } + } + private void EnsureCollectionPropertyIsConsistent(string key, object? storeItem) { if(storeItem is Tuple collectionTuple) // check if we put in a collection annotated with the size diff --git a/tests/abstractions/Store/InMemoryBackingStoreTests.cs b/tests/abstractions/Store/InMemoryBackingStoreTests.cs index 2f393f34..a6963803 100644 --- a/tests/abstractions/Store/InMemoryBackingStoreTests.cs +++ b/tests/abstractions/Store/InMemoryBackingStoreTests.cs @@ -1,9 +1,7 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Reflection; +using Microsoft.Kiota.Abstractions.Extensions; using Microsoft.Kiota.Abstractions.Serialization; using Microsoft.Kiota.Abstractions.Store; using Microsoft.Kiota.Abstractions.Tests.Mocks; @@ -80,7 +78,7 @@ public void TestsBackingStoreEmbeddedInModel() Assert.Equal("businessPhones", changedValues.First().Key); } [Fact] - public void TestsBackingStoreEmbeddedInModelWithAdditionDataValues() + public void TestsBackingStoreEmbeddedInModelWithAdditionalDataValues() { // Arrange dummy user with initialized backingstore var testUser = new TestEntity @@ -443,6 +441,53 @@ public void TestsBackingStoreEmbeddedInModelWithByUpdatingNestedIBackedModelColl Assert.Single(colleagueSubscriptions);// only one subscription to be invoked for the collection "colleagues" } + [Fact] + public void TestsMakeRunnableOnMultipleNestingAndCollectionPatterns() + { + var testUser = new TestEntity + { + Id = "84c747c1-d2c0-410d-ba50-fc23e0b4abbe", + Manager = new TestEntity + { + Id = "1a1e218d-1a85-450a-b96e-ab0786b9022b" + }, + Colleagues = + [ + new TestEntity + { + Id = "2fe22fe5-1132-42cf-90f9-1dc17e325a74", + BusinessPhones = [ "+1 234 567 891" ] + } + ] + }; + testUser.BackingStore.InitializationCompleted = testUser.Colleagues[0].BackingStore.InitializationCompleted = testUser.Manager.BackingStore.InitializationCompleted = true; + + // Simulate a serialization. Verify that the model serializes to an empty result since all existing data values are marked as "not changed". + testUser.Manager.BackingStore.ReturnOnlyChangedValues = true; + testUser.Colleagues[0].BackingStore.ReturnOnlyChangedValues = true; //serializer will do this. + testUser.BackingStore.ReturnOnlyChangedValues = true; + var changedValues = testUser.BackingStore.Enumerate().ToDictionary(x => x.Key, y => y.Value!); + Assert.Empty(changedValues); + + // Make the top-level entity sendable and verify that the resulting setting of "changed" on + // every recursive IBackedModel and collection results in returning all objects in the result. + testUser.MakeSendable(); + changedValues = testUser.BackingStore.Enumerate().ToDictionary(x => x.Key, y => y.Value!); + Assert.NotEmpty(changedValues); + Assert.True(changedValues.TryGetValue("id", out var idObj)); + Assert.Equal("84c747c1-d2c0-410d-ba50-fc23e0b4abbe", idObj); + Assert.True(changedValues.TryGetValue("manager", out var managerObj)); + var manager = (TestEntity)managerObj; + Assert.Equal("1a1e218d-1a85-450a-b96e-ab0786b9022b", manager.Id); + Assert.True(changedValues.ContainsKey("colleagues")); + var colleagues = ((Tuple)changedValues["colleagues"]).Item1.Cast().ToList(); + Assert.Single(colleagues); + Assert.Equal("2fe22fe5-1132-42cf-90f9-1dc17e325a74", colleagues[0].Id); + Assert.NotNull(colleagues[0].BusinessPhones); + Assert.Single(colleagues[0].BusinessPhones!); + Assert.Equal("+1 234 567 891", colleagues[0].BusinessPhones![0]); + } + [Fact] public void TestsBackingStoreNestedInvocationCounts() {