Welcome to today’s blog.
In today’s blog I will show how to setup your .NET Core Web API to use OData.
What is OData? OData is the open data protocol for describing structured content.
The latest version of the OData protocol is 4.01 and includes extending the specification for the JSON format.
With HTTP REST APIs the responses are structured in JSON and our OData URLs that query data from our APIs use a data format known as EDM or Entity Data Model.
EDM was introduced in OData version 2.0. Some of the primitive types supported by OData are:
Edm.Boolean, Edm.DateTime, Edm.Int16, Edm.String and many others compatible with types we use in C#.
I will show how to use some of the primitive data types supported by OData to provide structure to the data we provide for our HTTP GET endpoints.
If our API has definitions for our data in the form on C# models, then we have most of the task of defining our EDM done as almost all the C# types are compatible with EDM primitive types.
Edm.Boolean
Edm.DateTime
Edm.Int16
Edm.String
and many others compatible with types we use in C#.
I will show how to use some of the primitive data types supported by OData to provide structure to the data we provide for our HTTP GET endpoints.
If our API has definitions for our data in the form on C# models, then we have most of the task of defining our EDM done as almost all the C# types are compatible with EDM primitive types.
Installation of OData within a .NET Core Application
To install OData we obtain the NuGet package as shown and install it into our .NET Core Web API project:
At this point, OData will not work with our .NET Core application.
Setup of OData within a .NET Core Application
There are a few steps we need to take to configure OData with our Web API for it to function.
Step 1. Build the EDM model
The first step we make is to build the EDM model.
We create a helper method to achieve this:
using BookLoan.Models;
using Microsoft.AspNet.OData.Builder;
using Microsoft.OData.Edm;
namespace BookLoan.Catalog.API.Helpers
{
public class EdmHelpers
{
public static IEdmModel GetEdmModel()
{
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<BookViewModel>("Books");
return builder.GetEdmModel();
}
}
}
The ODataConventionModelBuilder class is used to map CLR classes to EDM models.
We then call this helper from our Startup.cs to setup our EDM OData types.
Step 2. Register OData Services
In our ConfigureServices() method in Startup.cs we setup OData as shown:
using Microsoft.AspNet.OData.Extensions;
…
public void ConfigureServices(IServiceCollection services)
{
…
services.AddOData();
services.AddMvc();
…
}
Step 3. Register OData Endpoint
In our Configure() method in Startup.cs we setup OData endpoint as shown:
using Microsoft.AspNet.OData.Extensions;
...
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseMvc(o =>
{
o.MapODataServiceRoute("odata", "odata", EdmHelpers.GetEdmModel());
});
...
}
The next step is to allow our API to provide HTTP GET URIs to retrieve and return data, and to achieve this we will need to create a controller derived from ODataController. Note that deriving from Controller will not work!
using Microsoft.Extensions.Logging;
using BookLoan.Data;
using BookLoan.Services;
using Microsoft.AspNet.OData;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNet.OData.Routing;
namespace BookLoan.Catalog.API.Controllers
{
public class BookDataController: ODataController
{
ApplicationDbContext _db;
IBookService _bookService;
private readonly ILogger _logger;
public BookDataController(ApplicationDbContext db,
ILogger<BookDataController> logger,
IBookService bookService)
{
_db = db;
_logger = logger;
_bookService = bookService;
}
[ODataRoute("Books")]
[EnableQuery]
public IActionResult GetBooks()
{
return Ok(_db.Books);
}
[ODataRoute]
[EnableQuery]
public IActionResult Get()
{
return Ok(_db.Books);
}
[ODataRoute("Books({id})")]
[EnableQuery]
public IActionResult Get(int id)
{
return Ok(_db.Books.Where(b => b.ID == id));
}
}
}
Note: We are not using any HTTP routing attributes (HTTPGET, HTTPPOST etc) on our methods. We just need the attribute ODataRoute with a routing OData URL path.
Note: If the OData URL does not include any of the entities with our EDM, then the following exception will be thrown:
Exception thrown: 'Microsoft.OData.UriParser.ODataUnrecognizedPathException' in Microsoft.AspNetCore.OData.
Step 4. Fix Issue with Swagger UI
The next step is enabling our OData API to be visible from our Web API Swagger UI.
If you get the following error from the output console when running the Swagger UI:
System.InvalidOperationException: No media types found in 'Microsoft.AspNet.OData.Formatter.ODataInputFormatter.SupportedMediaTypes'. Add at least one media type to the list of supported media types.
The fix is courtesy from the Stack overflow issue that explains why OData breaks Swagger, and this OData Web API GitHub issue.
Then we will have to add supported media headers to the input and output formatters to resolve the above issue.
In our ConfigureServices() method in Startup.cs we setup OData as shown:
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
…
public void ConfigureServices(IServiceCollection services)
{
…
services.AddOData();
services.AddMvc();
services.AddMvcCore(options =>
{
foreach (var outputFormatter in
options.OutputFormatters.OfType<OutputFormatter>().Where(x =>
x.SupportedMediaTypes.Count == 0))
{
outputFormatter.SupportedMediaTypes.Add(
new MediaTypeHeaderValue(
"application/prs.odatatestxx-odata"));
}
foreach (var inputFormatter in
options.InputFormatters.OfType<InputFormatter>().Where(
x => x.SupportedMediaTypes.Count == 0))
{
inputFormatter.SupportedMediaTypes.Add(
new MediaTypeHeaderValue(
"application/prs.odatatestxx-odata"));
}
});
…
}
Note: formatters need to be added after OData is registered.
After we have applied the above tasks in the above steps, we can run and test our OData .NET Core Web API.
Testing our OData Web API
The first test we can perform is to check that OData returns our metadata from our OData URI endpoint.
Retrieving our EDM Metadata
In POSTMAN, create a request with the following URI (with the port of your Web API running under Visual Studio):
http://localhost:25138/odata/$metadata
The response will resemble the XML formatted response below:
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="BookLoan.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="BookViewModel">
<Key>
<PropertyRef Name="ID" />
</Key>
<Property Name="ID" Type="Edm.Int32" Nullable="false" />
<Property Name="Title" Type="Edm.String" Nullable="false" />
<Property Name="Author" Type="Edm.String" Nullable="false" />
<Property Name="YearPublished" Type="Edm.Int32" Nullable="false" />
<Property Name="Genre" Type="Edm.String" />
<Property Name="Edition" Type="Edm.String" />
<Property Name="ISBN" Type="Edm.String" />
<Property Name="Location" Type="Edm.String" Nullable="false" />
<Property Name="DateCreated" Type="Edm.DateTimeOffset" Nullable="false" />
<Property Name="DateUpdated" Type="Edm.DateTimeOffset" Nullable="false" />
</EntityType>
<EntityType Name="BookStatusViewModel" BaseType="BookLoan.Models.BookViewModel">
<Property Name="Status" Type="Edm.String" />
<Property Name="DateLoaned" Type="Edm.DateTimeOffset" Nullable="false" />
<Property Name="DateDue" Type="Edm.DateTimeOffset" Nullable="false" />
<Property Name="DateReturn" Type="Edm.DateTimeOffset" Nullable="false" />
<Property Name="Borrower" Type="Edm.String" />
<Property Name="OnShelf" Type="Edm.Boolean" Nullable="false" />
</EntityType>
</Schema>
<Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityContainer Name="Container">
<EntitySet Name="Books" EntityType="BookLoan.Models.BookViewModel" />
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
Notice that you will see the EDM primitive types that correspond to the CLR types we defined within our C# models.
Running Queries on our EDM Data
Before we can run queries on our EDM data, we will need to enable some standard query commands like:
- Select
- OrderBy
- Filter
- Count
In our Configure() method in Startup.cs we setup OData routes as shown:
using Microsoft.AspNet.OData.Extensions;
...
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseMvc(o =>
{
o.Select().Expand().Filter().OrderBy().Count();
…
…
});
...
}
The above queries can be used within our URI to send HTTP GET requests like:
http://localhost:25138/odata/Books?$select=Title,Author&$orderby=Title
This resembles a query like:
SELECT Title, Author FROM BOOKS ORDER BY Title
This allows us to query or manipulate data directly from a HTTP REST call.
Let’s try some sample queries.
The following URI’s will produce the same output from our OData Web API:
http://localhost:25138/odata
http://localhost:25138/odata/Books
The JSON output is shown below:
{
"@odata.context": "http://localhost:25138/odata/$metadata#Collection(BookLoan.Models.BookViewModel)",
"value": [
{
"@odata.type": "#BookLoan.Models.BookViewModel",
"ID": 1,
"Title": "The Lord of the Rings",
"Author": "J. R. R. Tolkien",
"YearPublished": 1954,
"Genre": "fantasy",
"Edition": "0",
"ISBN": "654835",
"Location": "sydney",
"DateCreated": "2019-11-05T00:00:00+11:00",
"DateUpdated": "2020-12-14T15:13:35.2735839+11:00"
},
…
{
"@odata.type": "#BookLoan.Models.BookViewModel",
"ID": 13,
"Title": "Test Book 1",
"Author": "Test Author 1",
"YearPublished": 2019,
"Genre": null,
"Edition": "1",
"ISBN": "1",
"Location": "sydney",
"DateCreated": "2020-07-26T19:33:05.68913+10:00",
"DateUpdated": "0001-01-01T00:00:00Z"
}
]
}
To retrieve a specific book, we use the following URI:
http://localhost:25138/odata/Books(1)
The JSON response is shown below:
{
"@odata.context": "http://localhost:25138/odata/$metadata#Books",
"value": [
{
"ID": 1,
"Title": "The Lord of the Rings",
"Author": "J. R. R. Tolkien",
"YearPublished": 1954,
"Genre": "fantasy",
"Edition": "0",
"ISBN": "654835",
"Location": "sydney",
"DateCreated": "2019-11-05T00:00:00+11:00",
"DateUpdated": "2020-12-14T15:13:35.2735839+11:00"
}
]
}
Below are variations of a more complex SELECT query than can be ordered using ORDERBY and columns projected:
http://localhost:25138/odata/Books?$orderby=Title
http://localhost:25138/odata/Books?$select=Title,Author
http://localhost:25138/odata/Books?$select=Title,Author&$orderby=Title
The results of the last URI combining all the above is shown:
{
"@odata.context": "http://localhost:25138/odata/$metadata#Books(Title,Author)",
"value": [
{
"Title": "American Dirt",
"Author": "Jeanine Cummins"
},
{
"Title": "And Then There Were None",
"Author": "Agatha Christie"
},
{
"Title": "Dream of the Red Chamber (红楼梦)",
"Author": "Cao Xueqin"
},
…
{
"Title": "The Lord of the Rings",
"Author": "J. R. R. Tolkien"
},
{
"Title": "The Night Dragon",
"Author": "Matthew Condon"
},
{
"Title": "Thief River Falls",
"Author": "Brian Freeman"
}
]
}
The above has given you a useful guide on configuring OData into your .NET Core Web API service. In additional to the read-only queries, you can experiment further with data modification queries.
In a future post I will discuss using OData .NET Core Web API to use OData JSON batching.
That’s all for today’s post.
I hope you found this post useful and informative.
Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.