OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core
Other
446 stars 156 forks source link

Conflicting routes when using NSwag and OData #1218

Open Gaven-F opened 2 months ago

Gaven-F commented 2 months ago

Assemblies affected ASP.NET Core OData 8.2.5 image

Describe the bug When I use NSwag, I use OData to set up Get (id) and find an error: Nswag cannot generate openapi, which seems to mean that Get routes are created multiple times

Reproduce steps Use the sample code and load Nswag (I don't know if it's the only one that contains this error)

Data Model

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

EDM (CSDL) Model image

Screenshots image

Additional context

CustomerController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;

namespace Server.Controllers;

[Route("[controller]/[action]")]
public class CustomerController
{
    [EnableQuery]
    public ActionResult<IEnumerable<Customer>> Get() => new List<Customer>
    {
        new() { Id = 1, Name = "Customer 1" },
        new() { Id = 2, Name = "Customer 2" }
    };

    public ActionResult<Customer> Get([FromRoute] int key)
    {
        return new Customer { Id = key, Name = $"Customer {key}" };
    }
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

program.cs


using Microsoft.AspNetCore.OData;
using Microsoft.OData.ModelBuilder;
using Server.Controllers;
using Server.Models;
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);

var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Customer>("Customer");
modelBuilder.EntitySet<TestModel>("Test");

builder.Services
    .AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
    })
    .AddOData(options =>
    {
        options
        .EnableQueryFeatures()
        .AddRouteComponents("oData", modelBuilder.GetEdmModel());
    });

builder.Services.AddOpenApiDocument(confi =>
{
    confi.DocumentName = "default";

    confi.PostProcess = doc =>
    {
        doc.Info.Title = "BMS API";
    };
});

var app = builder.Build();

app
    .UseOpenApi()
    .UseSwaggerUi()
    .UseReDoc(config => config.Path = "/redoc");

app.UseAuthorization();
app.UseODataBatching().UseODataQueryRequest().UseODataRouteDebug();

app.MapControllers();

app.Run();

Maybe I should ask Nswag again.

julealgon commented 2 months ago

This is a NSwag limitation due to the way they "generate" operation names. You can provide your own delegate to generate those names the way you want so that no clashes happen.

I don't believe this is in any way related to OData. I've had this exact same problem in a normal MVC project before.

corranrogue9 commented 2 months ago

@xuzhg to follow up if this is related to OData or if there's configuration that needs to happen on the NSwag side.

xuzhg commented 2 months ago

@Gaven-F Based on your "Edm Model" (You have entity set named 'Customer'), so, OData routing mechanism tries to build endpoints for all actions in Controller (CustomerController) based on the "OData Conventional rules".

So, public ActionResult<Customer> Get([FromRoute] int key) meets one rule: [ a) controller name is entity set name, b) action name is Get, c) the parameters contains "key" or "Id"

So, two endpoints for public ActionResult<Customer> Get([FromRoute] int key) are created as:

~/oData/Customer({key}) ~/oData/Customer/{key}

Meanwhile, since CustomerController has attribute routing as '[Route("[controller]/[action]")]', so I assume another 'non-odata' endpoint is created as:

~/customer/get (This is from ASP.NET Core attribute routing using [RouteAttribute])

So, I am using your above sample codes to test it, it seems the endpoints work fine:

image

Gaven-F commented 2 months ago

I deleted the RouteAttribute and it worked fine, I think it's because the route attribute creates a route once and then OData creates a route itself ...... I think that's why

Gaven-F commented 2 months ago

In addition, I seem to have found a new problem: The $count route gave me a wrong report, but it is more inexplicable:

image image image

There is no httpcode return when using swagger. Access it directly using the browser. The httpcode is 200 but the content is empty.

My code hasn't changed much, but I took it with me just in case.

using Microsoft.AspNetCore.Mvc;

namespace Server.Controllers;

public class CustomerController
{
    private readonly List<Customer> data = [new() { Id = 1, Name = "Customer 1" }, new() { Id = 2, Name = "Customer 2" }];

    public ActionResult<IEnumerable<Customer>> Get() => data;

    public ActionResult<Customer> Get([FromRoute] int key) => new Customer { Id = key, Name = $"Customer {key}" };
}

public class Customer
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;
}
using System.Text.Json;
using Microsoft.AspNetCore.OData;
using Microsoft.OData.ModelBuilder;
using Server.Controllers;
using Server.Models;

var builder = WebApplication.CreateBuilder(args);

var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Customer>("Customer");
modelBuilder.EntitySet<TestModel>("Test");

var arg = Environment.GetEnvironmentVariables();

builder
    .Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
    })
    .AddOData(options =>
    {
        options
        .EnableQueryFeatures()
        .AddRouteComponents("oData", modelBuilder.GetEdmModel());
    });

builder.Services.AddOpenApiDocument(confi =>
{
    confi.DocumentName = "default";

    confi.PostProcess = doc =>
    {
        doc.Info.Title = "BMS API";
    };
});

var app = builder.Build();

app.UseOpenApi().UseSwaggerUi().UseReDoc(config => config.Path = "/redoc");

app.UseAuthorization();
app
    .UseODataQueryRequest()
    .UseODataBatching()
    .UseODataRouteDebug();

app.MapControllers();

app.Run();

In addition, I didn't find a relatively new documentation (even the documentation on Microsoft seems to be an old version)

julealgon commented 2 months ago

I deleted the RouteAttribute and it worked fine, I think it's because the route attribute creates a route once and then OData creates a route itself ...... I think that's why

Related: