diff --git a/src/Microsoft.OData.Core/Json/ODataJsonPropertySerializer.cs b/src/Microsoft.OData.Core/Json/ODataJsonPropertySerializer.cs index 71c34dab4e..bf5399978b 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonPropertySerializer.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonPropertySerializer.cs @@ -388,7 +388,9 @@ await WriteStreamValueAsync(streamReferenceValue, property.Name, metadataBuilder return; } - if (value is ODataNullValue || value == null) + if (value is ODataNullValue + || value == null + || value is ODataJsonElementValue jsonElement && jsonElement.Value.ValueKind == System.Text.Json.JsonValueKind.Null) { await this.WriteNullPropertyAsync(property).ConfigureAwait(false); return; diff --git a/src/Microsoft.OData.Core/ODataContextUrlInfo.cs b/src/Microsoft.OData.Core/ODataContextUrlInfo.cs index da4c2ff6ce..7ee8f06119 100644 --- a/src/Microsoft.OData.Core/ODataContextUrlInfo.cs +++ b/src/Microsoft.OData.Core/ODataContextUrlInfo.cs @@ -17,6 +17,7 @@ namespace Microsoft.OData using Microsoft.OData.UriParser; using Microsoft.OData.Edm; using Microsoft.OData.Core; + using System.Text.Json; #endregion Namespaces /// @@ -454,22 +455,35 @@ private static string GetTypeNameForValue(ODataValue value, IEdmModel model) return ODataConstants.ContextUriFragmentUntyped; } - ODataPrimitiveValue primitive = value as ODataPrimitiveValue; - if (primitive == null) + if (value is ODataPrimitiveValue primitive) { - Debug.Assert(value is ODataStreamReferenceValue, "value is ODataStreamReferenceValue"); - throw new ODataException(SRResources.ODataContextUriBuilder_StreamValueMustBePropertiesOfODataResource); + + // Try convert to underlying type if the primitive value is unsigned int. + IEdmTypeDefinitionReference typeDefinitionReference = model.ResolveUIntTypeDefinition(primitive.Value); + if (typeDefinitionReference != null) + { + return typeDefinitionReference.FullName(); + } + + IEdmPrimitiveTypeReference primitiveValueTypeReference = EdmLibraryExtensions.GetPrimitiveTypeReference(primitive.Value.GetType()); + return primitiveValueTypeReference == null ? null : primitiveValueTypeReference.FullName(); } - // Try convert to underlying type if the primitive value is unsigned int. - IEdmTypeDefinitionReference typeDefinitionReference = model.ResolveUIntTypeDefinition(primitive.Value); - if (typeDefinitionReference != null) + if (value is ODataJsonElementValue jsonElementValue) { - return typeDefinitionReference.FullName(); - } + JsonElement jsonElement = jsonElementValue.Value; + return jsonElement.ValueKind switch + { + JsonValueKind.True or JsonValueKind.False => EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Boolean, true).FullName(), + JsonValueKind.Number => EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, true).FullName(), + JsonValueKind.String => EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, true).FullName(), + JsonValueKind.Null => ODataConstants.ContextUriFragmentNull, + _ => ODataConstants.ContextUriFragmentUntyped + }; + }; - IEdmPrimitiveTypeReference primitiveValueTypeReference = EdmLibraryExtensions.GetPrimitiveTypeReference(primitive.Value.GetType()); - return primitiveValueTypeReference == null ? null : primitiveValueTypeReference.FullName(); + Debug.Assert(value is ODataStreamReferenceValue, "value is ODataStreamReferenceValue"); + throw new ODataException(SRResources.ODataContextUriBuilder_StreamValueMustBePropertiesOfODataResource); } private static string CreateApplyUriSegment(ApplyClause applyClause) diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/ODataMessageWriterTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/ODataMessageWriterTests.cs index 489ac46852..f5378d4330 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/ODataMessageWriterTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/ODataMessageWriterTests.cs @@ -19,6 +19,7 @@ using Microsoft.OData.Edm.Csdl; using Xunit; using Microsoft.OData.Core; +using System.Reflection.Metadata; namespace Microsoft.OData.Tests { @@ -309,6 +310,113 @@ public async Task SupportsODataUtf8JsonWriterAsync() Assert.Equal("{\"@odata.context\":\"http://www.example.com/$metadata#Edm.String\",\"value\":\"This is a test ия\"}", output); } + [Theory] + [InlineData("true", "Edm.Boolean")] + [InlineData("false", "Edm.Boolean")] + [InlineData("123", "Edm.Double")] + [InlineData("\"This is a string\"", "Edm.String")] + public async Task SupportsTopLevelPropertyWithPrimitiveJsonElementValueAsync(string rawValue, string expectedType) + { + // Arrange + EdmModel model = new EdmModel(); + JsonElement jsonElement = JsonDocument.Parse(rawValue).RootElement; + + // Act + string output = await WriteAndGetPayloadAsync( + model, + "application/json", + (writer) => writer.WritePropertyAsync(new ODataProperty() + { + Name = "Name", + Value = new ODataJsonElementValue(jsonElement) + }), + configureServices: (containerBuilder) => + { + containerBuilder.AddSingleton(sp => ODataUtf8JsonWriterFactory.Default); + }); + + // Assert + Assert.Equal($"{{\"@odata.context\":\"http://www.example.com/$metadata#{expectedType}\",\"value\":{rawValue}}}", output); + } + + [Theory] + [InlineData("[1,2,3]")] + [InlineData("{\"foo\":\"bar\"}")] + public async Task WritesUntypedContextJsonElementTopLevelPropertyWithArrayOrObject(string rawValue) + { + // Arrange + EdmModel model = new EdmModel(); + JsonElement jsonElement = JsonDocument.Parse(rawValue).RootElement; + + // Act + string output = await WriteAndGetPayloadAsync( + model, + "application/json", + (writer) => writer.WritePropertyAsync(new ODataProperty() + { + Name = "Name", + Value = new ODataJsonElementValue(jsonElement) + }), + configureServices: (containerBuilder) => + { + containerBuilder.AddSingleton(sp => ODataUtf8JsonWriterFactory.Default); + }); + + // Assert + Assert.Equal($"{{\"@odata.context\":\"http://www.example.com/$metadata#Edm.Untyped\",\"value\":{rawValue}}}", output); + } + + [Theory] + [InlineData(true, "Edm.Boolean")] + [InlineData(false, "Edm.Boolean")] + [InlineData(123, "Edm.Int32")] + [InlineData(123.0, "Edm.Double")] + [InlineData("\"This is a string\"", "Edm.String")] + public async Task SupportsTopLevelPropertyWithPrimitiveValueAsync(object value, string expectedType) + { + // Arrange + EdmModel model = new EdmModel(); + + // Act + string output = await WriteAndGetPayloadAsync( + model, + "application/json", + (writer) => writer.WritePropertyAsync(new ODataProperty() + { + Name = "Name", + Value = value + }), + configureServices: (containerBuilder) => + { + containerBuilder.AddSingleton(sp => ODataUtf8JsonWriterFactory.Default); + }); + + // Assert + Assert.Equal($"{{\"@odata.context\":\"http://www.example.com/$metadata#{expectedType}\",\"value\":{value}}}", output); + } + + [Fact] + public async Task ThrowsExceptionIfJsonElementValueContainsNullInTopLevelPropertyAsync() + { + // Arrange + EdmModel model = new EdmModel(); + JsonElement jsonElement = JsonDocument.Parse("null").RootElement; + + // Act + var testCode = async () => await WriteAndGetPayloadAsync( + model, + "application/json", + (writer) => writer.WritePropertyAsync(new ODataProperty() + { + Name = "Name", + Value = new ODataJsonElementValue(jsonElement) + })); + + // Assert + var exception = await Assert.ThrowsAsync(testCode); + Assert.Equal("Cannot write the value 'null' in top level property; return 204 instead.", exception.Message); + } + [Theory] [InlineData("utf-8")] [InlineData("utf-16")]