OData / odata.net

ODataLib: Open Data Protocol - .NET Libraries and Frameworks
https://docs.microsoft.com/odata
Other
683 stars 349 forks source link

OData client not parsing response from Action API #1859

Open jananiva opened 4 years ago

jananiva commented 4 years ago

We are using OData code client generator VS2017 extension to generate ODATA client for our ODATA APIs. It has been working fine for all APIs except Action APIs that return a response entity. When I make a call to an action API that returns an entity, through Fiddler I can see that the API has finished successfully and the response payload. However, OData client errors out while parsing the response with the error: System.ArgumentOutOfRangeException: 'Length cannot be less than zero.. I'll post the complete error message with the stack at the end of this query.

This is working fine for Action APIs that return a 204 No Content.

Assemblies affected

ODATA v4 Client Code Generator, v7.5.1

Reproduce steps

Call an action API that returns an entity in the response. The API is called with the header prefer: return=representation. On intercepting with Fiddler, it can be seen that the API returns valid response.

var timeCard = scheduleQuery.TimeCards.ClockIn().GetValue(); // Clock-In Action API that returns a 'TimeCard' entity
return timeCard;

Expected result

The timeCard variable should hold the value of the clocked-in TimeCard correctly after this snippet has been executed.

Actual result

There is an exception when this line of code gets executed.

System.ArgumentOutOfRangeException: Length cannot be less than zero.
Parameter name: length
   at System.String.Substring(Int32 startIndex, Int32 length)
   at Microsoft.OData.TypeUtils.ParseQualifiedTypeName(String qualifiedTypeName, String& namespaceName, String& typeName, Boolean& isCollection)
   at Microsoft.OData.JsonLight.ODataJsonLightContextUriParser.ResolveType(String typeName, Func`3 clientCustomTypeResolver, Boolean throwIfMetadataConflict)
   at Microsoft.OData.JsonLight.ODataJsonLightContextUriParser.ParseContextUriFragment(String fragment, Func`3 clientCustomTypeResolver, Boolean throwIfMetadataConflict, Boolean& isUndeclared)
   at Microsoft.OData.JsonLight.ODataJsonLightContextUriParser.ParseContextUri(ODataPayloadKind expectedPayloadKind, Func`3 clientCustomTypeResolver, Boolean throwIfMetadataConflict)
   at Microsoft.OData.JsonLight.ODataJsonLightContextUriParser.Parse(IEdmModel model, String contextUriFromPayload, ODataPayloadKind payloadKind, Func`3 clientCustomTypeResolver, Boolean needParseFragment, Boolean throwIfMetadataConflict)
   at Microsoft.OData.JsonLight.ODataJsonLightDeserializer.ReadPayloadStart(ODataPayloadKind payloadKind, PropertyAndAnnotationCollector propertyAndAnnotationCollector, Boolean isReadingNestedPayload, Boolean allowEmptyPayload)
   at Microsoft.OData.JsonLight.ODataJsonLightPayloadKindDetectionDeserializer.DetectPayloadKind(ODataPayloadKindDetectionInfo detectionInfo)
   at Microsoft.OData.JsonLight.ODataJsonLightInputContext.DetectPayloadKind(ODataPayloadKindDetectionInfo detectionInfo)
   at Microsoft.OData.Json.ODataJsonFormat.DetectPayloadKindImplementation(ODataMessageInfo messageInfo, ODataMessageReaderSettings settings)
   at Microsoft.OData.Json.ODataJsonFormat.DetectPayloadKind(ODataMessageInfo messageInfo, ODataMessageReaderSettings settings)
   at Microsoft.OData.ODataMessageReader.DetectPayloadKind()
   at Microsoft.OData.Client.Materialization.ODataMaterializer.CreateODataMessageReader(IODataResponseMessage responseMessage, ResponseInfo responseInfo, ODataPayloadKind& payloadKind)
   at Microsoft.OData.Client.Materialization.ODataMaterializer.CreateMaterializerForMessage(IODataResponseMessage responseMessage, ResponseInfo responseInfo, Type materializerType, QueryComponents queryComponents, ProjectionPlan plan, ODataPayloadKind payloadKind)
   at Microsoft.OData.Client.MaterializeAtom..ctor(ResponseInfo responseInfo, QueryComponents queryComponents, ProjectionPlan plan, IODataResponseMessage responseMessage, ODataPayloadKind payloadKind)
   at Microsoft.OData.Client.QueryResult.CreateMaterializer(ProjectionPlan plan, ODataPayloadKind payloadKind)
   at Microsoft.OData.Client.QueryResult.ProcessResult[TElement](ProjectionPlan plan)
   at Microsoft.OData.Client.DataServiceRequest.Execute[TElement](DataServiceContext context, QueryComponents queryComponents)
   at Microsoft.OData.Client.DataServiceContext.InnerSynchExecute[TElement](Uri requestUri, String httpMethod, Nullable`1 singleResult, OperationParameter[] operationParameters)
   at Microsoft.OData.Client.DataServiceContext.Execute[TElement](Uri requestUri, String httpMethod, Boolean singleResult, OperationParameter[] operationParameters)
   at Microsoft.OData.Client.DataServiceActionQuerySingle`1.GetValue()

Additional detail

The metadata is in the same file as the OData client, it is not in a separate temp file.

We de-compiled the source of the error it seems to be looking for a qualified type name.

internal static void ParseQualifiedTypeName(string qualifiedTypeName, out string namespaceName, out string typeName, out bool isCollection)
        {
            isCollection = qualifiedTypeName.StartsWith("Collection(", StringComparison.Ordinal);
            if (isCollection)
            {
                qualifiedTypeName = qualifiedTypeName.Substring("Collection".Length + 1).TrimEnd(')');
            }
            int num = qualifiedTypeName.LastIndexOf(".", StringComparison.Ordinal);
            namespaceName = qualifiedTypeName.Substring(0, num);
            typeName = qualifiedTypeName.Substring((num != 0) ? (num + 1) : 0);
        }

but qualifiedTypeName ("timeCard") doesn't have a "." in it, so num is -1 which fails Substring(0, -1) since output length is < 0

Workarounds tried:

Sreejithpin commented 4 years ago

Hi, We are deprecating the Client code generator in favor of Connected Services which is the super set. You can find the rep: here: https://marketplace.visualstudio.com/items?itemName=laylaliu.ODataConnectedService

Please try and let us know if you are able to use it

iansul commented 4 years ago

@Sreejithpin the issue is in the OData Core and Client libs, which as far as I an tell, are the same regardless of the code generation tool. I work with @jananiva and I've tried this with various Connected services, currently I have a Repro with Unchase.OData.ConnectedService, v1.4.1.0 . If you really think there is a difference I will try it. But I urge you to look at the CallStack, it's all in the OData Core and Client libs.

    mscorlib.dll!string.Substring(int startIndex, int length) Line 1282 C#
>   Microsoft.OData.Core.dll!Microsoft.OData.TypeUtils.ParseQualifiedTypeName(string qualifiedTypeName, out string namespaceName, out string typeName, out bool isCollection) Line 56   C#
    Microsoft.OData.Core.dll!Microsoft.OData.JsonLight.ODataJsonLightContextUriParser.ResolveType(string typeName, System.Func<Microsoft.OData.Edm.IEdmType, string, Microsoft.OData.Edm.IEdmType> clientCustomTypeResolver, bool throwIfMetadataConflict) Line 367 C#
    Microsoft.OData.Core.dll!Microsoft.OData.JsonLight.ODataJsonLightContextUriParser.ParseContextUriFragment(string fragment, System.Func<Microsoft.OData.Edm.IEdmType, string, Microsoft.OData.Edm.IEdmType> clientCustomTypeResolver, bool throwIfMetadataConflict, out bool isUndeclared) Line 280  C#
    Microsoft.OData.Core.dll!Microsoft.OData.JsonLight.ODataJsonLightContextUriParser.ParseContextUri(Microsoft.OData.ODataPayloadKind expectedPayloadKind, System.Func<Microsoft.OData.Edm.IEdmType, string, Microsoft.OData.Edm.IEdmType> clientCustomTypeResolver, bool throwIfMetadataConflict) Line 66 C#
    Microsoft.OData.Core.dll!Microsoft.OData.JsonLight.ODataJsonLightContextUriParser.Parse(Microsoft.OData.Edm.IEdmModel model, string contextUriFromPayload, Microsoft.OData.ODataPayloadKind payloadKind, System.Func<Microsoft.OData.Edm.IEdmType, string, Microsoft.OData.Edm.IEdmType> clientCustomTypeResolver, bool needParseFragment, bool throwIfMetadataConflict) Line 44    C#
    Microsoft.OData.Core.dll!Microsoft.OData.JsonLight.ODataJsonLightDeserializer.ReadPayloadStart(Microsoft.OData.ODataPayloadKind payloadKind, Microsoft.OData.PropertyAndAnnotationCollector propertyAndAnnotationCollector, bool isReadingNestedPayload, bool allowEmptyPayload) Line 110   C#
    Microsoft.OData.Core.dll!Microsoft.OData.JsonLight.ODataJsonLightPayloadKindDetectionDeserializer.DetectPayloadKind(Microsoft.OData.ODataPayloadKindDetectionInfo detectionInfo) Line 22    C#
    Microsoft.OData.Core.dll!Microsoft.OData.JsonLight.ODataJsonLightInputContext.DetectPayloadKind(Microsoft.OData.ODataPayloadKindDetectionInfo detectionInfo) Line 188   C#
    Microsoft.OData.Core.dll!Microsoft.OData.Json.ODataJsonFormat.DetectPayloadKindImplementation(Microsoft.OData.ODataMessageInfo messageInfo, Microsoft.OData.ODataMessageReaderSettings settings) Line 58    C#
    Microsoft.OData.Core.dll!Microsoft.OData.Json.ODataJsonFormat.DetectPayloadKind(Microsoft.OData.ODataMessageInfo messageInfo, Microsoft.OData.ODataMessageReaderSettings settings) Line 17  C#
    Microsoft.OData.Core.dll!Microsoft.OData.ODataMessageReader.DetectPayloadKind() Line 128    C#
    Microsoft.OData.Client.dll!Microsoft.OData.Client.Materialization.ODataMaterializer.CreateODataMessageReader(Microsoft.OData.IODataResponseMessage responseMessage, Microsoft.OData.Client.ResponseInfo responseInfo, ref Microsoft.OData.ODataPayloadKind payloadKind) Line 208    C#
    Microsoft.OData.Client.dll!Microsoft.OData.Client.Materialization.ODataMaterializer.CreateMaterializerForMessage(Microsoft.OData.IODataResponseMessage responseMessage, Microsoft.OData.Client.ResponseInfo responseInfo, System.Type materializerType, Microsoft.OData.Client.QueryComponents queryComponents, Microsoft.OData.Client.ProjectionPlan plan, Microsoft.OData.ODataPayloadKind payloadKind) Line 116  C#
    Microsoft.OData.Client.dll!Microsoft.OData.Client.MaterializeAtom.MaterializeAtom(Microsoft.OData.Client.ResponseInfo responseInfo, Microsoft.OData.Client.QueryComponents queryComponents, Microsoft.OData.Client.ProjectionPlan plan, Microsoft.OData.IODataResponseMessage responseMessage, Microsoft.OData.ODataPayloadKind payloadKind) Line 115   C#
    Microsoft.OData.Client.dll!Microsoft.OData.Client.QueryResult.CreateMaterializer(Microsoft.OData.Client.ProjectionPlan plan, Microsoft.OData.ODataPayloadKind payloadKind) Line 448 C#
    Microsoft.OData.Client.dll!Microsoft.OData.Client.QueryResult.ProcessResult<microsoft.graph.timeCard>(Microsoft.OData.Client.ProjectionPlan plan) Line 210  C#
    Microsoft.OData.Client.dll!Microsoft.OData.Client.DataServiceRequest.Execute<microsoft.graph.timeCard>(Microsoft.OData.Client.DataServiceContext context, Microsoft.OData.Client.QueryComponents queryComponents) Line 90   C#
    Microsoft.OData.Client.dll!Microsoft.OData.Client.DataServiceContext.InnerSynchExecute<microsoft.graph.timeCard>(System.Uri requestUri, string httpMethod, bool? singleResult, Microsoft.OData.Client.OperationParameter[] operationParameters) Line 1408   C#
    Microsoft.OData.Client.dll!Microsoft.OData.Client.DataServiceContext.Execute<microsoft.graph.timeCard>(System.Uri requestUri, string httpMethod, bool singleResult, Microsoft.OData.Client.OperationParameter[] operationParameters) Line 1048  C#
    Microsoft.OData.Client.dll!Microsoft.OData.Client.DataServiceActionQuerySingle<microsoft.graph.timeCard>.GetValue() Line 29 C#
    AGSScenarios.dll!AGSScenarios.AGSTimeClock.TestClockIn.AnonymousMethod__0(TestHarness.UserInfo user) Line 349   C#
    TestHarness.dll!TestHarness.RequestTools.RunRequest.AnonymousMethod__1() Line 342   C#
jananiva commented 3 years ago

Ping @Sreejithpin - please take a look at my colleague @iansul 's response and please suggest next steps.

Sreejithpin commented 3 years ago

Ian tried with our connected service and the issue is still happening. So reopening the same

marabooy commented 3 years ago

@jananiva & @iansul Could you share part of the model the one that contains the function and the entity set for Timecards. I was unable to reproduce the issue with ODL 7.6.4 while using https://marketplace.visualstudio.com/items?itemName=laylaliu.ODataConnectedService.

marabooy commented 3 years ago

@jananiva and @iansul could you also check if calling the Action using Postman returns the correct representation. My guess would be url would be serviceUrl/TimeCards/ClockIn or serviceUrl/TimeCards/ModelNamespace.ClockIn with Post.

francisharvey commented 3 years ago

@jananiva, I believe I'm getting these errors too when using ToArray on empty collection response. When adding a filter specific enough to a collection, I now use FirstOrDefault.

It may also be related to my queries with multiple level of expanded collections, when some are empty on deeper levels.

I will add more information here if it is the same error.

Francis Harvey

marabooy commented 3 years ago

@francisharvey is this issue still present? if so could you share part of the model to help me track the issue.

chalk-builder commented 1 year ago

I have a similar issue with a service that I am interacting with. I have a type that is not fully qualified in the response from the service and the problem is in this line of code https://github.com/OData/odata.net/blob/4e263ca4b94d01f2d105d8282c62ec272cfcd1bb/src/Microsoft.OData.Core/TypeUtils.cs#L106

Since the type is not fully qualified the lastindexof returns -1 and the code tries to do a substring and blows up. This line should be checking for that and returning the original type.