Getting related entities ASP.NET WebApi OData v4 results in "No HTTP resource was found that matches the request URI"

As I mention in the question, I tried many solutions to get this to work, but none were consistent in actually solving the problem and I kept avoiding the solution laid out in this SO question/answer because the tutorial is specifically for v4 and I figured that answer must be for an older version (how unwise).

So that answer does solve the problem, but requires some work to fit directly into OData v4 and an ODataConventionModelBuilder; this is why I have posted this question and answer; to provide a solution, specifically for OData v4 and ODataConventionModelBuilder, in the hope that others won't lose the time I have looking into this.

First, set up your EdmModel:

private static IEdmModel GetEdmModel()
{
    var builder = new ODataConventionModelBuilder();
    builder.EnableLowerCamelCase();
    builder.EntitySet<Menu>("Menus");
    builder.EntitySet<MenuPermission>("MenuPermissions");
    var edmModel = builder.GetEdmModel();
    AddNavigations(edmModel); //see below for this method
    return edmModel;
}

Second AddNavigations:

private static void AddNavigations(IEdmModel edmModel)
{
    AddMenuPermissionsNavigation(edmModel);
}

private static void AddMenuPermissionsNavigation(IEdmModel edmModel)
{
    var menus = (EdmEntitySet) edmModel.EntityContainer.FindEntitySet("Menus");
    var menuPermissions = (EdmEntitySet)edmModel.EntityContainer.FindEntitySet("MenuPermissions");
    var menuType = (EdmEntityType) edmModel.FindDeclaredType("iiid8.cms.data.models.Menu"); //"iiid8.cms.data.models" is the C# namespace
    var menuPermissionType = (EdmEntityType)edmModel.FindDeclaredType("iiid8.cms.data.models.MenuPermission"); //as above, "iiid8.cms.data.models" is the C# namespace
    AddOneToManyNavigation("MenuPermissions", menus, menuPermissions, menuType, menuPermissionType);
    AddManyToOneNavigation("Menu", menus, menuPermissions, menuType, menuPermissionType);
}

private static void AddOneToManyNavigation(string navTargetName, EdmEntitySet oneEntitySet, EdmEntitySet manyEntitySet,
    EdmEntityType oneEntityType, EdmEntityType manyEntityType)
{
    var navPropertyInfo = new EdmNavigationPropertyInfo
    {
        TargetMultiplicity = EdmMultiplicity.Many,
        Target = manyEntityType,
        ContainsTarget = false,
        OnDelete = EdmOnDeleteAction.None,
        Name = navTargetName
    };
    oneEntitySet.AddNavigationTarget(oneEntityType.AddUnidirectionalNavigation(navPropertyInfo), manyEntitySet);
}

private static void AddManyToOneNavigation(string navTargetName, EdmEntitySet oneEntitySet, EdmEntitySet manyEntitySet,
    EdmEntityType oneEntityType, EdmEntityType manyEntityType) {
    var navPropertyInfo = new EdmNavigationPropertyInfo {
        TargetMultiplicity = EdmMultiplicity.One,
        Target = oneEntityType,
        ContainsTarget = false,
        OnDelete = EdmOnDeleteAction.None,
        Name = navTargetName
    };
    manyEntitySet.AddNavigationTarget(manyEntityType.AddUnidirectionalNavigation(navPropertyInfo), oneEntitySet);
}

Finally, call GetEdmModel from WebApiConfig.Register

config.MapODataServiceRoute("odata", null, GetEdmModel());

Now call your OData service's one-to-many and many-to-one navigations from your client and all should be good with your world. In my case the calls look like this:

One-to-many:

http://localhost:19215/Menus(c94f7f98-6987-e411-8119-984be10349a2)/MenuPermissions

Many-to-one:

http://localhost:19215/MenuPermissions(ba0da52a-6c87-e411-8119-984be10349a2)/Menu

This answer assumes you set up the rest of your project just like Mike Wasson suggests in the tutorial linked in the question (that link is to Part 3 - you will need to follow Part 1 first!).


I am using ASP.NET 5, Web API 2.2, and Entity Framework.

Another developer and I have also spent hours trying to figure out why, after following that same tutorial to a T, we couldn't get a relational route like the following to return anything other than a 404:

/odata/Supplier(1)/Products

We also tried the route debugger referenced in the OP, and it failed to produce anything other than a blank screen.

Luckily, for our needs, one of our random experiments worked, and that was to use the ODataRoute attribute like such:

    [EnableQuery]
    [ODataRoute("Suppliers({key})/Products")]
    public IQueryable<Product> GetProductsForSupplier([FromODataUri] int key)
    {
        ...
    }