Reputation: 1
I have here my Minimal API setup with ASP.NET Core versioning and Carter. PutMethod
,PostMethod
, and PatchMethod
works just fine, but my GetMethod
and DeleteMethod
does not return the string.
This is my API Versions and minimal API:
public class ApiVersions
{
public const string vset = "API_Version_Set";
public static ApiVersion v1 = new ApiVersion(1);
public static ApiVersion v2 = new ApiVersion(2);
public static ApiVersion v3 = new ApiVersion(3);
}
public class GeneralController : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
var group = app.MapGroup("api/v{version:apiVersion}/general")
.WithTags("General")
.WithOpenApi()
.WithApiVersionSet(ApiVersions.vset)
;
group.MapGet("{url}", GetMethod).HasApiVersion(ApiVersions.v1);
group.MapDelete("{url}", DeleteMethod).HasApiVersion(ApiVersions.v1);
group.MapPut("{url}", PutMethod).HasApiVersion(ApiVersions.v1);
group.MapPost("{url}", PostMethod).HasApiVersion(ApiVersions.v1);
group.MapPatch("{url}", PatchMethod).HasApiVersion(ApiVersions.v1);
}
public static string GetMethod(string? url) => "This is from GET";
public static string DeleteMethod(string? url) => "this is from DELETE.";
public static string PutMethod(string? url, LoginDTO login) => "this is from PUT";
public static string PostMethod(LoginDTO login) => "this is from POST.";
public static string PatchMethod(string? url, LoginDTO login) => "this is from PATCH.";
}
and this is my configuration on Program.cs:
builder.Services.AddCarter()
.AddEndpointsApiExplorer()
.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.DefaultApiVersion = ApiVersions.v1;
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
}).AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
app.MapCarter().NewApiVersionSet(ApiVersions.vset)
.HasApiVersion(ApiVersions.v1)
.HasApiVersion(ApiVersions.v2)
.HasApiVersion(ApiVersions.v3)
.ReportApiVersions()
.Build();
GetMethod
and DeleteMethod
only works properly if I configure some lines on the code:
Method 1: if I change my GetMethod
and DeleteMethod
version:
group.MapGet("{url}", GetMethod).HasApiVersion(ApiVersions.v2);
group.MapGet("{url}", DeleteMethod).HasApiVersion(ApiVersions.v2);
Method 2: if I use [FromRoute]
and remove my LoginDTO
parameter in PutMethod
, PostMethod
, and PatchMethod
:
public static string GetMethod([FromRoute] string url) => "This is from GET";
public static string DeleteMethod([FromRoute] string url) => "this is from DELETE.";
public static string PutMethod([FromRoute] string url) => "this is from PUT";
public static string PostMethod([FromRoute] string url) => "this is from POST.";
public static string PatchMethod([FromRoute] string url) => "this is from PATCH.";
Method 3: If i change the path:
group.MapPut("put/{url}", PutMethod).HasApiVersion(ApiVersions.v1);
group.MapPost("post/{url}", PostMethod).HasApiVersion(ApiVersions.v1);
group.MapPatch("patch/{url}", PatchMethod).HasApiVersion(ApiVersions.v1);
Method 4: If I commented this three:
//group.MapPut("{url}", PutMethod).HasApiVersion(ApiVersions.v1);
//group.MapPost("{url}", PostMethod).HasApiVersion(ApiVersions.v1);
//group.MapPatch("{url}", PatchMethod).HasApiVersion(ApiVersions.v1);
Upvotes: 0
Views: 595
Reputation: 4368
I think I was able to reproduce your scenario. Based on your description, it would appear that things are working, but they are not showing up in the Swagger UI. You didn't share that part of your configuration, but I'm guessing it is incorrect. You didn't specify or indicate which OpenAPI generator you're using, but I presume it's Swashbuckle. There's a number of things going on, so let's tackle them one by one.
You defined the route template:
group.MapPost("{url}", PostMethod).HasApiVersion(ApiVersions.v1);
but the method:
public static string PostMethod(LoginDTO login) => "this is from POST.";
does not have a url
parameter. This will result in the literal {url}
being part of the route path.
Swashbuckle will eager evaluate the setup in AddSwaggerGen
so we need differ resolution through DI using Options. We'll want to use resolve the IApiVersionDescriptorProvider
from API Versioning which will collate all of the API versions in the application. This will enable you to configure OpenAPI documents without hardcoding anything. The world's simplest implementation would look like:
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider provider;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
=> this.provider = provider;
public void Configure(SwaggerGenOptions options)
{
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(
description.GroupName,
new()
{
Title = "Example API",
Description = "An example application with OpenAPI, " +
"Swashbuckle, and API versioning.",
Version = description.ApiVersion.ToString(),
});
}
}
}
You can still explicitly use the older ApiVersionSet
APIs, but implicitly configuring API versions on the endpoints and/or their groups is arguably more natural. There's always an ApiVersionSet
under the hood. The name associated with the ApiVersionSet
is the logical name of the API and will be used by default when you integrate the API Explorer extensions for OpenAPI.
API version metadata must roll up from a root group. When you use group and endpoint extension methods that behavior is strictly enforced. When you build the ApiVersionSet
by hand, the rules can only loosely be enforced and there are more opportunities for mistakes. There are likely two approaches that you want to use for configuration. There are other options, but these are the ones you'd likely be interested in.
This approach configures the route template for all API versions at the root. Each set of endpoints must roll up to a group for proper collation, so we need to add a new group (with no route template) to represent the collection of endpoints. By applying the API version on the group, all endpoints in that group will have the API versions defined by the parent group.
public class GeneralController : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
var general = app.NewVersionedApi("General")
.MapGroup("api/v{version:apiVersion}/general");
var v1 = general.MapGroup("").HasApiVersion(ApiVersions.v1);
v1.MapGet("{url}", GetMethod);
v1.MapDelete("{url}", DeleteMethod);
v1.MapPut("{url}", PutMethod);
v1.MapPost("/", PostMethod);
v1.MapPatch("{url}", PatchMethod);
var v2 = general.MapGroup("").HasApiVersion(ApiVersions.v2);
v2.MapGet("{url}", GetMethod);
v2.MapDelete("{url}", DeleteMethod);
}
}
In this approach, the route templates are duplicated in each group for each API version. This may or may not work for you. It's possible that different API versions use different route templates. Once again, adding the API version to the group implicitly applies the API version for all endpoints in that group.
public class GeneralController : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
var general = app.NewVersionedApi("General");
var v1 = general.MapGroup("api/v{version:apiVersion}/general")
.HasApiVersion(ApiVersions.v1);
v1.MapGet("{url}", GetMethod);
v1.MapDelete("{url}", DeleteMethod);
v1.MapPut("{url}", PutMethod);
v1.MapPost("/", PostMethod);
v1.MapPatch("{url}", PatchMethod);
var v2 = general.MapGroup("api/v{version:apiVersion}/general")
.HasApiVersion(ApiVersions.v2);
v2.MapGet("{url}", GetMethod);
v2.MapDelete("{url}", DeleteMethod);
}
}
The Microsoft OpenAPI extensions do not play nice with the API Versioning API Explorer extensions. Specifically, when you use WithOpenApi()
it adds an OpenApiOperation
directly to the endpoint as metadata. That is a flaw in the design. It is possible that an endpoint can serve multiple API versions with different OpenAPI options. Unfortunately, OpenAPI generators, such as Swashbuckle, look for the presence of OpenApiOoperation
in the metadata. When present, it uses that instance and skips everything else provided by the API Explorer. This may lead you to results you don't want.
I would recommend not using WithOpenApi()
if you are using API Versioning. You can use other extensions that influence OpenAPI such as WithSummary
, WithDescription
, Accepts
, Produces
, and so on as long as they don't create or use OpenApiOperation
directly under the hood. Most of the metadata extension methods add metadata only. You don't need to use WithTags
because the name configured with the ApiVersionSet
is the name used by default. No need to define it twice, but you can if you want to.
Your application configuration does not need to reapply the ApiVersionSet
. It will already be applied through the endpoint configuration with Carter. Putting all of the pieces together, your application configuration should look something like:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddCarter()
.AddEndpointsApiExplorer()
.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.DefaultApiVersion = ApiVersions.v1;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
builder.Services
.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen();
var app = builder.Build();
app.MapCarter();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(
options =>
{
var descriptions = app.DescribeApiVersions();
foreach (var description in descriptions)
{
var url = $"/swagger/{description.GroupName}/swagger.json";
var name = description.GroupName.ToUpperInvariant();
options.SwaggerEndpoint(url, name);
}
});
}
app.Run();
A key callout here is how UseSwaggerUI
is invoked. This setup is required to correlate with how the OpenAPI documents will be created as defined in the ConfigureSwaggerOptions
class. The two sides need to match up.
Finally, you may have noticed that I removed UseDefaultVersionWhenUnspecified
. This will almost certainly not do what you think it will do; at least, not as shown. This feature is only meant for backward compatibility with existing APIs, but is often abused. You are versioning by URL segment, so this option will have no effect. You cannot have optional route parameters in the middle of a template. The same would be true for order/{id}/items
. This option will only have an effect if you also have routes that do not include the apiVersion
route constraint in them (ex: api/general/{url}
). I would not recommend doing that for anything beyond backward compatibility.
Upvotes: 0