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!
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.
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...
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);
}
}
}
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();
}
}
}
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();
}
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.