Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds IAsyncParseNodeFactory, makes deserialization methods async, upd… #225

Merged
merged 13 commits into from
May 6, 2024
Merged
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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.9.0] - 2024-05-06

Adds asynchronous deserialization support and marks synchronous as obsolete. https://github.com/microsoft/kiota-abstractions-dotnet/issues/223

### Added

- Added asynchronous deserialization methods (to KiotaJsonSerializer.Deserialization and KiotaSerializer.Deserialization).
- Added IAsyncParseNodeFactory interface to provide asynchronous version of GetRootParseNode: GetRootParseNodeAsync.
- Added ParseNodeFactoryRegistry.GetRootParseNodeAsync method.
- Added ParseNodeProxyFactory.GetRootParseNodeAsync method
- Adds async overloads for serialization helpers

### Changed

- Marked synchronous deserialization methods as obsolete.
- Marked IParseNodeFactory.GetRootParseNode as obsolete.
- Refactored ParseNodeFactoryRegistry.GetFactory to support both asynchronous (IAsyncParseNodeFactory) and synchronous (IParseNodeFactory) factories.


## [1.8.4] - 2024-04-19

- Bumps Std.UriTemplate to version 0.0.57
Expand Down
4 changes: 2 additions & 2 deletions Microsoft.Kiota.Abstractions.Tests/ApiClientBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public void EnableBackingStoreForParseNodeFactory()
{
// Arrange
var parseNodeRegistry = new ParseNodeFactoryRegistry();
var mockParseNodeFactory = new Mock<IParseNodeFactory>();
var mockParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
parseNodeRegistry.ContentTypeAssociatedFactories.TryAdd(StreamContentType, mockParseNodeFactory.Object);

Assert.IsNotType<BackingStoreParseNodeFactory>(parseNodeRegistry.ContentTypeAssociatedFactories[StreamContentType]);
Expand All @@ -67,7 +67,7 @@ public void EnableBackingStoreForParseNodeFactoryAlsoEnablesForDefaultInstance()
{
// Arrange
var parseNodeRegistry = new ParseNodeFactoryRegistry();
var mockParseNodeFactory = new Mock<IParseNodeFactory>();
var mockParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
parseNodeRegistry.ContentTypeAssociatedFactories.TryAdd(StreamContentType, mockParseNodeFactory.Object);
ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories.TryAdd(StreamContentType, mockParseNodeFactory.Object);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions.Tests.Mocks;
using Moq;
Expand All @@ -12,6 +14,7 @@ public class DeserializationHelpersTests
{
private const string _jsonContentType = "application/json";
[Fact]
[Obsolete]
public void DefensiveObject()
{
Assert.Throws<ArgumentNullException>(() => KiotaSerializer.Deserialize<TestEntity>(null, (Stream)null, null));
Expand All @@ -21,6 +24,7 @@ public void DefensiveObject()
Assert.Throws<ArgumentNullException>(() => KiotaSerializer.Deserialize<TestEntity>(_jsonContentType, "", null));
}
[Fact]
[Obsolete]
public void DefensiveObjectCollection()
{
Assert.Throws<ArgumentNullException>(() => KiotaSerializer.DeserializeCollection<TestEntity>(null, (Stream)null, null));
Expand All @@ -30,6 +34,7 @@ public void DefensiveObjectCollection()
Assert.Throws<ArgumentNullException>(() => KiotaSerializer.DeserializeCollection<TestEntity>(_jsonContentType, "", null));
}
[Fact]
[Obsolete]
public void DeserializesObjectWithoutReflection()
{
var strValue = "{'id':'123'}";
Expand All @@ -38,7 +43,7 @@ public void DeserializesObjectWithoutReflection()
{
Id = "123"
});
var mockJsonParseNodeFactory = new Mock<IParseNodeFactory>();
var mockJsonParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
mockJsonParseNodeFactory.Setup(x => x.GetRootParseNode(It.IsAny<string>(), It.IsAny<Stream>())).Returns(mockParseNode.Object);
mockJsonParseNodeFactory.Setup(x => x.ValidContentType).Returns(_jsonContentType);
ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockJsonParseNodeFactory.Object;
Expand All @@ -48,6 +53,7 @@ public void DeserializesObjectWithoutReflection()
Assert.NotNull(result);
}
[Fact]
[Obsolete]
public void DeserializesObjectWithReflection()
{
var strValue = "{'id':'123'}";
Expand All @@ -56,7 +62,7 @@ public void DeserializesObjectWithReflection()
{
Id = "123"
});
var mockJsonParseNodeFactory = new Mock<IParseNodeFactory>();
var mockJsonParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
mockJsonParseNodeFactory.Setup(x => x.GetRootParseNode(It.IsAny<string>(), It.IsAny<Stream>())).Returns(mockParseNode.Object);
mockJsonParseNodeFactory.Setup(x => x.ValidContentType).Returns(_jsonContentType);
ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockJsonParseNodeFactory.Object;
Expand All @@ -66,6 +72,7 @@ public void DeserializesObjectWithReflection()
Assert.NotNull(result);
}
[Fact]
[Obsolete]
public void DeserializesCollectionOfObject()
{
var strValue = "{'id':'123'}";
Expand All @@ -76,7 +83,7 @@ public void DeserializesCollectionOfObject()
Id = "123"
}
});
var mockJsonParseNodeFactory = new Mock<IParseNodeFactory>();
var mockJsonParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
mockJsonParseNodeFactory.Setup(x => x.GetRootParseNode(It.IsAny<string>(), It.IsAny<Stream>())).Returns(mockParseNode.Object);
mockJsonParseNodeFactory.Setup(x => x.ValidContentType).Returns(_jsonContentType);
ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockJsonParseNodeFactory.Object;
Expand All @@ -86,4 +93,80 @@ public void DeserializesCollectionOfObject()
Assert.NotNull(result);
Assert.Single(result);
}

