diff --git a/src/Microsoft.OData.Core/CacheTasks.cs b/src/Microsoft.OData.Core/CacheTasks.cs index 606a3d1843..a724eb37a9 100644 --- a/src/Microsoft.OData.Core/CacheTasks.cs +++ b/src/Microsoft.OData.Core/CacheTasks.cs @@ -27,5 +27,7 @@ internal static class CachedTasks internal static readonly Task PrimitiveValue = Task.FromResult(JsonNodeType.PrimitiveValue); internal static readonly Task Property = Task.FromResult(JsonNodeType.Property); + + internal static readonly Task StringNull = Task.FromResult(null); } } diff --git a/src/Microsoft.OData.Core/Json/ODataAnnotationNames.cs b/src/Microsoft.OData.Core/Json/ODataAnnotationNames.cs index bbcefaffd7..fa62c8c425 100644 --- a/src/Microsoft.OData.Core/Json/ODataAnnotationNames.cs +++ b/src/Microsoft.OData.Core/Json/ODataAnnotationNames.cs @@ -8,7 +8,7 @@ namespace Microsoft.OData.Json { #region Namespaces using System; - using System.Collections.Generic; + using System.Collections.Frozen; using System.Diagnostics; using Microsoft.OData.Core; #endregion Namespaces @@ -21,8 +21,8 @@ internal static class ODataAnnotationNames /// /// Hash set of known odata annotation names that have special meanings to OData Lib. /// - internal static readonly HashSet KnownODataAnnotationNames = - new HashSet( + internal static readonly FrozenSet KnownODataAnnotationNames = + FrozenSet.ToFrozenSet( new[] { ODataContext, @@ -43,7 +43,7 @@ internal static class ODataAnnotationNames ODataDeltaLink, ODataRemoved, ODataDelta, - ODataNull, + ODataNull }, StringComparer.Ordinal); @@ -107,6 +107,66 @@ internal static class ODataAnnotationNames /// internal const string ODataNull = "odata.null"; + /// The OData Context annotation name with no prefix. + internal const string ODataContextNoPrefix = "context"; + + /// The OData Type annotation name with no prefix. + internal const string ODataTypeNoPrefix = "type"; + + /// The OData ID annotation name with no prefix. + internal const string ODataIdNoPrefix = "id"; + + /// The OData etag annotation name with no prefix. + internal const string ODataETagNoPrefix = "etag"; + + /// The OData edit link annotation name with no prefix. + internal const string ODataEditLinkNoPrefix = "editLink"; + + /// The OData read link annotation name with no prefix. + internal const string ODataReadLinkNoPrefix = "readLink"; + + /// The OData media edit link annotation name with no prefix. + internal const string ODataMediaEditLinkNoPrefix = "mediaEditLink"; + + /// The OData media read link annotation name with no prefix. + internal const string ODataMediaReadLinkNoPrefix = "mediaReadLink"; + + /// The OData media content type annotation name with no prefix. + internal const string ODataMediaContentTypeNoPrefix = "mediaContentType"; + + /// The OData media etag annotation name with no prefix. + internal const string ODataMediaETagNoPrefix = "mediaEtag"; + + /// The 'count' annotation name with no prefix. + internal const string ODataCountNoPrefix = "count"; + + /// The 'nextLink' annotation name with no prefix. + internal const string ODataNextLinkNoPrefix = "nextLink"; + + /// The 'navigationLink' annotation name with no prefix. + internal const string ODataNavigationLinkUrlNoPrefix = "navigationLink"; + + /// The 'bind' annotation name with no prefix. + internal const string ODataBindNoPrefix = "bind"; + + /// The 'associationLink' annotation name with no prefix. + internal const string ODataAssociationLinkUrlNoPrefix = "associationLink"; + + /// The 'deltaLink' annotation name with no prefix. + internal const string ODataDeltaLinkNoPrefix = "deltaLink"; + + /// The 'removed' annotation name with no prefix. + internal const string ODataRemovedNoPrefix = "removed"; + + /// The 'delta' annotation name with no prefix. + internal const string ODataDeltaNoPrefix = "delta"; + + /// + /// The OData Null annotation name with no prefix. This is an OData 3.0 protocol element + /// used for compatibility with 6.x library version. + /// + internal const string ODataNullNoPrefix = "null"; + /// /// Returns true if the starts with "odata.", false otherwise. /// diff --git a/src/Microsoft.OData.Core/Json/ODataJsonDeserializer.cs b/src/Microsoft.OData.Core/Json/ODataJsonDeserializer.cs index 6e2abf697b..5ea622bdff 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonDeserializer.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonDeserializer.cs @@ -12,6 +12,8 @@ namespace Microsoft.OData.Json using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; using System.Threading.Tasks; using Microsoft.OData; using Microsoft.OData.Core; @@ -327,11 +329,11 @@ internal void ReadPayloadStart( /// Post-Condition: The reader is positioned on the first property of the payload after having read (or skipped) the context URI property. /// Or the reader is positioned on an end-object node if there are no properties (other than the context URI which is required in responses and optional in requests). /// - internal async Task ReadPayloadStartAsync( + internal Task ReadPayloadStartAsync( ODataPayloadKind payloadKind, PropertyAndAnnotationCollector propertyAndAnnotationCollector, bool isReadingNestedPayload, - bool allowEmptyPayload, + bool allowEmptyPayload, IEdmNavigationSource navigationSource = null) { this.JsonReader.AssertNotBuffering(); @@ -339,31 +341,77 @@ internal async Task ReadPayloadStartAsync( isReadingNestedPayload || this.JsonReader.NodeType == JsonNodeType.None, "Pre-Condition: JSON reader must not have been used yet when not reading a nested payload."); - string contextUriAnnotationValue = await this.ReadPayloadStartImplementationAsync( + Task readPayloadStartTask = this.ReadPayloadStartImplementationAsync( payloadKind, propertyAndAnnotationCollector, isReadingNestedPayload, - allowEmptyPayload).ConfigureAwait(false); + allowEmptyPayload); - // The context URI is only recognized in non-error response top-level payloads. - // If the payload is nested (for example when we read URL literals) we don't recognize the context URI. - // Top-level error payloads don't need and use the context URI. - if (!isReadingNestedPayload && payloadKind != ODataPayloadKind.Error && contextUriAnnotationValue != null) + if (readPayloadStartTask.IsCompletedSuccessfully) { - this.contextUriParseResult = ODataJsonContextUriParser.Parse( - this.Model, - contextUriAnnotationValue, + ProcessContextUriAnnotationValue( + this, payloadKind, - this.MessageReaderSettings.ClientCustomTypeResolver, - this.JsonInputContext.ReadingResponse || payloadKind == ODataPayloadKind.Delta, - this.JsonInputContext.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata, - this.BaseUri, + isReadingNestedPayload, + readPayloadStartTask.Result, navigationSource); + + return Task.CompletedTask; } + return AwaitReadPayloadStartAsync( + this, + payloadKind, + isReadingNestedPayload, + readPayloadStartTask.Result, + navigationSource, + readPayloadStartTask); + + static void ProcessContextUriAnnotationValue( + ODataJsonDeserializer thisParam, + ODataPayloadKind payloadKindParam, + bool isReadingNestedPayloadParam, + string contextUriAnnotationValueParam, + IEdmNavigationSource navigationSourceParam) + { + // The context URI is only recognized in non-error response top-level payloads. + // If the payload is nested (for example when we read URL literals) we don't recognize the context URI. + // Top-level error payloads don't need and use the context URI. + if (!isReadingNestedPayloadParam && payloadKindParam != ODataPayloadKind.Error && contextUriAnnotationValueParam != null) + { + thisParam.contextUriParseResult = ODataJsonContextUriParser.Parse( + thisParam.Model, + contextUriAnnotationValueParam, + payloadKindParam, + thisParam.MessageReaderSettings.ClientCustomTypeResolver, + thisParam.JsonInputContext.ReadingResponse || payloadKindParam == ODataPayloadKind.Delta, + thisParam.JsonInputContext.MessageReaderSettings.ThrowIfTypeConflictsWithMetadata, + thisParam.BaseUri, + navigationSourceParam); + } + #if DEBUG - this.contextUriParseResultReady = true; + thisParam.contextUriParseResultReady = true; #endif + } + + static async Task AwaitReadPayloadStartAsync( + ODataJsonDeserializer thisParam, + ODataPayloadKind payloadKindParam, + bool isReadingNestedPayloadParam, + string contextUriAnnotationValueParam, + IEdmNavigationSource navigationSourceParam, + Task pendingReadPayloadStartTask) + { + string contextUriAnnotationValue = await pendingReadPayloadStartTask.ConfigureAwait(false); + + ProcessContextUriAnnotationValue( + thisParam, + payloadKindParam, + isReadingNestedPayloadParam, + contextUriAnnotationValue, + navigationSourceParam); + } } /// @@ -612,13 +660,35 @@ internal string ReadContextUriAnnotation( /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the string that was read. /// - internal async Task ReadAndValidateAnnotationStringValueAsync(string annotationName) + internal Task ReadAndValidateAnnotationStringValueAsync(string annotationName) { - string valueRead = await this.JsonReader.ReadStringValueAsync(annotationName) - .ConfigureAwait(false); - ODataJsonReaderUtils.ValidateAnnotationValue(valueRead, annotationName); - - return valueRead; + Task readStringValueTask = this.JsonReader.ReadStringValueAsync(); + + if (!readStringValueTask.IsCompletedSuccessfully) + { + return AwaitReadStringValueAsync(annotationName, readStringValueTask); + } + + try + { + string valueRead = readStringValueTask.Result; + ODataJsonReaderUtils.ValidateAnnotationValue(valueRead, annotationName); + + return Task.FromResult(valueRead); + } + catch (ODataException ex) + { + return Task.FromException(ex); + } + + static async Task AwaitReadStringValueAsync(string annotationNameParam, Task pendingReadStringValueTask) + { + string valueRead = await pendingReadStringValueTask.ConfigureAwait(false); + + ODataJsonReaderUtils.ValidateAnnotationValue(valueRead, annotationNameParam); + + return valueRead; + } } /// @@ -642,17 +712,33 @@ internal Task ReadAnnotationStringValueAsync(string annotationName) /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the Uri that was read. /// - internal async Task ReadAnnotationStringValueAsUriAsync(string annotationName) + internal Task ReadAnnotationStringValueAsUriAsync(string annotationName) { - string stringValue = await this.JsonReader.ReadStringValueAsync(annotationName) - .ConfigureAwait(false); + Task readStringValueTask = this.JsonReader.ReadStringValueAsync(); - if (stringValue == null) + if (readStringValueTask.IsCompletedSuccessfully) { - return null; + return Task.FromResult(ProcessUri(this, readStringValueTask.Result)); } - return this.ReadingResponse ? this.ProcessUriFromPayload(stringValue) : new Uri(stringValue, UriKind.RelativeOrAbsolute); + return AwaitReadStringValueAsync(this, readStringValueTask); + + static Uri ProcessUri(ODataJsonDeserializer thisParam, string stringValueParam) + { + if (stringValueParam == null) + { + return null; + } + + return thisParam.ReadingResponse ? thisParam.ProcessUriFromPayload(stringValueParam) : new Uri(stringValueParam, UriKind.RelativeOrAbsolute); + } + + static async Task AwaitReadStringValueAsync(ODataJsonDeserializer thisParam, Task pendingReadStringValueTask) + { + var stringValue = await pendingReadStringValueTask.ConfigureAwait(false); + + return ProcessUri(thisParam, stringValue); + } } /// @@ -663,11 +749,31 @@ internal async Task ReadAnnotationStringValueAsUriAsync(string annotationNa /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the Uri that was read. /// - internal async Task ReadAndValidateAnnotationStringValueAsUriAsync(string annotationName) + internal Task ReadAndValidateAnnotationStringValueAsUriAsync(string annotationName) { - string stringValue = await this.ReadAndValidateAnnotationStringValueAsync(annotationName) - .ConfigureAwait(false); - return this.ReadingResponse ? this.ProcessUriFromPayload(stringValue) : new Uri(stringValue, UriKind.RelativeOrAbsolute); + Task readAndValidateAnnotationStringValueTask = this.ReadAndValidateAnnotationStringValueAsync(annotationName); + + if (readAndValidateAnnotationStringValueTask.IsCompletedSuccessfully) + { + return Task.FromResult(ProcessUri(this, readAndValidateAnnotationStringValueTask.Result)); + } + + return AwaitReadAndValidateAnnotationStringValueAsync(this, readAndValidateAnnotationStringValueTask); + + static async Task AwaitReadAndValidateAnnotationStringValueAsync( + ODataJsonDeserializer thisParam, + Task pendingReadAndValidateAnnotationTask) + { + string stringValueParam = await pendingReadAndValidateAnnotationTask + .ConfigureAwait(false); + + return ProcessUri(thisParam, stringValueParam); + } + + static Uri ProcessUri(ODataJsonDeserializer thisParam, string stringValueParam) + { + return thisParam.ReadingResponse ? thisParam.ProcessUriFromPayload(stringValueParam) : new Uri(stringValueParam, UriKind.RelativeOrAbsolute); + } } /// @@ -679,25 +785,49 @@ internal async Task ReadAndValidateAnnotationStringValueAsUriAsync(string a /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the value that was read. /// - internal async Task ReadAndValidateAnnotationAsLongForIeee754CompatibleAsync(string annotationName) + internal Task ReadAndValidateAnnotationAsLongForIeee754CompatibleAsync(string annotationName) { - object value = await this.JsonReader.ReadPrimitiveValueAsync() - .ConfigureAwait(false); + Task readPrimitiveValueTask = this.JsonReader.ReadPrimitiveValueAsync(); - ODataJsonReaderUtils.ValidateAnnotationValue(value, annotationName); + if (readPrimitiveValueTask.IsCompletedSuccessfully) + { + try + { + return Task.FromResult(ProcessAnnotationValue(this, readPrimitiveValueTask.Result, annotationName)); + } + catch (ODataException ex) + { + return Task.FromException(ex); + } + } - if ((value is string) ^ this.JsonReader.IsIeee754Compatible) + return AwaitReadPrimitiveValueAsync(this, annotationName, readPrimitiveValueTask); + + static long ProcessAnnotationValue(ODataJsonDeserializer thisParam, object valueParam, string annotationNameParam) { - throw new ODataException(Error.Format(SRResources.ODataJsonReaderUtils_ConflictBetweenInputFormatAndParameter, Metadata.EdmConstants.EdmInt64TypeName)); + ODataJsonReaderUtils.ValidateAnnotationValue(valueParam, annotationNameParam); + + if ((valueParam is string) ^ thisParam.JsonReader.IsIeee754Compatible) + { + throw new ODataException( + Error.Format(SRResources.ODataJsonReaderUtils_ConflictBetweenInputFormatAndParameter, Metadata.EdmConstants.EdmInt64TypeName)); + } + + return (long)ODataJsonReaderUtils.ConvertValue( + valueParam, + EdmCoreModel.Instance.GetInt64(false), + thisParam.MessageReaderSettings, + validateNullValue: true, + propertyName: annotationNameParam, + converter: thisParam.JsonInputContext.PayloadValueConverter); } - return (long)ODataJsonReaderUtils.ConvertValue( - value, - EdmCoreModel.Instance.GetInt64(false), - this.MessageReaderSettings, - validateNullValue: true, - propertyName: annotationName, - converter: this.JsonInputContext.PayloadValueConverter); + static async Task AwaitReadPrimitiveValueAsync(ODataJsonDeserializer thisParam, string annotationNameParam, Task pendingReadPrimitiveValueTask) + { + object value = await pendingReadPrimitiveValueTask.ConfigureAwait(false); + + return ProcessAnnotationValue(thisParam, value, annotationNameParam); + } } /// @@ -707,7 +837,7 @@ internal async Task ReadAndValidateAnnotationAsLongForIeee754CompatibleAsy /// Delegate to read property annotation value. /// Delegate to handle the result of parsing property. /// A task that represents the asynchronous read operation. - internal async Task ProcessPropertyAsync( + internal Task ProcessPropertyAsync( PropertyAndAnnotationCollector propertyAndAnnotationCollector, Func> readPropertyAnnotationValueDelegate, Func handlePropertyDelegate) @@ -717,30 +847,201 @@ internal async Task ProcessPropertyAsync( Debug.Assert(handlePropertyDelegate != null, $"{nameof(handlePropertyDelegate)} != null"); this.AssertJsonCondition(JsonNodeType.Property); - (PropertyParsingResult propertyParsingResult, string propertyName) = await this.ParsePropertyAsync( + ValueTask<(PropertyParsingResult propertyParsingResult, string propertyName)> parsePropertyTask = this.ParsePropertyAsync( propertyAndAnnotationCollector, - readPropertyAnnotationValueDelegate).ConfigureAwait(false); + readPropertyAnnotationValueDelegate); - while (propertyParsingResult == PropertyParsingResult.CustomInstanceAnnotation && this.ShouldSkipCustomInstanceAnnotation(propertyName)) + if (!parsePropertyTask.IsCompletedSuccessfully) + { + return AwaitParsePropertyAsync( + this, + propertyAndAnnotationCollector, + readPropertyAnnotationValueDelegate, + handlePropertyDelegate, + parsePropertyTask); + } + + return ContinueAfterParsePropertyAsync( + this, + propertyAndAnnotationCollector, + readPropertyAnnotationValueDelegate, + handlePropertyDelegate, + parsePropertyTask.Result.propertyParsingResult, + parsePropertyTask.Result.propertyName); + + static Task ContinueAfterParsePropertyAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + Func handlePropertyDelegateParam, + PropertyParsingResult propertyParsingResultParam, + string propertyNameParam) + { + while (true) + { + if (propertyParsingResultParam != PropertyParsingResult.CustomInstanceAnnotation + || !thisParam.ShouldSkipCustomInstanceAnnotation(propertyNameParam)) + { + break; + } + + ValueTask<(PropertyParsingResult propertyParsingResult, string propertyName)> processPropertyLocalTask = ProcessPropertyLocalAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam); + + if (!processPropertyLocalTask.IsCompletedSuccessfully) + { + return AwaitParsePropertyLocalAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam, + handlePropertyDelegateParam, + processPropertyLocalTask); + } + } + + return FinalizeProcessPropertyAsync( + thisParam, + propertyAndAnnotationCollectorParam, + handlePropertyDelegateParam, + propertyParsingResultParam, + propertyNameParam); + } + + static ValueTask<(PropertyParsingResult propertyParsingResult, string propertyName)> ProcessPropertyLocalAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam) { // Read over the property name - await this.JsonReader.ReadAsync() - .ConfigureAwait(false); + Task readTask = thisParam.JsonReader.ReadAsync(); + + if (!readTask.IsCompletedSuccessfully) + { + return AwaitReadAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam, + readTask); + } + return ContinueAfterReadAsync(thisParam, propertyAndAnnotationCollectorParam, readPropertyAnnotationValueDelegateParam); + } + + static ValueTask<(PropertyParsingResult PropertyParsingResult, string PropertyName)> ContinueAfterReadAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam) + { // Skip over the instance annotation value - await this.JsonReader.SkipValueAsync() + Task skipValueTask = thisParam.JsonReader.SkipValueAsync(); + + if (!skipValueTask.IsCompletedSuccessfully) + { + return AwaitSkipValueAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam, + skipValueTask); + } + + return thisParam.ParsePropertyAsync( + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam); + } + + static async ValueTask<(PropertyParsingResult PropertyParsingResult, string PropertyName)> AwaitReadAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + Task pendingReadTask) + { + await pendingReadTask.ConfigureAwait(false); + + return await ContinueAfterReadAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam).ConfigureAwait(false); + } + + static async ValueTask<(PropertyParsingResult PropertyParsingResult, string PropertyName)> AwaitSkipValueAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + Task pendingSkipValueTask) + { + await pendingSkipValueTask.ConfigureAwait(false); + + return await thisParam.ParsePropertyAsync( + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam).ConfigureAwait(false); + } + + static async Task AwaitParsePropertyLocalAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + Func handlePropertyDelegateParam, + ValueTask<(PropertyParsingResult PropertyParsingResult, string PropertyName)> pendingProcessPropertyLocalTask) + { + (PropertyParsingResult propertyParsingResult, string propertyName) = await pendingProcessPropertyLocalTask.ConfigureAwait(false); + + while (true) + { + if (propertyParsingResult != PropertyParsingResult.CustomInstanceAnnotation + || !thisParam.ShouldSkipCustomInstanceAnnotation(propertyName)) + { + break; + } + + (propertyParsingResult, propertyName) = await ProcessPropertyLocalAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam).ConfigureAwait(false); + } + + await FinalizeProcessPropertyAsync( + thisParam, + propertyAndAnnotationCollectorParam, + handlePropertyDelegateParam, + propertyParsingResult, + propertyName).ConfigureAwait(false); + } + + static async Task FinalizeProcessPropertyAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func handlePropertyDelegateParam, + PropertyParsingResult propertyParsingResultParam, + string propertyNameParam) + { + await handlePropertyDelegateParam(propertyParsingResultParam, propertyNameParam) .ConfigureAwait(false); - (propertyParsingResult, propertyName) = await this.ParsePropertyAsync( - propertyAndAnnotationCollector, - readPropertyAnnotationValueDelegate).ConfigureAwait(false); + if (propertyParsingResultParam != PropertyParsingResult.EndOfObject + && propertyParsingResultParam != PropertyParsingResult.CustomInstanceAnnotation) + { + propertyAndAnnotationCollectorParam.MarkPropertyAsProcessed(propertyNameParam); + } } - await handlePropertyDelegate(propertyParsingResult, propertyName) - .ConfigureAwait(false); - if (propertyParsingResult != PropertyParsingResult.EndOfObject - && propertyParsingResult != PropertyParsingResult.CustomInstanceAnnotation) + static async Task AwaitParsePropertyAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + Func handlePropertyDelegateParam, + ValueTask<(PropertyParsingResult propertyParsingResult, string propertyName)> pendingParsePropertyTask) { - propertyAndAnnotationCollector.MarkPropertyAsProcessed(propertyName); + (PropertyParsingResult propertyParsingResult, string propertyName) = await pendingParsePropertyTask + .ConfigureAwait(false); + + await ContinueAfterParsePropertyAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam, + handlePropertyDelegateParam, + propertyParsingResult, + propertyName).ConfigureAwait(false); } } @@ -754,7 +1055,7 @@ await handlePropertyDelegate(propertyParsingResult, propertyName) /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the value of the context URI annotation. /// - internal async Task ReadContextUriAnnotationAsync( + internal Task ReadContextUriAnnotationAsync( ODataPayloadKind payloadKind, PropertyAndAnnotationCollector propertyAndAnnotationCollector, bool failOnMissingContextUriAnnotation) @@ -764,37 +1065,85 @@ internal async Task ReadContextUriAnnotationAsync( if (!failOnMissingContextUriAnnotation || payloadKind == ODataPayloadKind.Unsupported) { // Do not fail during payload kind detection - return null; + return Task.FromResult(null); } throw new ODataException(SRResources.ODataJsonDeserializer_ContextLinkNotFoundAsFirstProperty); } // Must make sure the input odata.context has a '@' prefix - string propertyName = await this.JsonReader.GetPropertyNameAsync() - .ConfigureAwait(false); - if (!string.Equals(ODataJsonConstants.PrefixedODataContextPropertyName, propertyName, StringComparison.Ordinal) - && !this.CompareSimplifiedODataAnnotation(ODataJsonConstants.SimplifiedODataContextPropertyName, propertyName)) + Task getPropertyNameTask = this.JsonReader.GetPropertyNameAsync(); + if (!getPropertyNameTask.IsCompletedSuccessfully) { - if (!failOnMissingContextUriAnnotation || payloadKind == ODataPayloadKind.Unsupported) + return AwaitGetPropertyNameAsync( + this, + payloadKind, + propertyAndAnnotationCollector, + failOnMissingContextUriAnnotation, + getPropertyNameTask); + } + + string propertyName = getPropertyNameTask.Result; + + return ContinueAfterGetPropertyNameAsync(this, payloadKind, propertyAndAnnotationCollector, failOnMissingContextUriAnnotation, propertyName); + + static Task ContinueAfterGetPropertyNameAsync( + ODataJsonDeserializer thisParam, + ODataPayloadKind payloadKindParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + bool failOnMissingContextUriAnnotationParam, + string propertyNameParam) + { + if (!string.Equals(ODataJsonConstants.PrefixedODataContextPropertyName, propertyNameParam, StringComparison.Ordinal) + && !thisParam.CompareSimplifiedODataAnnotation(ODataJsonConstants.SimplifiedODataContextPropertyName, propertyNameParam)) { - // Do not fail during payload kind detection - return null; + if (!failOnMissingContextUriAnnotationParam || payloadKindParam == ODataPayloadKind.Unsupported) + { + // Do not fail during payload kind detection + return Task.FromResult(null); + } + + throw new ODataException(SRResources.ODataJsonDeserializer_ContextLinkNotFoundAsFirstProperty); } - throw new ODataException(SRResources.ODataJsonDeserializer_ContextLinkNotFoundAsFirstProperty); + if (propertyAndAnnotationCollectorParam != null) + { + propertyAndAnnotationCollectorParam.MarkPropertyAsProcessed(propertyNameParam); + } + + // Read over the property name + Task readNextTask = thisParam.JsonReader.ReadNextAsync(); + if (!readNextTask.IsCompletedSuccessfully) + { + return AwaitReadNextAsync(thisParam.JsonReader, readNextTask); + } + + return thisParam.JsonReader.ReadStringValueAsync(); } - if (propertyAndAnnotationCollector != null) + static async Task AwaitGetPropertyNameAsync( + ODataJsonDeserializer thisParam, + ODataPayloadKind payloadKindParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + bool failOnMissingContextUriAnnotationParam, + Task pendingGetPropertyNameTask) { - propertyAndAnnotationCollector.MarkPropertyAsProcessed(propertyName); + string propertyName = await pendingGetPropertyNameTask.ConfigureAwait(false); + + return await ContinueAfterGetPropertyNameAsync( + thisParam, + payloadKindParam, + propertyAndAnnotationCollectorParam, + failOnMissingContextUriAnnotationParam, + propertyName); } - // Read over the property name - await this.JsonReader.ReadNextAsync() - .ConfigureAwait(false); - return await this.JsonReader.ReadStringValueAsync() - .ConfigureAwait(false); + static async Task AwaitReadNextAsync(IJsonReader jsonReaderParam, Task pendingReadNextTask) + { + await pendingReadNextTask.ConfigureAwait(false); + + return await jsonReaderParam.ReadStringValueAsync().ConfigureAwait(false); + } } /// @@ -819,13 +1168,99 @@ protected bool CompareSimplifiedODataAnnotation(string simplifiedPropertyName, s /// The complete OData annotation name. protected string CompleteSimplifiedODataAnnotation(string annotationName) { - if (this.JsonInputContext.OptionalODataPrefix && - annotationName.IndexOf('.', StringComparison.Ordinal) == -1) + if (!this.JsonInputContext.OptionalODataPrefix + || annotationName.IndexOf('.', StringComparison.Ordinal) >= 0) { - annotationName = ODataJsonConstants.ODataAnnotationNamespacePrefix + annotationName; + return annotationName; } - return annotationName; + return annotationName switch + { + ODataAnnotationNames.ODataTypeNoPrefix => ODataAnnotationNames.ODataType, + ODataAnnotationNames.ODataContextNoPrefix => ODataAnnotationNames.ODataContext, + ODataAnnotationNames.ODataIdNoPrefix => ODataAnnotationNames.ODataId, + ODataAnnotationNames.ODataCountNoPrefix => ODataAnnotationNames.ODataCount, + ODataAnnotationNames.ODataETagNoPrefix => ODataAnnotationNames.ODataETag, + ODataAnnotationNames.ODataDeltaNoPrefix => ODataAnnotationNames.ODataDelta, + ODataAnnotationNames.ODataRemovedNoPrefix => ODataAnnotationNames.ODataRemoved, + ODataAnnotationNames.ODataNextLinkNoPrefix => ODataAnnotationNames.ODataNextLink, + ODataAnnotationNames.ODataDeltaLinkNoPrefix => ODataAnnotationNames.ODataDeltaLink, + ODataAnnotationNames.ODataReadLinkNoPrefix => ODataAnnotationNames.ODataReadLink, + ODataAnnotationNames.ODataEditLinkNoPrefix => ODataAnnotationNames.ODataEditLink, + ODataAnnotationNames.ODataBindNoPrefix => ODataAnnotationNames.ODataBind, + ODataAnnotationNames.ODataNavigationLinkUrlNoPrefix => ODataAnnotationNames.ODataNavigationLinkUrl, + ODataAnnotationNames.ODataAssociationLinkUrlNoPrefix => ODataAnnotationNames.ODataAssociationLinkUrl, + ODataAnnotationNames.ODataMediaETagNoPrefix => ODataAnnotationNames.ODataMediaETag, + ODataAnnotationNames.ODataMediaReadLinkNoPrefix => ODataAnnotationNames.ODataMediaReadLink, + ODataAnnotationNames.ODataMediaEditLinkNoPrefix => ODataAnnotationNames.ODataMediaEditLink, + ODataAnnotationNames.ODataMediaContentTypeNoPrefix => ODataAnnotationNames.ODataMediaContentType, + ODataAnnotationNames.ODataNullNoPrefix => ODataAnnotationNames.ODataNull, + _ => string.Concat(ODataJsonConstants.ODataAnnotationNamespacePrefix, annotationName) + }; + } + + /// + /// Completes the simplified OData annotation name with "odata.". + /// + /// The annotation name to be completed. + /// The complete OData annotation name. + protected ReadOnlyMemory CompleteSimplifiedODataAnnotation(ReadOnlyMemory annotationName) + { + ReadOnlySpan annotationNameSpan = annotationName.Span; + + if (!this.JsonInputContext.OptionalODataPrefix || annotationNameSpan.IndexOf('.') >= 0) + { + // Return the original memory slice without allocating + return annotationName; + } + + // Match known no-prefix names using guards + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataTypeNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataType.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataContextNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataContext.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataIdNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataId.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataCountNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataCount.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataETagNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataETag.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataDeltaNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataDelta.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataRemovedNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataRemoved.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataNextLinkNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataNextLink.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataDeltaLinkNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataDeltaLink.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataReadLinkNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataReadLink.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataEditLinkNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataEditLink.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataBindNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataBind.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataNavigationLinkUrlNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataNavigationLinkUrl.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataAssociationLinkUrlNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataAssociationLinkUrl.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataMediaETagNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataMediaETag.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataMediaReadLinkNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataMediaReadLink.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataMediaEditLinkNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataMediaEditLink.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataMediaContentTypeNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataMediaContentType.AsMemory(); + if (annotationNameSpan.SequenceEqual(ODataAnnotationNames.ODataNullNoPrefix.AsSpan())) + return ODataAnnotationNames.ODataNull.AsMemory(); + + // Fallback: allocate once for "odata." + name + ReadOnlySpan prefix = ODataJsonConstants.ODataAnnotationNamespacePrefix.AsSpan(); + char[] buffer = new char[prefix.Length + annotationNameSpan.Length]; + prefix.CopyTo(buffer); + annotationNameSpan.CopyTo(buffer.AsSpan(prefix.Length)); + + return buffer.AsMemory(); } /// @@ -853,6 +1288,7 @@ private bool ShouldSkipCustomInstanceAnnotation(string annotationName) /// /// the origin annotation name from reader /// true is the instance annotation, false is not + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsInstanceAnnotation(string annotationName) { if (!string.IsNullOrEmpty(annotationName) && annotationName[0] == ODataJsonConstants.ODataPropertyAnnotationSeparatorChar) @@ -1173,54 +1609,156 @@ private string ReadPayloadStartImplementation( /// 1) true if the annotation name and value is skipped; otherwise false. /// 2) The annotation value that was read. /// - private async ValueTask<(bool IsUnknownODataAnnotationName, object AnnotationValue)> SkipOverUnknownODataAnnotationAsync(string annotationName) + private ValueTask<(bool IsUnknownODataAnnotationName, object AnnotationValue)> SkipOverUnknownODataAnnotationAsync(string annotationName) { Debug.Assert(!string.IsNullOrEmpty(annotationName), "!string.IsNullOrEmpty(annotationName)"); this.AssertJsonCondition(JsonNodeType.Property); - object annotationValue = null; - bool isUnknownODataAnnotationName = false; + if (!ODataAnnotationNames.IsUnknownODataAnnotationName(annotationName)) + { + return ValueTask.FromResult((false, (object)null)); + } - if (ODataAnnotationNames.IsUnknownODataAnnotationName(annotationName)) + ValueTask readODataOrCustomInstanceAnnotationValueTask = this.ReadODataOrCustomInstanceAnnotationValueAsync(); + if (readODataOrCustomInstanceAnnotationValueTask.IsCompletedSuccessfully) { - annotationValue = await ReadODataOrCustomInstanceAnnotationValueAsync(annotationName) - .ConfigureAwait(false); - isUnknownODataAnnotationName = true; + return ValueTask.FromResult((true, readODataOrCustomInstanceAnnotationValueTask.Result)); } - return (isUnknownODataAnnotationName, annotationValue); + return AwaitReadODataOrCustomInstanceAnnotationValueAsync(readODataOrCustomInstanceAnnotationValueTask); + + static async ValueTask<(bool, object)> AwaitReadODataOrCustomInstanceAnnotationValueAsync( + ValueTask pendingReadODataOrCustomInstanceAnnotationValueTask) + { + object annotationValue = await pendingReadODataOrCustomInstanceAnnotationValueTask.ConfigureAwait(false); + + return (true, annotationValue); + } } /// /// Asynchronously reads "odata." or custom instance annotation's value. /// - /// The annotation name. /// /// A task that represents the asynchronous read operation. /// The value of the TResult parameter contains the annotation value. /// - private async Task ReadODataOrCustomInstanceAnnotationValueAsync(string annotationName) + private ValueTask ReadODataOrCustomInstanceAnnotationValueAsync() { - // Read over the name. - await this.JsonReader.ReadAsync() - .ConfigureAwait(false); - object annotationValue; + Task readTask = this.JsonReader.ReadAsync(); + if (!readTask.IsCompletedSuccessfully) + { + return AwaitReadAsync(this, readTask); + } + if (this.JsonReader.NodeType != JsonNodeType.PrimitiveValue) { - annotationValue = await this.JsonReader.ReadAsUntypedOrNullValueAsync() - .ConfigureAwait(false); + Task readAsUntypedOrNullValueTask = this.JsonReader.ReadAsUntypedOrNullValueAsync(); + if (readAsUntypedOrNullValueTask.IsCompletedSuccessfully) + { + return ValueTask.FromResult(readAsUntypedOrNullValueTask.Result); + } + + return AwaitReadAsUntypedOrNullValueAsync(readAsUntypedOrNullValueTask); } - else + + Task getValueTask = this.JsonReader.GetValueAsync(); + if (!getValueTask.IsCompletedSuccessfully) { - annotationValue = await this.JsonReader.GetValueAsync() - .ConfigureAwait(false); - await this.JsonReader.SkipValueAsync() - .ConfigureAwait(false); + return AwaitGetValueThenSkipValueAsync(this.JsonReader, getValueTask); } - return annotationValue; + object annotationValue = getValueTask.Result; + Task skipValueTask = this.JsonReader.SkipValueAsync(); + if (skipValueTask.IsCompletedSuccessfully) + { + return ValueTask.FromResult(annotationValue); + } + + return AwaitSkipValueAsync(annotationValue, skipValueTask); + + static async ValueTask AwaitReadAsync(ODataJsonDeserializer thisParam, Task pendingReadTask) + { + await pendingReadTask.ConfigureAwait(false); + + if (thisParam.JsonReader.NodeType != JsonNodeType.PrimitiveValue) + { + return await thisParam.JsonReader.ReadAsUntypedOrNullValueAsync().ConfigureAwait(false); + } + + object annotationValue = await thisParam.JsonReader.GetValueAsync().ConfigureAwait(false); + await thisParam.JsonReader.SkipValueAsync().ConfigureAwait(false); + + return annotationValue; + } + + static async ValueTask AwaitReadAsUntypedOrNullValueAsync(Task pendingReadAsUntypedOrNullValueTask) + { + return await pendingReadAsUntypedOrNullValueTask.ConfigureAwait(false); + } + + static async ValueTask AwaitGetValueThenSkipValueAsync(IJsonReader jsonReaderParam, Task pendingGetValueTask) + { + object annotationValue = await pendingGetValueTask.ConfigureAwait(false); + await jsonReaderParam.SkipValueAsync().ConfigureAwait(false); + + return annotationValue; + } + + static async ValueTask AwaitSkipValueAsync(object annotationValueParam, Task pendingSkipValueTask) + { + await pendingSkipValueTask.ConfigureAwait(false); + + return annotationValueParam; + } } + /// + /// Local classification used by to cheaply categorize a JSON property name. + /// + /// + /// The classification is dictated upon by the presence and position of the '@' character in the property name from the reader"/> + /// + private enum ParsedPropertyKind + { + /// + /// Property name contains '@' character in the middle (not at index 0) and not as the last character; + /// treated as a property annotation: Property@an.notation. + /// + PropertyAnnotation, + + /// + /// Property name starts with '@' character; treated as an instance annotation: @an.notation. + /// + InstanceAnnotation, + + /// + /// Name has no valid '@' character; treated as a normal property. + /// + NormalProperty + } + + /// + /// Lightweight result for a single parse step used to drive property parsing in . + /// + /// true if parsing should continue; otherwise, false. + /// + /// The result when parsing has terminated (e.g., PropertyWithValue, PropertyWithoutValue, ODataInstanceAnnotation, + /// CustomInstanceAnnotation,MetadataReferenceProperty, NestedDeltaResourceSet, EndOfObject). + /// Ignored when is true. + /// + /// + /// The current property or annotation name associated with or carried forward when continuing. + /// + /// + /// The last property annotation name found for the property being parsed. + /// + private readonly record struct PropertyParsingLocalResult( + bool ShouldContinueParsing, + PropertyParsingResult ParsingResult, + ReadOnlyMemory ParsedPropertyName, + ReadOnlyMemory LastPropertyAnnotationNameFound); + /// /// Asynchronously parses JSON object property starting with the current position of the JSON reader. /// @@ -1244,174 +1782,462 @@ await this.JsonReader.SkipValueAsync() /// 7). The first component contains EndOfObject if end of the object scope was reached and no properties are to be reported, while the second component contains null. /// This can only happen if there's a property annotation which is ignored (for example custom one) at the end of the object. /// - private async Task<(PropertyParsingResult ParsingResult, string PropertyName)> ParsePropertyAsync( + private ValueTask<(PropertyParsingResult ParsingResult, string PropertyName)> ParsePropertyAsync( PropertyAndAnnotationCollector propertyAndAnnotationCollector, Func> readPropertyAnnotationValueDelegate) { Debug.Assert(propertyAndAnnotationCollector != null, $"{nameof(propertyAndAnnotationCollector)} != null"); Debug.Assert(readPropertyAnnotationValueDelegate != null, $"{nameof(readPropertyAnnotationValueDelegate)} != null"); - string lastPropertyAnnotationNameFound = null; - string parsedPropertyName = null; - while (this.JsonReader.NodeType == JsonNodeType.Property) + ReadOnlyMemory lastPropertyAnnotationNameFound = ReadOnlyMemory.Empty; + ReadOnlyMemory parsedPropertyName = ReadOnlyMemory.Empty; + + while (true) { - string nameFromReader = await this.JsonReader.GetPropertyNameAsync() - .ConfigureAwait(false); + if (this.JsonReader.NodeType != JsonNodeType.Property) + { + break; + } - string propertyNameFromReader, annotationNameFromReader; - bool isPropertyAnnotation = TryParsePropertyAnnotation(nameFromReader, out propertyNameFromReader, out annotationNameFromReader); + ValueTask parsePropertyLocalTask = ParsePropertyLocalAsync( + this, + propertyAndAnnotationCollector, + readPropertyAnnotationValueDelegate, + parsedPropertyName, + lastPropertyAnnotationNameFound); - // Reading a nested delta resource set - if (isPropertyAnnotation && string.Equals(this.CompleteSimplifiedODataAnnotation(annotationNameFromReader), ODataAnnotationNames.ODataDelta, StringComparison.Ordinal)) + if (!parsePropertyLocalTask.IsCompletedSuccessfully) { - // Read over the property name. - await this.JsonReader.ReadAsync() - .ConfigureAwait(false); - parsedPropertyName = propertyNameFromReader; - return (PropertyParsingResult.NestedDeltaResourceSet, parsedPropertyName); + return ContinueAfterParsePropertyLocalAsync( + this, + propertyAndAnnotationCollector, + readPropertyAnnotationValueDelegate, + parsedPropertyName, + lastPropertyAnnotationNameFound, + parsePropertyLocalTask); } - bool isInstanceAnnotation = false; - // If this is not a property annotation, determine whether it's an instance annotation, i.e. starts with @ prefix - // If we find that it's an instance annotation and OptionalODataPrefix setting is set to true, complete the simplified - // annotation (if necessary) by prepending it with "odata." - if (!isPropertyAnnotation) + PropertyParsingLocalResult propertyParsingLocalResult = parsePropertyLocalTask.Result; + + if (propertyParsingLocalResult.ShouldContinueParsing) { - isInstanceAnnotation = IsInstanceAnnotation(nameFromReader); - propertyNameFromReader = isInstanceAnnotation ? this.CompleteSimplifiedODataAnnotation(nameFromReader.Substring(1)) : nameFromReader; + lastPropertyAnnotationNameFound = propertyParsingLocalResult.LastPropertyAnnotationNameFound; + parsedPropertyName = propertyParsingLocalResult.ParsedPropertyName; + continue; + } + + Debug.Assert(propertyParsingLocalResult.ParsingResult != default, + $"{nameof(propertyParsingLocalResult.ParsingResult)} != default"); + Debug.Assert(!propertyParsingLocalResult.ParsedPropertyName.IsEmpty, + $"!{nameof(propertyParsingLocalResult.ParsedPropertyName)}.IsEmpty"); + return ValueTask.FromResult((propertyParsingLocalResult.ParsingResult, propertyParsingLocalResult.ParsedPropertyName.GetString())); + } + + return FinalizeParseProperty(this, parsedPropertyName, lastPropertyAnnotationNameFound); + + static async ValueTask<(PropertyParsingResult, string)> ContinueAfterParsePropertyLocalAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + ReadOnlyMemory parsedPropertyNameParam, + ReadOnlyMemory lastPropertyAnnotationNameFoundParam, + ValueTask pendingParsePropertyLocalTask) + { + PropertyParsingLocalResult propertyParsingLocalResult = await pendingParsePropertyLocalTask.ConfigureAwait(false); + + while (true) + { + if (propertyParsingLocalResult.ShouldContinueParsing) + { + lastPropertyAnnotationNameFoundParam = propertyParsingLocalResult.LastPropertyAnnotationNameFound; + parsedPropertyNameParam = propertyParsingLocalResult.ParsedPropertyName; + + if (thisParam.JsonReader.NodeType != JsonNodeType.Property) + { + break; + } + + propertyParsingLocalResult = await ParsePropertyLocalAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam, + parsedPropertyNameParam, + lastPropertyAnnotationNameFoundParam).ConfigureAwait(false); + + continue; + } + + Debug.Assert(propertyParsingLocalResult.ParsingResult != default, + $"{nameof(propertyParsingLocalResult.ParsingResult)} != default"); + Debug.Assert(!propertyParsingLocalResult.ParsedPropertyName.IsEmpty, + $"!{nameof(propertyParsingLocalResult.ParsedPropertyName)}.IsEmpty"); + return (propertyParsingLocalResult.ParsingResult, propertyParsingLocalResult.ParsedPropertyName.GetString()); + } + + return await FinalizeParseProperty(thisParam, parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam); + } + + static ValueTask ParsePropertyLocalAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + ReadOnlyMemory parsedPropertyNameParam, + ReadOnlyMemory lastPropertyAnnotationNameFoundParam) + { + Task getPropertyNameTask = thisParam.JsonReader.GetPropertyNameAsync(); + + if (!getPropertyNameTask.IsCompletedSuccessfully) + { + return AwaitGetPropertyNameAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam, + parsedPropertyNameParam, + lastPropertyAnnotationNameFoundParam, + getPropertyNameTask); + } + + string nameFromReader = getPropertyNameTask.Result; + + return ContinueAfterGetPropertyNameAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam, + parsedPropertyNameParam, + lastPropertyAnnotationNameFoundParam, + nameFromReader); + } + + static async ValueTask AwaitGetPropertyNameAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + ReadOnlyMemory parsedPropertyNameParam, + ReadOnlyMemory lastPropertyAnnotationNameFoundParam, + Task pendingGetPropertyNameTask) + { + string nameFromReader = await pendingGetPropertyNameTask.ConfigureAwait(false); + + return await ContinueAfterGetPropertyNameAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam, + parsedPropertyNameParam, + lastPropertyAnnotationNameFoundParam, + nameFromReader).ConfigureAwait(false); + } + + static ValueTask ContinueAfterGetPropertyNameAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + ReadOnlyMemory parsedPropertyNameParam, + ReadOnlyMemory lastPropertyAnnotationNameFoundParam, + string nameFromReaderParam) + { + // Most payload properties are not annotations; optimize for that case + int annotationSeparatorIndex = nameFromReaderParam.IndexOf( + ODataJsonConstants.ODataPropertyAnnotationSeparatorChar, + StringComparison.Ordinal); + ParsedPropertyKind parsedPropertyKind = ParsePropertyKind(nameFromReaderParam, annotationSeparatorIndex); + (ReadOnlyMemory propertyNameFromReaderMem, ReadOnlyMemory annotationNameFromReaderMem, ReadOnlyMemory prefixedAnnotationNameMem) = ParseNameFromReader( + thisParam, + nameFromReaderParam.AsMemory(), + annotationSeparatorIndex, + parsedPropertyKind); + + // Reading a nested delta resource set + if (parsedPropertyKind == ParsedPropertyKind.PropertyAnnotation && prefixedAnnotationNameMem.Span.SequenceEqual(ODataAnnotationNames.ODataDelta.AsSpan())) + { + return HandleNestedDeltaResourceSetAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam, + parsedPropertyNameParam, + lastPropertyAnnotationNameFoundParam, + propertyNameFromReaderMem); } // If parsedPropertyName is set and is different from the property name the reader is currently on, // we have parsed a property annotation for a different property than the one at the current position. - if (parsedPropertyName != null && !string.Equals(parsedPropertyName, propertyNameFromReader, StringComparison.Ordinal)) + if (!parsedPropertyNameParam.IsEmpty && !parsedPropertyNameParam.Span.SequenceEqual(propertyNameFromReaderMem.Span)) { - if (ODataJsonReaderUtils.IsAnnotationProperty(parsedPropertyName)) + if (ODataJsonReaderUtils.IsAnnotationProperty(parsedPropertyNameParam.Span)) { - throw new ODataException(Error.Format(SRResources.ODataJsonDeserializer_AnnotationTargetingInstanceAnnotationWithoutValue, - lastPropertyAnnotationNameFound, - parsedPropertyName)); + return ValueTask.FromException( + new ODataException(Error.Format( + SRResources.ODataJsonDeserializer_AnnotationTargetingInstanceAnnotationWithoutValue, + lastPropertyAnnotationNameFoundParam.GetString(), + parsedPropertyNameParam.GetString()))); } - return (PropertyParsingResult.PropertyWithoutValue, parsedPropertyName); + return ValueTask.FromResult( + new PropertyParsingLocalResult(false, PropertyParsingResult.PropertyWithoutValue, parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam)); } - object annotationValue = null; - if (isPropertyAnnotation) + if (parsedPropertyKind == ParsedPropertyKind.PropertyAnnotation) { - annotationNameFromReader = this.CompleteSimplifiedODataAnnotation(annotationNameFromReader); + return HandlePropertyAnnotationAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam, + parsedPropertyNameParam, + lastPropertyAnnotationNameFoundParam, + propertyNameFromReaderMem, + annotationNameFromReaderMem, + prefixedAnnotationNameMem); + } + else if (parsedPropertyKind == ParsedPropertyKind.InstanceAnnotation) + { + return HandleInstanceAnnotationAsync( + thisParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam, + parsedPropertyNameParam, + lastPropertyAnnotationNameFoundParam, + propertyNameFromReaderMem, + annotationNameFromReaderMem); + } + else + { + // We are encountering the property name for the first time. + // Don't read over property name, as that would cause the buffering reader to read ahead in the StartObject + // state, which would break our ability to stream inline json. Instead, callers of ParsePropertyAsync will have to + // call this.JsonReader.ReadAsync() as appropriate to read past the property name. + parsedPropertyNameParam = propertyNameFromReaderMem; - // If this is a unknown odata annotation targeting a property, we skip over it. - // See remark on the method SkipOverUnknownODataAnnotationAsync() for detailed explanation. - // Note that we don't skip over unknown odata annotations targeting another annotation. - // We don't allow annotations (except odata.type) targeting other annotations, - // so ProcessPropertyAnnotationAsync() will test and fail for that case. - if (!ODataJsonReaderUtils.IsAnnotationProperty(propertyNameFromReader)) + if (ODataJsonUtils.IsMetadataReferenceProperty(propertyNameFromReaderMem.Span)) { - (bool isUnknownODataAnnotationName, object tempAnnotationValue) = await this.SkipOverUnknownODataAnnotationAsync( - annotationNameFromReader).ConfigureAwait(false); - if (isUnknownODataAnnotationName) + return ValueTask.FromResult( + new PropertyParsingLocalResult(false, PropertyParsingResult.MetadataReferenceProperty, parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam)); + } + + if (!ODataJsonReaderUtils.IsAnnotationProperty(propertyNameFromReaderMem.Span)) + { + // Normal property + return ValueTask.FromResult( + new PropertyParsingLocalResult(false, PropertyParsingResult.PropertyWithValue, parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam)); + } + } + + return ValueTask.FromResult( + HandleCustomInstanceAnnotation(parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam, annotationNameFromReaderMem)); + } + + static ParsedPropertyKind ParsePropertyKind(ReadOnlySpan nameFromReaderParam, int annotationSeparatorIndexParam) + { + // Most payload properties are not annotations; optimize for that case + // TODO: Verify behaviour for odata.type or custom.annotation (i.e., missing @) + if (annotationSeparatorIndexParam == 0) + { + return ParsedPropertyKind.InstanceAnnotation; + } + + if (annotationSeparatorIndexParam > 0 && annotationSeparatorIndexParam != nameFromReaderParam.Length - 1) + { + return ParsedPropertyKind.PropertyAnnotation; + } + + // If there's no '@' anywhere in the name, it's either: + // - a normal property (most common) -> PropertyWithValue + // - a metadata reference property -> MetadataReferenceProperty + return ParsedPropertyKind.NormalProperty; + } + + static (ReadOnlyMemory propertyName, ReadOnlyMemory annotationName, ReadOnlyMemory prefixedAnnotationName) ParseNameFromReader( + ODataJsonDeserializer thisParam, + ReadOnlyMemory nameFromReaderParam, + int annotationSeparatorIndexParam, + ParsedPropertyKind parsedPropertyKindParam) + { + switch (parsedPropertyKindParam) + { + case ParsedPropertyKind.PropertyAnnotation: { - annotationValue = tempAnnotationValue; - propertyAndAnnotationCollector.AddODataPropertyAnnotation(propertyNameFromReader, annotationNameFromReader, annotationValue); - continue; + ReadOnlyMemory propertyName = nameFromReaderParam[..annotationSeparatorIndexParam]; + ReadOnlyMemory annotationName = nameFromReaderParam[(annotationSeparatorIndexParam + 1)..]; + ReadOnlyMemory prefixedAnnotationName = thisParam.CompleteSimplifiedODataAnnotation(annotationName); + + return (propertyName, annotationName, prefixedAnnotationName); } - } - // We have another property annotation for the same property we parsed. - parsedPropertyName = propertyNameFromReader; - lastPropertyAnnotationNameFound = annotationNameFromReader; + case ParsedPropertyKind.InstanceAnnotation: + { + // If this is not a property annotation, determine whether it's an instance annotation, i.e. starts with @ prefix + // If we find that it's an instance annotation and OptionalODataPrefix setting is set to true, complete the simplified + // annotation (if necessary) by prepending it with "odata." + ReadOnlyMemory propertyNameMem = thisParam.CompleteSimplifiedODataAnnotation(nameFromReaderParam[1..]); - await this.ProcessPropertyAnnotationAsync( - propertyNameFromReader, - annotationNameFromReader, - propertyAndAnnotationCollector, - readPropertyAnnotationValueDelegate).ConfigureAwait(false); - continue; + return (propertyNameMem, ReadOnlyMemory.Empty, ReadOnlyMemory.Empty); + } + + default: + return (nameFromReaderParam, ReadOnlyMemory.Empty, ReadOnlyMemory.Empty); } + } - // If this is a unknown odata annotation, skip over it. See remark on the method SkipOverUnknownODataAnnotationAsync() for detailed explanation. - if (isInstanceAnnotation) + static async ValueTask HandleNestedDeltaResourceSetAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + ReadOnlyMemory parsedPropertyNameParam, + ReadOnlyMemory lastPropertyAnnotationNameFoundParam, + ReadOnlyMemory propertyNameFromReaderParam) + { + // Read over the property name. + await thisParam.JsonReader.ReadAsync() + .ConfigureAwait(false); + parsedPropertyNameParam = propertyNameFromReaderParam; + + return new PropertyParsingLocalResult(false, PropertyParsingResult.NestedDeltaResourceSet, parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam); + } + + static async ValueTask HandlePropertyAnnotationAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + ReadOnlyMemory parsedPropertyNameParam, + ReadOnlyMemory lastPropertyAnnotationNameFoundParam, + ReadOnlyMemory propertyNameFromReaderParam, + ReadOnlyMemory annotationNameFromReaderParam, + ReadOnlyMemory prefixedAnnotationNameParam) + { + object annotationValue = null; + // If this is a unknown odata annotation targeting a property, we skip over it. + // See remark on the method SkipOverUnknownODataAnnotationAsync() for detailed explanation. + // Note that we don't skip over unknown odata annotations targeting another annotation. + // We don't allow annotations (except odata.type) targeting other annotations, + // so ProcessPropertyAnnotationAsync() will test and fail for that case. + if (!ODataJsonReaderUtils.IsAnnotationProperty(propertyNameFromReaderParam.Span)) { - (bool isUnknownODataAnnotationName, object tempAnnotationValue) = await this.SkipOverUnknownODataAnnotationAsync( - propertyNameFromReader).ConfigureAwait(false); + string prefixedAnnotationName = prefixedAnnotationNameParam.GetString(); + (bool isUnknownODataAnnotationName, object tempAnnotationValue) = await thisParam.SkipOverUnknownODataAnnotationAsync( + prefixedAnnotationName).ConfigureAwait(false); if (isUnknownODataAnnotationName) { + string propertyNameFromReader = propertyNameFromReaderParam.GetString(); annotationValue = tempAnnotationValue; - // collect 'odata.' annotation: - // here we know the original property name contains no '@', but '.' dot - Debug.Assert(annotationNameFromReader == null, $"{nameof(annotationNameFromReader)} == null"); - propertyAndAnnotationCollector.AddODataScopeAnnotation(propertyNameFromReader, annotationValue); - continue; + propertyAndAnnotationCollectorParam.AddODataPropertyAnnotation(propertyNameFromReader, prefixedAnnotationName, annotationValue); + return new PropertyParsingLocalResult(true, default, parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam); } } - // We are encountering the property name for the first time. - // Don't read over property name, as that would cause the buffering reader to read ahead in the StartObject - // state, which would break our ability to stream inline json. Instead, callers of ParsePropertyAsync will have to - // call this.JsonReader.ReadAsync() as appropriate to read past the property name. - parsedPropertyName = propertyNameFromReader; + // We have another property annotation for the same property we parsed. + parsedPropertyNameParam = propertyNameFromReaderParam; + lastPropertyAnnotationNameFoundParam = prefixedAnnotationNameParam; - if (!isInstanceAnnotation && ODataJsonUtils.IsMetadataReferenceProperty(propertyNameFromReader)) - { - return (PropertyParsingResult.MetadataReferenceProperty, parsedPropertyName); - } + await thisParam.ProcessPropertyAnnotationAsync( + propertyNameFromReaderParam.Span, + prefixedAnnotationNameParam.Span, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam).ConfigureAwait(false); + return new PropertyParsingLocalResult(true, default, parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam); + } - if (!isInstanceAnnotation && !ODataJsonReaderUtils.IsAnnotationProperty(propertyNameFromReader)) + static async ValueTask HandleInstanceAnnotationAsync( + ODataJsonDeserializer thisParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + ReadOnlyMemory parsedPropertyNameParam, + ReadOnlyMemory lastPropertyAnnotationNameFoundParam, + ReadOnlyMemory propertyNameFromReaderParam, + ReadOnlyMemory annotationNameFromReaderParam) + { + object annotationValue = null; + string propertyNameFromReader = propertyNameFromReaderParam.GetString(); + (bool isUnknownODataAnnotationName, object tempAnnotationValue) = await thisParam.SkipOverUnknownODataAnnotationAsync( + propertyNameFromReader).ConfigureAwait(false); + if (isUnknownODataAnnotationName) { - // Normal property - return (PropertyParsingResult.PropertyWithValue, parsedPropertyName); + annotationValue = tempAnnotationValue; + // collect 'odata.' annotation: + // here we know the original property name contains no '@', but '.' dot + Debug.Assert(annotationNameFromReaderParam.IsEmpty, $"{nameof(annotationNameFromReaderParam)}.IsEmpty"); + propertyAndAnnotationCollectorParam.AddODataScopeAnnotation(propertyNameFromReader, annotationValue); + return new PropertyParsingLocalResult(true, default, parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam); } + // We are encountering the property name for the first time. + // Don't read over property name, as that would cause the buffering reader to read ahead in the StartObject + // state, which would break our ability to stream inline json. Instead, callers of ParsePropertyAsync will have to + // call this.JsonReader.ReadAsync() as appropriate to read past the property name. + parsedPropertyNameParam = propertyNameFromReaderParam; + // collect 'xxx.yyyy' annotation: // here we know the original property name contains no '@', but '.' dot - Debug.Assert(annotationNameFromReader == null, $"{nameof(annotationNameFromReader)} == null"); + Debug.Assert(annotationNameFromReaderParam.IsEmpty, $"{nameof(annotationNameFromReaderParam)}.IsEmpty"); // Handle 'odata.XXXXX' annotations - if (isInstanceAnnotation && ODataJsonReaderUtils.IsODataAnnotationName(propertyNameFromReader)) + if (ODataJsonReaderUtils.IsODataAnnotationName(propertyNameFromReaderParam.Span)) { - return (PropertyParsingResult.ODataInstanceAnnotation, parsedPropertyName); + return new PropertyParsingLocalResult(false, PropertyParsingResult.ODataInstanceAnnotation, parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam); } + return HandleCustomInstanceAnnotation(parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam, annotationNameFromReaderParam); + } + + static PropertyParsingLocalResult HandleCustomInstanceAnnotation( + ReadOnlyMemory parsedPropertyNameParam, + ReadOnlyMemory lastPropertyAnnotationNameFoundParam, + ReadOnlyMemory annotationNameFromReaderParam) + { + // collect 'xxx.yyyy' annotation: + // here we know the original property name contains no '@', but '.' dot + Debug.Assert(annotationNameFromReaderParam.IsEmpty, $"{nameof(annotationNameFromReaderParam)}.IsEmpty"); + + // We'd only end up here for "if (isInstanceAnnotation)" and "else" cases // Handle custom annotations - return (PropertyParsingResult.CustomInstanceAnnotation, parsedPropertyName); + return new PropertyParsingLocalResult(false, PropertyParsingResult.CustomInstanceAnnotation, parsedPropertyNameParam, lastPropertyAnnotationNameFoundParam); } - this.AssertJsonCondition(JsonNodeType.EndObject); - if (parsedPropertyName != null) + static ValueTask<(PropertyParsingResult, string)> FinalizeParseProperty( + ODataJsonDeserializer thisParam, + ReadOnlyMemory parsedPropertyNameParam, + ReadOnlyMemory lastPropertyAnnotationNameFoundParam) { - if (ODataJsonReaderUtils.IsAnnotationProperty(parsedPropertyName)) + thisParam.AssertJsonCondition(JsonNodeType.EndObject); + + string parsedPropertyName = parsedPropertyNameParam.IsEmpty ? null : parsedPropertyNameParam.GetString(); + if (!parsedPropertyNameParam.IsEmpty) { - throw new ODataException( - Error.Format(SRResources.ODataJsonDeserializer_AnnotationTargetingInstanceAnnotationWithoutValue, - lastPropertyAnnotationNameFound, - parsedPropertyName)); + if (ODataJsonReaderUtils.IsAnnotationProperty(parsedPropertyNameParam.Span)) + { + string lastPropertyAnnotationNameFound = lastPropertyAnnotationNameFoundParam.GetString(); + return ValueTask.FromException<(PropertyParsingResult, string)>(new ODataException( + Error.Format(SRResources.ODataJsonDeserializer_AnnotationTargetingInstanceAnnotationWithoutValue, + lastPropertyAnnotationNameFound, + parsedPropertyName))); + } + + return ValueTask.FromResult((PropertyParsingResult.PropertyWithoutValue, parsedPropertyName)); } - return (PropertyParsingResult.PropertyWithoutValue, parsedPropertyName); + return ValueTask.FromResult((PropertyParsingResult.EndOfObject, parsedPropertyName)); } - - return (PropertyParsingResult.EndOfObject, parsedPropertyName); } /// /// Asynchronously process the current property annotation. /// - /// The name being annotated. Can be a property or an instance annotation. - /// The annotation targeting the . + /// The name being annotated. Can be a property or an instance annotation. + /// The annotation targeting the . /// The duplicate property names checker. /// Delegate to read the property annotation value. /// A task that represents the asynchronous read operation. - private Task ProcessPropertyAnnotationAsync( - string annotatedPropertyName, - string annotationName, + private ValueTask ProcessPropertyAnnotationAsync( + ReadOnlySpan annotatedPropertyNameSpan, + ReadOnlySpan annotationNameSpan, PropertyAndAnnotationCollector propertyAndAnnotationCollector, Func> readPropertyAnnotationValueDelegate) { + string annotatedPropertyName = annotatedPropertyNameSpan.ToString(); + string annotationName = annotationNameSpan.ToString(); + // We don't currently support annotation targeting an instance annotation except for the @odata.type property annotation. if (ODataJsonReaderUtils.IsAnnotationProperty(annotatedPropertyName) && !string.Equals(annotationName, ODataAnnotationNames.ODataType, StringComparison.Ordinal)) { - return Task.FromException( + return ValueTask.FromException( new ODataException(Error.Format(SRResources.ODataJsonDeserializer_OnlyODataTypeAnnotationCanTargetInstanceAnnotation, annotationName, annotatedPropertyName, @@ -1455,38 +2281,117 @@ internal Task ReadPayloadEndAsync(bool isReadingNestedPayload) /// The PropertyAndAnnotationCollector. /// Delegate to read the property annotation value. /// A task that represents the asynchronous read operation. - private async Task ReadODataOrCustomInstanceAnnotationValueAsync( + private ValueTask ReadODataOrCustomInstanceAnnotationValueAsync( string annotatedPropertyName, string annotationName, PropertyAndAnnotationCollector propertyAndAnnotationCollector, Func> readPropertyAnnotationValueDelegate) { // Read over the property name. - await this.JsonReader.ReadAsync() - .ConfigureAwait(false); - if (ODataJsonReaderUtils.IsODataAnnotationName(annotationName)) + Task readTask = this.JsonReader.ReadAsync(); + + if (!readTask.IsCompletedSuccessfully) { - // OData annotations are read - object propertyAnnotationValue = await readPropertyAnnotationValueDelegate(annotationName) - .ConfigureAwait(false); - propertyAndAnnotationCollector.AddODataPropertyAnnotation(annotatedPropertyName, annotationName, propertyAnnotationValue); + return AwaitReadAsync(this, annotatedPropertyName, annotationName, propertyAndAnnotationCollector, readPropertyAnnotationValueDelegate, readTask); } - else + + return ContinueAfterReadAsync(this, annotatedPropertyName, annotationName, propertyAndAnnotationCollector, readPropertyAnnotationValueDelegate); + + static ValueTask ContinueAfterReadAsync( + ODataJsonDeserializer thisParam, + string annotatedPropertyNameParam, + string annotationNameParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam) { - if (this.ShouldSkipCustomInstanceAnnotation(annotationName) - || (this is ODataJsonErrorDeserializer && this.MessageReaderSettings.ShouldIncludeAnnotation == null)) + if (ODataJsonReaderUtils.IsODataAnnotationName(annotationNameParam)) { - propertyAndAnnotationCollector.CheckIfPropertyOpenForAnnotations(annotatedPropertyName, annotationName); - await this.JsonReader.SkipValueAsync() - .ConfigureAwait(false); + // OData annotations are read + Task readPropertyAnnotationValueTask = readPropertyAnnotationValueDelegateParam(annotationNameParam); + if (!readPropertyAnnotationValueTask.IsCompletedSuccessfully) + { + return AwaitReadPropertyAnnotationValueDelegate( + annotatedPropertyNameParam, + annotationNameParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueTask); + } + + propertyAndAnnotationCollectorParam.AddODataPropertyAnnotation(annotatedPropertyNameParam, annotationNameParam, readPropertyAnnotationValueTask.Result); + + return ValueTask.CompletedTask; } - else + + if (thisParam.ShouldSkipCustomInstanceAnnotation(annotationNameParam) + || (thisParam is ODataJsonErrorDeserializer && thisParam.MessageReaderSettings.ShouldIncludeAnnotation == null)) { - Debug.Assert(this.ReadPropertyCustomAnnotationValueAsync != null, $"{nameof(ReadPropertyCustomAnnotationValueAsync)} != null"); - object propertyCustomAnnotationValue = await this.ReadPropertyCustomAnnotationValueAsync(propertyAndAnnotationCollector, annotationName) - .ConfigureAwait(false); - propertyAndAnnotationCollector.AddCustomPropertyAnnotation(annotatedPropertyName, annotationName, propertyCustomAnnotationValue); + propertyAndAnnotationCollectorParam.CheckIfPropertyOpenForAnnotations(annotatedPropertyNameParam, annotationNameParam); + Task skipValueTask = thisParam.JsonReader.SkipValueAsync(); + if (!skipValueTask.IsCompletedSuccessfully) + { + return AwaitSkipValueAsync(skipValueTask); + } + + return ValueTask.CompletedTask; + } + + Debug.Assert(thisParam.ReadPropertyCustomAnnotationValueAsync != null, $"{nameof(ReadPropertyCustomAnnotationValueAsync)} != null"); + Task readPropertyCustomAnnotationValueTask = thisParam.ReadPropertyCustomAnnotationValueAsync( + propertyAndAnnotationCollectorParam, + annotationNameParam); + if (!readPropertyCustomAnnotationValueTask.IsCompletedSuccessfully) + { + return AwaitReadPropertyCustomAnnotationValueAsync(annotatedPropertyNameParam, annotationNameParam, propertyAndAnnotationCollectorParam, readPropertyCustomAnnotationValueTask); } + + propertyAndAnnotationCollectorParam.AddCustomPropertyAnnotation(annotatedPropertyNameParam, annotationNameParam, readPropertyCustomAnnotationValueTask.Result); + + return ValueTask.CompletedTask; + } + + static async ValueTask AwaitReadAsync( + ODataJsonDeserializer thisParam, + string annotatedPropertyNameParam, + string annotationNameParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Func> readPropertyAnnotationValueDelegateParam, + Task pendingReadTask) + { + await pendingReadTask.ConfigureAwait(false); + + await ContinueAfterReadAsync( + thisParam, + annotatedPropertyNameParam, + annotationNameParam, + propertyAndAnnotationCollectorParam, + readPropertyAnnotationValueDelegateParam); + } + + static async ValueTask AwaitReadPropertyAnnotationValueDelegate( + string annotatedPropertyNameParam, + string annotationNameParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Task pendingReadPropertyAnnotationValueTask) + { + object propertyAnnotationValue = await pendingReadPropertyAnnotationValueTask.ConfigureAwait(false); + + propertyAndAnnotationCollectorParam.AddODataPropertyAnnotation(annotatedPropertyNameParam, annotationNameParam, propertyAnnotationValue); + } + + static async ValueTask AwaitReadPropertyCustomAnnotationValueAsync( + string annotatedPropertyNameParam, + string annotationNameParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + Task pendingReadPropertyCustomAnnotationValueTask) + { + object propertyCustomAnnotationValue = await pendingReadPropertyCustomAnnotationValueTask.ConfigureAwait(false); + + propertyAndAnnotationCollectorParam.AddCustomPropertyAnnotation(annotatedPropertyNameParam, annotationNameParam, propertyCustomAnnotationValue); + } + + static async ValueTask AwaitSkipValueAsync(Task pendingSkipValueTask) + { + await pendingSkipValueTask.ConfigureAwait(false); } } @@ -1507,7 +2412,7 @@ await this.JsonReader.SkipValueAsync() /// Post-Condition: The reader is positioned on the first property of the payload after having read (or skipped) the context URI property. /// Or the reader is positioned on an end-object node if there are no properties (other than the context URI which is required in responses and optional in requests). /// - private async Task ReadPayloadStartImplementationAsync( + private Task ReadPayloadStartImplementationAsync( ODataPayloadKind payloadKind, PropertyAndAnnotationCollector propertyAndAnnotationCollector, bool isReadingNestedPayload, @@ -1518,40 +2423,97 @@ private async Task ReadPayloadStartImplementationAsync( isReadingNestedPayload || this.JsonReader.NodeType == JsonNodeType.None, "Pre-Condition: JSON reader must not have been used yet when not reading a nested payload."); - if (!isReadingNestedPayload) + if (isReadingNestedPayload) { - // Position the reader on the first node inside the outermost object. - await this.JsonReader.ReadAsync() - .ConfigureAwait(false); + return CachedTasks.StringNull; + } - if (allowEmptyPayload && this.JsonReader.NodeType == JsonNodeType.EndOfInput) + // Position the reader on the first node inside the outermost object. + Task readTask = this.JsonReader.ReadAsync(); + if (!readTask.IsCompletedSuccessfully) + { + return AwaitReadAsync(this, payloadKind, propertyAndAnnotationCollector, allowEmptyPayload, readTask); + } + + return ContinueAfterReadAsync(this, payloadKind, propertyAndAnnotationCollector, allowEmptyPayload); + + static Task ContinueAfterReadAsync( + ODataJsonDeserializer thisParam, + ODataPayloadKind payloadKindParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + bool allowEmptyPayloadParam) + { + if (allowEmptyPayloadParam && thisParam.JsonReader.NodeType == JsonNodeType.EndOfInput) { - return null; + return CachedTasks.StringNull; } // Read the StartObject node and position the reader on the first property // (or the end object node). - await this.JsonReader.ReadStartObjectAsync() - .ConfigureAwait(false); - - if (payloadKind != ODataPayloadKind.Error) + ValueTask readStartObjectTask = thisParam.JsonReader.ReadStartObjectAsync(); + if (!readStartObjectTask.IsCompletedSuccessfully) { - // Skip over the context URI annotation in request payloads or when we've already read it - // during payload kind detection. - bool failOnMissingContextUriAnnotation = this.jsonInputContext.ReadingResponse; + return AwaitReadStartObjectAsync( + thisParam, + payloadKindParam, + propertyAndAnnotationCollectorParam, + readStartObjectTask); + } - // In responses we expect the context URI to be the first thing in the payload - // (except for error payloads). In requests we ignore the context URI. - return await this.ReadContextUriAnnotationAsync( - payloadKind, - propertyAndAnnotationCollector, - failOnMissingContextUriAnnotation).ConfigureAwait(false); + return ContinueAfterReadStartObjectAsync( + thisParam, + payloadKindParam, + propertyAndAnnotationCollectorParam); + } + + static Task ContinueAfterReadStartObjectAsync(ODataJsonDeserializer thisParam, + ODataPayloadKind payloadKindParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam) + { + if (payloadKindParam == ODataPayloadKind.Error) + { + thisParam.AssertJsonCondition(JsonNodeType.Property, JsonNodeType.EndObject); + return CachedTasks.StringNull; } - this.AssertJsonCondition(JsonNodeType.Property, JsonNodeType.EndObject); + // Skip over the context URI annotation in request payloads or when we've already read it + // during payload kind detection. + bool failOnMissingContextUriAnnotation = thisParam.jsonInputContext.ReadingResponse; + + // In responses we expect the context URI to be the first thing in the payload + // (except for error payloads). In requests we ignore the context URI. + return thisParam.ReadContextUriAnnotationAsync( + payloadKindParam, + propertyAndAnnotationCollectorParam, + failOnMissingContextUriAnnotation); + } - return null; + static async Task AwaitReadAsync( + ODataJsonDeserializer thisParam, + ODataPayloadKind payloadKindParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + bool allowEmptyPayloadParam, + Task pendingReadTask) + { + await pendingReadTask.ConfigureAwait(false); + + return await ContinueAfterReadAsync(thisParam, payloadKindParam, propertyAndAnnotationCollectorParam, allowEmptyPayloadParam); + } + + static async Task AwaitReadStartObjectAsync( + ODataJsonDeserializer thisParam, + ODataPayloadKind payloadKindParam, + PropertyAndAnnotationCollector propertyAndAnnotationCollectorParam, + ValueTask pendingStartObjectTask) + { + await pendingStartObjectTask.ConfigureAwait(false); + + return await ContinueAfterReadStartObjectAsync( + thisParam, + payloadKindParam, + propertyAndAnnotationCollectorParam); + } } } } diff --git a/src/Microsoft.OData.Core/Json/ODataJsonReaderUtils.cs b/src/Microsoft.OData.Core/Json/ODataJsonReaderUtils.cs index 8cf216c719..3c002b63c6 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonReaderUtils.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonReaderUtils.cs @@ -12,6 +12,7 @@ namespace Microsoft.OData.Json using Microsoft.OData.Metadata; using Microsoft.OData.Edm; using Microsoft.OData.Core; + using System.Runtime.CompilerServices; #endregion Namespaces /// @@ -142,11 +143,27 @@ internal static void EnsureInstance(ref T instance) /// /// The property name to test. /// true if the property name is an OData annotation property name, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsODataAnnotationName(string propertyName) { Debug.Assert(!string.IsNullOrEmpty(propertyName), "!string.IsNullOrEmpty(propertyName)"); - return propertyName.StartsWith(ODataJsonConstants.ODataAnnotationNamespacePrefix, StringComparison.Ordinal); + return propertyName.Length >= ODataJsonConstants.ODataAnnotationNamespacePrefix.Length && + propertyName.StartsWith(ODataJsonConstants.ODataAnnotationNamespacePrefix, StringComparison.Ordinal); + } + + /// + /// Determines if the specified is an OData annotation property name. + /// + /// The property name to test. + /// true if the property name is an OData annotation property name, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsODataAnnotationName(ReadOnlySpan propertyNameSpan) + { + Debug.Assert(!propertyNameSpan.IsEmpty, "!propertyNameSpan.IsEmpty"); + + return propertyNameSpan.Length >= ODataJsonConstants.ODataAnnotationNamespacePrefix.Length && + propertyNameSpan.StartsWith(ODataJsonConstants.ODataAnnotationNamespacePrefix.AsSpan(), StringComparison.Ordinal); } /// @@ -157,6 +174,7 @@ internal static bool IsODataAnnotationName(string propertyName) /// /// This method returns true both for normal annotation as well as property annotations. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsAnnotationProperty(string propertyName) { Debug.Assert(!string.IsNullOrEmpty(propertyName), "!string.IsNullOrEmpty(propertyName)"); @@ -164,6 +182,22 @@ internal static bool IsAnnotationProperty(string propertyName) return propertyName.IndexOf('.', StringComparison.Ordinal) >= 0; } + /// + /// Determines if the specified property name is a name of an annotation property. + /// + /// The name of the property. + /// true if is a name of an annotation property, false otherwise. + /// + /// This method returns true both for normal annotation as well as property annotations. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsAnnotationProperty(ReadOnlySpan propertyNameSpan) + { + Debug.Assert(!propertyNameSpan.IsEmpty, "!propertyNameSpan.IsEmpty"); + + return propertyNameSpan.IndexOf('.') >= 0; + } + /// /// Validates that the annotation value is valid. /// diff --git a/src/Microsoft.OData.Core/Json/ODataJsonUtils.cs b/src/Microsoft.OData.Core/Json/ODataJsonUtils.cs index 3adf313189..849dedd096 100644 --- a/src/Microsoft.OData.Core/Json/ODataJsonUtils.cs +++ b/src/Microsoft.OData.Core/Json/ODataJsonUtils.cs @@ -28,6 +28,7 @@ internal static class ODataJsonUtils /// /// The name of the property. /// true if is a name of a metadata reference property, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static bool IsMetadataReferenceProperty(string propertyName) { Debug.Assert(!String.IsNullOrEmpty(propertyName), "!string.IsNullOrEmpty(propertyName)"); @@ -35,6 +36,19 @@ internal static bool IsMetadataReferenceProperty(string propertyName) return propertyName.IndexOf(ODataConstants.ContextUriFragmentIndicator, StringComparison.Ordinal) >= 0; } + /// + /// Determines if the specified property name is a name of a metadata reference property. + /// + /// The name of the property. + /// true if is a name of a metadata reference property, false otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsMetadataReferenceProperty(ReadOnlySpan propertyNameSpan) + { + Debug.Assert(!propertyNameSpan.IsEmpty, "!propertyNameSpan.IsEmpty"); + + return propertyNameSpan.IndexOf(ODataConstants.ContextUriFragmentIndicator) >= 0; + } + /// /// Gets the fully qualified operation import name from the metadata reference property name. /// diff --git a/src/Microsoft.OData.Core/MemoryExtensions.cs b/src/Microsoft.OData.Core/MemoryExtensions.cs new file mode 100644 index 0000000000..111578009f --- /dev/null +++ b/src/Microsoft.OData.Core/MemoryExtensions.cs @@ -0,0 +1,38 @@ +//--------------------------------------------------------------------- +// +// Copyright (C) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. +// +//--------------------------------------------------------------------- + +namespace Microsoft.OData +{ + #region Namespaces + using System; + using System.Runtime.InteropServices; + + #endregion Namespaces + + internal static class MemoryExtensions + { + /// + /// Returns a string representation of the specified character memory without unnecessary allocation. + /// + /// The character memory to convert to a string. + /// + /// The underlying string when is backed by a string slice that starts at index 0 and spans the full length; + /// otherwise, a new string created from . + /// + /// + /// Uses MemoryMarshal.TryGetString to avoid allocation when possible; falls back to ReadOnlyMemory<char>.ToString(). + /// + public static string GetString(this ReadOnlyMemory memory) + { + if (MemoryMarshal.TryGetString(memory, out string value, out int length, out int start) && start == 0 && length == memory.Length) + { + return value; + } + + return memory.ToString(); + } + } +} diff --git a/test/UnitTests/Microsoft.OData.Core.Tests/Json/ODataAnnotationNamesTests.cs b/test/UnitTests/Microsoft.OData.Core.Tests/Json/ODataAnnotationNamesTests.cs index 73c2f836e0..6b5a99f0f0 100644 --- a/test/UnitTests/Microsoft.OData.Core.Tests/Json/ODataAnnotationNamesTests.cs +++ b/test/UnitTests/Microsoft.OData.Core.Tests/Json/ODataAnnotationNamesTests.cs @@ -20,7 +20,8 @@ public class ODataAnnotationNamesTests typeof(ODataAnnotationNames) .GetFields(BindingFlags.NonPublic | BindingFlags.Static) .Where(f => f.FieldType == typeof(string)) - .Select(f => (string)f.GetValue(null)).ToArray(); + .Select(f => (string)f.GetValue(null)) + .Where(f => f.StartsWith("odata.")).ToArray(); // Not applicable to .NET Core due to changes in framework [Fact]