Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Odata is not compatible with AddOpenApi of Microsoft.AspNetCore.OpenApi #1381

Open
Angelinsky7 opened this issue Jan 4, 2025 · 4 comments
Open
Assignees
Labels
bug Something isn't working followup

Comments

@Angelinsky7
Copy link

Assemblies affected
Microsoft.AspNetCore.OpenApi: 9.0.0
Microsoft.AspNetCore.OData: 9.1.1

Describe the bug
To be able to use AddOpenApi and MapOpenApi the controllers need to have a ApiControllerAttribute but Odata documentation ask use to remove it. The controller is then not found inside the openApi document.
The actual snippet of code to add openApiDocument create another document instead of adding the routes inside the already created document.
I would like to have only one openApi document and a usable openapi.json file.

Is there a workaround to be able to use ApiController and OData (without removing all routing conventions or have duplicate actions ? )

Thanks !

@Angelinsky7 Angelinsky7 added the bug Something isn't working label Jan 4, 2025
@julealgon
Copy link
Contributor

the controllers need to have a ApiControllerAttribute but Odata documentation ask use to remove it.

@Angelinsky7 would you mind sharing a link to where that recommendation is made? That sounds like bad advice to me. I assume it is linked to the requirement that [ApiController] requires attribute routing, whereas OData doesn't.

Yet another reason to consider deprecating routing conventions in OData (IMHO).

@Angelinsky7
Copy link
Author

Angelinsky7 commented Jan 6, 2025

@julealgon thanks for your answer...

here you can find it : https://github.com/OData/AspNetCoreOData/blob/main/docs/routing-pitfalls.md

the thing is that to make odata works correctly the only solution that i have found is to EXACTLY do the incorrect code...

some code for those you are interested to try to make this work correctly

Program.cs

builder.Services.AddControllersWithViews()
    .AddApplicationPart(typeof(_ApiAssembly).Assembly)
    .AddControllersAsServices()
    .AddOData(options => {
        options.EnableAttributeRouting = true;
        options.TimeZone = TimeZoneInfo.Utc;
        options.EnableQueryFeatures();
        options.RouteOptions.EnableNonParenthesisForEmptyParameterFunction = true;
        options.RouteOptions.EnableActionNameCaseInsensitive = true;
        options.RouteOptions.EnableControllerNameCaseInsensitive = true;
        options.RouteOptions.EnablePropertyNameCaseInsensitive = true;
        options.RouteOptions.EnableKeyAsSegment = true;
        options.RouteOptions.EnableKeyInParenthesis = false;
        options.AddRouteComponents(routePrefix: "api", model: modelBuilder.GetEdmModel(), configureServices: services => {
            //services.AddSingleton<IFilterBinder, CustomFilterBinder>();
            services.AddOdataLinkProvider(typeof(_ApiAssembly).Assembly);
            services.AddSingleton<IODataSerializerProvider, CustomODataSerializerProvider>();
        });
    })
    .AddJsonOptions(options => {
        options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
        options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
        options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    });

builder.Services.AddOpenApi();

ModelBuilder

EntitySetConfiguration<Thing> entitySet = modelBuilder.EntitySet<Thing>("things");
EntityTypeConfiguration<Thing> entityType = entitySet .EntityType;
entityType .HasKey(p => p.Id);

Controller

using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using Microsoft.EntityFrameworkCore;

namespace Controllers {

    [ApiController]
    [Route("/api/things")]
    public class ThingsController : ODataController {

        [HttpGet]
        [EnableQuery]
        [ProducesResponseType(typeof(PageResult<Thing>), statusCode: (Int32)HttpStatusCode.OK)]
        public async Task<ActionResult<IAsyncEnumerable<Thing>>> Get(CancellationToken cancellationToken = default) {}

        [HttpGet("{key}")]
        [EnableQuery]
        [ProducesResponseType(typeof(SingleResult<Thing>), statusCode: (Int32)HttpStatusCode.OK)]
        [ProducesResponseType(typeof(ProblemDetails), statusCode: (Int32)HttpStatusCode.NotFound)]
        [ProducesResponseType((Int32)HttpStatusCode.BadRequest)]
        [ProducesResponseType((Int32)HttpStatusCode.InternalServerError)]
        public async Task<IActionResult> GetById([FromRoute] Int64 key, CancellationToken cancellationToken = default) {}

        [HttpPost]
        [ProducesResponseType(typeof(CreatedAtActionResult), statusCode: (Int32)HttpStatusCode.OK)]
        [ProducesResponseType(typeof(ProblemDetails), statusCode: (Int32)HttpStatusCode.NotFound)]
        [ProducesResponseType((Int32)HttpStatusCode.BadRequest)]
        [ProducesResponseType((Int32)HttpStatusCode.InternalServerError)]
        public async Task<IActionResult> Post([FromBody] ThingCreateRequest request, CancellationToken cancellationToken = default) {}

        [HttpPut("{key}")]
        [ProducesResponseType((Int32)HttpStatusCode.NoContent)]
        [ProducesResponseType(typeof(ProblemDetails), statusCode: (Int32)HttpStatusCode.NotFound)]
        [ProducesResponseType((Int32)HttpStatusCode.BadRequest)]
        [ProducesResponseType((Int32)HttpStatusCode.InternalServerError)]
        public async Task<IActionResult> Put([FromRoute] Int64 key, [FromBody] ThingUpdateRequest request, CancellationToken cancellationToken = default) {}

        [HttpDelete("{key}")]
        [ProducesResponseType((Int32)HttpStatusCode.NoContent)]
        [ProducesResponseType(typeof(ProblemDetails), statusCode: (Int32)HttpStatusCode.NotFound)]
        [ProducesResponseType((Int32)HttpStatusCode.BadRequest)]
        [ProducesResponseType((Int32)HttpStatusCode.InternalServerError)]
        public async Task<IActionResult> Delete([FromRoute] Int64 key, CancellationToken cancellationToken = default) {}

    }
}

Model

namespace Models {

    public class Thing {

        public Int64 Id { get; set; } = default!;

        public required String Name { get; set; }

    }
}

With all of this... everything is working... in $odata i have everything in duplicate but it's working correctly.

@julealgon
Copy link
Contributor

in $odata i have everything in duplicate but it's working correctly.

Yep.... this is a known problem:

You can work around it by either making sure your methods don't match the built-in conventions (super cryptic approach, wouldn't recommend), or by disabling the OData route conventions (also not great as there is no "simple" API to do that, but is usually what I'd suggest).

The only other comments I'd add based on your example @Angelinsky7 is that you can use API conventions to simplify some of that nasty [Produces...] boilerplate if you have many controllers.

And I would strongly recommend leveraging ActionResult<T> instead of IActionResult as that provides stronger types and can help with some OData-specific issues where returning IActionResult causes it to not match the EDM model due to lack of typing information. ActionResult<T> has an added bonus in that if you provide a concrete type there, you don't need to specify the success return type using [ProducesResponseType]: it is picked up automatically.

@Angelinsky7
Copy link
Author

thanks i didn't know the api conventions things... and will do the stronger types for ActionResult...
i'll be waiting for #428 to be resolved.
maybe we could have a better (or a complete) documentation about the "correct" config ?!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working followup
Projects
None yet
Development

No branches or pull requests

4 participants