Skip to content

Commit

Permalink
Merge pull request #225 from MihaMarkic/feature/asyncdeserialization
Browse files Browse the repository at this point in the history
Adds IAsyncParseNodeFactory, makes deserialization methods async, upd…
  • Loading branch information
andrueastman authored May 6, 2024
2 parents 0994df5 + bc3f8e1 commit 554b2b6
Show file tree
Hide file tree
Showing 12 changed files with 514 additions and 22 deletions.
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

0 comments on commit 554b2b6

Please sign in to comment.