Custom OData Provider Without Underlying DB

Thursday, September 25, 2014 by Rainer Stropek

Today I will do my OData session at BASTA conference in Mainz. This time I have a bit more time so I will add a demo of creating a custom OData provider without any underlying database. The result is generated based on the OData query on the fly. In this blog article I share the code.

You can download the entire source code from my GitHub Samples repository.

The OData Controller

Let's start with the important part of the sample: The ODataController.

using Microsoft.OData.Core.UriParser.Semantic;
using Microsoft.OData.Core.UriParser.TreeNodeKinds;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Http;
using System.Web.OData;
using System.Web.OData.Query;
using System.Web.OData.Routing;

namespace CustomODataProvider.Provider.Controller
{
	[ODataRoutePrefix("Customers")]
	public class CustomerController : ODataController
	{
		// Helpers for generating customer names
		private readonly char[] letters1 = "aeiou".ToArray();
		private readonly char[] letters2 = "bcdfgklmnpqrstvw".ToArray();
		private Random random = new Random();

		private const int pageSize = 100;

		[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Filter | AllowedQueryOptions.Top | AllowedQueryOptions.Skip | AllowedQueryOptions.OrderBy)]
		[ODataRoute]
		public IHttpActionResult Get(ODataQueryOptions<Customer> options)
		{
			// Calculate number of results based on $top
			var numberOfResults = pageSize;
			if (options.Top != null)
			{
				numberOfResults = options.Top.Value;
			}

			// Analyze $filter
			string equalFilter = null;
			if (options.Filter != null)
			{
				// We only support a single "eq" filter
				var binaryOperator = options.Filter.FilterClause.Expression as BinaryOperatorNode;
				if (binaryOperator == null || binaryOperator.OperatorKind != BinaryOperatorKind.Equal)
				{
					return InternalServerError();
				}

				// One side has to be a reference to CustomerName property, the other side has to be a constant
				var propertyAccess = binaryOperator.Left as SingleValuePropertyAccessNode ?? binaryOperator.Right as SingleValuePropertyAccessNode;
				var constant = binaryOperator.Left as ConstantNode ?? binaryOperator.Right as ConstantNode;
				if (propertyAccess == null || propertyAccess.Property.Name != "CustomerName" || constant == null)
				{
					return InternalServerError();
				}

				// Save equal filter value
				equalFilter = constant.Value.ToString();

				// Return between 1 and 2 rows (CustomerName is not a primary key)
				numberOfResults = Math.Min(random.Next(1, 3), numberOfResults);
			}

			// Generate result
			var result = new List<Customer>();
			for (var i = 0; i < numberOfResults; i++)
			{
				result.Add(new Customer() { CustomerID = Guid.NewGuid(), CustomerName = equalFilter ?? GenerateCustomerName() });
			}

			return Ok(result.AsQueryable());
		}

		private string GenerateCustomerName()
		{
			var length = random.Next(5, 8);
			var result = new StringBuilder(length);
			for (var i = 0; i < length; i++)
			{
				var letter = (i % 2 == 0 ? letters1[random.Next(letters1.Length)] : letters2[random.Next(letters2.Length)]).ToString();
				result.Append(i == 0 ? letter.ToUpper() : letter);
			}

			return result.ToString();
		}
	}
}

Configuration the OData Endpoint

using Microsoft.OData.Edm;
using System.Net.Http.Formatting;
using System.Web.Http;
using System.Web.OData.Builder;
using System.Web.OData.Extensions;
using System.Web.OData.Routing.Conventions;

namespace CustomODataProvider.Provider
{
	public static class ODataConfiguration
	{
		public static void RegisterOData(HttpConfiguration config)
		{
			config.Formatters.Clear();
			config.Formatters.Add(new JsonMediaTypeFormatter());

			var routeConventions = ODataRoutingConventions.CreateDefault();
			config.MapODataServiceRoute("odata", "odata", GetModel());
		}

		private static IEdmModel GetModel()
		{
			var modelBuilder = new ODataConventionModelBuilder();
			modelBuilder.EntitySet<Customer>("Customers");
			return modelBuilder.GetEdmModel();
		}
	}
}

The Self Host

using CustomODataProvider.Provider;
using Microsoft.Owin.Hosting;
using Owin;
using System;
using System.Web.Http;

namespace CustomODataProvider.Hosting
{
	class Program
	{
		static void Main(string[] args)
		{
			using (WebApp.Start<Startup>("http://localhost:5000")) 
			{ 
				Console.WriteLine( "Server ready... Press Enter to quit."); 
				Console.ReadLine(); 
			}
		}
	}

	public class Startup
	{
		public void Configuration(IAppBuilder app)
		{
			var config = new HttpConfiguration();
			ODataConfiguration.RegisterOData(config);
			app.UseWebApi(config);
		}
	}
}
comments powered by Disqus

Rainer Stropek

Rainer Stropek

Co-founder, architect, developer

Bio

I am co-founder and CEO of the company software architects and have been serving this role since 2008. At software architects my team and I are developing the award-winning SaaS solution time cockpit. Previously, I founded and led IT consulting firms that worked in the area of developing software solutions based on the Microsoft technology stack.

In my work I focus on .NET development and software architecture. I have written some books and articles on C#, database development, Windows Azure, Windows 8 development, WPF, and Silverlight. Regularly I speak at conferences, do workshops and conduct trainings in Europe and the US. Since 2010 I have been MVP for Windows Azure.

I graduated the Higher Technical School Leonding (AT) for MIS with honors and hold a BSc (Hons) Computer Studies of the University of Derby (UK).

Contact

Twitter: @rstropek
Facebook
Google+
Xing
LinkedIn

Authors