diff --git a/CHANGELOG.md b/CHANGELOG.md index 97f7ede..a52b7fa 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.13.2] - 2024-10-28 + +### Changed + +- Added Non-Generic Solution to KiotaDeserialization helper method. [#436](https://github.com/microsoft/kiota-dotnet/pull/436) + ## [1.13.1] - 2024-10-10 ### Changed diff --git a/Directory.Build.props b/Directory.Build.props index bfec8f6..9988389 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 1.13.1 + 1.13.2 false diff --git a/Microsoft.Kiota.lutconfig b/Microsoft.Kiota.lutconfig new file mode 100644 index 0000000..596a860 --- /dev/null +++ b/Microsoft.Kiota.lutconfig @@ -0,0 +1,6 @@ + + + true + true + 180000 + \ No newline at end of file diff --git a/src/abstractions/serialization/KiotaJsonSerializer.Deserialization.NonGeneric.cs b/src/abstractions/serialization/KiotaJsonSerializer.Deserialization.NonGeneric.cs new file mode 100644 index 0000000..809885d --- /dev/null +++ b/src/abstractions/serialization/KiotaJsonSerializer.Deserialization.NonGeneric.cs @@ -0,0 +1,74 @@ +// ------------------------------------------------------------------------------ +// 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.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +#if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Microsoft.Kiota.Abstractions.Serialization; +public static partial class KiotaJsonSerializer +{ + + /// + /// Deserializes the given string into an object. + /// + /// The target type to deserialize + /// The serialized representation of the object. + /// The cancellation token for the task +#if NET7_0_OR_GREATER + [RequiresDynamicCode("Activator creates an instance of a generic class with the Target Type as the generic type argument.")] +#endif +#if NET5_0_OR_GREATER + public static Task DeserializeAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type targetType, string serializedRepresentation, CancellationToken cancellationToken = default) +#else + public static Task DeserializeAsync(Type targetType, string serializedRepresentation, CancellationToken cancellationToken = default) +#endif + => KiotaSerializer.DeserializeAsync(targetType, _jsonContentType, serializedRepresentation, cancellationToken); + + /// + /// Deserializes the given stream into an object. + /// + /// The target type to deserialize + /// The stream to deserialize. + /// The cancellation token for the task +#if NET7_0_OR_GREATER + [RequiresDynamicCode("Activator creates an instance of a generic class with the Target Type as the generic type argument.")] +#endif +#if NET5_0_OR_GREATER + public static Task DeserializeAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type targetType, Stream stream, CancellationToken cancellationToken = default) +#else + public static Task DeserializeAsync(Type targetType, Stream stream, CancellationToken cancellationToken = default) +#endif + => KiotaSerializer.DeserializeAsync(targetType, _jsonContentType, stream, cancellationToken); + + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The target type to deserialize + /// The stream to deserialize. + /// The cancellation token for the task +#if NET7_0_OR_GREATER + [RequiresDynamicCode("Activator creates an instance of a generic class with the Target Type as the generic type argument.")] +#endif + public static Task> DeserializeCollectionAsync(Type targetType, Stream stream, CancellationToken cancellationToken = default) + => KiotaSerializer.DeserializeCollectionAsync(targetType, _jsonContentType, stream, cancellationToken); + + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The target type to deserialize + /// The serialized representation of the object. + /// The cancellation token for the task +#if NET7_0_OR_GREATER + [RequiresDynamicCode("Activator creates an instance of a generic class with the Target Type as the generic type argument.")] +#endif + public static Task> DeserializeCollectionAsync(Type targetType, string serializedRepresentation, CancellationToken cancellationToken = default) + => KiotaSerializer.DeserializeCollectionAsync(targetType, _jsonContentType, serializedRepresentation, cancellationToken); +} diff --git a/src/abstractions/serialization/KiotaSerializer.Deserialization.NonGeneric.cs b/src/abstractions/serialization/KiotaSerializer.Deserialization.NonGeneric.cs new file mode 100644 index 0000000..4602f79 --- /dev/null +++ b/src/abstractions/serialization/KiotaSerializer.Deserialization.NonGeneric.cs @@ -0,0 +1,132 @@ +// ------------------------------------------------------------------------------ +// 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.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; + + + +#if NET5_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + +namespace Microsoft.Kiota.Abstractions.Serialization; + +internal interface IKiotaDeserializationWrapper +{ + Task DeserializeAsync(string contentType, Stream stream, CancellationToken cancellationToken); + Task DeserializeAsync(string contentType, string serializedRepresentation, CancellationToken cancellationToken); + Task> DeserializeCollectionAsync(string contentType, Stream stream, CancellationToken cancellationToken); + Task> DeserializeCollectionAsync(string contentType, string serializedRepresentation, CancellationToken cancellationToken); +} +#if NET5_0_OR_GREATER +internal class KiotaDeserializationWrapper<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T> : IKiotaDeserializationWrapper where T : IParsable +#else +internal class KiotaDeserializationWrapper : IKiotaDeserializationWrapper where T : IParsable +#endif +{ + public async Task DeserializeAsync(string contentType, Stream stream, CancellationToken cancellationToken) => await KiotaSerializer.DeserializeAsync(contentType, stream, cancellationToken).ConfigureAwait(false); + public async Task DeserializeAsync(string contentType, string serializedRepresentation, CancellationToken cancellationToken) => await KiotaSerializer.DeserializeAsync(contentType, serializedRepresentation, cancellationToken).ConfigureAwait(false); + public async Task> DeserializeCollectionAsync(string contentType, Stream stream, CancellationToken cancellationToken) => (await KiotaSerializer.DeserializeCollectionAsync(contentType, stream, cancellationToken).ConfigureAwait(false)).OfType(); + public async Task> DeserializeCollectionAsync(string contentType, string serializedRepresentation, CancellationToken cancellationToken) => (await KiotaSerializer.DeserializeCollectionAsync(contentType, serializedRepresentation, cancellationToken).ConfigureAwait(false)).OfType(); +} +static internal class KiotaDeserializationWrapperFactory +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsIParsable(Type type) => typeof(IParsable).IsAssignableFrom(type); + private static readonly ConcurrentDictionary _deserializers = new ConcurrentDictionary(); +#if NET7_0_OR_GREATER + [RequiresDynamicCode("Activator creates an instance of a generic class with the Target Type as the generic type argument.")] +#endif + static internal IKiotaDeserializationWrapper Create(Type type) => IsIParsable(type) ? _deserializers.GetOrAdd(type, CreateInternal) : throw new ArgumentException("The given Type is not of IParsable", nameof(type)); + +#if NET7_0_OR_GREATER + [RequiresDynamicCode("Activator creates an instance of a generic class with the Target Type as the generic type argument.")] +#endif +#if NET5_0_OR_GREATER + private static IKiotaDeserializationWrapper CreateInternal([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type targetType) +#else + private static IKiotaDeserializationWrapper CreateInternal(Type targetType) +#endif + { + if(Activator.CreateInstance(typeof(KiotaDeserializationWrapper<>).MakeGenericType(targetType)) is IKiotaDeserializationWrapper deserializer) + return deserializer; + else + throw new InvalidOperationException($"Unable to create deserializer for type {targetType}"); + } +} + +public static partial class KiotaSerializer +{ + /// + /// Deserializes the given string into a collection of objects based on the content type. + /// + /// The target type to deserialize + /// The content type of the stream. + /// The serialized representation of the object. + /// The cancellation token for the task + /// + +#if NET7_0_OR_GREATER + [RequiresDynamicCode("Activator creates an instance of a generic class with the Target Type as the generic type argument.")] +#endif +#if NET5_0_OR_GREATER + public static Task DeserializeAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type, string contentType, string serializedRepresentation, CancellationToken cancellationToken = default) +#else + public static Task DeserializeAsync(Type type, string contentType, string serializedRepresentation, CancellationToken cancellationToken = default) +#endif + => KiotaDeserializationWrapperFactory.Create(type).DeserializeAsync(contentType, serializedRepresentation, cancellationToken); + + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The target type to deserialize + /// The content type of the stream. + /// The stream to deserialize. + /// The cancellation token for the task + /// +#if NET7_0_OR_GREATER + [RequiresDynamicCode("Activator creates an instance of a generic class with the Target Type as the generic type argument.")] +#endif +#if NET5_0_OR_GREATER + public static Task DeserializeAsync([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type, string contentType, Stream stream, CancellationToken cancellationToken = default) +#else + public static Task DeserializeAsync(Type type, string contentType, Stream stream, CancellationToken cancellationToken = default) +#endif + => KiotaDeserializationWrapperFactory.Create(type).DeserializeAsync(contentType, stream, cancellationToken); + + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The target type to deserialize + /// The content type of the stream. + /// The stream to deserialize. + /// The cancellation token for the task +#if NET7_0_OR_GREATER + [RequiresDynamicCode("Activator creates an instance of a generic class with the Target Type as the generic type argument.")] +#endif + public static Task> DeserializeCollectionAsync(Type type, string contentType, Stream stream, CancellationToken cancellationToken = default) + => KiotaDeserializationWrapperFactory.Create(type).DeserializeCollectionAsync(contentType, stream, cancellationToken); + + /// + /// Deserializes the given stream into a collection of objects based on the content type. + /// + /// The target type to deserialize + /// The content type of the stream. + /// The serialized representation of the object. + /// The cancellation token for the task +#if NET7_0_OR_GREATER + [RequiresDynamicCode("Activator creates an instance of a generic class with the Target Type as the generic type argument.")] +#endif + public static Task> DeserializeCollectionAsync(Type type, string contentType, string serializedRepresentation, CancellationToken cancellationToken = default) + => KiotaDeserializationWrapperFactory.Create(type).DeserializeCollectionAsync(contentType, serializedRepresentation, cancellationToken); +} \ No newline at end of file diff --git a/tests/abstractions/Serialization/DeserializationHelpersTests.NonGeneric.cs b/tests/abstractions/Serialization/DeserializationHelpersTests.NonGeneric.cs new file mode 100644 index 0000000..c2251d8 --- /dev/null +++ b/tests/abstractions/Serialization/DeserializationHelpersTests.NonGeneric.cs @@ -0,0 +1,56 @@ + +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions.Tests.Mocks; +using Moq; +using Xunit; + +namespace Microsoft.Kiota.Abstractions.Tests.Serialization; + +public partial class DeserializationHelpersTests +{ + + [Fact] + public async Task DeserializesObjectUntypedWithoutReflectionAsync() + { + var strValue = "{'id':'123'}"; + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetObjectValue(It.IsAny>())).Returns(new TestEntity() + { + Id = "123" + }); + var mockJsonParseNodeFactory = new Mock(); + mockJsonParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(mockParseNode.Object)); + mockJsonParseNodeFactory.Setup(x => x.ValidContentType).Returns(_jsonContentType); + ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockJsonParseNodeFactory.Object; + + var result = (TestEntity?)await KiotaSerializer.DeserializeAsync(typeof(TestEntity), _jsonContentType, strValue); + + Assert.NotNull(result); + Assert.Equal("123", result.Id); + } + + [Fact] + public async Task DeserializesCollectionOfObjectUntypedAsync() + { + var strValue = "{'id':'123'}"; + var mockParseNode = new Mock(); + mockParseNode.Setup(x => x.GetCollectionOfObjectValues(It.IsAny>())).Returns(new List { + new TestEntity() + { + Id = "123" + } + }); + var mockJsonParseNodeFactory = new Mock(); + mockJsonParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(mockParseNode.Object)); + mockJsonParseNodeFactory.Setup(x => x.ValidContentType).Returns(_jsonContentType); + ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockJsonParseNodeFactory.Object; + + var result = await KiotaSerializer.DeserializeCollectionAsync(typeof(TestEntity), _jsonContentType, strValue); + + Assert.NotNull(result); + Assert.Single(result); + var first = result.First() as TestEntity; + Assert.NotNull(first); + Assert.Equal("123", first.Id); + } +} diff --git a/tests/abstractions/Serialization/DeserializationHelpersTests.cs b/tests/abstractions/Serialization/DeserializationHelpersTests.cs index 0cc79e4..e86dd56 100644 --- a/tests/abstractions/Serialization/DeserializationHelpersTests.cs +++ b/tests/abstractions/Serialization/DeserializationHelpersTests.cs @@ -10,7 +10,7 @@ namespace Microsoft.Kiota.Abstractions.Tests.Serialization; -public class DeserializationHelpersTests +public partial class DeserializationHelpersTests { private const string _jsonContentType = "application/json"; [Fact]