Introduction
The Microsoft OData Team is proud to announce general availability (GA) of OData (Open OData Protocol) on ASP.NET Core 2.0. It is now available through Nuget package at https://www.nuget.org/packages/Microsoft.AspNetCore.OData, current its version is 7.0.0.
Along this release, it will allow customers to create OData v4.0 endpoints and leverage the OData query syntax easily on multiple platforms, not only on Windows.
This blog is intended to give you a tutorial about how to build/consume OData service through ASP.NET Core OData package. Let’s getting started.
ASP.NET Core Web Application
For simplicity, we will start by creating a simple ASP.NET Core Web Application called BookStore.
Create Visual Studio Project
In Visual studio 2017, from the “File” menu, select “New > Project …”.
In the “New Project” dialog, select “.NET Core” and pick “ASP.NET Core Web Application” template, give a name as “BookStore” and set up the location, see the following picture:
Click “OK” button, then in the “New ASP.NET Core Web Application – BookStore” dialog, select “API” and un-check the “Configure for HTTPs” for simplicity as below:
Click “Ok” button, we will get an empty ASP.NET Core Web Application project.
Install the Nuget Package
Once the empty application has been created, the first thing is to install the ASP.NET Core OData Nuget package from Nuget.org. In the solution explorer, right click on “Dependencies” in the BookStore project and select “Manage Nuget Packages” into the Nuget packages Management dialog. In this dialog, Search and select “Microsoft.AspNetCore.OData” package and click the install button to install the package into the Web application.See the below picture:
EF Core is also used in this tutorial, so do the same process to install “Microsoft.EntityFrameworkCore.InMemory” and its dependencies (for simplicity, we use In Memory data source).
Now, we have the following project configuration:
<Project Sdk="Microsoft.NET.Sdk.Web"> | |
<PropertyGroup> | |
<TargetFramework>netcoreapp2.1</TargetFramework> | |
</PropertyGroup> | |
<ItemGroup> | |
<Folder Include="wwwroot\" /> | |
</ItemGroup> | |
<ItemGroup> | |
<PackageReference Include="Microsoft.AspNetCore.App" /> | |
<PackageReference Include="Microsoft.AspNetCore.OData" Version="7.0.0" /> | |
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="2.1.0" /> | |
</ItemGroup> | |
</Project> |
Add the Model classes
A model is an object representing the data in the application. In this tutorial, we use the POCOs (Plain Old CLR Object) classes to represent our book store models.
Right click BookStore project in the solution explorer, from the popup menu, select Add > New Folder. Name the folder as Models. Add the following classes into the Models folder:
// Book | |
public class Book | |
{ | |
public int Id { get; set; } | |
public string ISBN { get; set; } | |
public string Title { get; set; } | |
public string Author { get; set; } | |
public decimal Price { get; set; } | |
public Address Location { get; set; } | |
public Press Press { get; set; } | |
} | |
// Press | |
public class Press | |
{ | |
public int Id { get; set; } | |
public string Name { get; set; } | |
public string Email { get; set; } | |
public Category Category { get; set; } | |
} | |
// Category | |
public enum Category | |
{ | |
Book, | |
Magazine, | |
EBook | |
} | |
// Address | |
public class Address | |
{ | |
public string City { get; set; } | |
public string Street { get; set; } | |
} |
Where:
- Book, Press will be served as Entity types
- Address will be served as a Complex type.
- Category will be served as an Enum type.
Build the Edm Model
OData uses the Entity Data Model (EDM) to describe the structure of data. In ASP.NET Core OData, it’s easily to build the EDM Model based on the above CLR types. So, add the following private static method at the end of class “Startup”.
public class Startup | |
{ | |
// ... | |
private static IEdmModel GetEdmModel() | |
{ | |
ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); | |
builder.EntitySet<Book>("Books"); | |
builder.EntitySet<Press>("Presses"); | |
return builder.GetEdmModel(); | |
} | |
} |
Where, we define two entity set named “Books” and “Presses”.
Register Services through Dependency Injection
Register the OData Services
ASP.NET Core OData requires some services registered ahead to provide its functionality. The library provides an extension method called “AddOData()” to register the required OData services through the built-in dependency injection. So, add the following codes into “ConfigureServices” method in the “Startup” class:
public class Startup | |
{ | |
// ... | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddOData(); | |
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); | |
} | |
} |
Register the OData Endpoint
We also need to add OData route to register the OData endpoint. We are going to add an OData route named “odata” with “odata” prefix to the MVC routes, and call the “GetEdmModel()” to bind the Edm model to the endpoint. So, change the “Configure()” method in “Startup” class as:
public class Startup | |
{ | |
// ... | |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
} | |
app.UseMvc(b => | |
{ | |
b.MapODataServiceRoute("odata", "odata", GetEdmModel()); | |
}); | |
} | |
} |
Query the metadata
The OData service is ready to run and can provide basic functionalities, for example to query the metadata (XML representation of the EDM). So, let’s build and run the Web Application. Once it’s running, we can use any client tools (for example, Postman) to issue the following request (remember to change the port in the URI):
GET http://localhost:5000/odata/$metadata
Then, you can get the metadata as following xml:
<?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="BookStore.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm"> | |
<EntityType Name="Book"> | |
<Key> | |
<PropertyRef Name="Id" /> | |
</Key> | |
<Property Name="Id" Type="Edm.Int32" Nullable="false" /> | |
<Property Name="ISBN" Type="Edm.String" /> | |
<Property Name="Title" Type="Edm.String" /> | |
<Property Name="Author" Type="Edm.String" /> | |
<Property Name="Price" Type="Edm.Decimal" Nullable="false" /> | |
<Property Name="Location" Type="BookStore.Models.Address" /> | |
<NavigationProperty Name="Press" Type="BookStore.Models.Press" /> | |
</EntityType> | |
<EntityType Name="Press"> | |
<Key> | |
<PropertyRef Name="Id" /> | |
</Key> | |
<Property Name="Id" Type="Edm.Int32" Nullable="false" /> | |
<Property Name="Name" Type="Edm.String" /> | |
<Property Name="Email" Type="Edm.String" /> | |
<Property Name="Category" Type="BookStore.Models.Category" Nullable="false" /> | |
</EntityType> | |
<ComplexType Name="Address"> | |
<Property Name="City" Type="Edm.String" /> | |
<Property Name="Street" Type="Edm.String" /> | |
</ComplexType> | |
<EnumType Name="Category"> | |
<Member Name="Book" Value="0" /> | |
<Member Name="Magazine" Value="1" /> | |
<Member Name="EBook" Value="2" /> | |
</EnumType> | |
</Schema> | |
<Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm"> | |
<EntityContainer Name="Container"> | |
<EntitySet Name="Books" EntityType="BookStore.Models.Book"> | |
<NavigationPropertyBinding Path="Press" Target="Presses" /> | |
</EntitySet> | |
<EntitySet Name="Presses" EntityType="BookStore.Models.Press" /> | |
</EntityContainer> | |
</Schema> | |
</edmx:DataServices> | |
</edmx:Edmx> |
Create the Database Context
Now, it’s ready to add more real functionalities. First, let’s introduce the database context into the Web Application. The database context is the main class that maps Entity Framework Core database to a given data model (CLR classes).
In the “Models” folder, add a new class named “BookStoreContext” with the following contents:
public class BookStoreContext : DbContext | |
{ | |
public BookStoreContext(DbContextOptions<BookStoreContext> options) | |
: base(options) | |
{ | |
} | |
public DbSet<Book> Books { get; set; } | |
public DbSet<Press> Presses { get; set; } | |
protected override void OnModelCreating(ModelBuilder modelBuilder) | |
{ | |
modelBuilder.Entity<Book>().OwnsOne(c => c.Location); | |
} | |
} |
The code in “OnModelCreating” maps the “Address” as complex type.
Register the Database Context
Second, we should register the database context through built-in dependency injection at service configuration. So, change the “ConfigureServices” method in “Startup” class as:
public class Startup | |
{ | |
// ... | |
public void ConfigureServices(IServiceCollection services) | |
{ | |
services.AddDbContext<BookStoreContext>(opt => opt.UseInMemoryDatabase("BookLists")); | |
services.AddOData(); | |
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); | |
} | |
} |
Inline Model Data
For simplicity, we build a class to contain the inline model data. In the following codes, I add two books in this container.
public static class DataSource | |
{ | |
private static IList<Book> _books { get; set; } | |
public static IList<Book> GetBooks() | |
{ | |
if (_books != null) | |
{ | |
return _books; | |
} | |
_books = new List<Book>(); | |
// book #1 | |
Book book = new Book | |
{ | |
Id = 1, | |
ISBN = "978-0-321-87758-1", | |
Title = "Essential C#5.0", | |
Author = "Mark Michaelis", | |
Price = 59.99m, | |
Location = new Address { City = "Redmond", Street = "156TH AVE NE" }, | |
Press = new Press | |
{ | |
Id = 1, | |
Name = "Addison-Wesley", | |
Category = Category.Book | |
} | |
}; | |
_books.Add(book); | |
// book #2 | |
book = new Book | |
{ | |
Id = 2, | |
ISBN = "063-6-920-02371-5", | |
Title = "Enterprise Games", | |
Author = "Michael Hugos", | |
Price = 49.99m, | |
Location = new Address { City = "Bellevue", Street = "Main ST" }, | |
Press = new Press | |
{ | |
Id = 2, | |
Name = "O'Reilly", | |
Category = Category.EBook, | |
} | |
}; | |
_books.Add(book); | |
return _books; | |
} | |
} |
Manipulate Resources
Build the Controller
In the “Controllers” folder, rename “ValuesController.cs” to “BooksController.cs” and replace its content with:
public class BooksController : ODataController | |
{ | |
private BookStoreContext _db; | |
public BooksController(BookStoreContext context) | |
{ | |
_db = context; | |
if (context.Books.Count() == 0) | |
{ | |
foreach (var b in DataSource.GetBooks()) | |
{ | |
context.Books.Add(b); | |
context.Presses.Add(b.Press); | |
} | |
context.SaveChanges(); | |
} | |
} | |
} |
Retrieve the Resources
Add the following methods to the “BooksController”:
public class BooksController : ODataController | |
{ | |
// ... | |
[EnableQuery] | |
public IActionResult Get() | |
{ | |
return Ok(_db.Books); | |
} | |
[EnableQuery] | |
public IActionResult Get(int key) | |
{ | |
return Ok(_db.Books.FirstOrDefault(c => c.Id == key)); | |
} | |
} |
- Get() returns the entire books
- Get(int key) returns a certain book by its key.
Now, we can query the whole books as “GET http://localhost:5000/odata/Books”, the result should be:
{ | |
"@odata.context": "http://localhost:5000/odata/$metadata#Books", | |
"value": [ | |
{ | |
"Id": 1, | |
"ISBN": "978-0-321-87758-1", | |
"Title": "Essential C#5.0", | |
"Author": "Mark Michaelis", | |
"Price": 59.99, | |
"Location": { | |
"City": "Redmond", | |
"Street": "156TH AVE NE" | |
} | |
}, | |
{ | |
"Id": 2, | |
"ISBN": "063-6-920-02371-5", | |
"Title": "Enterprise Games", | |
"Author": "Michael Hugos", | |
"Price": 49.99, | |
"Location": { | |
"City": "Bellevue", | |
"Street": "Main ST" | |
} | |
} | |
] | |
} |
Create the Resources
Adding the following lines of code into “BooksController” will allow to create book to the service:
public class BooksController : ODataController | |
{ | |
// ... | |
[EnableQuery] | |
public IActionResult Post([FromBody]Book book) | |
{ | |
_db.Books.Add(book); | |
_db.SaveChanges(); | |
return Created(book); | |
} | |
} |
As an example, we can issue a post request as follows to create a new Book:
POST http://localhost:5000/odata/Books
Content-Type: application/json
Content:
{ | |
"Id":3,"ISBN":"82-917-7192-5","Title":"Hary Potter","Author":"J. K. Rowling", | |
"Price":199.99, | |
"Location":{ | |
"City":"Shanghai", | |
"Street":"Zhongshan RD" | |
} | |
} |
Query the Resources
Adding the following line of code in Startup.cs enables all OData query options, for example $filter, $orderby, $expand, etc.
public class Startup | |
{ | |
// ... | |
public void Configure(IApplicationBuilder app, IHostingEnvironment env) | |
{ | |
if (env.IsDevelopment()) | |
{ | |
app.UseDeveloperExceptionPage(); | |
} | |
app.UseMvc(b => | |
{ | |
b.Select().Expand().Filter().OrderBy().MaxTop(100).Count(); | |
b.MapODataServiceRoute("odata", "odata", GetEdmModel()); | |
}); | |
} | |
} |
GET http://localhost:5000/odata/Books?$filter=Price le 50
The response content is:
{ | |
"@odata.context": "http://localhost:5000/odata/$metadata#Books", | |
"value": [ | |
{ | |
"Id": 2, | |
"ISBN": "063-6-920-02371-5", | |
"Title": "Enterprise Games", | |
"Author": "Michael Hugos", | |
"Price": 49.99, | |
"Location": { | |
"City": "Bellevue", | |
"Street": "Main ST" | |
} | |
} | |
] | |
} |
It also supports complex query option, for example:
GET http://localhost:5000/odata/Books?$filter=Price le 50&$expand=Press($select=Name)&$select=ISBN
The response content is:
{ | |
"@odata.context": "http://localhost:5000/odata/$metadata#Books(ISBN,Press(Name))", | |
"value": [ | |
{ | |
"ISBN": "063-6-920-02371-5", | |
"Press": { | |
"Name": "O'Reilly" | |
} | |
} | |
] | |
} |
Summary
Thanks to the OData Community for their feedbacks, questions, issues, and contributions on GitHub. Without their help, we can’t deliver this version.
We encourage you to download the latest package from Nuget.org and start building amazing OData service running on ASP.NET Core. Enjoy it!
You can refer to the below links for other detail information:
- Open Data Protocol (OData) Specification
- ASP.NET Core & EF Core
- OData .NET Open Source (ODL & Web API)
- OData Tutorials & Samples
- This blog’s sample project
Same issue like “Dean Wilson” and used nuget packages from “Abhishek Bansal”‘s provided medium article but unfortunately that nuget package also does not work with above code. Thanks though for the article but as dot net core is still evolving its really hard to try out some sample code like above and blazor also has similar issues in my experience.
That is some freaking great news.
Hope OData comes back.
Ideally, I’m hoping OData to become the new beloved RIA Services that was taken from us with the death of Silverlight.As a LoB dev, there should be an official “Breeze#” or “TrackableEntities” backed by Microsoft, see this feature suggestion.
AddOData() method is NOT in .net Core. It is NOT found in Microsoft.AspNetCore.OData, but instead found in Microsoft.AspNet.OData…..Thanks,
Great Article. A similar implementation can be found at below link:
https://medium.com/@abhi.bansal08/why-odata-and-how-to-enable-odata-in-c-net-core-application-1f13ad6ca3d1
Many code samples, which should have appeared inline with this article, are not visible here. You can find the code using the “This blog’s sample project” link.
can we use OData for exsiting api with sql database (ADO.net — Stored procedures) and what about the filtering of data, is there anyway to dont get all records to memory and do the filter.
Best Regards