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

ADT: Fix the issue with non-default object serializers in Query APIs #18379

Merged
merged 7 commits into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions eng/Packages.Data.props
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
<PackageReference Update="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Update="Microsoft.AspNetCore.Server.WebListener" Version="1.0.2" />
<PackageReference Update="Microsoft.Azure.Core.Spatial" Version="1.0.0" />
<PackageReference Update="Microsoft.Azure.Core.NewtonsoftJson" Version="1.0.0" />
<PackageReference Update="Microsoft.Azure.Devices" Version="1.19.0" />
<PackageReference Update="Microsoft.Azure.Devices.Client" Version="1.27.0" />
<PackageReference Update="Microsoft.Azure.Graph.RBAC" Version="2.2.2-preview" />
Expand Down
35 changes: 28 additions & 7 deletions sdk/digitaltwins/Azure.DigitalTwins.Core/src/Models/QueryResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using Azure.Core;
using Azure.Core.Serialization;

namespace Azure.DigitalTwins.Core
Expand All @@ -26,12 +25,11 @@ internal partial class QueryResult<T>
internal string ContinuationToken { get; }

/// <summary>
/// Initializes a new instance of QueryResult.
/// In order to serialize/deserialize JsonElements into and out of a stream, we have to use the out of the box ObjectSerializer (JsonObjectSerializer).
/// This static property is instantiated whenever it is used so we can efficiently re-use the same object serializer that is provided by default
/// If the user specifies a different type of serializer to instantiate the client, the SDK will instantiate a new JsonObjectSerializer of its own.
/// </summary>
internal QueryResult()
{
Value = new ChangeTrackingList<T>();
}
internal static ObjectSerializer defaultObjectSerializer { get; private set; }
azabbasi marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Initializes a new instance of QueryResult.
Expand All @@ -54,6 +52,21 @@ internal static QueryResult<T> DeserializeQueryResult(JsonElement element, Objec
{
IReadOnlyList<T> items = default;
string continuationToken = default;

// If the provided objectSerializer is of type JsonObjectSerializer, we will re-use the same object and set it as the defaultObjectSerializer.
if (objectSerializer is JsonObjectSerializer)
Copy link
Contributor

Choose a reason for hiding this comment

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

If we know that objectSerializer will always be a JsonObjectSerliazer, no matter what is passed to the DeserializeQueryResult, can't we get rid of the objectSerializer parameter for the method and use a static defaultObjectSerializer that is initialized at constructor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We still have to use that provided serializer to deserialize the stream into the user specified type. which may be a different serializer than the JsonObjectSerializer

azabbasi marked this conversation as resolved.
Show resolved Hide resolved
{
defaultObjectSerializer = objectSerializer;
}
// Otherwise, if the defaultObjectSerializer is null, we will statically instantiate it and re-use it in the future.
else
drwill-ms marked this conversation as resolved.
Show resolved Hide resolved
{
if (defaultObjectSerializer == null)
{
defaultObjectSerializer = new JsonObjectSerializer();
}
}

foreach (JsonProperty property in element.EnumerateObject())
{
if (property.NameEquals("value"))
Expand All @@ -62,13 +75,21 @@ internal static QueryResult<T> DeserializeQueryResult(JsonElement element, Objec
{
continue;
}

var array = new List<T>();

foreach (JsonElement item in property.Value.EnumerateArray())
{
using MemoryStream streamedObject = StreamHelper.WriteToStream(item, objectSerializer, default);
// defaultObjectSerializer of type JsonObjectSerializer needs to be used to serialize the JsonElement into a stream.
// Using any other ObjectSerializer (ex: NewtonsoftJsonObjectSerializer) won't be able to deserialize the JsonElement into
azabbasi marked this conversation as resolved.
Show resolved Hide resolved
// a MemoryStream correctly.
using MemoryStream streamedObject = StreamHelper.WriteToStream(item, defaultObjectSerializer, default);
Copy link
Contributor

Choose a reason for hiding this comment

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

@pakrym if we could use a JsonElement or a string instead of a stream to feed into the object serializer, we wouldn't have to go through these hoops. Any thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

We would still have to turn the JsonElement into the string/stream before feeding to Newtonsoft.Json Parse.

We are adding a BinaryData overload in this release though.


// To deserialize the stream object into the generic type of T, the provided ObjectSerializer will be used.
T obj = (T)objectSerializer.Deserialize(streamedObject, typeof(T), default);
array.Add(obj);
}

items = array;
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ internal static MemoryStream WriteToStream<T>(T obj, ObjectSerializer objectSeri
var memoryStream = new MemoryStream();

objectSerializer.Serialize(memoryStream, obj, typeof(T), cancellationToken);

memoryStream.Seek(0, SeekOrigin.Begin);

return memoryStream;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Azure.Core.NewtonsoftJson"/>
azabbasi marked this conversation as resolved.
Show resolved Hide resolved
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="FluentAssertions" />
<PackageReference Update="Newtonsoft.Json" />
azabbasi marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Azure.Core.Serialization;
using NUnit.Framework;

namespace Azure.DigitalTwins.Core.Tests
{
/// <summary>
/// Tests for custom ObjectSerializer.
azabbasi marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public class NewtonsoftObjectSerializerTests : E2eTestBase
{
public NewtonsoftObjectSerializerTests(bool isAsync)
: base(isAsync)
{
}

[Test]
public async Task TestNewtonsoftObjectSerializerWithDigitalTwins()
{
DigitalTwinsClient defaultClient = GetClient();

string roomTwinId = await GetUniqueTwinIdAsync(defaultClient, TestAssetDefaults.RoomTwinIdPrefix).ConfigureAwait(false);
string floorModelId = await GetUniqueModelIdAsync(defaultClient, TestAssetDefaults.FloorModelIdPrefix).ConfigureAwait(false);
string roomModelId = await GetUniqueModelIdAsync(defaultClient, TestAssetDefaults.RoomModelIdPrefix).ConfigureAwait(false);

try
{
// arrange

// create room model
string roomModel = TestAssetsHelper.GetRoomModelPayload(roomModelId, floorModelId);
await CreateAndListModelsAsync(defaultClient, new List<string> { roomModel }).ConfigureAwait(false);

// act

// create room twin
BasicDigitalTwin roomTwin = TestAssetsHelper.GetRoomTwinPayload(roomModelId);
await defaultClient.CreateOrReplaceDigitalTwinAsync<BasicDigitalTwin>(roomTwinId, roomTwin).ConfigureAwait(false);

// Create a client with NewtonsoftJsonObjectSerializer configured as the serializer.
DigitalTwinsClient testClient = GetClient(
new DigitalTwinsClientOptions
{
Serializer = new NewtonsoftJsonObjectSerializer()
});

// Get digital twin using the simple DigitalTwin model annotated with Newtonsoft attributes
SimpleNewtonsoftDtModel getResponse = await testClient.GetDigitalTwinAsync<SimpleNewtonsoftDtModel>(roomTwinId).ConfigureAwait(false);

Assert.IsNotNull(getResponse.Id, "Digital twin Id should not be null or empty");
azabbasi marked this conversation as resolved.
Show resolved Hide resolved

// Query DigitalTwins using the simple DigitalTwin model annotated with Newtonsoft attributes
AsyncPageable<SimpleNewtonsoftDtModel> queryResponse = testClient.QueryAsync<SimpleNewtonsoftDtModel>("SELECT * FROM DIGITALTWINS");

await foreach (SimpleNewtonsoftDtModel twin in queryResponse)
{
Assert.IsNotNull(twin.Id, "Digital twin Id should not be null or empty");
}
}
catch (Exception ex)
drwill-ms marked this conversation as resolved.
Show resolved Hide resolved
{
Assert.Fail($"Failure in executing a step in the test case: {ex.Message}.");
}
finally
{
// cleanup
try
{
// delete twin
if (!string.IsNullOrWhiteSpace(roomTwinId))
{
await defaultClient.DeleteDigitalTwinAsync(roomTwinId).ConfigureAwait(false);
}

// delete models
if (!string.IsNullOrWhiteSpace(roomModelId))
{
await defaultClient.DeleteModelAsync(roomModelId).ConfigureAwait(false);
drwill-ms marked this conversation as resolved.
Show resolved Hide resolved
}
}
catch (Exception ex)
{
Assert.Fail($"Test clean up failed: {ex.Message}");
}
}
}
}
}
Loading