From 17511b7f197104f495f38c045573c706b90ad659 Mon Sep 17 00:00:00 2001 From: John Gathogo Date: Fri, 16 Apr 2021 14:02:42 +0300 Subject: [PATCH] Implement asynchronous support in ODataJsonLightServiceDocumentSerializer (#2048) Co-authored-by: John Gathogo --- ...ODataJsonLightServiceDocumentSerializer.cs | 127 +++++++++++ ...JsonLightServiceDocumentSerializerTests.cs | 198 +++++++++++++++++- 2 files changed, 322 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightServiceDocumentSerializer.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightServiceDocumentSerializer.cs index fc08b19491..71243025d6 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightServiceDocumentSerializer.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightServiceDocumentSerializer.cs @@ -10,6 +10,7 @@ namespace Microsoft.OData.JsonLight using System; using System.Collections.Generic; using System.Diagnostics; + using System.Threading.Tasks; #endregion Namespaces /// @@ -132,5 +133,131 @@ private void WriteServiceDocumentElement(ODataServiceDocumentElement serviceDocu // "}" this.JsonWriter.EndObjectScope(); } + + /// + /// Asynchronously writes a service document in JsonLight format. + /// + /// The service document to write. + 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 functionImportsWritten = new HashSet(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); + }); + } + + /// + /// Asynchronously writes a element (EntitySet, Singleton or FunctionImport) in service document. + /// + /// The element in service document to write. + /// Kind of the service document element, optional for entitysets must for FunctionImport and Singleton. + 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); + } } } diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightServiceDocumentSerializerTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightServiceDocumentSerializerTests.cs index 751c95435a..37fe1d0835 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightServiceDocumentSerializerTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightServiceDocumentSerializerTests.cs @@ -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 @@ -112,7 +113,186 @@ public void WriteNullFunctionImportShouldThrow() WriteServiceDocumentShouldError(serviceDocument).Throws(Strings.ValidationUtils_WorkspaceResourceMustNotContainNullItem); } - private static ODataJsonLightServiceDocumentSerializer CreateODataJsonLightServiceDocumentSerializer(MemoryStream memoryStream, IODataPayloadUriConverter urlResolver = null) + public static IEnumerable GetWriteServiceDocumentTestData() + { + yield return new object[] + { + new ODataServiceDocument {}, + "{\"value\":[]}" + }; + + var entitySets = new List + { + 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 + { + 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 + { + 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(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 GetWriteServiceDocumentExceptionsTestData() + { + // Null FunctionImport + yield return new object[] + { + new ODataServiceDocument + { + FunctionImports = new List { null } + }, + Strings.ValidationUtils_WorkspaceResourceMustNotContainNullItem + }; + + // Null EntitySet (Singletons handled the same) + yield return new object[] + { + new ODataServiceDocument + { + EntitySets = new List { null } + }, + Strings.ValidationUtils_WorkspaceResourceMustNotContainNullItem + }; + + // EntitySet with null value as Url (Singletons handled the same) + yield return new object[] + { + new ODataServiceDocument + { + EntitySets = new List + { + 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 + { + 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( + () => 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(); @@ -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 }; @@ -149,5 +329,17 @@ private static void WriteServiceDocumentVerifyOutput(ODataServiceDocument servic string actualResult = Encoding.UTF8.GetString(memoryStream.ToArray()); Assert.Equal(expectedOutput, actualResult); } + + private async Task SetupJsonLightServiceDocumentSerializerAndRunTestAsync(Func 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(); + } } }