Skip to content

Commit

Permalink
Implement asynchronous support in ODataJsonLightEntityReferenceLinkSe…
Browse files Browse the repository at this point in the history
…rializer (#2050)

Co-authored-by: John Gathogo <[email protected]>
  • Loading branch information
gathogojr and John Gathogo authored Apr 16, 2021
1 parent 17511b7 commit 66ee246
Show file tree
Hide file tree
Showing 2 changed files with 362 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
namespace Microsoft.OData.JsonLight
{
#region Namespaces

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;

#endregion Namespaces

Expand Down Expand Up @@ -51,6 +53,30 @@ internal void WriteEntityReferenceLinks(ODataEntityReferenceLinks entityReferenc
() => this.WriteEntityReferenceLinksImplementation(entityReferenceLinks));
}

/// <summary>
/// Asynchronously writes a single top-level Uri in response to a $ref query.
/// </summary>
/// <param name="link">The entity reference link to write out.</param>
internal Task WriteEntityReferenceLinkAsync(ODataEntityReferenceLink link)
{
Debug.Assert(link != null, "link != null");

return this.WriteTopLevelPayloadAsync(
() => this.WriteEntityReferenceLinkImplementationAsync(link, /* isTopLevel */ true));
}

/// <summary>
/// Asynchronously writes a set of links (Uris) in response to a $ref query; includes optional count and next-page-link information.
/// </summary>
/// <param name="entityReferenceLinks">The set of entity reference links to write out.</param>
internal Task WriteEntityReferenceLinksAsync(ODataEntityReferenceLinks entityReferenceLinks)
{
Debug.Assert(entityReferenceLinks != null, "entityReferenceLinks != null");

return this.WriteTopLevelPayloadAsync(
() => this.WriteEntityReferenceLinksImplementationAsync(entityReferenceLinks));
}

/// <summary>
/// Writes a single Uri in response to a $ref query.
/// </summary>
Expand Down Expand Up @@ -160,5 +186,135 @@ private void WriteCountAnnotation(long countValue)
this.ODataAnnotationWriter.WriteInstanceAnnotationName(ODataAnnotationNames.ODataCount);
this.JsonWriter.WriteValue(countValue);
}

/// <summary>
/// Asynchronously writes a single Uri in response to a $ref query.
/// </summary>
/// <param name="entityReferenceLink">The entity reference link to write out.</param>
/// <param name="isTopLevel">true if the entity reference link being written is at the top level of the payload.</param>
private async Task WriteEntityReferenceLinkImplementationAsync(ODataEntityReferenceLink entityReferenceLink, bool isTopLevel)
{
Debug.Assert(entityReferenceLink != null, "entityReferenceLink != null");

WriterValidationUtils.ValidateEntityReferenceLink(entityReferenceLink);

await this.AsynchronousJsonWriter.StartObjectScopeAsync()
.ConfigureAwait(false);

if (isTopLevel)
{
await this.WriteContextUriPropertyAsync(ODataPayloadKind.EntityReferenceLink)
.ConfigureAwait(false);
}

await this.AsynchronousODataAnnotationWriter.WriteInstanceAnnotationNameAsync(ODataAnnotationNames.ODataId)
.ConfigureAwait(false);
await this.AsynchronousJsonWriter.WriteValueAsync(this.UriToString(entityReferenceLink.Url))
.ConfigureAwait(false);

await this.InstanceAnnotationWriter.WriteInstanceAnnotationsAsync(entityReferenceLink.InstanceAnnotations)
.ConfigureAwait(false);
await this.AsynchronousJsonWriter.EndObjectScopeAsync()
.ConfigureAwait(false);
}

