Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

CRUD with oData V4 and ASP.NET Web API

0.00/5 (No votes)
10 Feb 2016 1  
Quick introduction to oData (v4) by understanding how to implement CRUD method using ASP.NET Web API

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.

Model classes

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.

// Person.cs file
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; }
    }
}
// StarRating.cs file
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace ODataMovies.Models
{
    public enum StarRating
    {
        OneStar,
        TwoStar,
        ThreeStar,
        FourStar,
        FiveStar
    }
}
// Movie.cs file
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:

// WebApiConfig.cs file
public static void Register(HttpConfiguration config)
{
	ODataConventionModelBuilder modelBuilder = new ODataConventionModelBuilder();
	modelBuilder.EntitySet<Movie>("Movies"); // We are exposing only Movies via oData
        config.MapODataServiceRoute
        ("Movies", "odata", modelBuilder.GetEdmModel()); // Specify the routing
            
        /* Old Stuff which we don't need
        // Web API configuration and services

	// Web API routes
	config.MapHttpAttributeRoutes();

	config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
	);
	*/
}

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.

// DataService.cs file
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.

// MoviesController.cs file
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).

/// <summary>
/// Creates a new movie. 
/// Use the POST http verb.
/// Set Content-Type:Application/Json
/// Set body as: { "Id":0,"Title":"A new movie",
///                "ReleaseDate":"2015-10-25T00:00:00+05:30","Rating":"FourStar" }
/// </summary>
/// <param name="movie"></param>
/// <returns></returns>
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:

Creating a new movie

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

Image 3

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.

/// <summary>
/// Saves the entire Movie object to the object specified by key (id). 
/// Is supposed to overwrite all properties
/// Use the PUT http verb
/// Set Content-Type:Application/Json
/// Set body as: { "Id":0,"Title":"StarWars - The Force Awakens",
///                "ReleaseDate":"2015-10-25T00:00:00+05:30","Rating":"FourStar" }
/// </summary>
/// <param name="key"></param>
/// <param name="movie"></param>
/// <returns></returns>
public IHttpActionResult Put(int key, Movie movie)
{
    try
    {
            movie.Id = key;
        return Ok&lt;Movie&gt;(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 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:

Image 4

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.

/// <summary>
/// Use the DELETE http verb
/// Request for odata/Movies(1)
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
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.

/// <summary>
/// Use the PATCH http Verb
/// Set Content-Type:Application/Json
/// Call this using following in request body: { "Rating":"ThreeStar" }        /// 
/// </summary>
/// <param name="key"></param>
/// <param name="moviePatch"></param>
/// <returns></returns>
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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here