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"
}