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);
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.
Issue 1 - Undefined Route Parameter
You defined the route template:
but the method:
does not have a
url
parameter. This will result in the literal{url}
being part of the route path.Issue 2 - OpenAPI Doc Generation
Swashbuckle will eager evaluate the setup in
AddSwaggerGen
so we need differ resolution through DI using Options. We'll want to use resolve theIApiVersionDescriptorProvider
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:Issue 3 - API Version Metadata
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 anApiVersionSet
under the hood. The name associated with theApiVersionSet
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.Option 1 - Common Route Template with Nested Groups
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.
Option 2 - Groups with Repeat Route Templates
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.
Issue 4 - OpenAPI Extension Conflicts
The Microsoft OpenAPI extensions do not play nice with the API Versioning API Explorer extensions. Specifically, when you use
WithOpenApi()
it adds anOpenApiOperation
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 ofOpenApiOoperation
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 asWithSummary
,WithDescription
,Accepts
,Produces
, and so on as long as they don't create or useOpenApiOperation
directly under the hood. Most of the metadata extension methods add metadata only. You don't need to useWithTags
because the name configured with theApiVersionSet
is the name used by default. No need to define it twice, but you can if you want to.Issue 5 - Application Configuration
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: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 theConfigureSwaggerOptions
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 fororder/{id}/items
. This option will only have an effect if you also have routes that do not include theapiVersion
route constraint in them (ex:api/general/{url}
). I would not recommend doing that for anything beyond backward compatibility.