Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

How to add URL path version to ODataController using attribute routing in OData 8.x #1095

Discussion options

I am trying out version interleaving example with URL path version + OData, but I could not get OData to recognize the versions on the controller as expected with route attribute. I am looking for guidance for how to setup URL path version correctly with OData 8.x

.NET version

6.0

Package versions

<PackageReference Include="Asp.Versioning.OData" Version="6.4.0" />
<PackageReference Include="Microsoft.AspNetCore.OData" Version="8.2.5" />

Routes that I want

WeatherForecastController
|--- Get()   -> /api/v1.0/weatherforecast
|--- GetV2() -> /api/v2.0/weatherforecast
|--- GetV3() -> /api/v3.0/weatherforecast

Setup code:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers().AddOData(options =>
{
    options.Select().Filter();
});

builder.Services.AddApiVersioning().AddOData(
    options =>
    {
        options.ModelBuilder.DefaultModelConfiguration = (builder, apiVersion, routePrefix) =>
        {
            builder.EntitySet<WeatherForecast>("WeatherForecast");
        };
        options.AddRouteComponents("api/v{version:apiVersion}");
    });

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseODataRouteDebug();
app.UseRouting();

app.MapControllers();

app.Run();

Controller:

namespace InterleavingVersions.Controllers
{
    [ApiVersion(1.0)]
    [ApiVersion(2.0)]
    [ApiVersion(3.0)]
    // [Route("api/v{version:apiVersion}")] -> adding this header will result in runtime exception at startup time. see the exception in the below 
    public class WeatherForecastController : ODataController
    {
        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        // Conventional
        [EnableQuery]
        [HttpGet]
        public IQueryable<WeatherForecast> Get()
        {
            return new[] { new WeatherForecast() { Id = "v1" } }.AsQueryable();
        }

        // Attribute WITHOUT the version prefix
        [EnableQuery]
        [HttpGet("WeatherForecast"), MapToApiVersion(2.0)]
        public IQueryable<WeatherForecast> GetV2()
        {
            return new[] { new WeatherForecast() { Id = "v2" } }.AsQueryable();
        }

        // Attribute WITH the version prefix
        [EnableQuery]
        [HttpGet("api/v{version:apiVersion}/WeatherForecast"), MapToApiVersion(3.0)]
        public IQueryable<WeatherForecast> GetV3()
        {
            return new[] { new WeatherForecast() { Id = "v3" } }.AsQueryable();
        }
    }
}

Actual OData routes:

image

Expected vs actual

  • Conventional: this generates the template that I expected.
  • Attribute WITHOUT the version prefix
    • Expected: route template has URL path prefix api/v{version:apiVersion}
    • Actual: the prefix does not present in the URL path, and versioning only works with query string like https://localhost:xxxx/weatherforecast?api-version=2.0
  • Attribute WITH the version prefix: this generates the template that I expected
  • I also tried to add the version route prefix api/v{version:apiVersion} with RouteAttribute, and this will result in a runtime exception at startup time.
Error 1:
Attribute routes with the same name 'api/v{version:apiVersion}/WeatherForecast' must have the same template:
Action: 'InterleavingVersions.Controllers.WeatherForecastController.Get (InterleavingVersions)' - Template: 'api/v{version:apiVersion}/api/v{version:apiVersion}/WeatherForecast'
Action: 'InterleavingVersions.Controllers.WeatherForecastController.Get (InterleavingVersions)' - Template: 'api/v{version:apiVersion}/api/v{version:apiVersion}/WeatherForecast'
Action: 'InterleavingVersions.Controllers.WeatherForecastController.Get (InterleavingVersions)' - Template: 'api/v{version:apiVersion}/WeatherForecast'

Error 2:
Attribute routes with the same name 'api/v{version:apiVersion}/WeatherForecast/$count' must have the same template:
Action: 'InterleavingVersions.Controllers.WeatherForecastController.Get (InterleavingVersions)' - Template: 'api/v{version:apiVersion}/api/v{version:apiVersion}/WeatherForecast/$count'
Action: 'InterleavingVersions.Controllers.WeatherForecastController.Get (InterleavingVersions)' - Template: 'api/v{version:apiVersion}/api/v{version:apiVersion}/WeatherForecast/$count'
Action: 'InterleavingVersions.Controllers.WeatherForecastController.Get (InterleavingVersions)' - Template: 'api/v{version:apiVersion}/WeatherForecast/$count''

Note that since 8.0, ASP.NET Core OData deprecated the OdataRouteAttribute: https://devblogs.microsoft.com/odata/attribute-routing-in-asp-net-core-odata-8-0-rc/, and attribute routes are specified with the ASP.NET Core route attributes.

You must be logged in to vote

Navigating the mess that is OData routing conventions is daunting. I empathize.

Observations

  1. You shouldn't have to call UseRouting() as that is done for you (it worked for me without it)
  2. Naming with OData is everything. I'm 99% sure an entity and entity set cannot have the same name
    a. This means you should have WeatherForecasts and WeatherForecast
  3. OData is intrinsically not version-aware and, therefore, makes many assumptions in its naming conventions
    a. There can only be one Get in code, which is enforced by the compiler
    b. GetV2 will never be seen or treated as matching Get without some help

Honestly, I don't think I have a lot of examples of version interleaving with OData. This is …

Replies: 1 comment

Comment options

Navigating the mess that is OData routing conventions is daunting. I empathize.

Observations

  1. You shouldn't have to call UseRouting() as that is done for you (it worked for me without it)
  2. Naming with OData is everything. I'm 99% sure an entity and entity set cannot have the same name
    a. This means you should have WeatherForecasts and WeatherForecast
  3. OData is intrinsically not version-aware and, therefore, makes many assumptions in its naming conventions
    a. There can only be one Get in code, which is enforced by the compiler
    b. GetV2 will never be seen or treated as matching Get without some help

Honestly, I don't think I have a lot of examples of version interleaving with OData. This is probably due to the version-to-EDM affinity, but it is possible and supported.

Solution

To make things work the way you want, this simple change should do the trick:

[ApiVersion( 1.0 )]
[ApiVersion( 2.0 )]
[ApiVersion( 3.0 )]
public class WeatherForecastsController : ODataController
{
    [EnableQuery]
    public IQueryable<WeatherForecast> Get() =>
        new WeatherForecast[] { new() { Id = "v1" } }.AsQueryable();

    [EnableQuery]
    [ActionName( nameof( Get ) )]
    [MapToApiVersion( 2.0 )]
    public IQueryable<WeatherForecast> GetV2() =>
        new WeatherForecast[] { new() { Id = "v2" } }.AsQueryable();

    [EnableQuery]
    [ActionName( nameof( Get ) )]
    [MapToApiVersion( 3.0 )]
    public IQueryable<WeatherForecast> GetV3() =>
        new WeatherForecast[] { new() { Id = "v3" } }.AsQueryable();
}
Name
Entity Set WeatherForecasts
Entity WeatherForecast
Operation Get
You must be logged in to vote
0 replies
Answer selected by bedding
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
🙏
Q&A
Labels
None yet
2 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.