OData / WebApi

OData Web API: A server library built upon ODataLib and WebApi
https://docs.microsoft.com/odata
Other
856 stars 473 forks source link

The operation cannot be completed because no ODataPath is available for the request. #1866

Open weitzhandler opened 5 years ago

weitzhandler commented 5 years ago

Hi, I'm trying to use

Assemblies affected

Assembly Microsoft.AspNetCore.OData, Version=7.1.0.21120

*Which assemblies and versions are known to be affected e.g. OData WebApi lib 6.1.0

Reproduce steps

Here's my OData setup:

/*Startup.ConfigureServices*/
services
  .AddMvc(options =>
  {
    options.EnableEndpointRouting = false;
  })
  .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
  .AddJsonOptions(jsonOptions =>
  {
    var jsonSettings = jsonOptions.SerializerSettings;
    jsonSettings.TypeNameHandling = TypeNameHandling.All;
  });

/*Startup.Configure*/

app.UseMvc(routeBuilder =>
{
  routeBuilder.EnableDependencyInjection();
  routeBuilder
    .Select()
    .Expand()
    .Filter()
    .OrderBy()
    .MaxTop(128)
    .Count();
  routeBuilder.MapODataServiceRoute("API Route", Consts.ApiRoute, 
    modelBuilder.GetEdmModel());
});

/*EdmModelBuilder*/
EntitySet<Contact>("contacts");      

Expected result

No excceptions

Actual result

When posting data using OData, it gets to the controller action and data saves just fine, but then, during processing of the action-result, in some of the middlewares, the following exception is thrown:

InvalidOperationException: The operation cannot be completed because no ODataPath is available for the request.

Additional detail

StackTrace:

at Microsoft.AspNet.OData.Results.ResultHelpers.GenerateODataLink(HttpRequest request, Object entity, Boolean isEntityId) at Microsoft.AspNet.OData.Results.CreatedODataResult1.GenerateLocationHeader(HttpRequest request) at Microsoft.AspNet.OData.Results.CreatedODataResult1.ExecuteResultAsync(ActionContext context) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultAsync(IActionResult result) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResultFilterAsync[TFilter,TFilterAsync]() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResultExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultFilters() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync() at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext) at IdentityServer4.Hosting.IdentityServerMiddleware.Invoke(HttpContext context, IEndpointRouter router, IUserSession session, IEventService events) in C:\local\identity\server4\IdentityServer4\src\IdentityServer4\src\Hosting\IdentityServerMiddleware.cs:line 72

axelgenus commented 5 years ago

It happened also to me and I cannot understand why... is there any update on this problem?

pawel-zolty commented 4 years ago

it happened to me too :/

freemstr commented 4 years ago

Are there any updates on this? Thank you

xuzhg commented 4 years ago

@freemstr Is it still happening in latest 7.4.1 version? Would you please share me a repo?

to remove routeBuilder.EnableDependencyInjection(); can help to resolve the problem?

freemstr commented 4 years ago

@xuzhg I worked around the issue, for now, by using $expand but here is what I ran into:

Project is being migrated from OData v3 to v4 7.4.1 so this is happening in a new project with the following configurations and no explicitly called model builder and no entity framework:

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.EnableDependencyInjection();
                endpoints.Select().Filter().OrderBy().Count().Expand().MaxTop(100);
                endpoints.SetUrlKeyDelimiter(ODataUrlKeyDelimiter.Parentheses);
            });

The error is happening only when calling GetGroupMembers

public class GroupController {
       IGroupManager groupManager;

       public GroupController (IGroupManager di_groupManager) 
        {
            groupManager= di_groupManager;
        }

        [HttpGet]
        [ODataRoute]
        [Route("/api/[controller]({key})")]
        public virtual Group Get([FromODataUri]  int key)
        {
            return groupManager.GetById(key);
        }

        [HttpPost]
        [Route("Delete({id})")]
        [ODataRoute]
        public virtual ActionResult DeletePost([FromODataUri] TKey id)
        {
              var i = groupManager.DeleteEntity(id);
             return i ? NoContent() : ValidationProblem(ModelState);
        }

        [HttpGet]
        [ODataRoute]
        [Route("GetGroupMember({id})")]
        public ICollection<GroupMembers> GetGroupMembers([FromODataUri] int key)
        {
            return groupManager.GetGroupMembersById(key);
        }
}

Thank you for looking into this.

DerekGn commented 3 years ago

For anyone looking for a possible work around to this problem that's been open a very long time, this workaround has probably been found multiple times.

The issue is still in Microsoft.AspNetCore.OData, Version=7.5.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35

You can simply return an OK() rather than a created.

/// <summary>
/// Create a new <see cref="PaCase"/> instance
/// </summary>
/// <param name="paCase">The <see cref="PaCase"/> to create</param>
/// <returns>The created <see cref="PaCase"/></returns>
[HttpPost]
[ODataRoute("Post")]
[Produces(typeof(PaCase))]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> Post(PaCase paCase)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    Context.PaCases.Add(paCase);

    await Context.SaveChangesAsync();

