In this blog post, we take a look at how to create an OData controller which leverages generics to offer the same CRUD services for multiple models. The full source is available on GitHub.
OData is a great way to wrap data sources in a standard and simple access method. It is a mature protocol, having been created by Microsoft in 2007, offering a simple abstraction layer to accessing data. It provides a common protocol for accessing any type of data source, allowing clients and data sources to be mixed and matched together, while allowing the flexibility to say how data should be sorted or filtered at request-time.
Sometimes when working with OData, each OData Controller needs to treat it’s model differently and has a lot of business logic built into each controller. However sometimes, all you need to provide is a thin middle-ware abstraction between the data layer and the application. I found myself working on a problem of the latter kind recently, and thought I could save some time by building an OData Controller making use of Generics in C#. Initially I tried to find a solution via Google, but only found fixes to individual issues rather than an overall solution to the problem.
This approach works if each model in your application can be treated the same, allowing CRUD (Create, Read, Update, Delete) on each. If you want to allow Delete calls on some models but not others, you will need custom controllers for each, or an additional abstraction layer above which can handle rejecting these calls. If not, read on!
Models and Controllers
First and foremost, we need to define a common type for our models. Certain methods are going to expect to look up data by an Id, so we need to have all models using a common type with an Id.
If there is no possible way for any data in your database to have 2 billion different entries over time, then you can use
int here. When in doubt,
long sacrifices some space for some peace of mind later. This also assumes that all of the Ids will be numerical, so will need some extra work if you have a
Next comes our controller;
T to be generic, but still specify that it must inherit from
IndexedModel to include an
Id field on it.
Now on to the CRUD methods. PUT, POST, DELETE etc. are implemented in the full source, but for now I will only cover GET.
There are a few things to note here. We use the
[EnableQuery] annotation, to allow query parameters such as
$filter to be called on the results.
In the GET by Id, we need to retrieve the “key” from the query string, using the
[FromODataUri]. Note that the parameter by convention must be called “key” unless the
ODataRouteAttribute specifies a different value.
Finally, we have the
This makes use of a rather clever method from
Set<T>() finds the
DbSet on the
DbContext matching type
T was a
Product, it would return the matching
DbSet<Product>. If there is no matching
InvalidOperationException is thrown, however because of the way we are building the app this would only occur if you had a valid model which had been mapped to a valid path but had not been added to the
For now, the
DbContext should like this:
If you ran the app as it is now, you will get a 403 or 404 page, as no OData routes are yet set up. We need to do something special in
WebApiConfig.cs, but to start with we will use a standard OData routing mechanism.
We tell the app that we’re using the route “Products” to correspond to the
Product model and
DbSet<Product> database object on our context. However running this will give us a new error.
This is where the magic happens. The controller selector is used to seeing
Product as a model and trying to find the
ProductsController, but we don’t have one of those, we only have a
GenericController<Product> to use.
Instead, we use our own controller selector to select the generic controller.
We make use of the
EntitySets to find our mappings of path to model, keeping the setup for this in one place. If any more mappings are added, the correct generic controller will still be selected. Our
CustomControllerSelector takes these mappings and sets itself up like so:
The important part here is the
typeof().MakeGenericType(). This lets us create a
GenericController for each model in our mapping, which is then stored against the corresponding path for the model.
With this in place, the two required methods for
IHttpControllerSelector become trivial:
With this in place, our Generic OData Controller is complete. You should be able to fire it up and make GET requests against “/Products” to fetch values from the database, and “/Products(1)” to get a specific entry from the database. If you got stuck anywhere along the way, the full source is also available.