diff --git a/CHANGELOG.md b/CHANGELOG.md index e5435b6f..e2ec3a3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.12.0] - 2024-08-20 + +### Changed + +- Improved serialization helper methods to take boolean parameter to override the BackingStore functionality. [#310](https://github.com/microsoft/kiota-dotnet/issues/310) + ## [1.11.3] - 2024-08-16 ### Changed diff --git a/Directory.Build.props b/Directory.Build.props index 5b9bcc79..cd688d28 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 1.11.3 + 1.12.0 false diff --git a/src/abstractions/serialization/KiotaJsonSerializer.Serialization.cs b/src/abstractions/serialization/KiotaJsonSerializer.Serialization.cs index a35df57a..dba6a24d 100644 --- a/src/abstractions/serialization/KiotaJsonSerializer.Serialization.cs +++ b/src/abstractions/serialization/KiotaJsonSerializer.Serialization.cs @@ -4,9 +4,11 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Threading; using System.Threading.Tasks; + #if NET5_0_OR_GREATER using System.Diagnostics.CodeAnalysis; #endif @@ -22,7 +24,7 @@ public static partial class KiotaJsonSerializer /// The object to serialize. /// The serialized representation as a stream. public static Stream SerializeAsStream(T value) where T : IParsable - => KiotaSerializer.SerializeAsStream(_jsonContentType, value); + => KiotaSerializer.SerializeAsStream(_jsonContentType, value); /// /// Serializes the given object into a string based on the content type. @@ -30,7 +32,8 @@ public static Stream SerializeAsStream(T value) where T : IParsable /// Type of the object to serialize /// The object to serialize. /// The serialized representation as a string. - [Obsolete("This method is obsolete, use the async method instead")] + [Obsolete("This method is obsolete, use SerializeAsStringAsync instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public static string SerializeAsString(T value) where T : IParsable => KiotaSerializer.SerializeAsString(_jsonContentType, value); @@ -41,8 +44,8 @@ public static string SerializeAsString(T value) where T : IParsable /// The object to serialize. /// The token to monitor for cancellation requests. /// The serialized representation as a string. - public static Task SerializeAsStringAsync(T value, CancellationToken cancellationToken = default) where T : IParsable - => KiotaSerializer.SerializeAsStringAsync(_jsonContentType, value, cancellationToken); + public static Task SerializeAsStringAsync(T value, CancellationToken cancellationToken) where T : IParsable + => KiotaSerializer.SerializeAsStringAsync(_jsonContentType, value, true, cancellationToken); /// /// Serializes the given object into a string based on the content type. @@ -59,7 +62,8 @@ public static Stream SerializeAsStream(IEnumerable value) where T : IParsa /// Type of the object to serialize /// The object to serialize. /// The serialized representation as a string. - [Obsolete("This method is obsolete, use the async method instead")] + [Obsolete("This method is obsolete, use SerializeAsStringAsync instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public static string SerializeAsString(IEnumerable value) where T : IParsable => KiotaSerializer.SerializeAsString(_jsonContentType, value); /// @@ -69,7 +73,6 @@ public static string SerializeAsString(IEnumerable value) where T : IParsa /// The object to serialize. /// The token to monitor for cancellation requests. /// The serialized representation as a string. - public static Task SerializeAsStringAsync(IEnumerable value, CancellationToken cancellationToken = default) where T : IParsable - => KiotaSerializer.SerializeAsStringAsync(_jsonContentType, value, cancellationToken); - -} \ No newline at end of file + public static Task SerializeAsStringAsync(IEnumerable value, CancellationToken cancellationToken) where T : IParsable => + KiotaSerializer.SerializeAsStringAsync(_jsonContentType, value, true, cancellationToken); +} diff --git a/src/abstractions/serialization/KiotaSerializer.Serialization.cs b/src/abstractions/serialization/KiotaSerializer.Serialization.cs index 1a01f2e5..b331212b 100644 --- a/src/abstractions/serialization/KiotaSerializer.Serialization.cs +++ b/src/abstractions/serialization/KiotaSerializer.Serialization.cs @@ -4,9 +4,12 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Threading; using System.Threading.Tasks; + + #if NET5_0_OR_GREATER using System.Diagnostics.CodeAnalysis; #endif @@ -25,12 +28,14 @@ public static partial class KiotaSerializer /// Type of the object to serialize /// Content type to serialize the object to /// The object to serialize. + /// By default, you'll only get the changed properties. /// The serialized representation as a stream. - public static Stream SerializeAsStream(string contentType, T value) where T : IParsable + public static Stream SerializeAsStream(string contentType, T value, bool serializeOnlyChangedValues = true) where T : IParsable { - using var writer = GetSerializationWriter(contentType, value); - writer.WriteObjectValue(string.Empty, value); - return writer.GetSerializedContent(); + using var writer = GetSerializationWriter(contentType, value, serializeOnlyChangedValues); + writer.WriteObjectValue(null, value); + var stream = writer.GetSerializedContent(); + return stream; } /// /// Serializes the given object into a string based on the content type. @@ -53,9 +58,20 @@ public static string SerializeAsString(string contentType, T value) where T : /// The object to serialize. /// The token to monitor for cancellation requests. /// The serialized representation as a string. - public static Task SerializeAsStringAsync(string contentType, T value, CancellationToken cancellationToken = default) where T : IParsable + [EditorBrowsable(EditorBrowsableState.Never)] + public static Task SerializeAsStringAsync(string contentType, T value, CancellationToken cancellationToken) where T : IParsable => SerializeAsStringAsync(contentType, value, true, cancellationToken); + /// + /// Serializes the given object into a string based on the content type. + /// + /// Type of the object to serialize + /// Content type to serialize the object to + /// The object to serialize. + /// By default, you'll only get the changed properties. + /// The token to monitor for cancellation requests. + /// The serialized representation as a string. + public static Task SerializeAsStringAsync(string contentType, T value, bool serializeOnlyChangedValues = true, CancellationToken cancellationToken = default) where T : IParsable { - using var stream = SerializeAsStream(contentType, value); + using var stream = SerializeAsStream(contentType, value, serializeOnlyChangedValues); return GetStringFromStreamAsync(stream, cancellationToken); } /// @@ -64,12 +80,14 @@ public static Task SerializeAsStringAsync(string contentType, T value /// Type of the object to serialize /// Content type to serialize the object to /// The object to serialize. + /// By default, you'll only get the changed properties. /// The serialized representation as a stream. - public static Stream SerializeAsStream(string contentType, IEnumerable value) where T : IParsable + public static Stream SerializeAsStream(string contentType, IEnumerable value, bool serializeOnlyChangedValues = true) where T : IParsable { - using var writer = GetSerializationWriter(contentType, value); - writer.WriteCollectionOfObjectValues(string.Empty, value); - return writer.GetSerializedContent(); + using var writer = GetSerializationWriter(contentType, value, serializeOnlyChangedValues); + writer.WriteCollectionOfObjectValues(null, value); + var stream = writer.GetSerializedContent(); + return stream; } /// /// Serializes the given object into a string based on the content type. @@ -90,13 +108,25 @@ public static string SerializeAsString(string contentType, IEnumerable val /// Type of the object to serialize /// Content type to serialize the object to /// The object to serialize. + /// /// The token to monitor for cancellation requests. /// The serialized representation as a string. - public static Task SerializeAsStringAsync(string contentType, IEnumerable value, CancellationToken cancellationToken = default) where T : IParsable + public static Task SerializeAsStringAsync(string contentType, IEnumerable value, bool serializeOnlyChangedValues = true, CancellationToken cancellationToken = default) where T : IParsable { - using var stream = SerializeAsStream(contentType, value); + using var stream = SerializeAsStream(contentType, value, serializeOnlyChangedValues); return GetStringFromStreamAsync(stream, cancellationToken); } + /// + /// Serializes the given object into a string based on the content type. + /// + /// Type of the object to serialize + /// Content type to serialize the object to + /// The object to serialize. + /// The token to monitor for cancellation requests. + /// The serialized representation as a string. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Task SerializeAsStringAsync(string contentType, IEnumerable value, CancellationToken cancellationToken) where T : IParsable => SerializeAsStringAsync(contentType, value, true, cancellationToken); + [Obsolete("This method is obsolete, use the async method instead")] private static string GetStringFromStream(Stream stream) { @@ -112,10 +142,10 @@ private static async Task GetStringFromStreamAsync(Stream stream, Cancel return await reader.ReadToEndAsync().ConfigureAwait(false); #endif } - private static ISerializationWriter GetSerializationWriter(string contentType, object value) + private static ISerializationWriter GetSerializationWriter(string contentType, object value, bool serializeOnlyChangedValues = true) { if(string.IsNullOrEmpty(contentType)) throw new ArgumentNullException(nameof(contentType)); if(value == null) throw new ArgumentNullException(nameof(value)); - return SerializationWriterFactoryRegistry.DefaultInstance.GetSerializationWriter(contentType); + return SerializationWriterFactoryRegistry.DefaultInstance.GetSerializationWriter(contentType, serializeOnlyChangedValues); } } \ No newline at end of file diff --git a/src/abstractions/serialization/SerializationWriterFactoryRegistry.cs b/src/abstractions/serialization/SerializationWriterFactoryRegistry.cs index 0ffd5c25..b4f65d1e 100644 --- a/src/abstractions/serialization/SerializationWriterFactoryRegistry.cs +++ b/src/abstractions/serialization/SerializationWriterFactoryRegistry.cs @@ -26,30 +26,63 @@ public string ValidContentType /// Default singleton instance of the registry to be used when registering new factories that should be available by default. /// public static readonly SerializationWriterFactoryRegistry DefaultInstance = new(); + /// /// List of factories that are registered by content type. /// public ConcurrentDictionary ContentTypeAssociatedFactories { get; set; } = new(); + /// /// Get the relevant instance for the given content type /// /// The content type in use /// A instance to parse the content public ISerializationWriter GetSerializationWriter(string contentType) + => GetSerializationWriter(contentType, true); + + /// + /// Get the relevant instance for the given content type + /// + /// The content type in use + /// If will only return changed values, otherwise will return the full object + /// A instance to parse the content + public ISerializationWriter GetSerializationWriter(string contentType, bool serializeOnlyChangedValues) + { + var factory = GetSerializationWriterFactory(contentType, out string actualContentType); + if(!serializeOnlyChangedValues && factory is Store.BackingStoreSerializationWriterProxyFactory backingStoreFactory) + return backingStoreFactory.GetSerializationWriter(actualContentType, false); + + return factory.GetSerializationWriter(actualContentType); + } + + /// + /// Get the relevant instance for the given content type + /// + /// The content type in use + /// The content type where a writer factory is found for + /// + /// + /// + private ISerializationWriterFactory GetSerializationWriterFactory(string contentType, out string actualContentType) { if(string.IsNullOrEmpty(contentType)) throw new ArgumentNullException(nameof(contentType)); var vendorSpecificContentType = contentType.Split(";".ToCharArray(), StringSplitOptions.RemoveEmptyEntries)[0]; if(ContentTypeAssociatedFactories.TryGetValue(vendorSpecificContentType, out var vendorFactory)) - return vendorFactory.GetSerializationWriter(vendorSpecificContentType); + { + actualContentType = vendorSpecificContentType; + return vendorFactory; + } var cleanedContentType = ParseNodeFactoryRegistry.contentTypeVendorCleanupRegex.Replace(vendorSpecificContentType, string.Empty); if(ContentTypeAssociatedFactories.TryGetValue(cleanedContentType, out var factory)) - return factory.GetSerializationWriter(cleanedContentType); + { + actualContentType = cleanedContentType; + return factory; + } throw new InvalidOperationException($"Content type {cleanedContentType} does not have a factory registered to be parsed"); } - } } diff --git a/src/abstractions/serialization/SerializationWriterProxyFactory.cs b/src/abstractions/serialization/SerializationWriterProxyFactory.cs index 76e14d5c..d5a59786 100644 --- a/src/abstractions/serialization/SerializationWriterProxyFactory.cs +++ b/src/abstractions/serialization/SerializationWriterProxyFactory.cs @@ -14,24 +14,28 @@ public class SerializationWriterProxyFactory : ISerializationWriterFactory /// /// The valid content type for the /// - public string ValidContentType { get { return _concrete.ValidContentType; } } - private readonly ISerializationWriterFactory _concrete; + public string ValidContentType { get { return ProxiedSerializationWriterFactory.ValidContentType; } } + + /// + /// The factory that is being proxied. + /// + protected readonly ISerializationWriterFactory ProxiedSerializationWriterFactory; private readonly Action _onBefore; private readonly Action _onAfter; private readonly Action _onStartSerialization; /// /// Creates a new proxy factory that wraps the specified concrete factory while composing the before and after callbacks. /// - /// The concrete factory to wrap. + /// The concrete factory to wrap. /// The callback to invoke before the serialization of any model object. /// The callback to invoke after the serialization of any model object. /// The callback to invoke when serialization of the entire model has started. - public SerializationWriterProxyFactory(ISerializationWriterFactory concrete, + public SerializationWriterProxyFactory(ISerializationWriterFactory factoryToWrap, Action onBeforeSerialization, Action onAfterSerialization, Action onStartSerialization) { - _concrete = concrete ?? throw new ArgumentNullException(nameof(concrete)); + ProxiedSerializationWriterFactory = factoryToWrap ?? throw new ArgumentNullException(nameof(factoryToWrap)); _onBefore = onBeforeSerialization; _onAfter = onAfterSerialization; _onStartSerialization = onStartSerialization; @@ -43,7 +47,7 @@ public SerializationWriterProxyFactory(ISerializationWriterFactory concrete, /// A new instance for the given content type. public ISerializationWriter GetSerializationWriter(string contentType) { - var writer = _concrete.GetSerializationWriter(contentType); + var writer = ProxiedSerializationWriterFactory.GetSerializationWriter(contentType); var originalBefore = writer.OnBeforeObjectSerialization; var originalAfter = writer.OnAfterObjectSerialization; var originalStart = writer.OnStartObjectSerialization; diff --git a/src/abstractions/store/BackingStoreSerializationWriterProxyFactory.cs b/src/abstractions/store/BackingStoreSerializationWriterProxyFactory.cs index 2f82038a..87915a7c 100644 --- a/src/abstractions/store/BackingStoreSerializationWriterProxyFactory.cs +++ b/src/abstractions/store/BackingStoreSerializationWriterProxyFactory.cs @@ -37,5 +37,19 @@ public BackingStoreSerializationWriterProxyFactory(ISerializationWriterFactory c } }) { } + + /// + /// Get the serialization writer for the given content type. + /// + /// The content type for which a serialization writer should be created. + /// By default, a backing store is used, and you'll only get changed properties + /// + public ISerializationWriter GetSerializationWriter(string contentType, bool serializeOnlyChangedValues) + { + if(serializeOnlyChangedValues) + return base.GetSerializationWriter(contentType); + + return ProxiedSerializationWriterFactory.GetSerializationWriter(contentType); + } } } diff --git a/tests/serialization/json/IParsableExtensionsTests.cs b/tests/serialization/json/IParsableExtensionsTests.cs new file mode 100644 index 00000000..4f5a9410 --- /dev/null +++ b/tests/serialization/json/IParsableExtensionsTests.cs @@ -0,0 +1,99 @@ +using System.IO; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions.Store; +using Microsoft.Kiota.Serialization.Json.Tests.Mocks; +using Xunit; + +namespace Microsoft.Kiota.Serialization.Json.Tests +{ + public class IParsableExtensionsTests + { + private const string _jsonContentType = "application/json"; + private readonly SerializationWriterFactoryRegistry _serializationWriterFactoryRegistry; + + public IParsableExtensionsTests() + { + _serializationWriterFactoryRegistry = new SerializationWriterFactoryRegistry(); + _serializationWriterFactoryRegistry.ContentTypeAssociatedFactories.TryAdd(_jsonContentType, new BackingStoreSerializationWriterProxyFactory(new JsonSerializationWriterFactory())); + } + + [Theory] + [InlineData(null)] + [InlineData(true)] + [InlineData(false)] + public void GetSerializationWriter_RetunsJsonSerializationWriter(bool? serializeOnlyChangedValues) + { + // Arrange + + // Act + using var writer = serializeOnlyChangedValues.HasValue + ? _serializationWriterFactoryRegistry.GetSerializationWriter(_jsonContentType, serializeOnlyChangedValues.Value) + : _serializationWriterFactoryRegistry.GetSerializationWriter(_jsonContentType); + + // Assert + Assert.NotNull(writer); + Assert.IsType(writer); + } + + [Fact] + public void GetSerializationWriterSerializedChangedTrue_RetunsEmptyJson() + { + // Arrange + var testUser = new BackedTestEntity { Id = "1", Name = "testUser" }; + testUser.BackingStore.InitializationCompleted = true; + using var writer = _serializationWriterFactoryRegistry.GetSerializationWriter(_jsonContentType, true); + + // Act + writer.WriteObjectValue(null, testUser); + using var stream = writer.GetSerializedContent(); + var serializedContent = GetStringFromStream(stream); + + // Assert + Assert.NotNull(serializedContent); + Assert.Equal("{}", serializedContent); + } + + [Fact] + public void GetSerializationWriterSerializedChangedTrue_ChangedName_ReturnsJustName() + { + // Arrange + var testUser = new BackedTestEntity { Id = "1", Name = "testUser" }; + testUser.BackingStore.InitializationCompleted = true; + testUser.Name = "Stephan"; + using var writer = _serializationWriterFactoryRegistry.GetSerializationWriter(_jsonContentType, true); + + // Act + writer.WriteObjectValue(null, testUser); + using var stream = writer.GetSerializedContent(); + var serializedContent = GetStringFromStream(stream); + + // Assert + Assert.NotNull(serializedContent); + Assert.Equal("{\"name\":\"Stephan\"}", serializedContent); + } + + [Fact] + public void GetSerializationWriterSerializedChangedFalse_SerializesEntireObject() + { + // Arrange + var testUser = new BackedTestEntity { Id = "1", Name = "testUser" }; + testUser.BackingStore.InitializationCompleted = true; + using var writer = _serializationWriterFactoryRegistry.GetSerializationWriter(_jsonContentType, false); + + // Act + writer.WriteObjectValue(null, testUser); + using var stream = writer.GetSerializedContent(); + var serializedContent = GetStringFromStream(stream); + + // Assert + Assert.NotNull(serializedContent); + Assert.Equal("{\"id\":\"1\",\"name\":\"testUser\"}", serializedContent); + } + + private static string GetStringFromStream(Stream stream) + { + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + } +} diff --git a/tests/serialization/json/Mocks/BackedTestEntity.cs b/tests/serialization/json/Mocks/BackedTestEntity.cs new file mode 100644 index 00000000..bd5cbeaa --- /dev/null +++ b/tests/serialization/json/Mocks/BackedTestEntity.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions.Store; + +namespace Microsoft.Kiota.Serialization.Json.Tests.Mocks +{ + public class BackedTestEntity : IParsable, IBackedModel + { + public BackedTestEntity() + { + BackingStore = new InMemoryBackingStore(); + } + + public IBackingStore BackingStore { get; private set; } + + public string? Id + { + get { return BackingStore?.Get("id"); } + set { BackingStore?.Set("id", value); } + } + public string? Name + { + get { return BackingStore?.Get("name"); } + set { BackingStore?.Set("name", value); } + } + + public IDictionary> GetFieldDeserializers() => + new Dictionary> { + { "id", n => { Id = n.GetStringValue(); } }, + { "name", n => { Name = n.GetStringValue(); } }, + }; + public void Serialize(ISerializationWriter writer) + { + _ = writer ?? throw new ArgumentNullException(nameof(writer)); + writer.WriteStringValue("id", Id); + writer.WriteStringValue("name", Name); + } + } +}