Duplicate endpoint route on same controller method with OData 8.x and Route attribute

Viewed 814

I am running into a problem with OData 8.x and attribute routing where duplicate routes are being generated to the same controller action. This duplicate route is causing Swagger / Swashbuckle to throw "Conflicting method/path combination" error.

I have been able to par this down to a slightly tweaked default Weather Forecasts template from Visual Studio 2019.

// Change name of controller to plural
// Added Route() attribute
// Changed ControllerBase to ODataController (issue happens with ControllerBase too)

[Route("api/[controller]")]
public class WeatherForecastsController : ODataController{

    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

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

}

Adding OData was as simple as including the AddOData() call in Startup.cs

private IEdmModel GetEdmModel() {
    var builder = new ODataConventionModelBuilder();

    builder.EntitySet<WeatherForecast>("WeatherForecasts");

    return builder.GetEdmModel();
}

public void ConfigureServices(IServiceCollection services) {
    services
        .AddControllers()

        // Added this AddOData call
        .AddOData(opt => {
            opt
                .Count()
                .Filter()
                .Select()
                .OrderBy()
                .SetMaxTop(20)
                .AddRouteComponents("api", GetEdmModel());
        });

    services.AddSwaggerGen(c => {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "SwaggerODataTest", Version = "v1" });
    });
}

As you can see there is only the single default Get() method in the controller -- i.e. ONE endpoint defined. Yet adding the Route() attribute causes the following exception

Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException:
Conflicting method/path combination "GET api/WeatherForecasts" for actions - 
SwaggerODataTest.Controllers.WeatherForecastsController.Get (SwaggerODataTest),
SwaggerODataTest.Controllers.WeatherForecastsController.Get (SwaggerODataTest).
Actions require a unique method/path combination for Swagger/OpenAPI 3.0.
Use ConflictingActionsResolver as a workaround

It seems that both OData and ASP.NET Core (??) are processing the same controller twice and generating duplicate (conflicting) endpoints. Am I doing something wrong? Is there a way to tell ASP.NET Core not to parse controllers to generate routing endpoints and let OData do it?

1 Answers

AddRouteComponents adds routes based on OData route conventions (https://docs.microsoft.com/en-us/odata/webapi/built-in-routing-conventions)

Following is a good explanation: https://stackoverflow.com/a/67764179/2863098

Just remove the Route and the HttpGet attribute in your controller and it should work.

I just started with OData so I am struggling as well :-). In the case the following is useful information for anyone else who is struggling...

I do the following (in ASP.Net Core 6 program.cs, but the logic is easily transferable to ASP.Net Core 5 startup.cs) to have $top, $count, [controller](key), [controller]/key and all that. I am using Entity Framework (.Net 6, Sql Server) in this example.

string connectionString = builder.Configuration.GetConnectionString("ExampleDb");
builder.Services.AddDbContext<ExampleDbContext>(options => options.UseSqlServer(connectionString));

builder.Services.AddControllers().AddOData(options => {
    options.EnableQueryFeatures();
    var routeOptions = options.AddRouteComponents(EdmModelBuilder.GetEdmModel()).RouteOptions;
    
    routeOptions.EnableQualifiedOperationCall =
    routeOptions.EnableKeyAsSegment = 
    routeOptions.EnableKeyInParenthesis = true;
});

And this in a separate EdmModelBuilder.cs file. (Test is a .Net 6 Entity Framework model)

public static class EdmModelBuilder {
    public static IEdmModel GetEdmModel() {
        var builder = new ODataConventionModelBuilder();
        builder.EntitySet<Test>("Test");

        return builder.GetEdmModel();
    }
}

And this in my controller. (key can be another primary data type or string; IActionResult plays nice with swagger; no Route nor HttpGet attribute(s))

public class TestController : ODataController {
    private readonly ExampleDbContext _context;
    private readonly ILogger<TestController> _logger;

    public TestController(ILogger<TestController> logger, ExampleDbContext context) {
        _logger = logger;
        _context = context;
    }

    [EnableQuery(PageSize = 10)]
    public IActionResult Get() {
        return Ok(_context.Test);
    }
    public IActionResult Get(int key) {
        return Ok(_context.Test.SingleOrDefault(c => c.Id == key));
    }
}
Related