aspnet-api-versioning: Versioned OData attribute routing not working on ASP.NET Core 3.1 and ASP.NET Core 5.0 (with or without Endpoint routing enabled)

Hi,

I’m using Microsoft.AspNetCore.OData.Versioning version 5.0.0.

I just created a sample WeatherForecastsController project for ASP.NET Core 5.0, added the recommended startup settings (along with the ones for versioning the API), decorated the controller and the action methods with the OData routing attributes but it’s not working as expected. Note: This used to work for me in ASP.NET Core 2.2 with the API versioning. I tried with Endpoint routing enabled as well as disabled with no luck. I don’t know what changed or if I’m doing anything wrong.

I have 3 methods defined in the controller:

  • CreateWeatherForecast (POST)
  • GetWeatherForecasts (essentially a “List”)
  • GetWeatherForecast (GET by ID)

As you can see from the REST requests below, it’s not honoring the OData routes and seems to be only going by convention:

GET /v1 HTTP/1.1
HTTP/1.1 200 OK
{
	"@odata.context": "https://localhost:5001/v1/$metadata",
	"value": [
		{
			"name": "WeatherForecasts",
			"kind": "EntitySet",
			"url": "WeatherForecasts"
		}
	]
}

GET https://localhost:5001/v1/weatherforecasts/1ad7afe0-49df-48f4-be0c-d9c1452865a4 HTTP/1.1
HTTP/1.1 200 OK
{
	"@odata.context": "https://localhost:5001/v1/$metadata#WeatherForecasts/$entity",
	"id": "1ad7afe0-49df-48f4-be0c-d9c1452865a4",
	"date": "2020-12-18T19:31:12.5176592-08:00",
	"temperatureC": -15,
	"summary": "Cool"
}

GET https://localhost:5001/v1/weatherforecasts HTTP/1.1
HTTP/1.1 404 Not Found
Content-Length: 0

When I rename the List method (GetWeatherForecasts) to just “Get”, the List call starts working again:

GET /v1/weatherforecasts HTTP/1.1
HTTP/1.1 200 OK
{
    "@odata.context": "https://localhost:5001/v1/$metadata#WeatherForecasts",
    "value": [
    {
	    "id": "80a99a08-fd0b-4b75-8875-c1aa4693fde3",
	    "date": "2020-12-18T19:34:20.3323084-08:00",
	    "temperatureC": 38,
	    "summary": "Sweltering"
    },
    {
	    "id": "3912b265-0ada-4ac9-a32d-af799e7fa4c7",
	    "date": "2020-12-19T19:34:20.3959579-08:00",
	    "temperatureC": 44,
	    "summary": "Hot"
    },
    {
	    "id": "3acc83a1-dafb-4f56-9e44-c6172873a23a",
	    "date": "2020-12-20T19:34:20.3963945-08:00",
	    "temperatureC": 48,
	    "summary": "Cool"
    },
    {
	    "id": "351ef82f-c18f-48b7-b057-dd6f67f940b1",
	    "date": "2020-12-21T19:34:20.3965475-08:00",
	    "temperatureC": 50,
	    "summary": "Sweltering"
    },
    {
	    "id": "5ff56225-4f22-49a6-b00a-9a3277e8195d",
	    "date": "2020-12-22T19:34:20.3966482-08:00",
	    "temperatureC": 43,
	    "summary": "Cool"
    }
    ]
}

Same with POST - when I rename it to PostWeatherForecast or just Post, it works; not otherwise.

The test code is available at: https://github.com/deepakku-work/WeatherForecast. For reference, the relevant details are shared below. Any pointers on what I’m doing wrong / if this is a known issue / pointers on how I can debug this further will be very much appreciated.

Project file:

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OData" Version="7.5.2" />
    <PackageReference Include="Microsoft.AspNetCore.OData.Versioning" Version="5.0.0" />
    <PackageReference Include="Microsoft.AspNetCore.OData.Versioning.ApiExplorer" Version="5.0.0" />
  </ItemGroup>

