Jason
Jason

Reputation: 2887

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

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?

Upvotes: 5

Views: 3400

Answers (2)

MindingData
MindingData

Reputation: 12470

Here's what worked for me, and a little bit of why it worked.

First, when you call

AddRouteComponents()

OData actually adds a bunch of "Convention" based routing to your application for that edmModel you give it. Why this is important is that even if you aren't using routes anywhere else (For example you aren't calling app.MapControllers() or using the [Route] attribute), OData is adding routing to your application.

I finally found this chart from Microsoft : https://learn.microsoft.com/en-us/odata/webapi/built-in-routing-conventions

I'll copy two of the charts here just incase the links ever break :

enter image description here

enter image description here

The most important thing is the ControllerName and the ControllerAction. For example a controller called ProductsController with an action of Get() will automatically be routed for the OData Get.

Where this becomes doubly important is your POST, PUT, and DELETE methods are also auto magically routed, as long as the action name themselves is Post(). This caught me out because my method names were "Update".

Where things can go awry is if you add things like the [Route] or [HttpGet] attribute on your controller or on methods that are automagically routed. In these cases, duplicate routes are created and your application crashes/swagger doesn't work etc.

You can however add [Route] or [HttpGet] to individual actions that do not fall under the OData convention. This can be annoying because it will also have to contain the controller name in the route template on the action, but it does work.

One final note is that often the endpoint must match the convention exactly. As an example :

 public HttpResponseMessage Delete(int id)

Will not work. It must be :

 public HttpResponseMessage Delete(int key)

Upvotes: 2

Dieter Vandroemme
Dieter Vandroemme

Reputation: 51

AddRouteComponents adds routes based on OData route conventions (https://learn.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));
    }
}

Upvotes: 3

Related Questions