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

Add ApiGroupNames setting, remove ApiVersionProcessor and improve api versioning #1701

Merged
merged 8 commits into from
Nov 2, 2018

Conversation

RicoSuter
Copy link
Owner

@RicoSuter RicoSuter commented Oct 31, 2018

  • Add ApiGroupNames setting
  • Remove ApiVersionProcessor (in AspNetCoreToSwaggerGeneratorSettings)
  • Remove IncludedVersions setting on aspnetcore2swagger / nswag.json (CLI)

Should solve lots of api versioning problems, see #1355

This is a breaking change for users of the old ApiVersionProcessor: Use AddVersionedApiExplorer() in ConfigureServices() and define ApiGroupNames instead of IncludedVersions

services.AddApiVersioning(options =>
{
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddMvcCore()
.AddVersionedApiExplorer(options =>
{
    options.GroupNameFormat = "VVV";
    options.SubstituteApiVersionInUrl = true;
});

Without UseDocumentProvider:

image

With UseDocumentProvider:

image

Sample project: https://github.com/RSuter/NSwag/tree/master/src/NSwag.SwaggerGeneration.AspNetCore.Tests.Web

@RicoSuter
Copy link
Owner Author

@commonsensesoftware now one of the problems i have is that even if i set SubstituteApiVersionInUrl = true; the parameter api-version is still declared. How can I disable the reporting of this parameter? It doesnt make sense to report it because all placeholders have been removed...

@commonsensesoftware
Copy link

Ah … this is because the IApiVersionReader supports composition (e.g. combining methods together). The query string method is encouraged as the default behavior, but the URL segment method is so common it's also enabled by default because it doesn't have any visible side effects to the query string method. Unfortunately, the inverse is not true. What you're observing is the api-version parameter reported for the query string. To limit things to only the URL segment method, change the configuration as:

AddApiVersioning(options =>
{
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
})

@RicoSuter
Copy link
Owner Author

@commonsensesoftware thanks for the info. PR looks fine now...

@RicoSuter RicoSuter merged commit 5ad66e2 into master Nov 2, 2018
@RicoSuter RicoSuter mentioned this pull request Nov 15, 2018
5 tasks
@OculiViridi
Copy link

@RSuter I can't find the AddVersionedApiExplorer() method.

@RicoSuter
Copy link
Owner Author

RicoSuter commented Nov 21, 2018

It's in the Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer package

See sample project:
https://github.com/RSuter/NSwag/blob/master/src/NSwag.SwaggerGeneration.AspNetCore.Tests.Web/NSwag.SwaggerGeneration.AspNetCore.Tests.Web.csproj

@Furynation
Copy link

Furynation commented Dec 4, 2018

@RSuter I can't find the AddVersionedApiExplorer() method.

@OculiViridi Make sure you are adding MvcCore directly before calling AddVersionedApiExplorer():
services.AddMvcCore();
services.AddVersionedApiExplorer();

If you need the full MVC package, then:
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddMvcCore().AddVersionedApiExplorer();

@RicoSuter RicoSuter deleted the feature/add-api-group-names branch December 19, 2018 15:48
@RicoSuter
Copy link
Owner Author

Ref: #1355

@acds
Copy link

acds commented Jul 25, 2019

Hi All, Per #2325

I'm still having issues with 2.2 and API versioning.

When trying to use services.AddMvcCore().AddVersionedApiExplorer(); the AddVersionedApiExplorer does not resolve.

I was hoping migration to NSwag would be easy...

@commonsensesoftware
Copy link

You shouldn't have to use AddMvcCore anymore. This was improved in API Versioning quite a long while ago. Calling services.AddVersionedApiExplorer() will implicitly call the dependent AddApiExplorer() extension.

In attempting to stitch the two issues together, it would appear that your group names are incongruent. That would explain why things do not match up. Swagger generators and API Versioning do not know about each other. API Versioning collates and groups actions according to their API versions. Swagger generators simply use the grouped actions defined by the API Explorer - however that magically happened.

I see that your document has the Name and Version of v1.0. The configured group name format; however is VVV. This format code will yield major[.minor][-status]. This would yield an API version of 1.0 as simply 1. Note how the v is not included. The v is not (nor ever is) part of the API version. Some people like having the v prefix, but you need to include it in the format; for example, 'v'VVV. This particular built-in format code uses an optional minor version. The numeric, minor version is considered optional if it equals 0. This would yield a result of v1 in your example. To match the format in your Swagger document, use 'v'VV which will format 1.0 as v1.0.

For more information, review API Version Formatting. I hope that helps.

@acds
Copy link

acds commented Jul 25, 2019

@commonsensesoftware Thanks for the reply. I read the docs as linked.

Yes, in my eagerness to migrate to NSwag, I'd not correctly decoded, the other implementation.

with

 services.AddVersionedApiExplorer(options =>
 {
     options.GroupNameFormat = "VVV";
     options.SubstituteApiVersionInUrl = true;
});

...

services.AddApiVersioning(options =>
{
    options.ReportApiVersions = true;
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0);
});

Swashbuckles:

services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new Info { Title = "My Title API", Version = "v1" });
});

