OData Web API Techniques

Feb 20, 2015 Web Development Mark Bow

ODATAIn this post, I’m going to show you three techniques to customize your OData service:

  1. Define parameters and call them through the query string of the OData service URL
  2. Provide defaults values for the standard OData query options
  3. Generate ATOM XML and JSON output formats

The project setup

To play along with the techniques and code examples, you will need to create an ASP.NET MVC 4 Web Application project in Visual Studio (I’m using 2012 Update 4) and select Web API as your project template.  Also, you will need to set up the Northwind database which the OData service will be querying from.  Specifically, I will be using Entity Framework and the customers table in the code examples.  Your OData Customers Controller (CustomersController.cs) should look like this:

public class CustomersController : ODataController
    {
        readonly NorthwindEntities _dbContext = new NorthwindEntities();

        [Queryable(AllowedQueryOptions = AllowedQueryOptions.All)]
        public IQueryable<Customer> Get()
        {
            return _dbContext.Customers;
        }
        protected override void Dispose(bool disposing)         {             if (disposing)             {                 _dbContext.Dispose();             }             base.Dispose(disposing);         }     }

The code in the Register method of the WebApiConfig class should look like this:

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

            var builder = new ODataConventionModelBuilder();
            builder.EntitySet<Customer>("Customers");
            builder.EntitySet<Order>("Orders");
            builder.EntitySet<Order_Detail>("OrderDetails");
            builder.EntitySet<CustomerDemographic>("CustomerDemographics");
            builder.EntitySet<Employee>("Employees");
            builder.EntitySet<Region>("Regions");
            builder.EntitySet<Territory>("Territories");
            builder.EntitySet<Shipper>("Shippers");
            builder.EntitySet<Category>("Categories");
            builder.EntitySet<Product>("Products");
            builder.EntitySet<Supplier>("Suppliers");

            config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());
}

For client output, I will use Postman to view the response returned from the OData service. 

#1: Define parameters and call them through the query string of the OData service URL

The OData Standard defines many query options to help you query your data. These query options can be specified as query string parameters of the URL with each query option prepended with the ‘$’ symbol.  If we query the OData service for customers and, for example, apply the $filter option using the expression $filter=Country eq ‘USA’, we would get back customers located in USA.   The $filter option and its output is shown in Postman:

OData1

For most cases, the query options that the standards provide will be enough for your specific needs. In those few cases where you need more control, or if you have a complicated logic that can’t easily be expressed in any combination of query options, you can define and specify query string parameters in the OData service URL.

These parameters can be accessed by calling GetQueryNameValuePairs method of the Request object.  GetQueryNameValuePairs return a list of key/value pairs of the query string parameters. You can handle the parameters in your Get method of your OData Customers Controller.  The modified code snippet is shown below:

       [Queryable(AllowedQueryOptions = AllowedQueryOptions.All)]        
       public IQueryable<Customer> Get()
        {
            var customers = _dbContext.Customers;

            foreach (var pair in Request.GetQueryNameValuePairs())
            {
                switch (pair.Key)
                {
                    case "Parameter1":
                        DoThing1(customers, pair.Value);
                        break;
                    case "Parmeter2":
                        DoThing2(customers, pair.Value);
                        break;
                }
            }


            return customers;
        }

In the code above, I loop through the parameters checking for Parameter1 and Parameter2 to do something with the customer data. 

Keep in mind that any parameters you choose to define and handle get applied first to the customer data.  For example, if I specify the following URL:

http://localhost:61012/odata/Customers?$filter=Country eq ‘USA’&Parameter1=Value1

Parameter1 is passed into the OData service which is handled first and then $filter query option is applied after the Get method returns.

#2: Provide defaults values the standard OData query options

If you need default values for any of the standard query options, you can following the technique below. For the examples, I will be using $top and $orderby query options when query for customer data.

