Search code examples
odatarest

different routing to different OData controllers


I want to create several OData controllers that each has it's own route which is NOT the controller name. I've tried using ODataRoutePrefix attribute but it I get an error:

The path template 'People/Person' on the action 'Add' in controller 'Person' is not a valid OData path template. Resource not found for the segment 'People'.

This is the controller:

  namespace MyODataWebApplication.Controllers
{
    [ODataRoutePrefix("People")]
    public class PersonController : ODataController
    {
        private readonly List<Person> _persons = new List<Person>
        {
            new Person {Id = "1234", FirstName = "Person1", LastName = "Last1", Age = 25},
            new Person {Id = "2345", FirstName = "Person2", LastName = "Last2", Age = 45},
            new Person {Id = "3456", FirstName = "Person3", LastName = "Last3", Age = 25}
        };

        [ODataRoute("Person")]
        [HttpGet]
        public IQueryable<Person> Get()
        {
            return _persons.AsQueryable();
        }
    }
}

This is my WebApiConfig:

 public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var r = ODataRoutingConventions.CreateDefaultWithAttributeRouting("ODataRoute", config);
        config.MapODataServiceRoute("ODataRoute", "odata", GetEdmModel(), new DefaultODataPathHandler(), r);
    }

    public static IEdmModel GetEdmModel()
    {
        var builder = new ODataConventionModelBuilder
        {
            Namespace = "ODataTesting",
            ContainerName = "ODataTestingContainer"
        };

        builder.EntitySet<Person>("Person");
        builder.EntitySet<Animal>("Animal");

        return builder.GetEdmModel();
    }

}

The person entity is pretty straight forward:

   public sealed class Person
{
    [Key]
    public string Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

Any thoughts on what should I do in order to be able to call {my prefix}/odata/people/Person? I'm using .Net framework 4.7 for now Thanks!


Solution

  • In your Register method you have pretty said that you want to create the default routing convention, but allow attributes to override the controller and end point names, then map your EdmModel using those routes.

    The common misconception is that CreateDefaultWithAttributeRouting allows you to define your own custom routes, all it is really doing though is allowing you to change how those default routes are mapped to your controllers.

    This is sort of ambiguous in the documentation, the whole point of OData is to follow the standards, not to break them. The purpose of the ODataRoutePrefix is to allow you to map controllers in your code that do not follow the standard OData naming convention to the standard routes, that way you can still be compliant to the standards without having to refactor your entire code base for an existing API, or at least so you can manage your code and class names how you want to.

    Lets be clear: OData Routes are validated against and therefore MUST be mapped against the EdmModel. You cannot make arbitrary routes via attributes, the attributes on your controller class and methods simply tell the HTTP processing pipeline how to identify the correct methods to invoke.

    TL;DR

    The following is a walkthrough starting from standard examples, scroll through to a final solution that matches your requirement, then read back across the rest to decide if you still want to implement your original route, I suspect that /people/person is a mistake, or that /people is supposed to return all and /people/person(key) should return just one...

    Correct Usage of OData Route Attributes

    OData v4 Routing Conventions are an OASIS standard, read the spec here.

    So the standard route for your example, if you removed the ODataRoutePrefix and the ODataRoute attributes, would map this url to your Get() method:

    /odata/Person
    

    This resolves because your controller class name matches the expected convention PersonController.

    This expectation is set by this line in GetEdmModel():

    builder.EntitySet<Person>("Person");
    

    To be clear, the default convention is that a class that has a prefix "Person", a suffix "Controller" (so is therefore named PersonController) and inherits from ODataController

    The same concept applies to ODataRouteAttribute. According to the specs, the expected route to return all the resources is /Resource, and the expected route for a single resource is /Resource(key).

    For the end points, you almost always need to specify the [ODataRoute] for the standard CRUD endpoints, even on your conforming controllers, however if your do not have to provide the template if the rest of your method matches the expected conventions.

    The default routing convention expects that on your controller will be an endpoint similar to this:

    [HttpGet]
    [ODataRoute]
    public IHttpActionResult Get([FromODataUri] int key)
    {
        return _persons.Single(x => x.Id == key);
    }
    

    Therefore the correct way to use ODataRoutePrefixAttribute is when your controller class does not have the conventional name, but you still want it to resolve into the OData standard convention, so the following would work, against the standard convention.