translates to:

services.AddOpenApiDocument(document =>
{
    document.DocumentName = "v1";
    document.Version = "v1";
    document.Title = "My Title API";
    document.ApiGroupNames = new[] {"1"};
});    

In the hopes that this help other migrators.

As a though... it might be a good idea to retrieve the IApiDescriptionGroupCollectionProvider via:

var explorer = services.BuildServiceProvider().GetRequiredService<IApiDescriptionGroupCollectionProvider>();

I'd figure that a useful model would be to loop though each group identified in the explore and add a services.AddOpenApiDocument for each group identified such that it was always a complete set and this code needs no further configuration as the API evolves...

However it seems the explorer at the point provides an empty collection, and figure this is because the startup has not proceeded far enough in the build process as yet to have the API Version information generated from the MVC code...where the Swashbuckle implementation would otherwise do this in the UseSwagger() or UseSwaggerUi3() in this case...

@commonsensesoftware
Copy link

@acds, the IApiVersionDescriptionProvider service defined by API Versioning will give you a simpler and fully aggregated view of the information you're looking for. The Swashbuckle setup used to be similar to the same setup you are describing here using services.BuildServiceProvider(). Swashbuckle ended up adding Options support, which enabled configuring generation later in the pipeline. You can see how these two concepts come together in this example.

I don't know enough about NSwag to tell you if this same concept is supported for configuring services.AddOpenApiDocument. If it's not, then that's the approach for a feature I would consider requesting.

I hope that's useful.

@ThomasVague
Copy link

Is there any updates on iterating through the IApiVersionDescriptionProvider? I've been looking for a more elegant solution, but this seems to be the only way still?

var explorer = services.BuildServiceProvider().GetRequiredService<IApiDescriptionGroupCollectionProvider>();

@commonsensesoftware
Copy link

@ThomasVague Are you looking for something beyond IApiDescriptionGroupCollectionProvider.ApiVersionDescriptions? That will enumerate all available ApiVersionDescription instances in order. If NSwag were to expose this information directly, I presume it should need to provide some type of adapter to be bridge it in a generic way; otherwise, you can also get this information from API Versioning from the DI container at various points in the application.

@ThomasVague
Copy link

ThomasVague commented Aug 22, 2022

Well, not really. This works just fine. I was just curios to see if there had been any changes since the post was made. I was just browsing different setup methods, and I came over this example for Swashbuckle, and started wondering if NSwag had something similar.

However, I am curios if there's any way to group documents even further. Let's say I want to group my controllers / namespaces into multiple documents for each version, so one document for version 1 with a subset of controllers, and another document for version 1 with another subset of controllers, and so on, for each existing version.

Thanks in advance :)

@commonsensesoftware
Copy link

I can't speak to the speak hooks for NSwag, but it should certainly possible to hook up the API Versioning metadata in a similar way as Swashbuckle. There are no special handling or conditions for specific OpenAPI generators, including Swashbuckle. If there are some community examples of NSwag with API Versioning, I'm willing to link to them in the documentation.

