Skip to content

Commit

Permalink
Implement asynchronous support in ODataJsonLightServiceDocumentSerial…
Browse files Browse the repository at this point in the history
…izer (#2048)

Co-authored-by: John Gathogo <[email protected]>
  • Loading branch information
gathogojr and John Gathogo authored Apr 16, 2021
1 parent 96f9a03 commit 17511b7
Show file tree
Hide file tree
Showing 2 changed files with 322 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Microsoft.OData.JsonLight
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
#endregion Namespaces

/// <summary>
Expand Down Expand Up @@ -132,5 +133,131 @@ private void WriteServiceDocumentElement(ODataServiceDocumentElement serviceDocu
// "}"
this.JsonWriter.EndObjectScope();
}

/// <summary>
/// Asynchronously writes a service document in JsonLight format.
/// </summary>
/// <param name="serviceDocument">The service document to write.</param>
internal Task WriteServiceDocumentAsync(ODataServiceDocument serviceDocument)
{
Debug.Assert(serviceDocument != null, "serviceDocument != null");

return this.WriteTopLevelPayloadAsync(
async () =>
{
// "{"
await this.AsynchronousJsonWriter.StartObjectScopeAsync()
.ConfigureAwait(false);

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

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

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

if (serviceDocument.EntitySets != null)
{
foreach (ODataEntitySetInfo collectionInfo in serviceDocument.EntitySets)
{
await this.WriteServiceDocumentElementAsync(collectionInfo, JsonLightConstants.ServiceDocumentEntitySetKindName)
.ConfigureAwait(false);
}
}

if (serviceDocument.Singletons != null)
{
foreach (ODataSingletonInfo singletonInfo in serviceDocument.Singletons)
{
await this.WriteServiceDocumentElementAsync(singletonInfo, JsonLightConstants.ServiceDocumentSingletonKindName)
.ConfigureAwait(false);
}
}

HashSet<string> functionImportsWritten = new HashSet<string>(StringComparer.Ordinal);

if (serviceDocument.FunctionImports != null)
{
foreach (ODataFunctionImportInfo functionImportInfo in serviceDocument.FunctionImports)
{
if (functionImportInfo == null)
{
throw new ODataException(Strings.ValidationUtils_WorkspaceResourceMustNotContainNullItem);
}

if (!functionImportsWritten.Contains(functionImportInfo.Name))
{
functionImportsWritten.Add(functionImportInfo.Name);
await this.WriteServiceDocumentElementAsync(functionImportInfo, JsonLightConstants.ServiceDocumentFunctionImportKindName)
.ConfigureAwait(false);
}
}
}

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

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

/// <summary>
/// Asynchronously writes a element (EntitySet, Singleton or FunctionImport) in service document.
/// </summary>
/// <param name="serviceDocumentElement">The element in service document to write.</param>
/// <param name="kind">Kind of the service document element, optional for entitysets must for FunctionImport and Singleton.</param>
private async Task WriteServiceDocumentElementAsync(ODataServiceDocumentElement serviceDocumentElement, string kind)
{
// validate that the resource has a non-null url.
ValidationUtils.ValidateServiceDocumentElement(serviceDocumentElement, ODataFormat.Json);

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

// "name": ...
await this.AsynchronousJsonWriter.WriteNameAsync(JsonLightConstants.ODataServiceDocumentElementName)
.ConfigureAwait(false);
await this.AsynchronousJsonWriter.WriteValueAsync(serviceDocumentElement.Name)
.ConfigureAwait(false);

// Do not write title if it is null or empty, or if title is the same as name.
if (!string.IsNullOrEmpty(serviceDocumentElement.Title) && !serviceDocumentElement.Title.Equals(serviceDocumentElement.Name, StringComparison.Ordinal))
{
// "title": ...
await this.AsynchronousJsonWriter.WriteNameAsync(JsonLightConstants.ODataServiceDocumentElementTitle)
.ConfigureAwait(false);
await this.AsynchronousJsonWriter.WriteValueAsync(serviceDocumentElement.Title)
.ConfigureAwait(false);
}

// Not always writing because it can be null if an ODataEntitySetInfo, not necessary to write this. Required for the others though.
if (kind != null)
{
// "kind": ...
await this.AsynchronousJsonWriter.WriteNameAsync(JsonLightConstants.ODataServiceDocumentElementKind)
.ConfigureAwait(false);
await this.AsynchronousJsonWriter.WriteValueAsync(kind)
.ConfigureAwait(false);
}

// "url": ...
await this.AsynchronousJsonWriter.WriteNameAsync(JsonLightConstants.ODataServiceDocumentElementUrlName)
.ConfigureAwait(false);
await this.AsynchronousJsonWriter.WriteValueAsync(this.UriToString(serviceDocumentElement.Url))
.ConfigureAwait(false);

// "}"
await this.AsynchronousJsonWriter.EndObjectScopeAsync()
.ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.OData.JsonLight;
using System.Threading.Tasks;
using Microsoft.OData.Edm;
using Microsoft.OData.JsonLight;
using Xunit;

namespace Microsoft.OData.Tests.JsonLight
Expand Down Expand Up @@ -112,7 +113,186 @@ public void WriteNullFunctionImportShouldThrow()
WriteServiceDocumentShouldError(serviceDocument).Throws<ODataException>(Strings.ValidationUtils_WorkspaceResourceMustNotContainNullItem);
}

private static ODataJsonLightServiceDocumentSerializer CreateODataJsonLightServiceDocumentSerializer(MemoryStream memoryStream, IODataPayloadUriConverter urlResolver = null)
public static IEnumerable<object[]> GetWriteServiceDocumentTestData()
{
yield return new object[]
{
new ODataServiceDocument {},
"{\"value\":[]}"
};

var entitySets = new List<ODataEntitySetInfo>
{
new ODataEntitySetInfo { Name = "Customers", Title = "Customers", Url = new Uri("http://tempuri.org/Customers") },
new ODataEntitySetInfo { Name = "Orders", Title = "Orders", Url = new Uri("http://tempuri.org/Orders") }
};

// EntitySet

yield return new object[]
{
new ODataServiceDocument
{
EntitySets = entitySets
},
"{\"value\":[" +
"{\"name\":\"Customers\",\"kind\":\"EntitySet\",\"url\":\"http://tempuri.org/Customers\"}," +
"{\"name\":\"Orders\",\"kind\":\"EntitySet\",\"url\":\"http://tempuri.org/Orders\"}" +
"]}"
};

var singletons = new List<ODataSingletonInfo>
{
new ODataSingletonInfo { Name = "Company", Title = "BusinessEntity", Url = new Uri("http://tempuri.org/Company") }
};

// Singleton (Title different from Name)

yield return new object[]
{
new ODataServiceDocument
{
Singletons = singletons
},
"{\"value\":[" +
"{\"name\":\"Company\",\"title\":\"BusinessEntity\",\"kind\":\"Singleton\",\"url\":\"http://tempuri.org/Company\"}" +
"]}"
};

var functionImports = new List<ODataFunctionImportInfo>
{
new ODataFunctionImportInfo { Name = "GetOpenOrders", Url = new Uri("http://tempuri.org/GetOpenOrders") },
new ODataFunctionImportInfo { Name = "GetTop5Customers", Url = new Uri("http://tempuri.org/GetTop5Customers") }
};

// FunctionImport

yield return new object[]
{
new ODataServiceDocument
{
FunctionImports = functionImports
},
"{\"value\":[" +
"{\"name\":\"GetOpenOrders\",\"kind\":\"FunctionImport\",\"url\":\"http://tempuri.org/GetOpenOrders\"}," +
"{\"name\":\"GetTop5Customers\",\"kind\":\"FunctionImport\",\"url\":\"http://tempuri.org/GetTop5Customers\"}" +
"]}"
};

// FunctionImport (Collection containing duplicates)

var duplicatedFunctionImports = new List<ODataFunctionImportInfo>(functionImports);
duplicatedFunctionImports[1] = duplicatedFunctionImports[0];

yield return new object[]
{
new ODataServiceDocument
{
FunctionImports = duplicatedFunctionImports
},
"{\"value\":[" +
"{\"name\":\"GetOpenOrders\",\"kind\":\"FunctionImport\",\"url\":\"http://tempuri.org/GetOpenOrders\"}" +
"]}"
};


// Multiple element types

yield return new object[]
{
new ODataServiceDocument
{
EntitySets = entitySets,
Singletons = singletons,
FunctionImports = functionImports
},
"{\"value\":[" +
"{\"name\":\"Customers\",\"kind\":\"EntitySet\",\"url\":\"http://tempuri.org/Customers\"}," +
"{\"name\":\"Orders\",\"kind\":\"EntitySet\",\"url\":\"http://tempuri.org/Orders\"}," +
"{\"name\":\"Company\",\"title\":\"BusinessEntity\",\"kind\":\"Singleton\",\"url\":\"http://tempuri.org/Company\"}," +
"{\"name\":\"GetOpenOrders\",\"kind\":\"FunctionImport\",\"url\":\"http://tempuri.org/GetOpenOrders\"}," +
"{\"name\":\"GetTop5Customers\",\"kind\":\"FunctionImport\",\"url\":\"http://tempuri.org/GetTop5Customers\"}" +
"]}"
};
}

[Theory]
[MemberData(nameof(GetWriteServiceDocumentTestData))]
public async Task WriteServiceDocumentAsync_WritesExpectedOutput(ODataServiceDocument serviceDocument, string expected)
{
var result = await SetupJsonLightServiceDocumentSerializerAndRunTestAsync(
(jsonLightServiceDocumentSerializer) =>
{
return jsonLightServiceDocumentSerializer.WriteServiceDocumentAsync(serviceDocument);
});

Assert.Equal(expected, result);
}

public static IEnumerable<object[]> GetWriteServiceDocumentExceptionsTestData()
{
// Null FunctionImport
yield return new object[]
{
new ODataServiceDocument
{
FunctionImports = new List<ODataFunctionImportInfo> { null }
},
Strings.ValidationUtils_WorkspaceResourceMustNotContainNullItem
};

// Null EntitySet (Singletons handled the same)
yield return new object[]
{
new ODataServiceDocument
{
EntitySets = new List<ODataEntitySetInfo> { null }
},
Strings.ValidationUtils_WorkspaceResourceMustNotContainNullItem
};

// EntitySet with null value as Url (Singletons handled the same)
yield return new object[]
{
new ODataServiceDocument
{
EntitySets = new List<ODataEntitySetInfo>
{
new ODataEntitySetInfo { Name = "Customers", Url = null }
}
},
Strings.ValidationUtils_ResourceMustSpecifyUrl
};

// EntitySet with null value as Name (Singletons handled the same)
yield return new object[]
{
new ODataServiceDocument
{
EntitySets = new List<ODataEntitySetInfo>
{
new ODataEntitySetInfo { Name = null, Url = new Uri("http://tempuri.org/Customers") }
}
},
Strings.ValidationUtils_ResourceMustSpecifyName("http://tempuri.org/Customers")
};
}

[Theory]
[MemberData(nameof(GetWriteServiceDocumentExceptionsTestData))]
public async Task WriteServiceDocumentAsync_ThrowsException(ODataServiceDocument serviceDocument, string exceptionMessage)
{
var exception = await Assert.ThrowsAsync<ODataException>(
() => SetupJsonLightServiceDocumentSerializerAndRunTestAsync(
(jsonLightServiceDocumentSerializer) =>
{
return jsonLightServiceDocumentSerializer.WriteServiceDocumentAsync(serviceDocument);
}));

Assert.Equal(exceptionMessage, exception.Message);
}

private static ODataJsonLightServiceDocumentSerializer CreateODataJsonLightServiceDocumentSerializer(MemoryStream memoryStream, IODataPayloadUriConverter urlResolver = null, bool isAsync = false)
{
var model = new EdmModel();
var messageWriterSettings = new ODataMessageWriterSettings();
Expand All @@ -123,7 +303,7 @@ private static ODataJsonLightServiceDocumentSerializer CreateODataJsonLightServi
MediaType = new ODataMediaType("application", "json"),
Encoding = Encoding.UTF8,
IsResponse = false,
IsAsync = false,
IsAsync = isAsync,
Model = mainModel,
PayloadUriConverter = urlResolver
};
Expand All @@ -149,5 +329,17 @@ private static void WriteServiceDocumentVerifyOutput(ODataServiceDocument servic
string actualResult = Encoding.UTF8.GetString(memoryStream.ToArray());
Assert.Equal(expectedOutput, actualResult);
}

private async Task<string> SetupJsonLightServiceDocumentSerializerAndRunTestAsync(Func<ODataJsonLightServiceDocumentSerializer, Task> func)
{
MemoryStream memoryStream = new MemoryStream();
var jsonLightServiceDocumentSerializer = CreateODataJsonLightServiceDocumentSerializer(memoryStream, /* urlResolver */ null, true);
await func(jsonLightServiceDocumentSerializer);
await jsonLightServiceDocumentSerializer.JsonLightOutputContext.FlushAsync();
await jsonLightServiceDocumentSerializer.AsynchronousJsonWriter.FlushAsync();

memoryStream.Position = 0;
return await new StreamReader(memoryStream).ReadToEndAsync();
}
}
}

0 comments on commit 17511b7

Please sign in to comment.