Startup.cs:

    public void ConfigureServices(IServiceCollection services)
    {
        // Note: I have tried services.AddMvcCore() with EnableEndpointRouting set to false here as well.
        services.AddControllers();
        services.AddApiVersioning();
        services.AddOData().EnableApiVersioning();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, VersionedODataModelBuilder modelBuilder)
    {
        app.UseRouting();

        // Note: I have tried app.UseMvc() here (when I disabled endpoint routing) as well with no luck.
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            // Note: I have tried endpoints.MapODataRoute() here as well and it does not work.
            endpoints.MapVersionedODataRoute("versioned-odata", "v{version:apiVersion}", modelBuilder);
        });
    }

WeatherForecast model:

      public class WeatherForecast
      {
              public string 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; }
      }

WeatherForecastODataConfiguration:

    public class WeatherForecastODataConfiguration : IModelConfiguration
    {
        public void Apply(ODataModelBuilder builder, ApiVersion apiVersion, string routePrefix)
        {
            switch (apiVersion.MajorVersion)
            {
                default:
                    ConfigureV1(builder);
                    break;
            }
        }

        private static void ConfigureV1(ODataModelBuilder builder)
        {
            var weatherForecastType = builder.EntitySet<WeatherForecast>("WeatherForecasts").EntityType;
            weatherForecastType.HasKey(r => r.Id);
            weatherForecastType.Property(r => r.Id).IsOptional();
        }
    }

WeatherForecastsController:

    [ApiVersion("1.0")]
    [ODataRoutePrefix("WeatherForecasts")]
    [ApiExplorerSettings(IgnoreApi = false)]
    public class WeatherForecastsController : ODataController
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastsController> _logger;

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

        [HttpPost]
        [ODataRoute]
        [ProducesResponseType(typeof(WeatherForecast), StatusCodes.Status200OK)]
        public WeatherForecast CreateWeatherForecast()
        {
            var rng = new Random();
            return new WeatherForecast
            {
                Id = Guid.NewGuid().ToString(),
                Date = DateTime.Now.AddDays(Enumerable.Range(1, 5).First()),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            };
        }

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

        [HttpGet]
        [ODataRoute("{id}")]
        [ProducesResponseType(typeof(WeatherForecast), StatusCodes.Status200OK)]
        public WeatherForecast GetWeatherForecast(string id)
        {
            var rng = new Random();
            return new WeatherForecast
            {
                Id = id,
                Date = DateTime.Now.AddDays(Enumerable.Range(1, 5).First()),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            };
        }

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 1
  • Comments: 16

Commits related to this issue

Most upvoted comments

Duplicate of #689 and #698. Do not use odata attribute routing with Microsoft.AspNetCore.OData.Versioning 5.0.0, it just behave same as convention routing (and GetWeatherForecasts is not allowed). If you decide to continue to use Microsoft.AspNetCore.OData.Versioning 5.0.0, also look at #695 and #693

It’s taken a while to circle back on this, but I can confirm it’s a bug. There is a copy and paste error here. This tests whether a controller action maps to an OData action or function. In other words, Get or Get<EntitySet> can never be confused with an OData function, which must be Get<Name>. The test was incorrectly testing for Post<EntitySet>. This is why Get works, but not Get<EntitySet>.

The reason things likely worked in earlier versions was due to a different bug that didn’t have this test. In that issue, it was possible that a function was incorrectly matched for a entity set.

OData has a huge number of combinations and test matrix. Unfortunately, this is further conflated by 3 routing models now! >_< I try to cover as many as I can, but it’s a lot of work. OData has historically continued to choose not to adopt the standard attribute routing system (though 8.0 will get much, much closer), so API Versioning creates a ODataTemplate and ODataAttributeRouteInfo that essentially looks like AttributeRouteInfo to the rest of ASP.NET. OData’s prefix concept requires API Versioning to fan out a clone of each matching action descriptor for each version and prefix combination as there can be more than one. This further illustrates how OData doesn’t really care able the prefix; at least, not by the time it gets to your controller. This bug caused the fanned out results to yield no results, which effectively removed some actions as candidates (yikes!). This would happen regardless of which routing approach you used.

This will be fixed in the next patch. In the meantime, you can work around the by using the short, method-only form. Honestly, I find it not only more succinct, but clearer in meaning. GET, for example, is not a verb, it’s a method. HTTP is the API. C#, ASP.NET, etc is just an impedance mismatch facade over it. It helps me to think of it as <response> Http.Get(<request>). Others mileage may vary, but a C# method is definitely not the method or API. 😜