Summary
In a previous article, I talked about how you can leverage the power of OData with your existing ASP.NET Core API to bring in more features to your API consumers.
But there are different ways you could enable OData on your existing API that are just as simple but offers more powerful features than overriding your existing routes and enabling dependency injection.
For instance, if you’ve tried to perform a count operation using our previous method you will notice it doesn’t really return or perform anything, the same thing goes with many other features that we will talk about extensively in future articles.
In this article, however, I’m going to show you how you can enable OData on your existing ASP.NET Core API using EDM.
What is EDM?
EDM is short for Entity Data Model, it plays the role of a mapper between whatever data source and format you have and the OData engine.
In other words, whether your source of data is SQL, Cosmos DB or just plain text files, and whether your format is XML, Json or raw text or any other type out there, What the entity data model does is to turn that raw data into entities that allow functionality like count, select, filter and expand to be performed seamlessly through your API.
Setting Things up
Let’s set our existing API up with OData using EDM.
First and foremost, add in Microsoft.AspNetCore.OData nuget package to your ASP.NET Core project.
Once the nuget package is installed, let’s setup the configuration to utilize that package.
In your Startup.cs file, in your ConfigureServices function, add in the following line:
services.AddOData();
Important Note: This will work with ASP.NET Core 2.1, if you are trying to set this up with ASP.NET Core 2.2, then you must add another line of code as follows:
services.AddMvcCore(action => action.EnableEndpointRouting = false);
OData doesn’t yet support .NET Core 3.0 – at the time of this article, .NET Core 3.0 is still in preview, OData support will extend to 3.0 once it’s production ready.
Once that part is done, let’s build a private method to do a handshake between your existing data models (Students in this case) and EDM, as follows:
private IEdmModel GetEdmModel() { var builder = new ODataConventionModelBuilder(); builder.EntitySet<Student>("Students"); return builder.GetEdmModel(); }
The student model we are using here is the same model we used in our previous article, as a reminder here’s how the model looks like:
public class Student { public Guid Id { get; set; } public string Name { get; set; } public int Score { get; set; } }
Now that we have created our EDM method, now let’s do one last configuration change in the Configure method in our Startup.cs file as follows:
app.UseMvc(routeBuilder => { routeBuilder.Select().Filter().OrderBy().Expand().Count().MaxTop(10); routeBuilder.MapODataServiceRoute("api", "api", GetEdmModel()); });
Just like our last article, we enabled the functionality we needed such as select, filter and order by then we used the MapODataServiceRoute method to utilize our EDM method.
We used “api” instead of “odata” as our first and second parameters as a route name and a route prefix to continue to support our existing APIs endpoints, but there’s a catch to that.
Your contract in this case will change, if your API returns a list of students like this:
[ { "id": "acc25b4f-c53d-4363-ad33-e0c860a83a1b", "name": "Hassan Habib", "score": 100 }, { "id": "d42daeb4-37d7-4a20-9e9b-7f7a60f27ff6", "name": "Cody Allen", "score": 90 }, { "id": "db246814-d34e-40e4-aa00-b9192cec447b", "name": "Sandeep Pal", "score": 120 }, { "id": "c4e9efc9-40b7-4a85-b000-ce9c076fcd57", "name": "David Pullara", "score": 50 } ]
With the EDM method, your contract will change a bit, your response will have some helpful metadata that we are going to talk about, and it will look like this:
{ "@odata.context": "https://localhost:44374/api/$metadata#Students", "value": [ { "Id": "9cef40f6-db31-4d4c-997d-8b802156dd4c", "Name": "Hassan Habib", "Score": 100 }, { "Id": "282be5ea-231b-4a59-8250-1247695f16c3", "Name": "Cody Allen", "Score": 90 }, { "Id": "b3b06596-729b-4c6f-b337-7ad11b01371b", "Name": "Sandeep Pal", "Score": 120 }, { "Id": "084bd81e-b8a2-471d-8396-ace675f73688", "Name": "David Pullara", "Score": 50 } ] }
That extra metadata is going to help us perform more operations than the old method.
In that case if you have existing consumers for your API, I recommend introducing a new endpoint, version or informing them to change their contracts, otherwise this will be a breaking change.
The other option is to change your route name and route prefix parameters to say “odata” instead, which is the standard way to implement OData.
The last thing we need to do to make this work for us is removing the notations on top of your existing API controller class, in our case we will remove these two lines:
[Route("api/[controller]")] [ApiController]
And don’t forget to add the enabling querying annotation on top of your API method:
[EnableQuery()]
Putting OData into Action
Once that’s done, now you can try to perform higher operations using OData like Count for instance, you can call your endpoint with /api/students?$count=true and you should get:
{ "@odata.context": "https://localhost:44374/api/$metadata#Students", "@odata.count": 4, "value": [ { "Id": "6a7e60b8-cea9-4132-aac7-be9995e8e048", "Name": "Hassan Habib", "Score": 100 }, { "Id": "d6661173-4370-4781-b016-a311b0e96f14", "Name": "Cody Allen", "Score": 90 }, { "Id": "caad33c3-d2bf-443e-8623-4a033ca77de2", "Name": "Sandeep Pal", "Score": 120 }, { "Id": "eee8bb79-df81-4cc8-b03f-a3887ecabb50", "Name": "David Pullara", "Score": 50 } ] }
As you can see here, you have a new property @odata.count that shows you the count of the items in your list.
Final Notes
Now that you’ve learned about the simplest way (8 lines of code) to create a handshake between ASP.NET Core, OData and EDM here’s few notes:
- EDM doesn’t have any dependency on the Entity Framework, in fact the whole purpose of creating an EDM is to link whatever data you have in any format it may be to the OData engine and serialize the results through an API endpoint.
- EDM can only be useful if you need some specific OData features such as count and nextlink and so many other features that we will explore in future articles.
- There’s more to learn about EDM, I encourage you to check all about EDM in this extensive, comprehensive documentation.
- OData is an open-source project, I encourage you as you benefit from it’s amazing features to contribute to the project, suggest new features and participate with documentation and your experiences to keep the community active and useful for everyone.
- You can clone the project I built and try things out from this github repo.
Many thanks for this great article Hasan.
Sorry for the double-post. I will simplify: Does OData with EF Core support $apply=aggregate()?
I don’t know if anyone else is experiencing this but I added the code to my project and now my swagger no longer works. It seems like swagger doesn’t work well with Odata and regular Api controllers?
Thanks for greate article but how can we solve OData N + 1 query problems with EntifyFrameWorkCore ?
If you’re working with IQueryable this shouldn’t be a problem – how does your code look like?
Hi Hassan, thanks for reply. I using EntityFrameWorkCore with Odata.
This is my code:
`[DefaultProperties(@"APK,DivisionID,ShopID")][EnableQuery]public IEnumerable<POST0016Entity> Get(){ return _context.Orders.Include(e => e.Items).AsNoTracking();}
`
and query http: http://localhost:60365/odata/Orders?$count=true&$expand=Items&$select=divisionID,APK.
The problem is if i let OData query then the EntityFrameWorkCore will generate 22 query statement like this:
`exec sp_executesql N'SELECT [p].[APK], [p].[DivisionID], [p].[APKDInherited], [p].[APKMInherited], [p].[APKMaster], [p].[APPDetailSuggestID], [p].[APPOrderID], [p].[ActualQuantity], [p].[Amount], [p].[Ana01ID], [p].[Ana02ID], [p].[Ana03ID], [p].[Ana04ID], [p].[Ana05ID], [p].[Ana06ID], [p].[Ana07ID], [p].[Ana08ID], [p].[Ana09ID], [p].[Ana10ID], [p].[BeforeVATDiscountAmount], [p].[BeforeVATUnitPrice], [p].[CA], [p].[CAAmount], [p].[DeleteFlg], [p].[DiscountAllocation], [p].[DiscountAmount], [p].[DiscountOneUnitOfProduct], [p].[DiscountRate],...
Hi, Hassan. Thanks for the great mini-series on OData and ASP.NET Core. Does OData with EF Core support $apply=aggregate()?
I ask because this tutorial works fine when I test with https://localhost:5001/api/students?$count=true&$apply=aggregate(Score with sum as Total). When I hook up the same code to the database from Supercharging your ASP.NET Core API with OData, I get the following error:
System.ArgumentException: Value does not fall within the expected range.at System.SharedTypeExtensions.GetSequenceType(Type type)at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.VisitMainFromClause(MainFromClause fromClause, QueryModel queryModel)at Remotion.Linq.Clauses.MainFromClause.Accept(IQueryModelVisitor visitor, QueryModel...
Sorry for the poor formatting. I've try to reformat this messatge multiple times, but the blog software keeps removing all the formatting. . ________. .
You can find a better formatted version here -> https://pastebin.com/dHrTLi35
_______
Thanks for the article! . I seem to be having mixed results trying to get my code working. . I'm running .net core 2.1 and Microsoft.AspNetCore.OData Version=7.1.0.. My startup extensions:. // invoked from startup, ConfigureServices()public static IServiceCollection AddODataSupport(this IServiceCollection services){services.AddOData();//Workaround: https://github.com/OData/WebApi/issues/1177services.AddMvcCore(options...
Hi, Hassan! I follow step for step this tutorial and too your the "Supercharging your Web APIs with OData and ASP.NET Core" video. In both cases, the API requests respond correctly, and with OData only correctly responds Select and OrderBy, but not Expand. Can you give me an idea, according to your experience, what may be failing or missing? I use Visual Studio 2019 .Net Core 2.1 (also probe with .Net 2.1) and SQL Server...
Can you post your code somewhere?
This comment has been deleted.