Channels ▼
RSS

.NET

Using OData from ASP.NET


OData (Open Data Protocol) is a standardized protocol for creating and consuming data APIs through regular HTTP requests and REST. It is the “ODBC of the Web” and provides a standard solution for very common Web API patterns. For example, OData includes support for queries, paging, and batching, among many other typical requirements.

Creating an OData Endpoint

Microsoft ASP.NET Web API OData adds most of the things necessary to create OData endpoints and easily provide OData query syntax using the ASP.NET Web API (initially introduced in .NET Framework 4.5). ASP.NET Web API 2.2 added many improvements that make it easier to work with OData, including support for OData 4.0. In this article, I provide an example that uses the features provided in Microsoft ASP.NET Web API 2.2 OData.

Creating an OData Endpoint

OData is a standardized protocol for creating and consuming data APIs through regular HTTP requests and REST. One of the most interesting things about OData is that it provides a standard solution for very common Web API patterns. For example, OData includes support for queries, paging, and batching, among many other typical requirements. ASP.NET Web API OData provides many components to make it easy to implement OData services.

ASP.NET Web API 2 was included in Visual Studio 2013 and provided support for OData 3.0. The recently launched ASP.NET Web API 2.2 added support for the newest version of the standardized protocol: OData 4.0. In addition, the newest ASP.NET Web API version includes many interesting improvements related to OData, such as attribute routing, model aliasing, support for Enums, ETags, the $format query option, and the ability to specify which properties can be sorted, expanded, filtered, or navigated.

I want to work with ASP.NET Web API 2.2 in Visual Studio 2013, so I have to run a few NuGet Package Manager Console commands. I'll explain the steps to create a project that generates an OData endpoint and allows you to run OData queries on a simple Games entity with support provided by Entity Framework, but you don't have to use Entity Framework to take advantage of ASP.NET Web API OData. You can use a similar structure with other data sources.

Create a new ASP.NET Web Application project in Visual Studio 2013. I'll use Games for both the project and solution names.

Select the Empty template and check the Web API checkbox in the New ASP.NET Project dialog box. Then, click OK. Visual Studio 2013 will add the folders and core references for the ASP.NET Web API.

Execute the following commands in the NuGet Package Manager Console to access the latest ASP.NET Web API 2.2. The package follows the Semantic Versioning specification, so it has version 5.2.0. After executing this command, the project will be working with ASP.NET Web API 2.2:

Install-Package Microsoft.AspNet.WebApi -Version 5.2.0
Install-Package Microsoft.AspNet.WebApi.OData

I'll use a simple Game entity model to represent the data for the OData service. Add a Game class to the Models folder. The following lines show the code for this new class. Notice that I use an int for the key (GameId). I don't use a Guid to keep the HTTP requests easier to understand.

namespace Games.Models
{
    public class Game
    {
        public int GameId { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public int ReleaseYear { get; set; }
    }
}

Now, it is necessary to build the project because I can take advantage of the Visual Studio 2013 scaffolding, which will use reflection to gather information about the Game class.

I want to add a Web API 2 OData controller with CRUD actions to allow me to create, read, update, delete, and list entities from an Entity Framework data context. Visual Studio 2013 will generate the class that handles HTTP requests to perform these actions. Right-click on the Controllers folder within Solution Explorer and select Add | Controller…. Select Web API 2 OData Controller with actions, using Entity Framework, and click Add.  Enter GamesController for the controller name and check the Use Async Controller Actions checkbox in the Add Controller dialog box. Select Game (Games.Models) in the Model class dropdown list, and click on the New Data Context… button. Visual Studio will specify Games.Models.GamesContext as the default new data context type name in a new dialog box. Leave the default name and click Add. Finally, click Add when you are back in the Add Controller dialog box.

Visual Studio will create the following GamesContext class in Models/GamesContext.cs:

using System.Data.Entity;

namespace Games.Models
{
    public class GamesContext : DbContext
    {
        public GamesContext() : base("name=GamesContext")
        {
        }

        public System.Data.Entity.DbSet<Games.Models.Game> Games { get; set; }
    
    }
}

The following lines show the generated code for the GamesController class in Controllers/GamesController.cs. GamesController implements the abstract class System.Web.Http.OData.ODataController. ODataController is the base class for OData controllers that supports writing and reading data using the OData formats. I've replaced the deprecated [Queryable] attribute with [EnableQuery]. Sadly, the scaffolding still works with the previous ASP.NET Web API OData version and generates code that includes the deprecated attribute. In fact, in some cases, you might have problems with scaffolding and the newest ASP.NET Web API OData-related libraries. If you notice mixed versions in the packages, you can copy and paste the code to your project instead of following the steps I mentioned before.

The [EnableQuery] attribute enables a controller action to support OData query parameters. The controller uses the async modifier to define asynchronous methods for the POST, PUT, PATCH/MERGE, and DELETE verbs. In addition, you will notice the use of the await keyword and many calls to methods that end with the Async suffix. Microsoft .NET Framework 4.5 introduced asynchronous methods. If you haven't worked with the async modified in ASP.NET MVC controllers, you can read Using Asynchronous Methods in ASP.NET 4.5 and in MVC 4 to get a feel for it.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using System.Web.Http.OData;
using System.Web.Http.OData.Routing;
using Games.Models;

namespace Games.Controllers
{
    public class GamesController : ODataController
    {
        private GamesContext db = new GamesContext();

        // GET odata/Games
        [EnableQuery]
        public IQueryable<Game> GetGames()
        {
            return db.Games;
        }

        // GET odata/Games(5)
        [EnableQuery]
        public SingleResult<Game> GetGame([FromODataUri] int key)
        {
            return SingleResult.Create(db.Games.Where(game => game.GameId == key));
        }

        // PUT odata/Games(5)
        public async Task<IHttpActionResult> Put([FromODataUri] int key, Game game)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (key != game.GameId)
            {
                return BadRequest();
            }

            db.Entry(game).State = EntityState.Modified;

            try
            {
                await db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!GameExists(key))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return Updated(game);
        }

        // POST odata/Games
        public async Task<IHttpActionResult> Post(Game game)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            db.Games.Add(game);
            await db.SaveChangesAsync();

            return Created(game);
        }

        // PATCH odata/Games(5)
        [AcceptVerbs("PATCH", "MERGE")]
        public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Game> patch)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            Game game = await db.Games.FindAsync(key);
            if (game == null)
            {
                return NotFound();
            }

            patch.Patch(game);

            try
            {
                await db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!GameExists(key))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return Updated(game);
        }

        // DELETE odata/Games(5)
        public async Task<IHttpActionResult> Delete([FromODataUri] int key)
        {
            Game game = await db.Games.FindAsync(key);
            if (game == null)
            {
                return NotFound();
            }

            db.Games.Remove(game);
            await db.SaveChangesAsync();

            return StatusCode(HttpStatusCode.NoContent);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }

        private bool GameExists(int key)
        {
            return db.Games.Count(e => e.GameId == key) > 0;
        }
    }
}

Notice the use of the System.Web.Http.AcceptVerbs attribute in the Patch method to make the action method respond to both the PATCH and MERGE HTTP verbs.

[AcceptVerbs("PATCH", "MERGE")]
public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Game> patch)

The parameterless GetGames method returns the entire Games collection. The GetGame method with a key parameter looks up a Game by its key (the GameId property). Both methods respond to HTTP GET; and the final method call will depend on the OData query. I'll discuss the effects of the [EnableQuery] attribute later.

The rest of the supported HTTP verbs use a single method for each verb: PUT (Put), POST (Post), and DELETE (Delete). Thus, you can query the entity set (GET), add a new game to the entity set (POST), perform a partial update to a game (PATCH/MERGE), replace an entire game (PUT), and delete a game (DELETE).


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video