Open commonsensesoftware opened 2 years ago
@commonsensesoftware Hi, Chris Martinez very glad to see you again!
It seems you are trying our 8.x. I'd like to learn from your perspective on API-versioning. Please don't hesitate to file new issues or discussions for us. Thanks.
Hey Sam (@xuzhg)
Indeed. I've had 8.0 working for a while now. Overall, you've done some great work and OData is much easier to integrate now. There are still pain points. This particular case only happens with a pretty specific setup, but it shouldn't happen IMO. 😉 Unless I missed something, if multiple IODataRoutingMetadata
are added to the endpoint metadata via the SelectorModel
, then they should all be considered before giving up. They can be different, even if they usually aren't.
Happy to spin up meeting some time if you want discuss paint points or limitations that I've hit. The API Versioning support for OData is significantly simpler with a lot less code. The number one issue is still the limitation of one EDM per registration of AddRouteComponents
. It was a little more straight forward when it was possible to resolve IEdmModel
out of the container, but now I've had to do some sorcery under the covers. It also requires a separate, version-specific AddRouteComponents
implementation because that was only way to get in front of and customize what happens. I would have preferred to keep things as natural to OData users as possible. Perhaps it's an area that can be improved upon in OData 9.0 or beyond.
I ran into this exact issue when I tried a simple scenario where I wanted to deprecate an older version of my OData API. In my case, I use an URL segment for the versioning instead of a query string parameter but I don't think it's relevant.
Suppose the following Program.cs
using Asp.Versioning;
using Microsoft.AspNetCore.OData;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddControllers()
.AddOData();
builder.Services
.AddApiVersioning(options =>
{
options.ApiVersionReader = new UrlSegmentApiVersionReader();
options.ReportApiVersions = true;
})
.AddOData(options =>
{
options.AddRouteComponents("odata/{apiVersion:apiVersion}");
});
var app = builder.Build();
app.MapControllers();
app.UseODataRouteDebug("odata/$debug");
app.MapGet("/", () => "Hello World!");
app.Run();
And the following OData.cs
namespace POC_ODataVersioning;
using Asp.Versioning;
using Asp.Versioning.OData;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using Microsoft.OData.ModelBuilder;
[ApiVersion("1.0")]
//[ApiVersion("1.0", Deprecated = true)]
//[ApiVersion("2.0")]
//[ApiVersion("2.0", Deprecated = true)]
public class DefaultController : ODataController
{
[HttpGet("odata/{apiVersion:apiVersion}/me")]
public IActionResult Me()
{
return Ok(new MeModel() { Name = "maboivin" });
}
}
public class MeModel
{
public string Name { get; set; }
}
public class ModelConfiguration : IModelConfiguration
{
public void Apply(ODataModelBuilder builder, ApiVersion apiVersion, string? routePrefix)
{
builder.Function("me").Returns<MeModel>();
}
}
I ended up with the following results | If I decorate my OData controller with | And I make a request to | I get | And I expect |
---|---|---|---|---|
[ApiVersion("1.0")] |
/odata/1.0/me | 200 | 200 | |
[ApiVersion("1.0")] [ApiVersion("2.0")] |
/odata/1.0/me | 200 | 200 | |
[ApiVersion("1.0")] [ApiVersion("2.0")] |
/odata/2.0/me | 404 | 200 | |
[ApiVersion("1.0", Deprecated = true)] |
/odata/1.0/me | 200 | 200 | |
[ApiVersion("1.0", Deprecated = true)] [ApiVersion("2.0")] |
/odata/1.0/me | 404 | 200 | |
[ApiVersion("1.0", Deprecated = true)] [ApiVersion("2.0")] |
/odata/2.0/me | 200 | 200 | |
[ApiVersion("1.0", Deprecated = true)] [ApiVersion("2.0", Deprecated = true)] |
/odata/1.0/me | 200 | 200 | |
[ApiVersion("1.0", Deprecated = true)] [ApiVersion("2.0", Deprecated = true)] |
/odata/2.0/me | 404 | 200 |
While debugging the ODataRoutingMatcherPolicy
, it seems to be due to the order of the IODataRoutingMetadata
items that is different depending on how you decorate your OData controllers.
As @commonsensesoftware already mentionned, looping through all the metadata items would probably solve the issue, unless there's another trivial way to solve it that I haven't found yet.
Assemblies affected ASP.NET Core OData 8.x
Describe the bug Various paths through
ODataRoutingApplicationModelProvider
can add multipleIODataRoutingMetadata
to anEndpoint
, butODataRoutingMatcherPolicy
will only ever consider the first one:https://github.com/OData/AspNetCoreOData/blob/61ae3232d4ad687bc552fc583b32103f1aeff41a/src/Microsoft.AspNetCore.OData/Routing/ODataRoutingMatcherPolicy.cs#L86
This can result in some endpoints to not match when they should. There doesn't appear to be any documentation that indicates this is the expected behavior.
Reproduce steps Consider a versioned API, which defines:
Each version has its own EDM and ultimately adds 4
ODataRoutingMetadata
instances to theEndpoint
. The appropriate EDM is matched by the appliedApiVersionAnnotation
to the incoming request. Consider the following controller.A version-neutral controller can match any API version, including none at all. OData, however, must have an EDM. In this scenario, a developer is expected to use the same function definition for each version, but that's their discretion. This configuration yields the following results:
api/GetSalesTaxRate(PostalCode=98052)
api/GetSalesTaxRate(PostalCode=98052)?api-version=1.0
api/GetSalesTaxRate(PostalCode=98052)?api-version=2.0
api/GetSalesTaxRate(PostalCode=98052)?api-version=3.0
This happens because once an explicit version is specified, it cannot match up to the correct EDM as only the first set of metadata is considered. This is only one example, but there are any number of other cases where this could happen.
Expected behavior
ODataRoutingMatcherPolicy.ApplyAsync
should consider allIODataRoutingMetadata
before invalidating a candidate.