Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/abstractions/extensions/IBackedModelExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Extension methods for <see cref="IBackedModel"/> instances.
/// </summary>
public static class IBackedModelExtensions
{
/// <summary>
/// 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.
/// </summary>
public static void MakeSendable(this IBackedModel kiotaModelObject) => kiotaModelObject.BackingStore.MakeSendable();
}
}
6 changes: 6 additions & 0 deletions src/abstractions/store/IBackingStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,11 @@ public interface IBackingStore
bool InitializationCompleted { get; set; }
/// <value>Whether to return only values that have changed since the initialization of the object when calling the Get and Enumerate methods.</value>
bool ReturnOnlyChangedValues { get; set; }
/// <summary>
/// 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.
/// </summary>
void MakeSendable();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this to a public interface would be breaking change.
Maybe we should look into a solution similar to 9c33bbd where we update the serialization registry to return a serializer that will ignore the backing store.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Examining alternate approaches. Microsot.Graph is not up to date with that commit, awaiting that to be completed.

}
}
32 changes: 32 additions & 0 deletions src/abstractions/store/InMemoryBackingStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,38 @@ public bool InitializationCompleted
}
}

/// <summary>
/// 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.
/// </summary>
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<ICollection, int> 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<ICollection, int> collectionTuple) // check if we put in a collection annotated with the size
Expand Down
53 changes: 49 additions & 4 deletions tests/abstractions/Store/InMemoryBackingStoreTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<ICollection, int>)changedValues["colleagues"]).Item1.Cast<TestEntity>().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()
{
Expand Down