Enabling Endpoint Routing in OData

Hassan Habib

Hassan

Few months ago we announced an experimental release of OData for ASP.NET Core 3.1, and for those who could move forward with their applications without leveraging endpoint routing, the release was considered final, although not ideal.

But for those who have existing APIs or were planning to develop new APIs leveraging endpoint routing, the OData 7.3.0 release didn’t quiet meet their expectations without having to disable endpoint routing.

Understandably this was quite a trade off between leveraging the capabilities of endpoints routing versus being able to use OData. Therefore in the past couple of months the OData team in coordination with ASP.NET team have worked together to achieve the desired compatibility between OData and Endpoint Routing to work seamlessly and offer the best capabilities of both worlds to our libraries consumers.

Today, we announce that this effort is over! OData release of 7.4.0 now allows using Endpoint Routing, which brings in a whole new spectrum of capabilities to take your APIs to the next level with the least amount of effort possible.

 

Getting Started

To fully bring this into action, we are going to follow the Entity Data Model (EDM) approach, which we have explored previously by disabling Endpoint Routing, so let’s get started.

We are going to create an ASP.NET Core Application from scratch as follows:

Image New Web Application

Since the API template we are going to select already comes with an endpoint to return a list of weather forecasts, let’s name our project WeatherAPI, with ASP.NET Core 3.1 as a project configuration as follows:

Image New API

 

Installing OData 7.4.0 (Beta)

Now that we have created a new project, let’s go ahead and install the latest release of OData with version 7.4.0 by either using PowerShell command as follows:

Install-Package Microsoft.AspNetCore.OData -Version 7.4.0-beta

You can also navigate to the Nuget Package Manager as follows:

Image ODataWithContextBeta

 

Startup Setup

Now that we have the latest version of OData installed, and an existing controller for weather forecasts, let’s go ahead and setup our startup.cs file as follows:

using System.Linq;
using Microsoft.AspNet.OData.Builder;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OData.Edm;

namespace WeatherAPI
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddOData();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.Select().Filter().OrderBy().Count().MaxTop(10);
                endpoints.MapODataRoute("odata", "odata", GetEdmModel());
            });
        }

        private IEdmModel GetEdmModel()
        {
            var odataBuilder = new ODataConventionModelBuilder();
            odataBuilder.EntitySet<WeatherForecast>("WeatherForecast");

            return odataBuilder.GetEdmModel();
        }
    }
}

 

As you can see in the code above, we didn’t have to disable EndpointRouting as we used to do in the past in the ConfigureServices method, you will also notice in the Configure method has all OData configurations as usual referencing creating an entity data model with whatever prefix we choose, in our case here we set it to odata but you can change that to virtually anything you want, including api.

 

Weather Forecast Model

Before you run your API, you will need to do a slight change to the demo WeatherForecast model that comes in with the API template, which is adding a key to it, otherwise OData wouldn’t know how to operate on a keyless model, so we are going to add an Id of type GUID to the model, and this is how the WeatherForecast model would look like:

    public class WeatherForecast
    {
        public Guid Id { get; set; }
        public DateTime Date { get; set; }
        public int TemperatureC { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
        public string Summary { get; set; }
    }

 

Weather Forecast Controller

We had to enable OData querying on the weather forecast endpoint while removing all the other unnecessary annotations, this is how our controller looks like:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.OData;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace WeatherAPI.Controllers
{
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        [EnableQuery]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Id = Guid.NewGuid(),
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

 

Hit Run

Now that we have everything in place, let’s run our API and hit our OData endpoint with an HTTP GET request as follows:

https://localhost:44344/odata/weatherforecast

The following result should be returned:

{
  "@odata.context": "https://localhost:44344/odata/$metadata#WeatherForecast",
  "value": [
    {
      "Id": "66b86d0d-375f-4133-afb4-82b44f7f2e79",
      "Date": "2020-03-02T23:07:52.4084956-08:00",
      "TemperatureC": 23,
      "Summary": "Mild"
    },
    {
      "Id": "d534a764-4fb8-4f49-96c5-8f09987a61d8",
      "Date": "2020-03-03T23:07:52.4085408-08:00",
      "TemperatureC": 9,
      "Summary": "Balmy"
    },
    {
      "Id": "07583c78-b2f5-4119-acdb-50511ac02e8a",
      "Date": "2020-03-04T23:07:52.4085416-08:00",
      "TemperatureC": -15,
      "Summary": "Hot"
    },
    {
      "Id": "05810360-d1fb-4f89-be18-2b8ddc75beff",
      "Date": "2020-03-05T23:07:52.4085421-08:00",
      "TemperatureC": 9,
      "Summary": "Hot"
    },
    {
      "Id": "35b23b1a-4803-4c3e-aebc-ced17807b1e1",
      "Date": "2020-03-06T23:07:52.4085426-08:00",
      "TemperatureC": 16,
      "Summary": "Hot"
    }
  ]
}

You can now try the regular operations of $select, $orderby, $filter, $count and $top on your data and examine the functionality yourself.

 

Non-Edm Approach

If you decide to go the non-Edm route, you will need to install an additional Nuget package to resolve a Json formatting issue as follows:

First of all install Microsoft.AspNetCore.Mvc.NewtonsoftJson package by running the following PowerShell command:

Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson -Version 3.1.2

You can also navigate for the package using Nuget Package manager as we did above.

Secondly, you will need to modify your ConfigureService in your Startup.cs file to enable the Json formatting extension method as follows:

using System.Linq;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace WeatherAPI
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers().AddNewtonsoftJson();
            services.AddOData();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
                endpoints.EnableDependencyInjection();
                endpoints.Select().Filter().OrderBy().Count().MaxTop(10);
            });
        }
    }
}