Unfortunately, you can't really group further; at least, not without some serious work. The issue isn't the code, NSwag, or API Versioning. The OpenAPI (formerly Swagger) UI only affords for a single level of grouping. Numerous changes would have to be made in order to make it work - likely more than you're willing to do. Another alternative is that you can combine the components together as a single string. For example:

  • v1/Namespace1
  • v1/Namespace2
  • v2/Namespace1
  • v2/Namespace2
  • v2/Namespace3

That's probably not what you're hoping for, but that will work with minimal effort. Yet one more option is to create your own UI and then you can group however you like.

@ThomasVague
Copy link

That's actually exactly what I'm looking for. Only a single level, but grouped in versions and namespaces. I've been looking around, but haven't found any examples to do grouping in that manner.

Currently my code looks like this :

services.AddVersionedApiExplorer(o =>
	{
		o.SubstituteApiVersionInUrl = true;
		o.ApiVersionParameterSource = new UrlSegmentApiVersionReader();
		o.GroupNameFormat = "'v'VVV";
	});

	var versionDescriptionProvider = services.BuildServiceProvider().GetService<IApiVersionDescriptionProvider>();

	foreach (var apiVersionDescription in versionDescriptionProvider?.ApiVersionDescriptions)
	{
		services.AddOpenApiDocument((document, serviceProvider) =>
		{
			document.DocumentName = "v" + apiVersionDescription.ApiVersion.MajorVersion;
			document.ApiGroupNames = new string[] { "v" + apiVersionDescription.ApiVersion.MajorVersion };
			document.Version = apiVersionDescription.ApiVersion.MajorVersion + "." + apiVersionDescription.ApiVersion.MinorVersion;
			document.Description = settings.Description;

			if (apiVersionDescription.IsDeprecated)
			{
				document.Description += " This API version has been deprecated.";
			}

			document.PostProcess = doc =>
			{
				doc.Info.Contact = new()
				{
					Name = settings.ContactName,
					Email = settings.ContactEmail,
					Url = settings.ContactUrl
				};
				doc.Info.License = new()
				{
					Name = settings.LicenseName,
					Url = settings.LicenseUrl
				};
			};


			document.AddSecurity(JwtBearerDefaults.AuthenticationScheme, new OpenApiSecurityScheme
			{
				Name = "Authorization",
				Description = "Input your Bearer token to access this API",
				In = OpenApiSecurityApiKeyLocation.Header,
				Type = OpenApiSecuritySchemeType.Http,
				Scheme = JwtBearerDefaults.AuthenticationScheme,
				BearerFormat = "JWT",
			});

			document.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor());
			document.OperationProcessors.Add(new SwaggerGlobalAuthProcessor());

			document.TypeMappers.Add(new PrimitiveTypeMapper(typeof(TimeSpan), schema =>
			{
				schema.Type = NJsonSchema.JsonObjectType.String;
				schema.IsNullableRaw = true;
				schema.Pattern = @"^([0-9]{1}|(?:0[0-9]|1[0-9]|2[0-3])+):([0-5]?[0-9])(?::([0-5]?[0-9])(?:.(\d{1,9}))?)?$";
				schema.Example = "02:00:00";
			}));

			document.OperationProcessors.Add(new SwaggerHeaderAttributeProcessor());

			var fluentValidationSchemaProcessor = serviceProvider.CreateScope().ServiceProvider.GetService<FluentValidationSchemaProcessor>();
			document.SchemaProcessors.Add(fluentValidationSchemaProcessor);
		});
	}
app.UseOpenApi(settings =>
	{
	});
	app.UseSwaggerUi3(settings =>
	{
		settings.DefaultModelsExpandDepth = -1;
		settings.DocExpansion = "none";
		settings.TagsSorter = "alpha";
	});

@commonsensesoftware
Copy link

Sidebar: You don't have to write a bunch of code to produce various API formats. ApiVersion intrinsically supports formatting for this exact reason (see version formatting).

document.DocumentName = apiVersionDescription.ApiVersion.ToString("'v'V");
document.ApiGroupNames = new[] { document.DocumentName };
document.Version = apiVersionDescription.ApiVersion.ToString("VV");

Custom Grouping

Repeating it here would be verbose, but there are a couple of options for custom grouping to achieve the results you want. The simplest way is likely using your own IApiDescriptionProvider that [re-]groups API descriptions the way you want. Here's a few of the links that explain it in more detail and with code:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants