Skip to content

Conversation

Copy link

Copilot AI commented Nov 19, 2025

Issues

This pull request fixes #2980.

Description

Function import requests with expanded navigation properties fail with ODataException: The Path property in ODataMessageWriterSettings.ODataUri must be set when writing contained elements:

// These requests fail:
GET /GetAppUsage()/{appId}?$expand=Credentials
GET /GetAppUsage()/{appId}/Credentials

Root cause: ConvertPath() in ODataOutputFormatterHelper.cs was discarding path segments after operation imports, causing ODL to serialize with an empty path.

Changes:

  • Simplified ConvertPath() to always return path.Path directly (removes 72 lines)
  • Deleted now-unused IsOperationPath() and GeneratePath() helper methods
  • Added E2E tests for function import expand scenarios

This aligns with AspNetCoreOData 8+ behavior which doesn't perform path conversion.

Checklist (Uncheck if it is not completed)

  • Test cases added
  • Build and test with one-click build and test script passed

Additional work necessary

None. Tests require .NET Core 3.1 runtime which is not available in the CI environment, but build succeeds and tests are correctly structured.

Original prompt

This section details on the original issue you should resolve

<issue_title>Cannot expand navigation property from entity collection returned from function import.</issue_title>
<issue_description>
If you have a composable unbound function GetAppStats that returns a collection of entities AppUsage, and you make a request that expands navigation properties of the entity type, like so:

  • GET /GetAppUsage()/{appId}?$expand=Credentials
  • GET /GetAppSuage()/{appId}/Credentials

Then you'll get an exception like the following:

Microsoft.OData.ODataException: The Path property in ODataMessageWriterSettings.ODataUri must be set when writing contained elements.
   at Microsoft.OData.ODataWriterCore.EnterScope(WriterState newState, ODataItem item)
   at Microsoft.OData.ODataWriterCore.WriteStartNestedResourceInfoImplementation(ODataNestedResourceInfo nestedResourceInfo)
   at Microsoft.OData.ODataWriterCore.<>c.<WriteStartAsync>b__64_0(ODataWriterCore thisParam, ODataNestedResourceInfo nestedResourceInfoParam)
   at Microsoft.OData.TaskUtils.GetTaskForSynchronousOperation[TArg1,TArg2](Action`2 synchronousOperation, TArg1 arg1, TArg2 arg2)
--- End of stack trace from previous location ---
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteExpandedNavigationPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer) in C:\Users\clhabins\source\repos\WebApi\src\Microsoft.AspNet.OData.Shared\Formatter\Serialization\ODataResourceSerializer.cs:line 1889
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteResourceAsync(Object graph, ODataWriter writer, ODataSerializerContext writeContext, IEdmTypeReference expectedType) in C:\Users\clhabins\source\repos\WebApi\src\Microsoft.AspNet.OData.Shared\Formatter\Serialization\ODataResourceSerializer.cs:line 1497
   at Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer.WriteObjectAsync(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext) in C:\Users\clhabins\source\repos\WebApi\src\Microsoft.AspNet.OData.Shared\Formatter\Serialization\ODataResourceSerializer.cs:line 81
   at Microsoft.AspNet.OData.Formatter.ODataOutputFormatterHelper.WriteToStreamAsync(Type type, Object value, IEdmModel model, ODataVersion version, Uri baseAddress, MediaTypeHeaderValue contentType, IWebApiUrlHelper internaUrlHelper, IWebApiRequestMessage internalRequest, IWebApiHeaders internalRequestHeaders, Func`2 getODataMessageWrapper, Func`2 getEdmTypeSerializer, Func`2 getODataPayloadSerializer, Func`1 getODataSerializerContext) in C:\Users\clhabins\source\repos\WebApi\src\Microsoft.AspNet.OData.Shared\Formatter\ODataOutputFormatterHelper.cs:line 229
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|30_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
 ...

Reproducing the error

Here's a link to a sample repro project I created on a fork of this repo to make it easier to demonstrate the error.

Run the project then execute the following request and check the debug output logs to see the error

GET http://localhost:5287/odata/GetAppUsage()/App1?$expand=KeyCredentials

Error details and root cause

The error is thrown by ODataWriterCore.EnterScope in ODL when serializing an expanded navigation property when the navigation property is contained and the OData path is empty (has no segments):

case EdmNavigationSourceKind.ContainedEntitySet:
      // Containment cannot be written alone without odata uri.
      if (!odataPath.Any())
      {
          throw new ODataException(SRResources.ODataWriterCore_PathInODataUriMustBeSetWhenWritingContainedElement);
      }

But why is the ODataPath empty at this point despite that our request url definitely has path segments?

Well, it turns out the issue is that WebAPI 7.x has a different ODataPath type from ODL's ODataPath. And when it starts serialization, it converts its ODataPath to ODL's ODataPath. During the conversion, it sets the path to null if it contains an operation segment:

// This function is used to determine whether an OData path includes operation (import) path segments.
// We use this function to make sure the value of ODataUri.Path in...

</details>

- Fixes OData/WebApi#2890

<!-- START COPILOT CODING AGENT TIPS -->
---Let Copilot coding agent [set things up for you](https://github.com/OData/WebApi/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

- Modified ConvertPath to return path.Path directly for all paths
- Removed IsOperationPath and GeneratePath methods as they are no longer needed
- This fixes the issue where expanding navigation properties from function import results would fail with "The Path property in ODataMessageWriterSettings.ODataUri must be set when writing contained elements"

Co-authored-by: habbes <[email protected]>
Copilot AI changed the title [WIP] Fix navigation property expansion for GetAppStats function Fix navigation property expansion from function imports Nov 19, 2025
Copilot finished work on behalf of habbes November 19, 2025 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants