C# OData GetKeyFromUri - Resource not found for the segment 'odata'

Viewed 1200

I've updated from Microsoft.AspNet.OData version 6.0.0 to OData version 7.0.1. The upgrade has broken my ability to get the Id from a path when linking one object to another. Here is my Web API call to add a role to a specific user using the OData standard:

POST: http://localhost:61506/odata/users('bob')/roles/$ref
Request body: {"@odata.id":"http://localhost:61506/odata/roles(1)"}

The Web API method verifies the user and then makes a call to Helpers.GetKeyFromUri to get the role Id value from the request body.

[HttpPost, HttpPut]
public IHttpActionResult CreateRef([FromODataUri] string key, string navigationProperty, [FromBody] Uri link)
{
    // Ensure the User exists
    User user = new User().GetById(key);
    if (user == null)
    {
        return NotFound();
    }

    // Determine which navigation property to use
    switch (navigationProperty)
    {
        case "roles":
            // Get the Role id
            int roleId;
            try
            {
                roleId = Helpers.GetKeyFromUri<int>(Request, link);
            }
            catch (Exception ex)
            {
                return BadRequest();
            }

            // Ensure the Role exists
            Role role = new Role().GetById(roleId);
            if (role == null)
            {
                return NotFound();
            }

            // Add the User/Role relationship
            user.Roles.Add(role);
            user.Update();

            break;

        default:
            return StatusCode(HttpStatusCode.NotImplemented);
    }            

    return StatusCode(HttpStatusCode.NoContent);
}

That function looks like this (Originally from here but with updated references: https://github.com/OData/ODataSamples/blob/master/RESTier/Trippin/Trippin/Helpers.cs)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web.Http.Routing;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNet.OData.Routing;
using Microsoft.OData.UriParser;

namespace Project1.Extensions
{
    public class Helpers
    {
        public static TKey GetKeyFromUri<TKey>(HttpRequestMessage request, Uri uri)
        {
            if (uri == null)
            {
                throw new ArgumentNullException("uri");
            }

            var urlHelper = request.GetUrlHelper() ?? new UrlHelper(request);

            var pathHandler = (IODataPathHandler)request.GetRequestContainer().GetService(typeof(IODataPathHandler));

            string serviceRoot = urlHelper.CreateODataLink(
                request.ODataProperties().RouteName,
                pathHandler, new List<ODataPathSegment>());
            var odataPath = pathHandler.Parse(serviceRoot, uri.LocalPath, request.GetRequestContainer());

            var keySegment = odataPath.Segments.OfType<KeySegment>().FirstOrDefault();
            if (keySegment == null)
            {
                throw new InvalidOperationException("The link does not contain a key.");
            }

            var value = keySegment.Keys.FirstOrDefault().Value;
            return (TKey)value;
        }
    }
}

This line of code is now throwing the following error: Resource not found for the segment 'odata'

var odataPath = pathHandler.Parse(serviceRoot, uri.LocalPath, request.GetRequestContainer());

This worked fine when using OData 6.0.0 but fails in 7.0.1. It seems to have some sort of issue parsing my odata segment or not being able to find it at all. Here is my routing setup if it helps:

public static void Register(HttpConfiguration config)
{
    // Setup the OData routes and endpoints
    config.MapODataServiceRoute(
        routeName: "ODataRoute",
        routePrefix: "odata",
        model: GetEdmModel());

    // Enable OData URL querying globally
    config.Count().Filter().Select().OrderBy().Expand().MaxTop(null);
}
2 Answers

I know I'm a bit late here, but I ran into this same problem with upgrading to Microsoft.AspNet.OData 7.x. After a bit of debugging and tinkering, I found that this code works for me - without having to remove the routePrefix:

public static TKey GetKeyFromUri<TKey>(HttpRequestMessage request, Uri uri)
{
    if (uri == null)
    {
        throw new ArgumentNullException(nameof(uri));
    }

    var urlHelper = request.GetUrlHelper() ?? new UrlHelper(request);

    string serviceRoot = urlHelper.CreateODataLink(
        request.ODataProperties().RouteName,
        request.GetPathHandler(),
        new List<ODataPathSegment>());

    var odataPath = request.GetPathHandler().Parse(
        serviceRoot,
        uri.AbsoluteUri,
        request.GetRequestContainer());

    var keySegment = odataPath.Segments.OfType<KeySegment>().FirstOrDefault();
    if (keySegment == null)
    {
        throw new InvalidOperationException("The link does not contain a key.");
    }

    return (TKey)keySegment.Keys.FirstOrDefault().Value;
}

It turns out that IODataPathHandler.Parse(...) can take an absolute URI and resolve it against the serviceRoot.

The other key difference is that KeySegment.Keys already has a mapping of Key-Values, where the value is already parsed - it just needs to be casted.

For reference, I am using Microsoft.AspNet.OData 7.4.0

Hope this helps!

I caused myself the same problem by changing routePrefix from null to odata just like you've done. Setting routePrefix to null will allow your code to work perfectly fine as long as you don't need a route prefix (such as /odata/).

Related