[Fact]
public async Task DefensiveObjectAsync()
{
await Assert.ThrowsAsync<ArgumentNullException>(async () => await KiotaSerializer.DeserializeAsync<TestEntity>(null, (Stream)null, null));
await Assert.ThrowsAsync<ArgumentNullException>(async () => await KiotaSerializer.DeserializeAsync<TestEntity>(_jsonContentType, (Stream)null, null));
using var stream = new MemoryStream();
await Assert.ThrowsAsync<ArgumentNullException>(async () => await KiotaSerializer.DeserializeAsync<TestEntity>(_jsonContentType, stream, null));
await Assert.ThrowsAsync<ArgumentNullException>(async () => await KiotaSerializer.DeserializeAsync<TestEntity>(_jsonContentType, "", null));
}
[Fact]
public async Task DefensiveObjectCollectionAsync()
{
await Assert.ThrowsAsync<ArgumentNullException>(async () => await KiotaSerializer.DeserializeCollectionAsync<TestEntity>(null, (Stream)null, null, default));
await Assert.ThrowsAsync<ArgumentNullException>(async () => await KiotaSerializer.DeserializeCollectionAsync<TestEntity>(_jsonContentType, (Stream)null, null));
using var stream = new MemoryStream();
await Assert.ThrowsAsync<ArgumentNullException>(async () => await KiotaSerializer.DeserializeCollectionAsync<TestEntity>(_jsonContentType, stream, null));
await Assert.ThrowsAsync<ArgumentNullException>(async () => await KiotaSerializer.DeserializeCollectionAsync<TestEntity>(_jsonContentType, "", null));
}
[Fact]
public async Task DeserializesObjectWithoutReflectionAsync()
{
var strValue = "{'id':'123'}";
var mockParseNode = new Mock<IParseNode>();
mockParseNode.Setup(x => x.GetObjectValue(It.IsAny<ParsableFactory<TestEntity>>())).Returns(new TestEntity()
{
Id = "123"
});
var mockJsonParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
mockJsonParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny<string>(), It.IsAny<Stream>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockParseNode.Object));
mockJsonParseNodeFactory.Setup(x => x.ValidContentType).Returns(_jsonContentType);
ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockJsonParseNodeFactory.Object;

var result = await KiotaSerializer.DeserializeAsync(_jsonContentType, strValue, TestEntity.CreateFromDiscriminatorValue);

Assert.NotNull(result);
}
[Fact]
public async Task DeserializesObjectWithReflectionAsync()
{
var strValue = "{'id':'123'}";
var mockParseNode = new Mock<IParseNode>();
mockParseNode.Setup(x => x.GetObjectValue(It.IsAny<ParsableFactory<TestEntity>>())).Returns(new TestEntity()
{
Id = "123"
});
var mockJsonParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
mockJsonParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny<string>(), It.IsAny<Stream>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockParseNode.Object));
mockJsonParseNodeFactory.Setup(x => x.ValidContentType).Returns(_jsonContentType);
ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockJsonParseNodeFactory.Object;

