Search code examples
asp.net-mvcodataattributerouting

Attribute routing for two actions leads to a "Not valid OData path template"


So i have two functions that return a customer, which get feeded by two different parameters. One being the ID of the customer and the other being his customer number.

My controller:

using System.Linq;
using System.Net;
using System.Web.Http;
using System.Web.OData;
using System.Web.OData.Routing;
using Models;
using AutoMapper;
using AutoMapper.QueryableExtensions;
using System.Web.OData.Extensions;
using Importing;
using Objects;
using Microsoft.OData;

namespace Controllers
{
    public class CustomersController : ODataController
    {
        // GET: CustomerByCNO(5)
        [HttpGet]
        [ODataRoute("CustomerByCNO({key})")]
        [EnableQuery]
        public SingleResult<CustomerDTO> GetCustomerByCNO([FromODataUri]string key)
        {
            Import i = new Import();

            var customer = i.GetCustomer(key).ProjectTo<CustomerDTO>().AsQueryable();

            return SingleResult.Create(customer);
        }

        // GET: Customer(5)
        [HttpGet]
        [ODataRoute("Customer({id})")]
        [EnableQuery]
        public SingleResult<CustomerDTO> Get([FromODataUri]int id)
        {
            Import i = new Import();

            var customer = i.GetCustomer(id).ProjectTo<CustomerDTO>().AsQueryable();

            return SingleResult.Create(customer);
        }
    }
}

Initialization:

using AutoMapper;
using Models;
using Objects;
using System.Web.Http;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
using Microsoft.OData.Edm;

namespace API
{
    public static class WebApiConfig
    {
        public static void ConfigureAPI(HttpConfiguration config)
        {
            config.MapODataServiceRoute(
                routeName: "odata",
                routePrefix: "",
                model: GetEdmModel()
            );

            config.EnsureInitialized();
        }

        private static IEdmModel GetEdmModel()
        {
            ODataConventionModelBuilder builder = new ODataConventionModelBuilder
            {
                Namespace = "Controllers",
                ContainerName = "DefaultContainer"
            };
            builder.EntitySet<CustomerDTO>("Customer")
                .EntityType.HasKey(c => c.Id)
                .CollectionProperty(c => c.CustomFields);

            var edmModel = builder.GetEdmModel();
            return edmModel;
        }
    }
}

While the second functions works as intended the first functions does not and the EnsureInitialized() function throws an InvalidOperationException saying, that it is no valid OData path template and that no resource has been found. How can i get this to work? Not quite sure what i am missing here.

UPDATE 1:

Changing the Controller method to this:

        [HttpGet]
        [ODataRoute("CustomerByNo(No={no})")]
        public SingleResult<CustomerDTO> CustomerByNo([FromODataUri] int no)
        {
            Import i = new Import();

            var customer = i.GetCustomer(no.ToString()).ProjectTo<CustomerDTO>().AsQueryable();

            return SingleResult.Create(customer);
        }

with this additional line in the config:

        builder.Function("CustomerByNo").Returns<SingleResult<CustomerDTO>>().Parameter<int>("No");

Made it so i can access the functions at least. I had to change the parameter to an int as well, seems like it doesnt like strings? However the return value is not deserialized and shown as usual. Also if i leave the [EnableQuery] line in the method declaration, the call will crash saying that it doesnt know how to deserialize since it is not bound to the entityset of Customer i guess.

Trying it this way however, leads to the original error message, that the resource could not be found:

        builder.EntityType<CustomerDTO>().Collection.Function("CustomerByNo").Returns<SingleResult<CustomerDTO>>().Parameter<int>("No");

Solution

  • You have to declare your custom odata functions in the convention model:

    FunctionConfiguration customerByCNOFunction = builder.Function("CustomerByCNO");
    customerByCNOFunction.Returns<CustomerDTO>();
    customerByCNOFunction.Parameter<string>("key");
    

    Update :

    My first answer was for declaring a functions that returns a type not queryable in odata. To enable query, the function needs to return an odata entity from an entity set :

    builder.Function("CustomerByNo").ReturnsFromEntitySet<CustomerDTO>("Customer").Parameter<int>("No")