    namespace MyODataWebApplication.Controllers
    {
        [ODataRoutePrefix("Person")]
        public class UnconventionalControllerName : ODataController
        {
            private readonly List<Person> _persons = new List<Person>
            {
                new Person {Id = "1234", FirstName = "Person1", LastName = "Last1", Age = 25},
                new Person {Id = "2345", FirstName = "Person2", LastName = "Last2", Age = 45},
                new Person {Id = "3456", FirstName = "Person3", LastName = "Last3", Age = 25}
            };
    
            [ODataRoute()]
            [HttpGet]
            public IQueryable<Person> ReturnThemAll()
            {
                return _persons.AsQueryable();
            }
    
            [ODataRoute("({theIdOfTheOneYouWant})")]
            [HttpGet]
            public Person JustOnePlease(int theIdOfTheOneYouWant)
            {
                return _persons.Single(x => x.Id == key);
            }
        }
    }
    

    Work Around

    Although I doubt this would work for your entire API, one option to satisfy your requirement is to map the whole EdmModel to the prefix odata/people.
    If you do this you would probably only specify people related entities in your EdmModel

    public static void Register(HttpConfiguration config)
    {
        var r = ODataRoutingConventions.CreateDefaultWithAttributeRouting("ODataRoute", config);
        config.MapODataServiceRoute("ODataRoute", "odata/people", GetEdmModel(), new DefaultODataPathHandler(), r);
    }
    

    However you would still need to remove the ODataRoutPrefixAttribute and the "Person" template from the ODataRouteAttribute:

    namespace MyODataWebApplication.Controllers
    {
        // Remove this attribute, this controller already matches the convention
        //[ODataRoutePrefix("People")]
        public class PersonController : ODataController
        {
            private readonly List<Person> _persons = new List<Person>
            {
                new Person {Id = "1234", FirstName = "Person1", LastName = "Last1", Age = 25},
                new Person {Id = "2345", FirstName = "Person2", LastName = "Last2", Age = 45},
                new Person {Id = "3456", FirstName = "Person3", LastName = "Last3", Age = 25}
            };
    
            [ODataRoute()] // remove "Person" template
            [HttpGet]
            public IQueryable<Person> Get()
            {
                return _persons.AsQueryable();
            }
        }
    }
    

    Custom Routes

    You can of course still use standard System.Web.Http Routes and map those to your OData controllers to execute, ODataController inherits from ApiController afterall, but those routes will not be published or exposed via the OData $metadata

    if you wanted to expose a custom collection of People from the PersonController via a url like this /odata/Person/People then that could be configured like this:

    builder.EntitySet<Person>("Person")
           .EntityType.Collection.Function("People")
           .ReturnsCollectionFromEntitySet<Person>("Person");
    

    then on your controll you could have:

    // using conventional name, no need for [ODataRoute] 
    [HttpGet]
    [EnableQuery]
    public IQueryable<Person> People(ODataQueryOptions<Person> options)
    {
        return _persons.AsQueryable();
    }
    

    finally the originally required route!

    No judgement... if you want {my prefix}/odata/people to map to your PersonController then you first need to change the configuration to expect this route, then your ODataRoutePrefix can be used, however it will not map the /Person route to the get method, to do that you would have to use the previous trick to declare a custom collection function, the result would look like this:

    builder.EntitySet<Person>("People")
           .EntityType.Collection.Function("Person")
           .ReturnsCollectionFromEntitySet<Person>("People");
    

    Controller:

    namespace MyODataWebApplication.Controllers
    {
        [ODataRoutePrefix("People")]
        public class PersonController : ODataController
        {
            private readonly List<Person> _persons = new List<Person>
            {
                new Person {Id = "1234", FirstName = "Person1", LastName = "Last1", Age = 25},
                new Person {Id = "2345", FirstName = "Person2", LastName = "Last2", Age = 45},
                new Person {Id = "3456", FirstName = "Person3", LastName = "Last3", Age = 25}
            };
    
            [ODataRoute("Person")]
            [HttpGet]
            public IQueryable<Person> Get()
            {
                return _persons.AsQueryable();
            }
        }
    }
    

    The difference is hard to spot, and is especially complicated in this example where the route, controller and class names all switch around between the english pluralisation conventions between People, Person and Persons. Its hard for instance for me to determine which one of these elements you have used incorrectly, so I went in based on the assumption that your data class names would be what you want.

    Either way your expected URL is unconventional which by definition makes it hard to implement using the standard conventions.