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 information providers like SalesForce, Netflix and others. There are a number of good introductory articles on oData like this one. 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
. It's a good idea to create these .cs files within the Models folder though it's 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 example, 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");
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 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
, let's 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, it's free.
Build the project & launch it by hitting F5 in Visual Studio. Launch Fiddler after installing it. In Fiddler, 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; it's 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
, it's 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
need 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, let's try to give FiveStar
s 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 existing object, where as in PATCH
, only properties that have changed are applied to an existing object. Property changes are passed using the Delta
class object. This is a template class which is 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
- 6th February, 2016: Initial version