var result = await KiotaSerializer.DeserializeAsync<TestEntity>(_jsonContentType, strValue);

Assert.NotNull(result);
}
[Fact]
public async Task DeserializesCollectionOfObjectAsync()
{
var strValue = "{'id':'123'}";
var mockParseNode = new Mock<IParseNode>();
mockParseNode.Setup(x => x.GetCollectionOfObjectValues(It.IsAny<ParsableFactory<TestEntity>>())).Returns(new List<TestEntity> {
new TestEntity()
{
Id = "123"
}
});
var mockJsonParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
mockJsonParseNodeFactory.Setup(x => x.GetRootParseNodeAsync(It.IsAny<string>(), It.IsAny<Stream>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockParseNode.Object));
mockJsonParseNodeFactory.Setup(x => x.ValidContentType).Returns(_jsonContentType);
ParseNodeFactoryRegistry.DefaultInstance.ContentTypeAssociatedFactories[_jsonContentType] = mockJsonParseNodeFactory.Object;

var result = await KiotaSerializer.DeserializeCollectionAsync(_jsonContentType, strValue, TestEntity.CreateFromDiscriminatorValue);

Assert.NotNull(result);
Assert.Single(result);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions.Serialization;
using Moq;
Expand All @@ -24,12 +25,13 @@ public void ParseNodeFactoryRegistryDoesNotStickToOneContentType()
}

[Fact]
[Obsolete]
public void ReturnsExpectedRootNodeForRegisteredContentType()
{
// Arrange
var streamContentType = "application/octet-stream";
using var testStream = new MemoryStream(Encoding.UTF8.GetBytes("test input"));
var mockParseNodeFactory = new Mock<IParseNodeFactory>();
var mockParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
var mockParseNode = new Mock<IParseNode>();
mockParseNodeFactory.Setup(parseNodeFactory => parseNodeFactory.GetRootParseNode(streamContentType, It.IsAny<Stream>())).Returns(mockParseNode.Object);
_parseNodeFactoryRegistry.ContentTypeAssociatedFactories.TryAdd(streamContentType, mockParseNodeFactory.Object);
Expand All @@ -40,12 +42,13 @@ public void ReturnsExpectedRootNodeForRegisteredContentType()
Assert.Equal(mockParseNode.Object, rootParseNode);
}
[Fact]
[Obsolete]
public void ReturnsExpectedRootNodeForVendorSpecificContentType()
{
// Arrange
var applicationJsonContentType = "application/json";
using var testStream = new MemoryStream(Encoding.UTF8.GetBytes("{\"test\": \"input\"}"));
var mockParseNodeFactory = new Mock<IParseNodeFactory>();
var mockParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
var mockParseNode = new Mock<IParseNode>();
mockParseNodeFactory.Setup(parseNodeFactory => parseNodeFactory.GetRootParseNode(applicationJsonContentType, It.IsAny<Stream>())).Returns(mockParseNode.Object);
_parseNodeFactoryRegistry.ContentTypeAssociatedFactories.TryAdd(applicationJsonContentType, mockParseNodeFactory.Object);
Expand All @@ -57,6 +60,7 @@ public void ReturnsExpectedRootNodeForVendorSpecificContentType()
}

