Web API OData V4 Lessons Learned
Developing an OData service using Web API is relatively simple if you stay within the routing conventions. This post shows you how to deal with pitfalls when your requirements require attribute based routing and non-standard entity keys.
The goal is to add OData support to an existing Web API service that returns Person entities from a resource path called “people”. The Email property is the key of a Person entity. An email address can contain periods.
This seems like a simple coding task but look at all the pitfalls you’ll encounter. Most are 404 Not Found errors due to routing issues which makes debugging difficult. To avoid these pitfalls start new services with the sample code below and test that it works using the sample OData calls before proceeding with coding.
The behavior was tested against Microsoft ASP.NET Web API 2.2 for OData v4.0 5.3.1, which is the latest released version available on NuGet.
Pitfall #1 – Existing Web API Controller
You have an existing Web API controller called PeopleController with routing attributes like [Route("api/people")]. You’d like your OData service to use the same /api/people path.
Solution
Web API in ASP.NET 5, the next major version of Web API, allows you to code Web API and OData methods alongside each other in the same controller.
Workaround
In the current Web API version Web API controllers derive from ApiController and OData controllers derive from ODataController, which itself derives from ApiController. You could try deriving PeopleController from ODataController but that will change your service.
To preserve backwards compatibility for existing service callers, create a new controller for your OData methods and isolate the routing to a new path such as /odata/people. The original service will continue to route to /api/people.
The odata portion of the path is controlled by the routePrefix argument of the MapODataServiceRoute() method call in WebApiConfig’s Register() method:
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: "odata",
model: builder.GetEdmModel());
Pitfall #2 – Controller Name is Already Taken
Routing conventions require your new controller to be called PeopleController but your existing Web API controller already uses that name.
Solution
Use the ODataRoutePrefix attribute to control routing:
[ODataRoutePrefix("people")]
public class FooController : ODataController
As shown above the controller name is now irrelevant.
Pitfall #3 – Entity Name is Different than Routing Name
The controller name is normally the plural form of the entity name returned by the service. The convention for pluralizing is to append an “s” to the entity name. For example, the ASP.NET samples use Product and ProductsController for names. In English the plural of person is people and hence the requirement for the route named “people” used in the routing prefix above.
Solution
In WebApiConfig change the Register() method so that Person EntitySet is associated with the people route:
builder.EntitySet<Person>("people")
Pitfall #4 – Order of Statements in WebApiConfig Register()
During Application_Start() System.InvalidOperationException is thrown by Web API when it calls System.Web.Http.Dispatcher.DefaultHttpControllerSelector.GetControllerMapping() with Message=ValueFactory attempted to access the Value property of this instance.
Solution
The order of calls in WebApiConfig’s Register() method is important. The OData related configuration calls must come after the Web API configuration calls. The config.MapHttpAttributeRoutes() call appears to require this.
Pitfall #5 – OData Method Routing Not Working
Calling the OData service returns 404 Not Found.
Solution
When the ODataRoutePrefix attribute is used it becomes important to add ODataRoute and EnableQuery attributes to the methods.
Note that the WebApiConfig config.AddODataQueryFilter() method call changes the behavior such that the attributes are not needed for IQueryable returning methods. There are other side effects though so I prefer to use the attributes instead of calling AddODataQueryFilter().
Pitfall #6 – Entity Key Property Name
System.Web.OData.Builder.ODataModelBuilder.ValidateModel(IEdmModel model) throws System.InvalidOperationException with Message=The entity 'Person' does not have a key defined.
Solution
By convention the entity key property is named Id but the Person key is Email so provide a key selecting function to the EntitySetConfiguration:
builder.EntitySet<Person>("people").EntityType.HasKey(p => p.Email);
Pitfall #7 – Get Entity by Key Argument Name
Calling the OData service to get a person by email address returns 404 Not Found.
Solution
By convention the method argument must be called “key” as in:
public SingleResult<Person> Get([FromODataUri] string key)
When using attribute routing the convention does not apply and the argument name must be specified in the ODataRoute attribute:
[ODataRoute("({email})")]
public SingleResult<Person> Get([FromODataUri] string email)
Note, even if the argument is named “key”, [ODataRoute("({key})")] is still required.
Pitfall #8 – Key Contains a Period
Calling the OData service to get a person by email address returns 404 Not Found.
Solution
Email addresses contain periods. A period in an HTTP URL normally identifies the file extension and IIS uses the extension to identify which module to route requests to. Change this behavior so that Web API gets a chance to route the request by adding the following setting to <system.webServer> in web.config:
<modules runAllManagedModulesForAllRequests="true" />
Requests for static content will experience a performance hit so this setting might not be appropriate for all servers.
I tried wrapping the setting in a <location path="odata"> element but apparently the location element only works with paths that exists. If anyone knows how to work around this please tell how in a comment. Thanks.
Pitfall #9 – Strings in OData URL’s
Calling the OData service to get a person by email address returns a custom 404 Not Found error with:
Server Error in '/' Application.
The resource cannot be found.
Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.
Solution
Most samples show entity keys which are of integer type, as in Get([FromODataUri] int key), and called via /odata/entities(1).
As a C# developer we’re used to typing strings with double quotes but URL’s require single quotes. Get([FromODataUri] string email) must be called with single quotes as https://localhost:8131/odata/people('2@email.com').
Starter Sample
Sample OData Calls
https://localhost:8131/odata/people('2\@email.com')
https://localhost:8131/odata/people
https://localhost:8131/odata/people?$count=true
https://localhost:8131/odata/people?$filter=Name eq 'User3'
https://localhost:8131/odata/people?$filter=startswith(Name,'User1')&$count=true
Results of last sample call shows that count and paging are working:
{
"@odata.context":"https://localhost:8131/odata/$metadata#people","@odata.count":11,"value":[
{
"Email":"1@email.com","Name":"User1"
},{
"Email":"10@email.com","Name":"User10"
},{
"Email":"11@email.com","Name":"User11"
},{
"Email":"12@email.com","Name":"User12"
},{
"Email":"13@email.com","Name":"User13"
}
],"@odata.nextLink":"https://localhost:8131/odata/people?$filter=startswith%28Name%2C%27User1%27%29&$count=true&$skip=5"
}
Comments
Anonymous
December 21, 2014
Hi David, As most of your question is about the existing Web API Controller, may I know how you auto generate the controller? As far as I know, webapi 2.2 for odata v4.0 need manually add the controllerAnonymous
December 29, 2014
I would suggest you add the pitfall of DateTime vs DateTimeOffset - that's the one I fell into :-) Here's a workaround: damienbod.wordpress.com/.../web-api-and-odata-v4-crud-and-actions-part-3 If you would like, you can vote to reinstate DateTime here: aspnetwebstack.codeplex.com/.../2072Anonymous
January 13, 2015
Liang and I exchanged emails offline. There is no VS tooling for creating OData v4 services yet. I manually created the code following the tutorial here: www.asp.net/.../create-an-odata-v4-endpoint It is much easier to get the code working when using convention based routing so I'll avoid attribute based routing when I can. Also, for #6 adding the [key] attribute to the entity is another solution.Anonymous
February 02, 2015
Thanks for the post, useful things that are good to know. Interestingly, in our case we ended up overriding convention based routing to route calls to right controller and actions based on versions(attributes on controller & model) which kind of achieves what ODataRoutePrefix.Anonymous
May 28, 2015
Everytime I see "Different.... than" I cringe. "Different from" is the correct usage. Ref: english.stackexchange.com/.../845Anonymous
June 09, 2015
I got into a situation that the POST always returns 404 Resource not found error even though the get works fine. Anyone has a clue?Anonymous
October 28, 2015
@Shiva...you have serious problems and need help. This post is a fantastic resource that is well organized and delivered with a purpose and the only thing you have to offer up is a grammar lesson? It must be sad to be you. I'm sooooooooo sorry that his grammar is DIFFERENT THAN yours. David, thanks for this post. It answered a lot of questions and was a great resource for helping me wade into the deep end of OData.Anonymous
November 14, 2015
This article solved some frustrating problems for me (especially #7). Thank you very much for taking time to write it.Anonymous
September 07, 2017
Hi David,Have you implemented a versioned Web API solution and tried to add (versioned or not) OData routes upon it? Could you comment on that pitfall?Regards- Anonymous
June 06, 2018
Hi, sorry for the late response. I have used the approach shown here: https://github.com/Microsoft/aspnet-api-versioning/blob/master/samples/webapi/BasicODataWebApiSample/Controllers/People2Controller.csAnd discussed here: https://www.hanselman.com/blog/ASPNETCoreRESTfulWebAPIVersioningMadeEasy.aspx
- Anonymous
Anonymous
December 20, 2017
Question, has pitfall #1 (Controller having both OData and WebAPI methods methods) been resolved other than the work around mentioned in the article? I am interested in a sample implementation if this has been overcome.- Anonymous
June 06, 2018
Hi, I just saw your comment. It has been a long time since I worked with OData so I don't know if anything changed. My current org within Microsoft feels that OData enables the calling developer to negatively impact the service so we are using .NET Core for pure REST API's. We are also using Azure Functions. If I encountered this problem today, I'd hide my service behind Azure API Management, which would enable me to define an API Gateway that transformed/forwarded requests to my controls, Azure Functions, or whatever else I needed to call.
- Anonymous