/// <summary>
/// Asynchronously writes a set of links (Uris) in response to a $ref query; includes optional count and next-page-link information.
/// </summary>
/// <param name="entityReferenceLinks">The set of entity reference links to write out.</param>
private async Task WriteEntityReferenceLinksImplementationAsync(ODataEntityReferenceLinks entityReferenceLinks)
{
Debug.Assert(entityReferenceLinks != null, "entityReferenceLinks != null");

bool wroteNextLink = false;

// {
await this.AsynchronousJsonWriter.StartObjectScopeAsync()
.ConfigureAwait(false);

// "@odata.context": ...
await this.WriteContextUriPropertyAsync(ODataPayloadKind.EntityReferenceLinks)
.ConfigureAwait(false);

if (entityReferenceLinks.Count.HasValue)
{
// We try to write the count property at the top of the payload if one is available.
// "@odata.count": ...
await this.WriteCountAnnotationAsync(entityReferenceLinks.Count.Value)
.ConfigureAwait(false);
}

if (entityReferenceLinks.NextPageLink != null)
{
// We try to write the next link at the top of the payload if one is available. If not, we try again at the end.
wroteNextLink = true;

// "@odata.next": ...
await this.WriteNextLinkAnnotationAsync(entityReferenceLinks.NextPageLink)
.ConfigureAwait(false);
}

this.InstanceAnnotationWriter.WriteInstanceAnnotations(entityReferenceLinks.InstanceAnnotations);

// "value":
await this.AsynchronousJsonWriter.WriteValuePropertyNameAsync()
.ConfigureAwait(false);

// "[":
await this.AsynchronousJsonWriter.StartArrayScopeAsync()
.ConfigureAwait(false);

IEnumerable<ODataEntityReferenceLink> entityReferenceLinksEnumerable = entityReferenceLinks.Links;
if (entityReferenceLinksEnumerable != null)
{
foreach (ODataEntityReferenceLink entityReferenceLink in entityReferenceLinksEnumerable)
{
WriterValidationUtils.ValidateEntityReferenceLinkNotNull(entityReferenceLink);
await this.WriteEntityReferenceLinkImplementationAsync(entityReferenceLink, /* isTopLevel */ false)
.ConfigureAwait(false);
}
}

// "]"
await this.AsynchronousJsonWriter.EndArrayScopeAsync()
.ConfigureAwait(false);

if (!wroteNextLink && entityReferenceLinks.NextPageLink != null)
{
// "@odata.next": ...
await this.WriteNextLinkAnnotationAsync(entityReferenceLinks.NextPageLink)
.ConfigureAwait(false);
}

// "}"
await this.AsynchronousJsonWriter.EndObjectScopeAsync()
.ConfigureAwait(false);
}

/// <summary>
/// Asynchronously writes the next link property, which consists of the property name and value.
/// </summary>
/// <param name="nextPageLink">The non-null value of the next link to write.</param>
private async Task WriteNextLinkAnnotationAsync(Uri nextPageLink)
{
Debug.Assert(nextPageLink != null, "Expected non-null next link.");

await this.AsynchronousODataAnnotationWriter.WriteInstanceAnnotationNameAsync(ODataAnnotationNames.ODataNextLink)
.ConfigureAwait(false);
await this.AsynchronousJsonWriter.WriteValueAsync(this.UriToString(nextPageLink))
.ConfigureAwait(false);
}