[Fact]
[Obsolete]
public void ThrowsInvalidOperationExceptionForUnregisteredContentType()
{
// Arrange
Expand All @@ -72,6 +76,7 @@ public void ThrowsInvalidOperationExceptionForUnregisteredContentType()
[Theory]
[InlineData(null)]
[InlineData("")]
[Obsolete]
public void ThrowsArgumentNullExceptionForNoContentType(string contentType)
{
// Arrange
Expand All @@ -82,5 +87,67 @@ public void ThrowsArgumentNullExceptionForNoContentType(string contentType)
Assert.NotNull(exception);
Assert.Equal("contentType", exception.ParamName);
}

// *****

[Fact]
public async Task ReturnsExpectedRootNodeForRegisteredContentTypeAsync()
{
// Arrange
var streamContentType = "application/octet-stream";
using var testStream = new MemoryStream(Encoding.UTF8.GetBytes("test input"));
var mockParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
var mockParseNode = new Mock<IParseNode>();
mockParseNodeFactory.Setup(parseNodeFactory => parseNodeFactory.GetRootParseNodeAsync(streamContentType, It.IsAny<Stream>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockParseNode.Object));
_parseNodeFactoryRegistry.ContentTypeAssociatedFactories.TryAdd(streamContentType, mockParseNodeFactory.Object);
// Act
var rootParseNode = await _parseNodeFactoryRegistry.GetRootParseNodeAsync(streamContentType, testStream);
// Assert
Assert.NotNull(rootParseNode);
Assert.Equal(mockParseNode.Object, rootParseNode);
}
[Fact]
public async Task ReturnsExpectedRootNodeForVendorSpecificContentTypeAsync()
{
// Arrange
var applicationJsonContentType = "application/json";
using var testStream = new MemoryStream(Encoding.UTF8.GetBytes("{\"test\": \"input\"}"));
var mockParseNodeFactory = new Mock<IAsyncParseNodeFactory>();
var mockParseNode = new Mock<IParseNode>();
mockParseNodeFactory.Setup(parseNodeFactory => parseNodeFactory.GetRootParseNodeAsync(applicationJsonContentType, It.IsAny<Stream>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(mockParseNode.Object));
_parseNodeFactoryRegistry.ContentTypeAssociatedFactories.TryAdd(applicationJsonContentType, mockParseNodeFactory.Object);
// Act
var rootParseNode = await _parseNodeFactoryRegistry.GetRootParseNodeAsync("application/vnd+json", testStream);
// Assert
Assert.NotNull(rootParseNode);
Assert.Equal(mockParseNode.Object, rootParseNode);
}

[Fact]
public async Task ThrowsInvalidOperationExceptionForUnregisteredContentTypeAsync()
{
// Arrange
var streamContentType = "application/octet-stream";
using var testStream = new MemoryStream(Encoding.UTF8.GetBytes("test input"));
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () => await _parseNodeFactoryRegistry.GetRootParseNodeAsync(streamContentType, testStream));
// Assert
Assert.NotNull(exception);
Assert.Equal($"Content type {streamContentType} does not have a factory registered to be parsed", exception.Message);
}

[Theory]
[InlineData(null)]
[InlineData("")]
public async Task ThrowsArgumentNullExceptionForNoContentTypeAsync(string contentType)
{
// Arrange
using var testStream = new MemoryStream(Encoding.UTF8.GetBytes("test input"));
// Act
var exception = await Assert.ThrowsAsync<ArgumentNullException>(async () => await _parseNodeFactoryRegistry.GetRootParseNodeAsync(contentType, testStream));
// Assert
Assert.NotNull(exception);
Assert.Equal("contentType", exception.ParamName);
}
}
}
2 changes: 1 addition & 1 deletion src/ApiClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public static ISerializationWriterFactory EnableBackingStoreForSerializationWrit
/// <returns>A new parse node factory with the backing store enabled.</returns>
public static IParseNodeFactory EnableBackingStoreForParseNodeFactory(IParseNodeFactory original)
{
IParseNodeFactory result = original ?? throw new ArgumentNullException(nameof(original));
var result = original ?? throw new ArgumentNullException(nameof(original));
if(original is ParseNodeFactoryRegistry registry)
{
EnableBackingStoreForParseNodeRegistry(registry);
Expand Down
21 changes: 21 additions & 0 deletions src/serialization/IAsyncParseNodeFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Kiota.Abstractions.Serialization
{
/// <summary>
/// Defines the contract for a factory that creates parse nodes in an sync and async way.
/// </summary>
public interface IAsyncParseNodeFactory : IParseNodeFactory
{
/// <summary>
/// Create a parse node from the given stream and content type.
/// </summary>
/// <param name="content">The stream to read the parse node from.</param>
/// <param name="contentType">The content type of the parse node.</param>
/// <param name="cancellationToken">The cancellation token for the task</param>
/// <returns>A parse node.</returns>
Task<IParseNode> GetRootParseNodeAsync(string contentType, Stream content, CancellationToken cancellationToken = default);
}
}
2 changes: 2 additions & 0 deletions src/serialization/IParseNodeFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// 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.IO;

namespace Microsoft.Kiota.Abstractions.Serialization
Expand All @@ -21,6 +22,7 @@ public interface IParseNodeFactory
/// <param name="content">The stream to read the parse node from.</param>
/// <param name="contentType">The content type of the parse node.</param>
/// <returns>A parse node.</returns>
[Obsolete("Use GetRootParseNodeAsync instead")]
IParseNode GetRootParseNode(string contentType, Stream content);
}
}
Loading
Loading