Exposing a queryable api with Odata
tl;dr: Odata provides a nice, conventional, powerful and easy-to-setup api. This post is a walk-through to expose a readonly model.
We will
- Initialize a new web api project
- Add Odata for ASP.NET Core and also Microsofts api versioning library
- In the
Startup.cs
class configure services and the middleware pipeline to enable Odata - Add a sample model and Odata controller
- Implement
IModelConfiguration
to configure the Odata model - Execute some queries against the Odata controller
The resulting source code is available on github:
git clone https://github.com/jannikbuschke/pragmatic-cqrs-with-asp-net-core-for-spas
Introduction
This is the first post of a mini series about creating an opinionated ASP.NET Core api with the help of Odata (among others).
Odata is a conventional restful api, meaning that some query operations like top, skip, select, order by
and filter
(typical sql-like operations) are standardized as part of the api. Odata also provides conventions for commands (if we think in terms of CQRS
), which I will leave out, as in my experience this part is overly complex to use and the normal webapi is powerful and easily adjusted/modified to provide custom conventions.
This post is a walk-through to setup a simple queryable (readonly) api which in my opinion gives the most "bang for your buck".
Pragmatic CQRS with ASP.NET Core and SPAs - Series overview
- ▶️ Expose a queryable api with Odata
- Enhance ASP.NET Core api controllers with Odata
- Expose ASP.NET Core validation for clients
- CQRS with MediatR (Commands) and Odata (Queries)
Project setup
Lets create our project and add Odata as well as mvc- and odata-versioning.
dotnet new webapi --name MyAppcd MyAppdotnet package add Microsoft.AspNetCore.Odatadotnet package add Microsoft.AspNetCore.Mvc.Versioningdotnet package add Microsoft.AspNetCore.OData.Versioning
Our .csproj file now should look something like this:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.2</TargetFramework> <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.App" /> <PackageReference Include="Microsoft.AspNetCore.Odata" Version="7.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="3.1.2" /> <PackageReference Include="Microsoft.AspNetCore.OData.Versioning" Version="3.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" /> </ItemGroup></Project>
Add Odata and ApiVersioning services in the ConfigureServices
method (Startup.cs):
public void ConfigureServices(IServiceCollection services){ services.AddMvcCore(options => { options.EnableEndpointRouting = false; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddApiVersioning(); services.AddOData().EnableApiVersioning();}
Also add Odata routes in the Configure
method (Startup.cs). The Configure
method needs a VersionedODataModelBuilder
as an an additional parameter:
public void Configure( IApplicationBuilder app, IHostingEnvironment env, VersionedODataModelBuilder modelBuilder){ if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseHsts(); } app.UseHttpsRedirection(); app.UseMvc(builder => { builder.Select().Expand().Filter().OrderBy().Count(); builder.MapVersionedODataRoutes("odata", "odata", modelBuilder.GetEdmModels()); });}
Adding a Model, Configuration and Controller
Next create a model (Person.cs):
public class Person{ public Guid Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; }}
and the Odata configuration (OdataModelConfigurations.cs)
public class OdataModelConfigurations : IModelConfiguration{ public void Apply(ODataModelBuilder builder, ApiVersion apiVersion) { builder.EntitySet<Person>("Persons"); }}
Finally let`s create a controller with sample data created on the fly (PersonsController.cs):
public class PersonsController : ODataController{ [EnableQuery] public IQueryable<Person> Get() { return new string[] { "Alice", "Bob", "Chloe", "Dorothy", "Emma", "Fabian", "Garry", "Hannah", "Julian" } .Select(v => new Person { FirstName = v, Id = Guid.NewGuid(), Age = new Random().Next(80) }) .AsQueryable(); }}
If you have experience with Entity Framework Core
you could just return a DbSet<T>
(or DbQuery<T>
) property of your DbContext
implementation in above Get()
method.
Run and test
Now that we have a controller we can run the project and execute queries in a browser:
dotnet run
open https://localhost:5001/odata/Persons?api-version=1.0
in a browser (or http://localhost:5000/...
).
You should get the full resultset in json format.
Some parameters that can be tried now:
/odata/Persons?api-version=1.0&$top=5 // first page with pagesize 5/odata/Persons?api-version=1.0&$top=5&$skip=5 // second page/odata/Persons?api-version=1.0&$orderby=age // order by age ascending (young to old)/odata/Persons?api-version=1.0&$orderby=age desc // order by age descending/odata/Persons?api-version=1.0&$filter=startswith(firstName,'G') // only show Persons who's first name starts with 'G'/odata/Persons?api-version=1.0&$filter=contains(firstName,'e') // only show Persons who's first name contains an 'e'/odata/Persons?api-version=1.0&$filter=age gt 20 and age lt 50 // only show Persons older than 20 and younger than 50