/// <summary>
/// Asynchronously writes the odata.count property, which consists of the property name and value.
/// </summary>
/// <param name="countValue">The value of the count property to write.</param>
private async Task WriteCountAnnotationAsync(long countValue)
{
await this.AsynchronousODataAnnotationWriter.WriteInstanceAnnotationNameAsync(ODataAnnotationNames.ODataCount)
.ConfigureAwait(false);
await this.AsynchronousJsonWriter.WriteValueAsync(countValue)
.ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
//---------------------------------------------------------------------
// <copyright file="ODataJsonLightEntityReferenceLinkSerializerTests.cs" company="Microsoft">
// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
// </copyright>
//---------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.OData.Edm;
using Microsoft.OData.JsonLight;
using Xunit;

namespace Microsoft.OData.Tests.JsonLight
{
public class ODataJsonLightEntityReferenceLinkSerializerTests
{
private EdmModel model;
private MemoryStream stream;
private ODataMessageWriterSettings messageWriterSettings;

public ODataJsonLightEntityReferenceLinkSerializerTests()
{
this.model = new EdmModel();
this.stream = new MemoryStream();
this.messageWriterSettings = new ODataMessageWriterSettings
{
EnableMessageStreamDisposal = false,
Version = ODataVersion.V4
};

this.messageWriterSettings.SetServiceDocumentUri(new Uri("http://tempuri.org"));
}

[Fact]
public async Task WriteEntityReferenceLinkAsync_WritesTopLevelUri()
{
ODataEntityReferenceLink entityReferenceLink = new ODataEntityReferenceLink
{
Url = new Uri("http://tempuri.org/Customers(1)")
};

var result = await SetupODataJsonLightEntityReferenceLinkSerializerAndRunTestAsync(
(jsonLightEntityReferenceLinkSerializer) =>
{
return jsonLightEntityReferenceLinkSerializer.WriteEntityReferenceLinkAsync(entityReferenceLink);
});

Assert.Equal("{\"@odata.context\":\"http://tempuri.org/$metadata#$ref\"," +
"\"@odata.id\":\"http://tempuri.org/Customers(1)\"}", result);
}

public static IEnumerable<object[]> GetWriteEntityReferenceLinksTestData()
{
var entityReferenceLinks = new List<ODataEntityReferenceLink>
{
new ODataEntityReferenceLink { Url = new Uri("http://tempuri.org/Customers(1)") },
new ODataEntityReferenceLink { Url = new Uri("http://tempuri.org/Orders(1)") }
};
var template = "{{\"@odata.context\":\"http://tempuri.org/$metadata#Collection($ref)\",{0}" +
"\"value\":[" +
"{{\"@odata.id\":\"http://tempuri.org/Customers(1)\"}}," +
"{{\"@odata.id\":\"http://tempuri.org/Orders(1)\"}}" +
"]}}";

// Entity reference links
yield return new object[]
{
new ODataEntityReferenceLinks
{
Links = entityReferenceLinks
},
string.Format(template, "")
};

// Entity reference links plus next page link
yield return new object[]
{
new ODataEntityReferenceLinks
{
Links = entityReferenceLinks,
NextPageLink = new Uri("http://tempuri.org/Customers?$skiptoken=Id-5")
},
string.Format(template, "\"@odata.nextLink\":\"http://tempuri.org/Customers?$skiptoken=Id-5\",")
};

// Entity reference links plus count
yield return new object[]
{
new ODataEntityReferenceLinks
{
Links = entityReferenceLinks,
Count = 10
},
string.Format(template, "\"@odata.count\":10,")
};

// Entity reference links plus next page link plus count
yield return new object[]
{
new ODataEntityReferenceLinks
{
Links = entityReferenceLinks,
NextPageLink = new Uri("http://tempuri.org/Customers?$skiptoken=Id-5"),
Count = 10
},
string.Format(
template,
"\"@odata.count\":10," +
"\"@odata.nextLink\":\"http://tempuri.org/Customers?$skiptoken=Id-5\",")
};
}

[Theory]
[MemberData(nameof(GetWriteEntityReferenceLinksTestData))]
public async Task WriteEntityReferenceLinkAsync_WritesExpectedOutput(ODataEntityReferenceLinks entityReferenceLinks, string expected)
{
var result = await SetupODataJsonLightEntityReferenceLinkSerializerAndRunTestAsync(
(jsonLightEntityReferenceLinkSerializer) =>
{
return jsonLightEntityReferenceLinkSerializer.WriteEntityReferenceLinksAsync(entityReferenceLinks);
});

Assert.Equal(expected, result);
}

[Fact]
public async Task WriteEntityReferenceLinksAsync_ExceptionThrownForNullEntityReferenceLink()
{
var entityReferenceLinks = new ODataEntityReferenceLinks
{
Links = new List<ODataEntityReferenceLink> { null }
};

var exception = await Assert.ThrowsAsync<ODataException>(
() => SetupODataJsonLightEntityReferenceLinkSerializerAndRunTestAsync(
(jsonLightEntityReferenceLinkSerializer) =>
{
return jsonLightEntityReferenceLinkSerializer.WriteEntityReferenceLinksAsync(entityReferenceLinks);
}));

Assert.Equal(Strings.WriterValidationUtils_EntityReferenceLinksLinkMustNotBeNull, exception.Message);
}

[Fact]
public async Task WriteEntityReferenceLinkAsync_ExceptionThrownForNullUrlInEntityReferenceLink()
{
var entityReferenceLink = new ODataEntityReferenceLink
{
Url = null
};

var exception = await Assert.ThrowsAsync<ODataException>(
() => SetupODataJsonLightEntityReferenceLinkSerializerAndRunTestAsync(
(jsonLightEntityReferenceLinkSerializer) =>
{
return jsonLightEntityReferenceLinkSerializer.WriteEntityReferenceLinkAsync(entityReferenceLink);
}));

Assert.Equal(Strings.WriterValidationUtils_EntityReferenceLinkUrlMustNotBeNull, exception.Message);
}

private ODataJsonLightEntityReferenceLinkSerializer CreateODataJsonLightEntityReferenceLinkSerializer(
bool writingResponse,
IServiceProvider container = null,
bool isAsync = false)
{
var messageInfo = new ODataMessageInfo
{
MessageStream = this.stream,
MediaType = new ODataMediaType("application", "json"),
#if NETCOREAPP1_1
Encoding = Encoding.GetEncoding(0),
#else
Encoding = Encoding.Default,
#endif
IsResponse = writingResponse,
IsAsync = isAsync,
Model = this.model,
Container = container
};

var jsonLightOutputContext = new ODataJsonLightOutputContext(messageInfo, this.messageWriterSettings);
return new ODataJsonLightEntityReferenceLinkSerializer(jsonLightOutputContext);
}

/// <summary>
/// Sets up an ODataJsonLightEntityReferenceLinkSerializer,
/// then runs the given test code asynchonously,
/// then flushes and reads the stream back as a string for customized verification.
/// </summary>
private async Task<string> SetupODataJsonLightEntityReferenceLinkSerializerAndRunTestAsync(Func<ODataJsonLightEntityReferenceLinkSerializer, Task> func, IServiceProvider container = null, bool writingTopLevelCollection = false)
{
var jsonLightEntityReferenceLinkSerializer = CreateODataJsonLightEntityReferenceLinkSerializer(true, container, true);
await func(jsonLightEntityReferenceLinkSerializer);
await jsonLightEntityReferenceLinkSerializer.JsonLightOutputContext.FlushAsync();
await jsonLightEntityReferenceLinkSerializer.AsynchronousJsonWriter.FlushAsync();

this.stream.Position = 0;

return await new StreamReader(this.stream).ReadToEndAsync();
}
}
}

0 comments on commit 66ee246

Please sign in to comment.