#warning Bug in odata framework throws System.InvalidOperationException: The operation cannot be completed because no ODataPath is available for the request. See https://github.com/OData/WebApi/issues/1866
    //return Created(paCase);
    return Ok(paCase);
}

The reason for the error is due to a check in the Microsoft.AspNet.OData.Results.ResultHelpers.GenerateODataLink method.

Microsoft.AspNet.OData.Routing.ODataPath path = request.ODataFeature().Path;
if (path == null)
{
    throw new InvalidOperationException(SRResources.ODataPathMissing);
}

The request.ODataFeature().Path is null because the ODataFeature() extension method calls another extension method, that creates an instance of the ODataFeature because there is none on the HttpContext

public static class HttpContextExtensions
{
    /// <summary>
    /// Extension method to return the <see cref="T:Microsoft.AspNet.OData.Interfaces.IODataFeature" /> from the <see cref="T:Microsoft.AspNetCore.Http.HttpContext" />.
    /// </summary>
    /// <param name="httpContext">The Http context.</param>
    /// <returns>The <see cref="T:Microsoft.AspNet.OData.Interfaces.IODataFeature" />.</returns>
    public static IODataFeature ODataFeature(this HttpContext httpContext)
    {
        if (httpContext == null)
        {
            throw Error.ArgumentNull("httpContext");
        }
        IODataFeature iODataFeature = httpContext.Features.Get<IODataFeature>();
        if (iODataFeature == null)
        {
            iODataFeature = new ODataFeature();
            httpContext.Features.Set(iODataFeature);
        }
        return iODataFeature;
    }

There is an interesting comment on the ODataFeature

/// <summary>
/// Contains the details of a given OData request. These properties should all be mutable.
/// None of these properties should ever be set to null.
/// </summary>
public class ODataFeature : IODataFeature, IDisposable
{

So somewhere further up the request response call chain the ODataFeature is not being set. No idea why because I did not investigate further.

garv347 commented 3 years ago

I am still facing this error. Was anyone able to find a workaround?

I am using Assembly Microsoft.AspNetCore.OData, Version=7.5.5.0

azizainunnajib commented 3 years ago

For anyone looking for a possible work around to this problem that's been open a very long time, this workaround has probably been found multiple times.

The issue is still in Microsoft.AspNetCore.OData, Version=7.5.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35

You can simply return an OK() rather than a created.

/// <summary>
/// Create a new <see cref="PaCase"/> instance
/// </summary>
/// <param name="paCase">The <see cref="PaCase"/> to create</param>
/// <returns>The created <see cref="PaCase"/></returns>
[HttpPost]
[ODataRoute("Post")]
[Produces(typeof(PaCase))]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> Post(PaCase paCase)
{
  if (!ModelState.IsValid)
  {
      return BadRequest(ModelState);
  }

  Context.PaCases.Add(paCase);

  await Context.SaveChangesAsync();

#warning Bug in odata framework throws System.InvalidOperationException: The operation cannot be completed because no ODataPath is available for the request. See https://github.com/OData/WebApi/issues/1866
  //return Created(paCase);
  return Ok(paCase);
}

The reason for the error is due to a check in the Microsoft.AspNet.OData.Results.ResultHelpers.GenerateODataLink method.

Microsoft.AspNet.OData.Routing.ODataPath path = request.ODataFeature().Path;
if (path == null)
{
  throw new InvalidOperationException(SRResources.ODataPathMissing);
}

The request.ODataFeature().Path is null because the ODataFeature() extension method calls another extension method, that creates an instance of the ODataFeature because there is none on the HttpContext

public static class HttpContextExtensions
{
  /// <summary>
  /// Extension method to return the <see cref="T:Microsoft.AspNet.OData.Interfaces.IODataFeature" /> from the <see cref="T:Microsoft.AspNetCore.Http.HttpContext" />.
  /// </summary>
  /// <param name="httpContext">The Http context.</param>
  /// <returns>The <see cref="T:Microsoft.AspNet.OData.Interfaces.IODataFeature" />.</returns>
  public static IODataFeature ODataFeature(this HttpContext httpContext)
  {
      if (httpContext == null)
      {
          throw Error.ArgumentNull("httpContext");
      }
      IODataFeature iODataFeature = httpContext.Features.Get<IODataFeature>();
      if (iODataFeature == null)
      {
          iODataFeature = new ODataFeature();
          httpContext.Features.Set(iODataFeature);
      }
      return iODataFeature;
  }

There is an interesting comment on the ODataFeature

/// <summary>
/// Contains the details of a given OData request. These properties should all be mutable.
/// None of these properties should ever be set to null.
/// </summary>
public class ODataFeature : IODataFeature, IDisposable
{

So somewhere further up the request response call chain the ODataFeature is not being set. No idea why because I did not investigate further.

thanks, i forgot the return should be Ok(), not Created()