-
Couldn't load subscription status.
- Fork 716
-
I am trying to add versioning to an existing project and part of it is that all code should move to a new route, the old routes have been declared version x, which is deprecated, however, I am unable to get the apiversiondescription in swagger to be declared deprecated.
I assume because the object is initialized from the DefaultApiVersion, which has no deprecated support, and not from the ApiVersionAttribute on my controllers, which do have it.
Any idea how I can still get my version be seen as deprecated?
Below the relevant snippets....
services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(0);
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
[Obsolete]
[ApiVersion(0, Deprecated = true)]
[Route("[controller]")]
[ApiController]
public class MyControllerController : Controller
{
}
Remco
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 2 comments 4 replies
-
There's a few things going on here. You are correct that there is no way to indicate that the DefaultApiVersion itself is deprecated. It wasn't meant to be used that way, but - admittedly - that is an interesting scenario. For the scenario you describe, DefaultApiVersion would come into play if there were no other attributes or conventions applied. It looks like you have already applied attributes (which is fine), but you could apply a custom convention that would apply the default, deprecated API version to all matching controllers.
When you say Swagger, I presume you mean Swashbuckle. There is no official mechanism that connects the API Explorer with the concept of deprecation. Swashbuckle and API Versioning support these concepts, but you are required to stitch them together (unfortunately). The simplest way to do that in Swashbuckle is to use an IOperationFilter, which would look like:
public class SwaggerDefaultValues : IOperationFilter { public void Apply( OpenApiOperation operation, OperationFilterContext context ) { // Swashbuckle ↓ API Versioning extension method ↓ operation.Deprecated |= context.ApiDescription.IsDeprecated(); } }
That isn't the only information that doesn't get picked up automatically by Swashbuckle, but it might be the only thing you need. For a full end-to-end solution, see the OpenAPI example project.
Beta Was this translation helpful? Give feedback.
All reactions
-
Ah, i already took the swagger defaults filter, but i didn't think to query the filter context to tweak the swagger output.
I will look into that,
Thank you
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
The |= operator is your choice. You could just use an assignment (e.g. =). The thinking is that if Swashbuckle somehow explicitly marked an operation as deprecated, it should not be blindly overwritten.
Beta Was this translation helpful? Give feedback.
All reactions
-
The swagger filter was already custom, so this feels like a good place. Of course i could have hard coded it in the filter already, but this feels cleaner.
Still, it might be nice to have some way of marking versions deprecated inside AddApiVersioning(), like having a delegate member in it that allows you to determine if a version is deprecated.
That also would allow you to centrally manage deprecation, instead of in each controller
Beta Was this translation helpful? Give feedback.
All reactions
-
There are many ways this can be achieved and it hasn't been a common ask, both of which is why there isn't an out-of-the-box solution. However, adding support for it is pretty straight forward. Under the assumption you will use attributes going forward, then all all existing controllers without any attribution will be considered to match the default version which is deprecated. This can be achieved with the following custom convention:
public sealed class DefaultDeprecatedApiVersionConvention : IControllerConvention { private readonly ApiVersioningOptions options; public DefaultDeprecatedApiVersionConvention(ApiVersioningOptions options) => this.options = option; public bool Apply(IControllerConventionBuilder builder, ControllerModel controller) { if (IsImplicitlyVersioned(controller)) { builder.HasDeprecatedApiVersion(options.DefaultApiVersion); return true; } return false; } private static bool IsImplicitlyVersioned(ControllerModel controller) { var attributes = controller.Attributes; for (var i = 0; i < attributes.Count; i++) { switch (attributes[i]) { case IApiVersionNeutral: return false; case IApiVersionProvider provider: // ApiVersionProviderOptions.Advertised is informational only // ApiVersionProviderOptions.Mapped only applies to actions switch (provider.Options) { case ApiVersionProviderOptions.None: case ApiVersionProviderOptions.Deprecated: if (provider.Versions.Count > 0) { return false; } break; default: break; } } } return true; } }
You would then wire it up with:
services.AddApiVersioning() .AddMvc(options => options.Conventions.Add(new DefaultDeprecatedApiVersionConvention(options)));
You might have any other number of way to identify the original, default, deprecated controllers such as by ObsoleteAttribute, route template, namespace, base type, and so on. There is no one convention to rule them all. 😉
Beta Was this translation helpful? Give feedback.
All reactions
-
Thank you for the helpful explanation... I was checking the IOperationFilter, but than one is only marking operations deprecated... I was looking to getting a deprecated message at the top of the swagger document.
I have now achieved this by not just checking the apiversiondescription in the swaggergeninfo for being deprecated, but also assuming that when a version is deprecated, it will have a sunset policy. And to be honest, this should be a temporary situation anyway, all consumers should be moving to a specific version anyway.
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions> { private readonly IApiVersionDescriptionProvider provider; /// <summary> /// Initializes a new instance of the <see cref="ConfigureSwaggerOptions"/> class. /// </summary> /// <param name="provider">The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger documents.</param> public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider; /// <inheritdoc /> public void Configure(SwaggerGenOptions options) { // add a swagger document for each discovered API version // note: you might choose to skip or document deprecated API versions differently foreach (var description in provider.ApiVersionDescriptions) { options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)); } } private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description) { var text = new StringBuilder(""); var info = new OpenApiInfo() { Title = description.GroupName, Version = description.ApiVersion.ToString(), }; if (description.SunsetPolicy != null || description.IsDeprecated) { text.AppendLine("This API version has been deprecated. Please switch to a newer version<br/>"); } if (description.SunsetPolicy != null) { if (description.SunsetPolicy.Date is DateTimeOffset when) { text.Append("The API will be sunset on ") .Append(when.Date.ToShortDateString()) .AppendLine(".<br/>"); } if (description.SunsetPolicy.HasLinks) { text.AppendLine("<br/>"); for (var i = 0; i < description.SunsetPolicy.Links.Count; i++) { var link = description.SunsetPolicy.Links[i]; if (link.Type == "text/html") { text.AppendLine("<br/>"); text.Append($"<a href='{link.LinkTarget.OriginalString}'>{(link.Title.HasValue ? link.Title : link.LinkTarget.OriginalString)}</a>"); } } } } info.Description = text.ToString(); return info; } }
Beta Was this translation helpful? Give feedback.
All reactions
-
🎉 1