From 1d51e3213ab842c6cfe8627f52195b1483ca3116 Mon Sep 17 00:00:00 2001 From: Erik Mavrinac Date: Wed, 5 Mar 2025 14:16:03 -0800 Subject: [PATCH 1/2] Allow sending a full IBackedModel instance including all recursive properties - common need for reusing objects from a GET to do a POST/PUT --- src/abstractions/store/IBackingStore.cs | 6 +++ .../store/InMemoryBackingStore.cs | 32 ++++++++++++ .../Store/InMemoryBackingStoreTests.cs | 52 +++++++++++++++++-- 3 files changed, 86 insertions(+), 4 deletions(-) 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..d85b774d 100644 --- a/tests/abstractions/Store/InMemoryBackingStoreTests.cs +++ b/tests/abstractions/Store/InMemoryBackingStoreTests.cs @@ -1,8 +1,5 @@ -using System; using System.Collections; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Reflection; using Microsoft.Kiota.Abstractions.Serialization; using Microsoft.Kiota.Abstractions.Store; @@ -80,7 +77,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 +440,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.BackingStore.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() { From 587714bfcf23ce061a132c7899992793ad76a7e8 Mon Sep 17 00:00:00 2001 From: Erik Mavrinac Date: Wed, 5 Mar 2025 14:46:36 -0800 Subject: [PATCH 2/2] Add extension method to allow `kiotaObject.MakeSendable()` --- .../extensions/IBackedModelExtensions.cs | 24 +++++++++++++++++++ .../Store/InMemoryBackingStoreTests.cs | 3 ++- 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/abstractions/extensions/IBackedModelExtensions.cs 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/tests/abstractions/Store/InMemoryBackingStoreTests.cs b/tests/abstractions/Store/InMemoryBackingStoreTests.cs index d85b774d..a6963803 100644 --- a/tests/abstractions/Store/InMemoryBackingStoreTests.cs +++ b/tests/abstractions/Store/InMemoryBackingStoreTests.cs @@ -1,6 +1,7 @@ using System.Collections; using System.Diagnostics; using System.Reflection; +using Microsoft.Kiota.Abstractions.Extensions; using Microsoft.Kiota.Abstractions.Serialization; using Microsoft.Kiota.Abstractions.Store; using Microsoft.Kiota.Abstractions.Tests.Mocks; @@ -470,7 +471,7 @@ public void TestsMakeRunnableOnMultipleNestingAndCollectionPatterns() // 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.BackingStore.MakeSendable(); + testUser.MakeSendable(); changedValues = testUser.BackingStore.Enumerate().ToDictionary(x => x.Key, y => y.Value!); Assert.NotEmpty(changedValues); Assert.True(changedValues.TryGetValue("id", out var idObj));