Introduction
If you're reading this article there's a good chance you already know about oData - what it is and why you want to use it. OData is a standard for querying for information and is built on top of REST. It standardizes certain query operations like limiting number of returned objects, performing paging, counting returned elements, selecting objects based on conditions and many more. We'll concentrate on implementing all the CRUD operations required on a resource using OData V4 and the ASP.NET Web API.
Background
OData is an open protocol and is gaining support from number of informaiton providers like SalesForce, Netflix and others.There are a number of good introductory articles on oData like this one: https://msdn.microsoft.com/en-us/data/hh237663.aspx. The code presented in this article is simple and even if you don't know much about oData but are familiar with REST or even Web Services in general, you should be able to understand how simple it is to use oData.
Using the code
For this article we'll be exposing movies via oData. A Movie has an id, title, rating, last-modified-timestamp, director. Rating is an enumeration of stars from one to five, director is a Person which has a first-name and a last-name. These classes make up the 'model' of our application.

OData services can be written using WCF or ASP.NET Web API. In this article we'll use ASP.NET Web API. We'll call the project ODataMovies and make the data available under the uri /odata. This means all endpoints will have an address like: http://localhost/odata/
Creating the project
Fire up Visual Studio 2015 and create a new Web Project. In the following screen select:
- Select Empty from the ASP.NET 4.5.2 Templates.
- Uncheck the 'Host in the cloud' checkbox.
- Uncheck 'Web Forms' and 'MVC' checkboxes.
- Select only the Web API checkbox.
Install oData Nuget package
From the main menu select Tools -> NuGet Package Manager -> Package Manager Console. At the PM> prompt enter:
Install-Package Microsoft.AspNet.OData -Version 5.8.0
We are explicitly specifying the version here. You may not be needed. As of this writing, 5.8.0 is the latest version and this is what will get installed if you don't specify the version explicitly. Having said that, the oData team seems to be changing some classes and even removing some like EnititySetManager which was present in earlier version so locking down the version will ensure that the project will work even if new version of oData is released.
Add the model classes
First we'll add the model classes which are Movie, Person and an enum called StarRating. Its a good idea to create these .cs files within the Models folder though its not necessary.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace ODataMovies.Models
{
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace ODataMovies.Models
{
public enum StarRating
{
OneStar,
TwoStar,
ThreeStar,
FourStar,
FiveStar
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace ODataMovies.Models
{
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public StarRating Rating { get; set; }
public Person Director { get; set; }
public DateTime LastModifiedOn
{
get { return m_lastModifiedOn; }
set { m_lastModifiedOn = value; }
}
public Movie CopyFrom(Movie rhs)
{
this.Title = rhs.Title;
this.ReleaseDate = rhs.ReleaseDate;
this.Rating = rhs.Rating;
this.LastModifiedOn = DateTime.Now;
return this;
}
private DateTime m_lastModifiedOn = DateTime.Now;
}
}
We will be exposing only the Movie object via our oData service.
Enable oData routing
Routing refers to understanding the URL format and translating that to method calls. For e.g. if someone accesses http://localhost/odata/Movies, this should fire a method where we can write code to access movie objects and make them available to the client. This routing setup is done in the App_Start/WebApiConfig.cs file. Replace the existing code in the Register method with:
public static void Register(HttpConfiguration config)
{
ODataConventionModelBuilder modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Movie>("Movies");
config.MapODataServiceRoute("Movies", "odata", modelBuilder.GetEdmModel());
}
Writing the business layer
We'll have a thin business layer which will provide services to store and retrieve model objects i.e. the movie objects.
Create a new folder called Business. Create a new file named DataService.cs. Instead of actually storing the data in a database, we'll just work with an in memory list of model objects.
using ODataMovies.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace ODataMovies.Business
{
public class DataService
{
public List<Movie> Movies
{
get { return m_movies; }
}
public Movie Find(int id)
{
return Movies.Where(m => m.Id == id).FirstOrDefault();
}
public Movie Add(Movie movie)
{
if (movie == null)
throw new ArgumentNullException("Movie cannot be null");
if (string.IsNullOrEmpty(movie.Title))
throw new ArgumentException("Movie must have a title");
if (m_movies.Exists(m => m.Title == movie.Title))
throw new InvalidOperationException("Movie already present in catalog");
lock(_lock)
{
movie.Id = m_movies.Max(m => m.Id) + 1;
m_movies.Add(movie);
}
return movie;
}
public bool Remove(int id)
{
int index = -1;
for (int n=0; n < Movies.Count && index == -1; n++) if (Movies[n].Id == id) index = n;
bool result = false;
if (index != -1)
{
lock(_lock)
{
Movies.RemoveAt(index);
result = true;
}
}
return result;
}
public Movie Save(Movie movie)
{
if (movie == null) throw new ArgumentNullException("movie");
Movie movieInstance = Movies.Where(m => m.Id == movie.Id).FirstOrDefault();
if (movieInstance == null) throw new ArgumentException(string.Format("Did not find movie with Id: {0}", movie.Id));
lock (_lock)
{
return movieInstance.CopyFrom(movie);
}
}
private static List<Movie> m_movies = new Movie[]
{
new Movie { Id = 1, Rating = StarRating.FiveStar, ReleaseDate = new DateTime(2015, 10, 25), Title = "StarWars - The Force Awakens", Director = new Person { FirstName="J.J.", LastName="Abrams" } },
new Movie { Id = 2, Rating = StarRating.FourStar, ReleaseDate = new DateTime(2015, 5, 15), Title = "Mad Max - The Fury Road", Director = new Person { FirstName ="George", LastName="Miller" } }
}.ToList();
private object _lock = new object();
}
}
Enabling the oData (REST) methods
In order to serve oData request we need to create a controller class. A controller is responsible for managing a resource, for us the resource is the movie collection. When we registered the route, the following line exposed the movie resource:
modelBuilder.EntitySet<Movie>("Movies"); // We are exposing only Movies via oData
This implies that we should create a controller class named MoviesController. Methods to handle various Http verbs like Get, Post, Put & others are implemented by methods of the same name e.g. to handle a Post verb, write a method called Post.
Let's first expose the movie collection. Create a class called MoviesController which derives from ODataController.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using ODataMovies.Models;
using ODataMovies.Business;
using System.Web.OData;
using System.Web.Http;
using System.Net;
using System.Diagnostics;
namespace ODataMovies.Controllers
{
public class MoviesController : ODataController
{
[EnableQuery]
public IList<Movie> Get()
{
return m_service.Movies;
}
private DataService m_service = new DataService();
}
}
Compile and run the project by hitting F5. Type in the following url in a browser: http://localhost:32097/odata/Movies
Note: replace the 32097 port with your port. You should see a JSON response like:
{
"@odata.context":"http://localhost:32097/odata/$metadata#Movies","value":[
{
"Id":1,"Title":"StarWars - The Force Awakens","ReleaseDate":"2015-10-25T00:00:00+05:30","Rating":"FiveStar","Director":{
"FirstName":"J.J.","LastName":"Abrams"
},"LastModifiedOn":"2016-01-26T13:29:10.2039858+05:30"
},{
"Id":2,"Title":"Mad Max - The Fury Road","ReleaseDate":"2015-05-15T00:00:00+05:30","Rating":"FourStar","Director":{
"FirstName":"George","LastName":"Miller"
},"LastModifiedOn":"2016-01-26T13:29:10.2044867+05:30"
}
]
}
That's quite cool to be able to expose data so easily. Not just that, try this:
http://localhost:32097/odata/Movies?$top=1
You'll see a just one record.
OData has a number of filters like eq, gt, lt and many more. Let's try the eq filter. Enter the following url in the web-browser:
http://localhost:32097/odata/Movies?$filter=Title eq 'Mad Max - The Fury Road'
The result will match the movie which has the title 'Mad Max - The Fury Road'. This magic is happening because of oData library which we are using. Since we used the [EnableQuery] attribute, oData library is adding a filter to the result-set. When working with EF (Entity Framework), it may be better to return IQueryable to leverage features like deferred execution and query optimization.
Retrieving a specific item
Retrieving a specific item is also handled by the GET verb, we just need some additional information in the url. Add the following code in the controller class:
public Movie Get([FromODataUri] int key)
{
IEnumerable<Movie> movie = m_service.Movies.Where(m => m.Id == key);
if (movie.Count() == 0)
throw new HttpResponseException(HttpStatusCode.NotFound);
else
return movie.FirstOrDefault();
}
To invoke this url, use a url like: http://localhost:32097/odata/Movies(1). Here 1 is the ID or key. Since we are using 5.8.0 oData library it's not necessary to use the [FromODataUri] attribute, even if you do remove it, the code will work just fine.
Adding an item
Having enabled fetching data by implementing Get, lets implement adding a new movie. Adding items is usually handled by using the POST verb. The data is sent along with the body of the http request. Since we are using 5.8.0, there is no need to specify the [FromBody] attribute e.g. public IHttpActionResult Post([FromBody] Movie movie).
public IHttpActionResult Post([FromBody] Movie movie)
{
try
{
return Ok<Movie>(m_service.Add(movie));
}
catch(ArgumentNullException e)
{
Debugger.Log(1, "Error", e.Message);
return BadRequest();
}
catch(ArgumentException e)
{
Debugger.Log(1, "Error", e.Message);
return BadRequest();
}
catch(InvalidOperationException e)
{
Debugger.Log(1, "Error", e.Message);
return Conflict();
}
}
To test this, you'll need an application which can make HTTP POST requests and pass along headers and content like Telerik's Fiddler. Get it from www.telerik.com/fiddler, its free.
Build the project & launch it by hitting F5 in Visual Studio. Launch Fiddler after installing it. In Fillder do the following:
- Click the 'Composer' tab. Copy the url from the browser which was launched by Visual Studio and modify the url to: http://localhost:32097/odata/Movies and paste it in the address textbox.
- Select 'POST' from the dropdown
- Enter Content-Type: Application/JSon in the header textbox.
- Paste
{ "Id":1,"Title":"Transformers - 4","ReleaseDate":"2015-10-25T00:00:00+05:30","Rating":"FiveStar","Director":{ "FirstName":"Not","LastName":"Sure" } }
into the request body textbox. - Hit execute.
Here is a screenshot of the request:

On Fiddler's left pane you should see your request. Double click to see the details:

The important thing to notice is that we got back HTTP status code of 200 which means OK. The response text contains a JSON object which is the newly created movie object, check the Id; its 3.
Implementing PUT
PUT method is used to update an existing resource. Paste the following code in the controller to implement PUT.
public IHttpActionResult Put(int key, Movie movie)
{
try
{
movie.Id = key;
return Ok<Movie>(m_service.Save(movie));
}
catch(ArgumentNullException)
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
catch(ArgumentException)
{
return NotFound();
}
}
In this case if we want to communicate back errors, we need to throw HttpResponseException with the correct HTTP error code. In the previous case we could just return the HTTP status using methods like NotFound(). When you throw an HttpResponseException its converted to an http status by the Web API framework anyway.
Let's hit the method using Fiddler using the URL below. We must be careful to specify the proper HTTP header which in this case is:
Content-Type: Application/Json
http://localhost:32097/odata/Movies(2)
The properties we want to update against the movie having the id 2 needs to be specified in the request body. Even the properties which haven't changed need to be specified as PUT is supposed to blindly update all the properties. For this example, et's try to give FiveStars to Mad-Max. Use the following text in the request body:
{
"Id":2,"Title":"Mad Max - The Fury Road","ReleaseDate":"2015-05-15T00:00:00+05:30","Rating":"FiveStar","Director":{
"FirstName":"George","LastName":"Miller" }
}
Here's a screenshot of how the request looks in Fiddler's composer:

If all goes well, the response should be the same movie with all the updated properties
Implementing DELETE
Delete is implemented the same way. Note that if a key is not found, we return the appropriate HTTP status code which is NOT FOUND.
public IHttpActionResult Delete(int key)
{
if (m_service.Remove(key))
return Ok();
else
return NotFound();
}
To test DELETE, use Fiddler and select the 'DELETE' verb. We need to specify which object to delete by its id. This is done by a url of the following format: http://localhost:32097/odata/Movies(2) This will delete the movie with id = 2.
Implementing PATCH
The last method we'll implement is PATCH. This method is like PUT except that in PUT all the properties of the passed-in object are copied into an exisitng object, where as in PATCH, only properties that have changed are applied to an exisitng object. Property changes are passed using the Delta class object. This is a template class which defined as method named CopyChangedValues() which copies the changed properties to the target object.
public IHttpActionResult Patch(int key, Delta<Movie> moviePatch)
{
Movie movie = m_service.Find(key);
if (movie == null) return NotFound();
moviePatch.CopyChangedValues(movie);
return Ok<Movie>(m_service.Save(movie));
}
To invoke the patch method, use the url http://localhost:32097/odata/Movies(1).
This will apply the properties to the movie having id = 1.
Set the content-type to Application/Json and select 'PATCH' verb in Fiddler.
In case of success, we are returning HTTP status code OK (200) and also returning the entire object in JSON format in the response body.
Summary
To conclude, oData formalizes exposing resources and supporting CRUD operations pretty well. The oData library provides useful filtering options which you get for free by using the [EnableQuery] attribute on the controller methods.
History
First written on 6th Feb, 2016.