다음을 통해 공유


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 controller

  • Anonymous
    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/.../2072

  • Anonymous
    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/.../845

  • Anonymous
    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
    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.