diff --git a/src/Microsoft.OData.Client/PublicAPI/net45/PublicAPI.Shipped.txt b/src/Microsoft.OData.Client/PublicAPI/net45/PublicAPI.Shipped.txt index 72bef56a45..d71cef5a80 100644 --- a/src/Microsoft.OData.Client/PublicAPI/net45/PublicAPI.Shipped.txt +++ b/src/Microsoft.OData.Client/PublicAPI/net45/PublicAPI.Shipped.txt @@ -1,3 +1,13 @@ +Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration.OnDeltaFeedEnded(System.Action action) -> Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration +Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration.OnDeltaFeedStarted(System.Action action) -> Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration +Microsoft.OData.Client.ReadingDeltaFeedArgs +Microsoft.OData.Client.ReadingDeltaFeedArgs.DeltaFeed.get -> Microsoft.OData.ODataDeltaResourceSet +Microsoft.OData.Client.ReadingDeltaFeedArgs.ReadingDeltaFeedArgs(Microsoft.OData.ODataDeltaResourceSet deltaFeed) -> void +Microsoft.OData.Client.SaveChangesOptions.BulkUpdate = 128 -> Microsoft.OData.Client.SaveChangesOptions +Microsoft.OData.Client.SendingRequest2EventArgs.IsBulkUpdate.get -> bool +virtual Microsoft.OData.Client.DataServiceContext.BulkUpdate(params T[] objects) -> Microsoft.OData.Client.DataServiceResponse +virtual Microsoft.OData.Client.DataServiceContext.BulkUpdateAsync(params T[] objects) -> System.Threading.Tasks.Task +virtual Microsoft.OData.Client.DataServiceContext.BulkUpdateAsync(System.Threading.CancellationToken cancellationToken, params T[] objects) -> System.Threading.Tasks.Task virtual Microsoft.OData.Client.DataServiceContext.AutoNullPropagation.get -> bool virtual Microsoft.OData.Client.DataServiceContext.AutoNullPropagation.set -> void abstract Microsoft.OData.Client.ALinq.UriParser.PathSegmentToken.Accept(Microsoft.OData.Client.ALinq.UriParser.IPathSegmentTokenVisitor visitor) -> void diff --git a/src/Microsoft.OData.Client/PublicAPI/net45/PublicAPI.Unshipped.txt b/src/Microsoft.OData.Client/PublicAPI/net45/PublicAPI.Unshipped.txt index 70ba02966a..e69de29bb2 100644 --- a/src/Microsoft.OData.Client/PublicAPI/net45/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OData.Client/PublicAPI/net45/PublicAPI.Unshipped.txt @@ -1,10 +0,0 @@ -Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration.OnDeltaFeedEnded(System.Action action) -> Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration -Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration.OnDeltaFeedStarted(System.Action action) -> Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration -Microsoft.OData.Client.ReadingDeltaFeedArgs -Microsoft.OData.Client.ReadingDeltaFeedArgs.DeltaFeed.get -> Microsoft.OData.ODataDeltaResourceSet -Microsoft.OData.Client.ReadingDeltaFeedArgs.ReadingDeltaFeedArgs(Microsoft.OData.ODataDeltaResourceSet deltaFeed) -> void -Microsoft.OData.Client.SaveChangesOptions.BulkUpdate = 128 -> Microsoft.OData.Client.SaveChangesOptions -Microsoft.OData.Client.SendingRequest2EventArgs.IsBulkUpdate.get -> bool -virtual Microsoft.OData.Client.DataServiceContext.BulkUpdate(params T[] objects) -> Microsoft.OData.Client.DataServiceResponse -virtual Microsoft.OData.Client.DataServiceContext.BulkUpdateAsync(params T[] objects) -> System.Threading.Tasks.Task -virtual Microsoft.OData.Client.DataServiceContext.BulkUpdateAsync(System.Threading.CancellationToken cancellationToken, params T[] objects) -> System.Threading.Tasks.Task \ No newline at end of file diff --git a/src/Microsoft.OData.Client/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/src/Microsoft.OData.Client/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt index 72bef56a45..d71cef5a80 100644 --- a/src/Microsoft.OData.Client/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/Microsoft.OData.Client/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt @@ -1,3 +1,13 @@ +Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration.OnDeltaFeedEnded(System.Action action) -> Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration +Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration.OnDeltaFeedStarted(System.Action action) -> Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration +Microsoft.OData.Client.ReadingDeltaFeedArgs +Microsoft.OData.Client.ReadingDeltaFeedArgs.DeltaFeed.get -> Microsoft.OData.ODataDeltaResourceSet +Microsoft.OData.Client.ReadingDeltaFeedArgs.ReadingDeltaFeedArgs(Microsoft.OData.ODataDeltaResourceSet deltaFeed) -> void +Microsoft.OData.Client.SaveChangesOptions.BulkUpdate = 128 -> Microsoft.OData.Client.SaveChangesOptions +Microsoft.OData.Client.SendingRequest2EventArgs.IsBulkUpdate.get -> bool +virtual Microsoft.OData.Client.DataServiceContext.BulkUpdate(params T[] objects) -> Microsoft.OData.Client.DataServiceResponse +virtual Microsoft.OData.Client.DataServiceContext.BulkUpdateAsync(params T[] objects) -> System.Threading.Tasks.Task +virtual Microsoft.OData.Client.DataServiceContext.BulkUpdateAsync(System.Threading.CancellationToken cancellationToken, params T[] objects) -> System.Threading.Tasks.Task virtual Microsoft.OData.Client.DataServiceContext.AutoNullPropagation.get -> bool virtual Microsoft.OData.Client.DataServiceContext.AutoNullPropagation.set -> void abstract Microsoft.OData.Client.ALinq.UriParser.PathSegmentToken.Accept(Microsoft.OData.Client.ALinq.UriParser.IPathSegmentTokenVisitor visitor) -> void diff --git a/src/Microsoft.OData.Client/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.OData.Client/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index 7e2cf2df44..e69de29bb2 100644 --- a/src/Microsoft.OData.Client/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.OData.Client/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,10 +0,0 @@ -Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration.OnDeltaFeedEnded(System.Action action) -> Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration -Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration.OnDeltaFeedStarted(System.Action action) -> Microsoft.OData.Client.DataServiceClientResponsePipelineConfiguration -Microsoft.OData.Client.ReadingDeltaFeedArgs -Microsoft.OData.Client.ReadingDeltaFeedArgs.DeltaFeed.get -> Microsoft.OData.ODataDeltaResourceSet -Microsoft.OData.Client.ReadingDeltaFeedArgs.ReadingDeltaFeedArgs(Microsoft.OData.ODataDeltaResourceSet deltaFeed) -> void -Microsoft.OData.Client.SaveChangesOptions.BulkUpdate = 128 -> Microsoft.OData.Client.SaveChangesOptions -Microsoft.OData.Client.SendingRequest2EventArgs.IsBulkUpdate.get -> bool -virtual Microsoft.OData.Client.DataServiceContext.BulkUpdate(params T[] objects) -> Microsoft.OData.Client.DataServiceResponse -virtual Microsoft.OData.Client.DataServiceContext.BulkUpdateAsync(params T[] objects) -> System.Threading.Tasks.Task -virtual Microsoft.OData.Client.DataServiceContext.BulkUpdateAsync(System.Threading.CancellationToken cancellationToken, params T[] objects) -> System.Threading.Tasks.Task diff --git a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightBatchPayloadItemPropertiesCache.cs b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightBatchPayloadItemPropertiesCache.cs index 0180311144..38de20f84c 100644 --- a/src/Microsoft.OData.Core/JsonLight/ODataJsonLightBatchPayloadItemPropertiesCache.cs +++ b/src/Microsoft.OData.Core/JsonLight/ODataJsonLightBatchPayloadItemPropertiesCache.cs @@ -220,6 +220,12 @@ private void ScanJsonProperties() // Convert to upper case to support case-insensitive request property names string propertyName = Normalize(this.jsonReader.ReadPropertyName()); + // Throw an ODataException, if a duplicate json property was detected + if (jsonProperties.ContainsKey(propertyName)) + { + throw new ODataException(Strings.ODataJsonLightBatchPayloadItemPropertiesCache_DuplicatePropertyForRequestInBatch(propertyName)); + } + switch (propertyName) { case PropertyNameId: @@ -269,6 +275,12 @@ private void ScanJsonProperties() string headerName = this.jsonReader.ReadPropertyName(); string headerValue = this.jsonReader.ReadPrimitiveValue().ToString(); + // Throw an ODataException, if a duplicate header was detected + if (headers.ContainsKeyOrdinal(headerName)) + { + throw new ODataException(Strings.ODataJsonLightBatchPayloadItemPropertiesCache_DuplicateHeaderForRequestInBatch(headerName)); + } + // Remember the Content-Type header value. if (headerName.Equals(ODataConstants.ContentTypeHeader, StringComparison.OrdinalIgnoreCase)) { @@ -359,6 +371,12 @@ await this.asynchronousJsonReader.ReadStartObjectAsync() // Convert to upper case to support case-insensitive request property names string propertyName = Normalize(await this.asynchronousJsonReader.ReadPropertyNameAsync().ConfigureAwait(false)); + // Throw an ODataException, if a duplicate json property was detected + if (jsonProperties.ContainsKey(propertyName)) + { + throw new ODataException(Strings.ODataJsonLightBatchPayloadItemPropertiesCache_DuplicatePropertyForRequestInBatch(propertyName)); + } + switch (propertyName) { case PropertyNameId: @@ -409,6 +427,12 @@ await this.asynchronousJsonReader.ReadStartObjectAsync() .ConfigureAwait(false); string headerValue = (await this.asynchronousJsonReader.ReadPrimitiveValueAsync().ConfigureAwait(false)).ToString(); + // Throw an ODataException, if a duplicate header was detected + if (headers.ContainsKeyOrdinal(headerName)) + { + throw new ODataException(Strings.ODataJsonLightBatchPayloadItemPropertiesCache_DuplicateHeaderForRequestInBatch(headerName)); + } + // Remember the Content-Type header value. if (headerName.Equals(ODataConstants.ContentTypeHeader, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Microsoft.OData.Core/Microsoft.OData.Core.cs b/src/Microsoft.OData.Core/Microsoft.OData.Core.cs index d1412751c7..637fd0bc56 100644 --- a/src/Microsoft.OData.Core/Microsoft.OData.Core.cs +++ b/src/Microsoft.OData.Core/Microsoft.OData.Core.cs @@ -194,6 +194,8 @@ internal sealed class TextRes { internal const string ODataBatchReaderStream_UnexpectedEndOfInput = "ODataBatchReaderStream_UnexpectedEndOfInput"; internal const string ODataBatchReaderStreamBuffer_BoundaryLineSecurityLimitReached = "ODataBatchReaderStreamBuffer_BoundaryLineSecurityLimitReached"; internal const string ODataJsonLightBatchPayloadItemPropertiesCache_UnknownPropertyForMessageInBatch = "ODataJsonLightBatchPayloadItemPropertiesCache_UnknownPropertyForMessageInBatch"; + internal const string ODataJsonLightBatchPayloadItemPropertiesCache_DuplicatePropertyForRequestInBatch = "ODataJsonLightBatchPayloadItemPropertiesCache_DuplicatePropertyForRequestInBatch"; + internal const string ODataJsonLightBatchPayloadItemPropertiesCache_DuplicateHeaderForRequestInBatch = "ODataJsonLightBatchPayloadItemPropertiesCache_DuplicateHeaderForRequestInBatch"; internal const string ODataJsonLightBatchBodyContentReaderStream_UnexpectedNodeType = "ODataJsonLightBatchBodyContentReaderStream_UnexpectedNodeType"; internal const string ODataJsonLightBatchBodyContentReaderStream_UnsupportedContentTypeInHeader = "ODataJsonLightBatchBodyContentReaderStream_UnsupportedContentTypeInHeader"; internal const string ODataAsyncWriter_CannotCreateResponseWhenNotWritingResponse = "ODataAsyncWriter_CannotCreateResponseWhenNotWritingResponse"; diff --git a/src/Microsoft.OData.Core/Microsoft.OData.Core.txt b/src/Microsoft.OData.Core/Microsoft.OData.Core.txt index fc88bb09b3..7da6f31a80 100644 --- a/src/Microsoft.OData.Core/Microsoft.OData.Core.txt +++ b/src/Microsoft.OData.Core/Microsoft.OData.Core.txt @@ -207,6 +207,8 @@ ODataBatchReaderStream_UnexpectedEndOfInput=Encountered an unexpected end of inp ODataBatchReaderStreamBuffer_BoundaryLineSecurityLimitReached=Too many white spaces after a boundary delimiter and before the terminating line resource set. For security reasons, the total number of characters for a boundary including white spaces must not exceed {0}. ODataJsonLightBatchPayloadItemPropertiesCache_UnknownPropertyForMessageInBatch=Unknown property name '{0}' for message in batch. +ODataJsonLightBatchPayloadItemPropertiesCache_DuplicatePropertyForRequestInBatch=Duplicate property name '{0}' for request in batch. +ODataJsonLightBatchPayloadItemPropertiesCache_DuplicateHeaderForRequestInBatch=Duplicate header name '{0}' for request in batch. ODataJsonLightBatchBodyContentReaderStream_UnexpectedNodeType=Unexpected reader.NodeType: {0}. ODataJsonLightBatchBodyContentReaderStream_UnsupportedContentTypeInHeader=Unknown/undefined type, new type that needs to be supported: {0}? diff --git a/src/Microsoft.OData.Core/ODataWriterCore.cs b/src/Microsoft.OData.Core/ODataWriterCore.cs index 9a0031e3a6..c68e810c07 100644 --- a/src/Microsoft.OData.Core/ODataWriterCore.cs +++ b/src/Microsoft.OData.Core/ODataWriterCore.cs @@ -2932,7 +2932,7 @@ private void PromoteNestedResourceInfoScope(ODataItem content) NestedResourceInfoScope newScope = previousScope.Clone(WriterState.NestedResourceInfoWithContent); this.scopeStack.Push(newScope); - if (newScope.ItemType == null && content != null && !SkipWriting && !(content is ODataPrimitiveValue)) + if ((newScope.ItemType == null || newScope.ItemType.IsUntyped()) && content != null && !SkipWriting && !(content is ODataPrimitiveValue)) { ODataPrimitiveValue primitiveValue = content as ODataPrimitiveValue; if (primitiveValue != null) @@ -2941,10 +2941,18 @@ private void PromoteNestedResourceInfoScope(ODataItem content) } else { - ODataResourceBase resource = content as ODataResourceBase; - newScope.ResourceType = resource != null - ? GetResourceType(resource) - : GetResourceSetType(content as ODataResourceSetBase); + if (content is ODataResourceBase resource) + { + newScope.ResourceType = string.IsNullOrEmpty(resource.TypeName) ? + EdmUntypedStructuredType.Instance : + GetResourceType(resource); + } + else if (content is ODataResourceSetBase resourceSet) + { + newScope.ResourceType = string.IsNullOrEmpty(resourceSet.TypeName) ? + EdmUntypedStructuredType.Instance : + GetResourceSetType(resourceSet); + } } } } diff --git a/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs b/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs index fa27bc4b55..3d5b11e4e9 100644 --- a/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs +++ b/src/Microsoft.OData.Core/Parameterized.Microsoft.OData.Core.cs @@ -1655,6 +1655,22 @@ internal static string ODataJsonLightBatchPayloadItemPropertiesCache_UnknownProp return Microsoft.OData.TextRes.GetString(Microsoft.OData.TextRes.ODataJsonLightBatchPayloadItemPropertiesCache_UnknownPropertyForMessageInBatch, p0); } + /// + /// A string like "Duplicate property name '{0}' for request in batch." + /// + internal static string ODataJsonLightBatchPayloadItemPropertiesCache_DuplicatePropertyForRequestInBatch(object p0) + { + return Microsoft.OData.TextRes.GetString(Microsoft.OData.TextRes.ODataJsonLightBatchPayloadItemPropertiesCache_DuplicatePropertyForRequestInBatch, p0); + } + + /// + /// A string like "Duplicate header name '{0}' for request in batch." + /// + internal static string ODataJsonLightBatchPayloadItemPropertiesCache_DuplicateHeaderForRequestInBatch(object p0) + { + return Microsoft.OData.TextRes.GetString(Microsoft.OData.TextRes.ODataJsonLightBatchPayloadItemPropertiesCache_DuplicateHeaderForRequestInBatch, p0); + } + /// /// A string like "Unexpected reader.NodeType: {0}." /// diff --git a/src/Microsoft.OData.Core/TypeNameOracle.cs b/src/Microsoft.OData.Core/TypeNameOracle.cs index 4cc1b4b465..142ae5968c 100644 --- a/src/Microsoft.OData.Core/TypeNameOracle.cs +++ b/src/Microsoft.OData.Core/TypeNameOracle.cs @@ -318,7 +318,10 @@ private static IEdmTypeReference ResolveTypeFromMetadataAndValue(IEdmTypeReferen return typeReferenceFromMetadata; } - Debug.Assert(typeReferenceFromValue.TypeKind() == typeReferenceFromMetadata.TypeKind(), "typeReferenceFromValue.TypeKind() == typeReferenceFromMetadata.TypeKind()"); + if (!typeReferenceFromMetadata.IsUntyped()) + { + Debug.Assert(typeReferenceFromValue.TypeKind() == typeReferenceFromMetadata.TypeKind(), "typeReferenceFromValue.TypeKind() == typeReferenceFromMetadata.TypeKind()"); + } writerValidator.ValidateTypeReference(typeReferenceFromMetadata, typeReferenceFromValue); diff --git a/src/Microsoft.OData.Core/ValidationUtils.cs b/src/Microsoft.OData.Core/ValidationUtils.cs index 0f0d661533..09c45b2e44 100644 --- a/src/Microsoft.OData.Core/ValidationUtils.cs +++ b/src/Microsoft.OData.Core/ValidationUtils.cs @@ -302,6 +302,13 @@ internal static void ValidateIsExpectedPrimitiveType(object value, IEdmPrimitive } Debug.Assert(valuePrimitiveTypeReference.IsEquivalentTo(EdmLibraryExtensions.GetPrimitiveTypeReference(value.GetType())), "The value and valuePrimitiveTypeReference don't match."); + + // If the expected type is 'Edm.Untyped', we don't need to verify the value type. + if (expectedTypeReference.IsUntyped()) + { + return; + } + if (!expectedTypeReference.IsODataPrimitiveTypeKind() && !expectedTypeReference.IsODataTypeDefinitionTypeKind()) { // non-primitive type found for primitive value. diff --git a/src/Microsoft.OData.Core/WriterValidator.cs b/src/Microsoft.OData.Core/WriterValidator.cs index 6246e9b7dc..32cd12579d 100644 --- a/src/Microsoft.OData.Core/WriterValidator.cs +++ b/src/Microsoft.OData.Core/WriterValidator.cs @@ -151,8 +151,12 @@ public virtual void ValidateTypeReference(IEdmTypeReference typeReferenceFromMet { if (settings.ThrowIfTypeConflictsWithMetadata) { + if (typeReferenceFromMetadata.IsUntyped()) + { + ; // do nothing here + } // Make sure the types are the same - if (typeReferenceFromValue.IsODataPrimitiveTypeKind()) + else if (typeReferenceFromValue.IsODataPrimitiveTypeKind()) { // Primitive types must match exactly except for nullability ValidationUtils.ValidateMetadataPrimitiveType(typeReferenceFromMetadata, @@ -172,14 +176,17 @@ public virtual void ValidateTypeReference(IEdmTypeReference typeReferenceFromMet } else if (typeReferenceFromMetadata.IsCollection()) { - // Collection types must match exactly. - if (!typeReferenceFromMetadata.Definition.IsElementTypeEquivalentTo( - typeReferenceFromValue.Definition)) + if (!typeReferenceFromMetadata.AsCollection().ElementType().IsUntyped()) { - throw new ODataException( - Strings.ValidationUtils_IncompatibleType( - typeReferenceFromValue.FullName(), - typeReferenceFromMetadata.FullName())); + // Collection types must match exactly. + if (!typeReferenceFromMetadata.Definition.IsElementTypeEquivalentTo( + typeReferenceFromValue.Definition)) + { + throw new ODataException( + Strings.ValidationUtils_IncompatibleType( + typeReferenceFromValue.FullName(), + typeReferenceFromMetadata.FullName())); + } } } else diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightBatchReaderTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightBatchReaderTests.cs index e17e1b77e4..1bf1432bf4 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightBatchReaderTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightBatchReaderTests.cs @@ -363,6 +363,134 @@ await DoReadAsync( isResponse: false); } + [Fact] + public void ReadBatchRequestWithDuplicateProperties() + { + var payload = "{\"requests\": [{" + + "\"id\": \"1\"," + + "\"atomicityGroup\": \"g1\"," + + "\"atomicityGroup\": \"g2\"," + + "\"method\": \"POST\"," + + "\"url\": \"http://tempuri.org/Customers\"," + + "\"headers\": {\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " + + "\"body\": {\"@odata.type\":\"#NS.Customer\",\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}"; + + var exception = Assert.Throws( + () => SetupJsonLightBatchReaderAndRunTest( + payload, + (jsonLightBatchReader) => + { + while (jsonLightBatchReader.Read()) + { + if (jsonLightBatchReader.State == ODataBatchReaderState.Operation) + { + // The json properties are just iterated through and have no purpose for this test case + jsonLightBatchReader.CreateOperationRequestMessage(); + } + } + Assert.False(true, "The test failed, because the duplicate header has not thrown an ODataException"); + }, + isResponse: false)); + + // Verify that the correct duplicate property has raised the ODataException + Assert.Equal(Strings.ODataJsonLightBatchPayloadItemPropertiesCache_DuplicatePropertyForRequestInBatch("ATOMICITYGROUP"), exception.Message); + } + + [Fact] + public async Task ReadBatchRequestWithDuplicatePropertiesAsync() + { + var payload = "{\"requests\": [{" + + "\"id\": \"1\"," + + "\"atomicityGroup\": \"g1\"," + + "\"atomicityGroup\": \"g2\"," + + "\"method\": \"POST\"," + + "\"url\": \"http://tempuri.org/Customers\"," + + "\"headers\": {\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\"}, " + + "\"body\": {\"@odata.type\":\"#NS.Customer\",\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}"; + + var exception = await Assert.ThrowsAsync( + () => SetupJsonLightBatchReaderAndRunTestAsync( + payload, + async (jsonLightBatchReader) => + { + while (await jsonLightBatchReader.ReadAsync()) + { + if (jsonLightBatchReader.State == ODataBatchReaderState.Operation) + { + // The json properties are just iterated through and have no purpose for this test case + await jsonLightBatchReader.CreateOperationRequestMessageAsync(); + } + } + Assert.False(true, "The test failed, because the duplicate header has not thrown an ODataException"); + }, + isResponse: false)); + + // Verify that the correct duplicate property has raised the ODataException + Assert.Equal(Strings.ODataJsonLightBatchPayloadItemPropertiesCache_DuplicatePropertyForRequestInBatch("ATOMICITYGROUP"), exception.Message); + } + + [Fact] + public void ReadBatchRequestWithDuplicateHeaders() + { + var payload = "{\"requests\": [{" + + "\"id\": \"1\"," + + "\"method\": \"POST\"," + + "\"url\": \"http://tempuri.org/Customers\"," + + "\"headers\": {\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\",\"duplicate-header\":\"value1\",\"duplicate-header\":\"value2\"}, " + + "\"body\": {\"@odata.type\":\"#NS.Customer\",\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}"; + + var exception = Assert.Throws( + () => SetupJsonLightBatchReaderAndRunTest( + payload, + (jsonLightBatchReader) => + { + while (jsonLightBatchReader.Read()) + { + if (jsonLightBatchReader.State == ODataBatchReaderState.Operation) + { + // The json properties are just iterated through and have no purpose for this test case + jsonLightBatchReader.CreateOperationRequestMessage(); + } + } + Assert.False(true, "The test failed, because the duplicate header has thrown no ODataException"); + }, + isResponse: false)); + + // Verify that the correct duplicate header has raised the ODataException + Assert.Equal(Strings.ODataJsonLightBatchPayloadItemPropertiesCache_DuplicateHeaderForRequestInBatch("duplicate-header"), exception.Message); + } + + [Fact] + public async Task ReadBatchRequestWithDuplicateHeadersAsync() + { + var payload = "{\"requests\": [{" + + "\"id\": \"1\"," + + "\"method\": \"POST\"," + + "\"url\": \"http://tempuri.org/Customers\"," + + "\"headers\": {\"odata-version\":\"4.0\",\"content-type\":\"application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8\",\"duplicate-header\":\"value1\",\"duplicate-header\":\"value2\"}, " + + "\"body\": {\"@odata.type\":\"#NS.Customer\",\"Id\":1,\"Name\":\"Customer 1\",\"Type\":\"Retail\"}}]}"; + + var exception = await Assert.ThrowsAsync( + () => SetupJsonLightBatchReaderAndRunTestAsync( + payload, + async (jsonLightBatchReader) => + { + while (await jsonLightBatchReader.ReadAsync()) + { + if (jsonLightBatchReader.State == ODataBatchReaderState.Operation) + { + // The json properties are just iterated through and have no purpose for this test case + await jsonLightBatchReader.CreateOperationRequestMessageAsync(); + } + } + Assert.False(true, "The test failed, because the duplicate header has thrown no ODataException"); + }, + isResponse: false)); + + // Verify that the correct duplicate header has raised the ODataException + Assert.Equal(Strings.ODataJsonLightBatchPayloadItemPropertiesCache_DuplicateHeaderForRequestInBatch("duplicate-header"), exception.Message); + } + [Fact] public async Task ReadBatchResponseAsync() { @@ -977,6 +1105,22 @@ private async Task DoReadAsync( } } + /// + /// Sets up an ODataJsonLightBatchReader, then runs the given test code synchronously + /// + private void SetupJsonLightBatchReaderAndRunTest( + string payload, + Action func, + bool isResponse = true) + { + using (var jsonLightInputContext = CreateJsonLightInputContext(payload, isAsync: false, isResponse: isResponse)) + { + var jsonLightBatchReader = new ODataJsonLightBatchReader(jsonLightInputContext, synchronous: true); + + func(jsonLightBatchReader); + } + } + /// /// Sets up an ODataJsonLightBatchReader, then runs the given test code asynchronously /// diff --git a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightEntryAndFeedSerializerUndecalredTests.cs b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightEntryAndFeedSerializerUndecalredTests.cs index 641d82b10f..cc50cdde31 100644 --- a/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightEntryAndFeedSerializerUndecalredTests.cs +++ b/test/FunctionalTests/Microsoft.OData.Core.Tests/JsonLight/ODataJsonLightEntryAndFeedSerializerUndecalredTests.cs @@ -35,6 +35,8 @@ public ODataJsonLightEntryAndFeedSerializerUndeclaredTests() this.serverEntityType = new EdmEntityType("Server.NS", "ServerEntityType"); this.serverModel.AddElement(this.serverEntityType); this.serverEntityType.AddKeys(this.serverEntityType.AddStructuralProperty("Id", EdmPrimitiveTypeKind.Int32)); + this.serverEntityType.AddStructuralProperty("Data", EdmCoreModel.Instance.GetUntyped()); + this.serverEntityType.AddStructuralProperty("Infos", new EdmCollectionTypeReference(new EdmCollectionType(EdmCoreModel.Instance.GetUntyped()))); this.serverEntityType.AddStructuralProperty("Address", new EdmComplexTypeReference(addressType, true)); // open entity type @@ -48,6 +50,11 @@ public ODataJsonLightEntryAndFeedSerializerUndeclaredTests() this.serverEntitySet = container.AddEntitySet("serverEntitySet", this.serverEntityType); this.serverOpenEntitySet = container.AddEntitySet("serverOpenEntitySet", this.serverOpenEntityType); this.serverModel.AddElement(container); + + EdmEnumType enumType = new EdmEnumType("Server.NS", "EnumType"); + enumType.AddMember(new EdmEnumMember(enumType, "Red", new EdmEnumMemberValue(1))); + enumType.AddMember(new EdmEnumMember(enumType, "Green", new EdmEnumMemberValue(2))); + this.serverModel.AddElement(enumType); } private ODataMessageWriterSettings writerSettings = new ODataMessageWriterSettings @@ -55,8 +62,228 @@ public ODataJsonLightEntryAndFeedSerializerUndeclaredTests() Validations = ~ValidationKinds.ThrowOnUndeclaredPropertyForNonOpenType }; - #region non-open entity's property unknown name + known value type + #region Declared Untyped Properties + [Fact] + public void WriteResourceDeclaredSingleUntypedProperty_WorksForUntypedValue() + { + var property = new ODataProperty { Name = "Data", Value = new ODataUntypedValue { RawValue = "\"#lje324$$\"" } }; + string result = WriteDeclaredUntypedProperty(property); + Assert.Equal("{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverEntitySet/$entity\",\"Data\":\"#lje324$$\"}", result); + } + + [Fact] + public void WriteResourceDeclaredSingleUntypedProperty_WorksForPrimitiveValue() + { + // String is one of default type + var property = new ODataProperty { Name = "Data", Value = new ODataPrimitiveValue("41") }; + string result = WriteDeclaredUntypedProperty(property); + Assert.Equal("{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverEntitySet/$entity\",\"Data@odata.type\":\"#String\",\"Data\":\"41\"}", result); + + // not-default type + property = new ODataProperty { Name = "Data", Value = new ODataPrimitiveValue(41) }; + result = WriteDeclaredUntypedProperty(property); + Assert.Equal("{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverEntitySet/$entity\",\"Data@odata.type\":\"#Int32\",\"Data\":41}", result); + } + + [Fact] + public void WriteResourceDeclaredSingleUntypedProperty_WorksForEnumValue() + { + // Without type name + var property = new ODataProperty { Name = "Data", Value = new ODataEnumValue("AnyMem") }; + string result = WriteDeclaredUntypedProperty(property); + Assert.Equal("{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverEntitySet/$entity\",\"Data\":\"AnyMem\"}", result); + + // with type name + property = new ODataProperty { Name = "Data", Value = new ODataEnumValue("Green", "Server.NS.EnumType") }; + result = WriteDeclaredUntypedProperty(property); + Assert.Equal("{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverEntitySet/$entity\",\"Data\":\"Green\"}", result); + } + + [Fact] + public void WriteResourceDeclaredSingleUntypedProperty_WorksForBinaryStreamValue() + { + // With type name + // ODataBinaryStreamValue + var stream = new MemoryStream(); + var writer = new BinaryWriter(stream); + writer.Write("1234567890"); + writer.Flush(); + stream.Position = 0; + + var property = new ODataProperty + { + Name = "Data", + Value = new ODataBinaryStreamValue(stream) + }; + + string result = WriteDeclaredUntypedProperty(property); + Assert.Equal("{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverEntitySet/$entity\",\"Data\":\"CjEyMzQ1Njc4OTA=\"}", result); + } + + [Fact] + public void WriteResourceDeclaredSingleUntypedProperty_WorksForCollectionValue() + { + // With type name + var property = new ODataProperty + { + Name = "Data", + Value = new ODataCollectionValue + { + TypeName = "Collection(Edm.String)", + Items = new[] + { + "abc", + "xyz" + } + } + }; + + string result = WriteDeclaredUntypedProperty(property); + Assert.Equal("{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverEntitySet/$entity\",\"Data\":[\"abc\",\"xyz\"]}", result); + + // without type name + property = new ODataProperty + { + Name = "Data", + Value = new ODataCollectionValue + { + Items = new object[] + { + "abc", + null, + 42 + } + } + }; + + result = WriteDeclaredUntypedProperty(property); + Assert.Equal("{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverEntitySet/$entity\",\"Data\":[\"abc\",null,42]}", result); + } + [Fact] + public void WriteResourceDeclaredCollectionUntypedProperty_ThrowsForNonCollectionValue() + { + // With type name + var property = new ODataProperty + { + Name = "Infos", + Value = new ODataPrimitiveValue(42) + }; + + Action test = () => WriteDeclaredUntypedProperty(property); + ODataException exception = Assert.Throws(test); + Assert.Equal(Strings.ValidationUtils_NonPrimitiveTypeForPrimitiveValue("Collection(Edm.Untyped)"), exception.Message); + } + + [Fact] + public void WriteResourceDeclaredCollectionUntypedProperty_WorksForCollectionValue() + { + var property = new ODataProperty + { + Name = "Infos", + Value = new ODataCollectionValue + { + Items = new object[] + { + "abc", + null, + 42 + } + } + }; + + string result = WriteDeclaredUntypedProperty(property); + Assert.Equal("{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverEntitySet/$entity\",\"Infos\":[\"abc\",null,42]}", result); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void WriteResourceCollectionUntypedProperty_WorksResourceInNestedCollectionOfCollection2(bool isOpen) + { + string typeName = isOpen ? "Server.NS.ServerOpenEntityType" : "Server.NS.ServerEntityType"; + EdmEntitySet entitySet = isOpen ? this.serverOpenEntitySet : this.serverEntitySet; + EdmEntityType entityType = isOpen ? this.serverOpenEntityType : this.serverEntityType; + string propertyName = isOpen ? "AnyDynamic" : "Infos"; + + string actual = WriteEntryPayload(entitySet, entityType, + writer => + { + writer.WriteStart(new ODataResource { TypeName = typeName }); + writer.WriteStart(new ODataNestedResourceInfo { Name = propertyName, IsCollection = true }); + writer.WriteStart(new ODataResourceSet { TypeName = "Collection(Edm.Untyped)" }); + writer.WriteStart(new ODataResourceSet()); + writer.WriteStart(new ODataResource + { + TypeName = "Edm.Untyped", + Properties = new ODataProperty[] + { + new ODataProperty { Name = "FirstName", Value = "Kerry"} + } + }); + writer.WriteEnd(); // End of "Edm.Untyped" + writer.WriteEnd(); + writer.WriteEnd(); // End of "Infos" / AnyDynamic + writer.WriteEnd(); + writer.WriteEnd(); + }); + + string result = isOpen ? + "{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverOpenEntitySet/$entity\",\"AnyDynamic\":[[{\"FirstName\":\"Kerry\"}]]}" : + "{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverEntitySet/$entity\",\"Infos\":[[{\"FirstName\":\"Kerry\"}]]}"; + + Assert.Equal(result, actual); + } + + private string WriteDeclaredUntypedProperty(ODataProperty untypedProperty) + { + var entry = new ODataResource + { + TypeName = "Server.NS.ServerEntityType", + Properties = new[] + { + untypedProperty + } + }; + + return this.WriteEntryPayload(this.serverEntitySet, this.serverEntityType, + writer => + { + writer.WriteStart(entry); + writer.WriteEnd(); + }); + } + + [Fact] + public void WriteResourceDeclaredUntypedProperty_WorksForNestedResourceInfo() + { + string result = WriteEntryPayload(this.serverEntitySet, this.serverEntityType, + writer => + { + writer.WriteStart(new ODataResource()); + + writer.WriteStart(new ODataNestedResourceInfo { Name = "Data", IsCollection = true }); + writer.WriteStart(new ODataResourceSet()); + writer.WriteStart(new ODataResource { TypeName = "Edm.Untyped" }); + writer.WriteEnd(); + writer.WriteEnd(); + writer.WriteEnd(); + + writer.WriteStart(new ODataNestedResourceInfo { Name = "Infos", IsCollection = true }); + writer.WriteStart(new ODataResourceSet()); + writer.WriteStart(resource: null); + writer.WriteEnd(); + writer.Write(new ODataPrimitiveValue(32)); + writer.WriteEnd(); + writer.WriteEnd(); + writer.WriteEnd(); + }); + + Assert.Equal("{\"@odata.context\":\"http://www.sampletest.com/$metadata#serverEntitySet/$entity\",\"Data\":[{}],\"Infos\":[null,32]}", result); + } + #endregion + + #region non-open entity's property unknown name + known value type [Fact] public void WriteEntryUndeclaredPropertiesTest() { diff --git a/tools/CustomMSBuild/Versioning.props b/tools/CustomMSBuild/Versioning.props index d58f5a42e3..9c533ecfac 100644 --- a/tools/CustomMSBuild/Versioning.props +++ b/tools/CustomMSBuild/Versioning.props @@ -3,7 +3,7 @@ 7 - 15 + 16 0