If the user does not specify $top and $orderby options when calling the OData service, these query options can default to a specific values.  In the Application_BeginRequest method of the Global.asax.cs file, I simply rewrite the request URL to add the default values.  Here is the code snippet:

     protected void Application_BeginRequest(object sender, EventArgs e)
        {   
            if (Path.GetFileName(Request.Url.LocalPath) == "Customers")
            {
                var keyValues = HttpUtility.ParseQueryString(string.Empty);
                string queryString = Request.Url.Query;

                if (!queryString.Contains("$orderby"))
                {
                    keyValues["$orderby"] = "ContactName";
                }

                if (!queryString.Contains("$top"))
                {
                    keyValues["$top"] = "5";
                }

                if (keyValues.Count > 0)
                {
                    string path = Request.Url.PathAndQuery;

                    if (string.IsNullOrEmpty(queryString))
                    {
                        path += "?";
                    }
                    else
                    {
                        path += "&";
                    }

                    Context.RewritePath(path + keyValues.ToString().Replace("%24", "$"));
                }
            }
        }

In the above code snippet, I make sure that the request is indeed querying for customer data so that I can apply defaults to $top and $orderby if necessary. Then I check the query string if there isn’t $top or $orderby already specified and add the defaults.  A few things to note:

  • The collection returned from the ParseQueryString method of the HttpUtility class is an empty key/value collection by passing in an empty string.  This allows me to add the default values easily without having to touch any of the query string parameters already specified through the request.  Also, the collection returned from the ParseQueryString method is special in that it will format and encode the keys/values as a query string by calling the collection’s ToString method, which I call when making the call to Context.RewritePath.
  • Before I can add any default values to the URL, I need to determine what symbol to post pend to the request URL.  If the URL does not have any query string values, then the ‘?’ symbol is added to signify that this is the beginning of the query string. Otherwise the ‘&’ symbol is added signifying that I’m adding to the query string.
  • I call the Replace method on the string returned from the ToString method of the key/value collection (keyValues) because the key/value collection's ToString method encodes the ‘$’ symbol to %24 and the Replace method undoes the encoding to prevent OData Web API from being confused.
  • The magic happens when calling RewritePath method of the Context object as I pass in the combined path and default values.  This ensures that OData Web API will pick up on the default values and apply them to the customer data.

Passing the request URL, http://localhost:61012/odata/Customers, without specifying $top or $orderby to Postman shows that the default values are applied to the customer data in the output below:

OData2

#3: Generate ATOM XML and JSON output formats

Prior to ASP.NET Web API 2.2, the OData Web API did not support the $format query option which would allow you to specify the output in either JSON or ATOM XML. You would have to explicitly send in as part of the request what output format you wanted via HTTP accept header as ‘application/json’ or ‘application/atom+xml’.  Note that if you don’t specify an output format, OData Web API will default to JSON (Light).  In Postman, you can specify HTTP headers by clicking on the ‘Header’ button and then type in ‘Accept’ and ‘application/atom+xml’ or ‘application/json’ as shown:

OData3

Rather than having to add the Accept Header to the request, why not specify it in the query string instead? This would be convenient if you could type the request in a web browser’s address bar like Chrome or Internet Explorer without having to open a web client (like Postman) to specify an output format.  The following technique allows you to do just that.

To avoid clashing with the OData standard $format query option, I used a custom output format parameter for this purpose.  The output format option support JSON Light, JSON Full, and ATOM XML.  Inheriting from DeletgatingHandler, I override SendAsync method to handle my output format parameter.  Here is the code snipet: 

public class OutputFormatHandler : DelegatingHandler
    {
        protected override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
        {
            var queryParams = request.GetQueryNameValuePairs();
            var outputFormat = queryParams.Where(kvp => kvp.Key.ToLower() == "outputformat").Select(kvp => kvp.Value).FirstOrDefault();

            if (!string.IsNullOrEmpty(outputFormat))
            {
                if (outputFormat.ToLower() == "jsonlight")
                {
                    var mediaTypeJson = MediaTypeWithQualityHeaderValue.Parse("application/json");
                    var accepts = request.Headers.Accept;

                    if (!accepts.Contains(mediaTypeJson))
                    {
                        request.Headers.Accept.Add(mediaTypeJson);
                    }
                }
                else if (outputFormat.ToLower() == "jsonfull")
                {
                    var mediaTypeJson = MediaTypeWithQualityHeaderValue.Parse("application/json");
                    var mediaTypeJsonWithVerbose = new MediaTypeWithQualityHeaderValue(mediaTypeJson.MediaType);
                    mediaTypeJsonWithVerbose.Parameters.Add(new NameValueHeaderValue("odata", "verbose"));
                    var accepts = request.Headers.Accept;

                    if (!accepts.Contains(mediaTypeJsonWithVerbose))
                    {
                        request.Headers.Accept.Add(mediaTypeJsonWithVerbose);
                    }
                }
                else if (outputFormat.ToLower() == "atomxml")
                {
                    var mediaTypeAtomXml = MediaTypeWithQualityHeaderValue.Parse("application/atom+xml");
                    var accepts = request.Headers.Accept;

                    if (!accepts.Contains(mediaTypeAtomXml))
                    {
                        request.Headers.Accept.Add(mediaTypeAtomXml);
                    }
                }
            }
            return base.SendAsync(request, cancellationToken);
        }
    }

All this code handler does is detect the presence of my output format parameter and determine the appropriate accept header value to add to the request which will notify OData Web API what output format to return in the response.  The code essentially does what you would have done manually by adding the accept header value to the request.

Now, I would need to hook this handler up so that OData Web API is aware of it.  This can be done by calling config.MessageHandlers.Add in the Register method of the WebApiConfig class:

config.MessageHandlers.Add(new Handlers.OutputFormatHandler ())

On the client side, I can query JSON Light, JSON Full, or ATOM XML by specifying my output format parameter with the value jsonlight, jsonfull, or atomxml respectively:

http://localhost:61012/odata/Customers?OutputFormat=jsonlight
http://localhost:61012/odata/Customers?OutputFormat=jsonfull
http://localhost:61012/odata/Customers?OutputFormat=atomxml

 The output for ATOM XML is shown in Postman, as I pass in the request URL with the output format of atomxml:

OData4

ASP.NET Web API 2.2 support for $format In OData v4

Starting with OData v4 and ASP .NET Web API 2.2, $format is now supported. Specifically, you can request JSON Light, or JSON full:

http://localhost:61012/odata/Customers?$format=json
http://localhost:61012/odata/Customers?$format=application/json;odata.metadata=full

To get it configured into your project, first you must update all of your packages using the following command at the package manager console:

PM> Update-Package

Then install Microsoft ASP.NET Web API 2.2 for OData v4.0:

PM> Install-Package Microsoft.AspNet.OData

Once the package is installed, you have to modify a few lines of code.

Anyone using directives with the line System.Web.Http.OData needs to change to System.Web.OData.  For example, if you’re using the follows lines:

using System.Web.Http.OData;
using System.Web.Http.OData.Query;
using System.Web.Http.OData.Builder;
using System.Web.Http.OData.Extentsions;

You should change them to:

using System.Web.OData;
using System.Web.OData.Query;
using System.Web.OData.Builder;
using System.Web.OData.Extentsions;

 In your OData Customers Controller class, you will need to replace the attribute on the Get method from

[Queryable(AllowedQueryOptions = AllowedQueryOptions.All)]

 to

[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]

In the Register method of the WebApiConfig Class, you will need to change the following line

config.Routes.MapODataRoute("odata", "odata", builder.GetEdmModel());

to

config.MapODataServiceRoute("odata", "odata", builder.GetEdmModel());

Make sure that you have the using directive ‘using System.Web.OData.Extensions’ included at the beginning of the WebApiConfig class. Also in the Register method, you will need to configure primary keys to Order_Details entity and the CustomerDemographcis entity. Otherwise the OData Web API will throw an exception telling you that these entities do not have primary keys.  The following lines of code need to change from

builder.EntitySet<Order_Detail>("OrderDetails");
builder.EntitySet<CustomerDemographic>("CustomerDemographics");

 To

var od = builder.EntitySet<Order_Detail>("OrderDetails");
od.EntityType.HasKey(t => t.OrderID);
od.EntityType.HasKey(t => t.ProductID);
var cd = builder.EntitySet<CustomerDemographic>("CustomerDemographics");
cd.EntityType.HasKey(t => t.CustomerTypeID);

Finally, replace a line in the Global.asax.cs file in the Application_Start method from

WebApiConfig.Register(GlobalConfiguration.Configuration); 

 To

GlobalConfiguration.Configure(WebApiConfig.Register);

After all of the code changes, build and run the OData service.

Passing in the URL for JSON full using the $format query option in Postman shows the output below:

OData5

Unfortunately, the $format does not support ATOM XML. This post is the closest explanation for this. You always have the option of sticking with my custom output format parameter without making the above code changes. 

Designing your website to match your business goals

Posted in: Web Development

Topics: Web Development Custom Software Code

Comments