Notice that we added AddNewtonsoftJson() to resolve the formatting issue with $select, we have also removed the MapODataRoute(..) and added EnableDependencyInjection() instead.

With that, we have added back the weather forecast controller [ApiController] and [Route] annotations in addition to [HttpGet] as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.OData;
using Microsoft.AspNetCore.Mvc;

namespace WeatherAPI.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        [HttpGet]
        [EnableQuery]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Id = Guid.NewGuid(),
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }
}

 

Now, run your API and try all the capabilities of OData with your ASP.NET Core 3.1 API including Endpoint Routing.

 

Final Notes

  1. The OData team will continue to address all the issues opened on the public github repo based on their priority.
  2. We are always open to feedback from the community, and we hope to get the community’s support on some of our public repos to keep OData and it’s client libraries running.
  3. With this current implementation of OData you can now enable Swagger easily on your API without any issues, here’s an example
  4. You can clone the example we used in this article from this repo here to try it for yourself.
  5. The final release of OData 7.4.0 library should be released within two weeks from the time this article was published.

42 comments

Comments are closed. Login to edit/delete your existing comments

  • Avatar
    Eddie Conner

    I setup the controller to use both the standard API routes for existing configurations using the non-edm configuration above and also setup odata mapping and edm to use for odata. The odata version returns the count of the number of filtered records as expected. However, the standard API version also only returns the filtered records but of course the count is not part of the non edm version. The API function does a GetAll and and the list is filled with all of the records and then the odata query filters the list before returning to the client. This looks like the filter is happening in the middleware. Is it possible to get the number of filtered records in the non-edm configuration from the middleware process? I would like to return the count in the response header in the non-edm approach.

    • Hassan Habib
      Hassan HabibMicrosoft employee

      Without the EDM approach, you won’t be able to have the metadata unless you capture OData response and modify the response header, I’d ask why would you go through all that trouble instead of just using the standardized OData response?

      • Avatar
        Eddie Conner

        I am looking at the option of supporting both OData as well as the standard API responses without the extra meta data and being wrapped within the value [] array. This allows using the same controllers to support existing interfaces that are not using OData but still allow the the API version to be queried. I currently have this working but the standard API version of course does not have the default meta data therefore there is not a record count. One option is to put the count in a response header for the non OData response. However, if a filter is applied to the non OData controller, the returned response is correct but the count for the header reflects the number of items returned from the database before the OData filter is applied to the response. Is there a way to access the response in the middleware before being returned to the client to get a item count to put into the response header?

        I did find the $count=true&$format=application/json;odata.metadata=none parameters to remove the meta data but the response is still wrapped within the value [] array.

        I am using MongoDB with the MongoDB C# Driver behind the OData API . Do you know if MongoDB has the ability to pass the OData filter into the database query and append the query like EF Core does with the SQL Server query? Or Is that a process EF Core is doing? Currently,
        the controller is querying the database and returning an IEnummerable type and then the filter is being applied. I also tried IQueryable but the MongoDB query on the server does not include the OData filter.

  • Avatar
    Boddu, Ram

    Hassan, Very good info. Thank you. I followed the steps and able to create and execute. Now, I am trying to get my solution work with EDM and sql db. I had services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString(“mysqlconn”)));
    Should I consider having odataBuilder.Function in GetEdmModel to have the stored proc? Please advise.

  • Avatar
    Al-Amin Hossain

    Hi Hassan,
    I have a question about implementation of odata with swagger. your example shows EDM approach with swagger. Can we use swagger with Non-EDM approach?

  • ivan zinov
    ivan zinov

    I have a different formatter, not using Newtonsoft currently, instead SystemTextJson that Microsoft was recommending. But adding Newtonsoft into the picture gives me back a 406 Not Acceptable. Do you know why? Even removing SystemTextJson as part of the Outputformatters.
    In my opinion, there is more work to be done around the package.

  • Avatar
    Steven Liekens

    Hassan, thanks for the update.

    I’m experimenting with the beta package and I have some questions, particularly about how to use route attributes:

    * Am I still required to inherit ODataController? The example code inherits ControllerBase.
    * If I can use ControllerBase, do I still need to use ODataRoutingAttribute at the controller level?
    * Should I use RouteAttribute or ODataRouteAttribute for actions?
    * Should I use RouteAttribute or ODataRoutePrefixAttribute for route prefixes?
    * Do I need to use HttpGetAttribute, HttpPostAttribute etc.?

    Thanks.

  • Maulik Modi
    Maulik Modi

    Hi Hassan,

    Eagerly awaiting 7.4 release.

    What would be the right Github repository for issues or feature requests relating to Odata? Here’s my questions:
    1) Does OData support EF Core keyless entity – https://stackoverflow.com/questions/60847616/how-can-i-expose-querable-api-using-odata-for-ef-core-keyless-entitydatabase-vi
    2) How can we have additional Parameters in URL Path segments? e.g. we have all our API routes begin with \customers\{customerid}\ and we want to expose them with Odata \customers\{customerid}\projects, \customers\{customerid}\invoices

    Thanks,
    Maulik.

    • Avatar
      SimonS

      @Mike, I’m looking at exactly the same thing. I kind of had it working with the non endpoint routing version (under .Net Core 3.1 with the 7.3.0 library); but this is all entirely different, and with limited documentation for the 3.1 OData library in its entirely is tough to make sense of it all…

      @Hassan Any help here (like updating this sample to Core 3.1/OData 7.4.0-beta appreciated!

      Thanks!

  • Avatar
    Chandankhede, Pravin

    Hi Hassan – thanks for this nice article. I am trying to use OData , WebApi, Endpoint routing & versioning with new System.Text.Json. However this combination doesn’t seems to be working. I am trying to get remove Newtonsoft from the project, however this looks like an blocker. Can you please provide some information around this?

    http://localhost:56542/v1/masterlists/PHASE?$select=name

    I get this json rather than actual entity
    [
    {
    “Instance”: null,
    “Container”: {},
    “ModelID”: “26fdaf5e-602e-4d6d-8480-80c576110494”,
    “UntypedInstance”: null,
    “InstanceType”: null,
    “UseInstanceForProperties”: false
    },
    {
    “Instance”: null,
    “Container”: {},
    “ModelID”: “26fdaf5e-602e-4d6d-8480-80c576110494”,
    “UntypedInstance”: null,
    “InstanceType”: null,
    “UseInstanceForProperties”: false
    },
    {
    “Instance”: null,
    “Container”: {},
    “ModelID”: “26fdaf5e-602e-4d6d-8480-80c576110494”,
    “UntypedInstance”: null,
    “InstanceType”: null,
    “UseInstanceForProperties”: false
    }
    ]

    • Rajat Kumar
      Rajat Kumar

      Hey @Pravin,

      you need to configure your service like below code

      services.AddControllers(mvcOptions =>
                      mvcOptions.EnableEndpointRouting = false).AddNewtonsoftJson();
                  services.AddOData();
  • Eskild Diderichsen
    Eskild Diderichsen

    I’m having issues pairing this with the ASP.NET Core SPA template `app.UseSpa(spa => …)`. Any chance you could demo that 🙂

    Also I tried enabling nullable reference types `<Nullable>enable</Nullable>` but that seems to break OData model binding. A demo showcasing this would also be much appreciated 🙂