-
Notifications
You must be signed in to change notification settings - Fork 712
How to support multiple swagger files per version/group? #516
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
Comments
Most Swagger generations, at least all the ones I've seen, provide a single hierarchy in the user interface. In ASP.NET Core, this is typically driven by the GroupName. The API Versioning extensions always group by API version, which is typically what service authors want. In your particular case, you have two hierarchical levels. This cannot be achieved directly though the GroupName alone; you need two properties/discriminators. To get your desired result, you've to customize things on the Swashbuckle side; specifically, the user interface. It should be simple enough to group and build endpoint URLs by API version and billing name, but you'll need two dropdown lists to drive it. To minimize the work, I would consider moving your billing group name to a custom attribute that you can retrieve by away of an extension method. Something like: [ApiVersion("1.0")]
[BillingGroup("authoring")]
public class Controller1 : ControllerBase
{
} Of course, you don't really need to have split the Swagger documents up this way unless you're trying to reduce the size of the documents or focus in on relevance. If you just want to filter by relevance, it might be easier to just tweak the client-side scripts to support filtering API sets. They'll already be grouped by API version. It's possible for you to flat the list down to It should be possible to use a custom Swagger document generator or operation filter in Swashbuckle to achieve a similar result. I hope that gives you a few ideas. |
Thanks for replying. It does give us some ideas although we're not quite sure how to take action on them. It sounds like we would need to customize the generation of Swagger files through modifying Swashbuckle and I'm not familiar with that process. I will try to rewrite what you suggested to make sure I understand and ask questions about parts. There were two main parts I was confused about.
I will have to look into this more. It was my understanding we associated a single unfiltered swagger doc per APIM endpoint. (E.g. authoring endpoint was 1 swagger.json and session endpoint was another swagger.json) This was the driving factor for needing separate documents. (We have 2 per version) And with addition of new version we would have 4. Are you saying it's possible for us to only generate 1 document per version (instead of sub split by author and session) and then somehow filter on APIM so it only sees a subset of the operations? On the suggestion of customizing swashbuckle:
That was the intention to end up with 4 swagger files. (v1 / authoring, v1 / session, v2 / authoring, v2 / session, etc) If the above question about filtering is possible then maybe this isn't needed. If we do need to generate individual files then yes, I think we would need 2 dropdowns (1 per hierarchy). There would be a top level dropdown for version which exists now. Which would map to the ApiVersion / GroupName as those would be in alignment. The second drop down would be for the billing group of authoring and session. How would we go about modifying Swashbuckle to have these 2 dropdowns using our second custom attribute? |
To clarify, when you use the API Explorer extensions provided by API versioning, you would end up with a Swagger document per API version that contains all applicable APIs. For example:
I've had people ask me about having a Swagger document per resource (e.g. group) and then list all the different versions in the document. APIs that follow the REST constraints aren't able to fit into this model because Swagger does not allow duplicate URLs. Since you're versioning by URL segment (the least RESTful method), it's technically possible for this work, but it don't think it would go far in helping clients review and test out APIs since you'd see the same name multiple times or names like Sessions 1.0 and Sessions 2.0 (unless you don't mind that). I don't profess to be an expert in Swagger (the doc or UI) nor any of the numerous frameworks that exist out there. I have some familiarity with Swashbuckle and its author. The type of advanced customization you seem to be describing isn't unheard of. If you were just trying to put up simple documentation endpoint for your internal services, I could see the resistance to investing in extensive customization. It seems you're trying to document a monetizable product, which may warrant the investment. The Swagger UI is meant to serve the 80%, but it sounds like you might be in the 20%. It's completely up to you whether you want to live within its intrinsic, out-of-the-box limitations or extend it, which may involve forking the UI. You might want to review the Swagger UI documentation for more information. Some folks within Microsoft built their own UI so that's always an option for you too. |
In other words, having a hierarchy of Version -> Group
I didn't understand what scenario or conditions would make this occur. I assumed thee would be v1 and v2 swagger docs and both have an session group / endpoint operation. I thought this was expected.
Yes, we will probably avoid that as maintaining the changes as the origin updates is a too much for our small team. Also, I think we're more concern about the generation of the files and not simply being able view them in the UI. Based on previous discussion in thread the |
This would happen in the Since changing the out-of-the-box experience is not an option for you, I really can only think of two recommendations. Option 1Use it as is to produce the Option 2Create and register a custom IActionDescriptionProvider that runs after API Versioning. In the OnExecuted method, you'll be at the end with everything collated by API version. At this point, you should reorder everything and bucketize based on The really tricky part is trying to filter the billing group. The IActionDescriptionProvider design doesn't really support this concept. I'm struggling to think of how you can make this work on the ASP.NET Core side. This is the kind of thing where a feature could be used based on the incoming request, but this API doesn't have a way to access the incoming request. That makes it very difficult to filter things. I think you'd have to do this filtering on the Swashbuckle side with a custom Swagger generator or something. I don't have an example of exactly how and where you do it, but it should be possible for the entire document or specific operations. You should be able to extend and replace the default implementation without a lot of effort. The only thing you're really doing is filtering things down. With all of this in place, you'll end up with single Swagger dropdown that looks something like:
I don't know if that's what you really want, but those are the only two ways I can think of you can make this work without doing some significant customization. |
Did this help or otherwise answer your question? |
It appears that the suggestions provided helped you find a solution or you used another approach. If you weren't able to solve your issue, feel free to come back or reopen the issue. Thanks. |
Sorry, I should have responding earlier. There were some changes with product that came above versioning which delayed investigation and also I kept waiting to find out more concrete answers from my co-worker about APIM and swagger, but I never did and thus didn't reply. I will just reply with what I know now. We haven't solved the issued, but it's mostly because of inaction. We feel a bit stuck. It seems option 1 didn't solve the issue (at least how we interpreted it, details below) and option 2 was likely too much custom work which we're resistant to invest in since we don't know it will work. However, you can keep the issue closed for now until we get more clarity on swagger requirements and we're at a point to put more effort in. Option 1There seems to be confusion about the default implementation and what we want. We want the
I meant to say it in the above post.
This was one of the pieces of information I was waiting on. If we can give APIM a swagger file and a filter on which operations from that file, this would work and remove need for separate files. I still need to confirm that this IS or IS NOT possible. Option 2This does produce what we want, but due to extra complexity we're not sure. If there was some sample we could compare and reference as we try that could work, but we haven't tried this. We were also worried about going non-standard and being compatible with other teams services so they could overlap the maintenance. Hope that at least clears up the situation. |
No problem. Since I'm running this project alone, I keep the issues list well-pruned. After question-based issues go dark, I usually ping with a follow up and give people a week or so. After that I assume they either fixed their issue, found another solution, or abandoned things altogether. If you only need a particular layout for APIM, then you might consider using the Swagger/OpenAPI library of choice and generating the *.json files yourself. The URL conventions are purely about serving the files directly from your own application. If your uploading the document to APIM, then you shouldn't have to worry about that. You can generate the documents however you like. A single document can contain one or more sets of APIs per version (which is the default behavior). There's nothing wrong with creating a single document per API, per version, but I believe you'd have to generate that yourself. A single document cannot contain multiple versions of an API unless you version by URL segment, which I never recommend. By generating the documents yourself, you might have tool that outputs:
Each document contains a single API per version. I'm not super familiar with the details of APIM, but I presume each of these documents should be suitable for upload. You could also customize a tool like Swashbuckle to serve up the same documents. The mapping ends up looking something I hope that helps. Happy to continue providing whatever guidance you may need. |
I had a similar intent. This will not get you exactly what you want but might be close enough for your needs with a very minimal amount of effort. which will produce a swagger document for each api per version and routes like this: which is very close to what you prob want which is this api/session-v3 Hope that helps |
I'm trying to do something similar and this looks like a great solution for us, thanks @tjmcnaboe! |
@tjmcnaboe nice solution! |
Glad it was helpful. Thanks. |
@tjmcnaboe @mattmazzola [ApiVersion( "3.0.session" )] services.AddVersionedApiExplorer(opt => Swagger doc definition: |
Ah, disregard, was some unrelated conflicts between classes and schemaIds for those classes preventing certain files from being generated. I should have read the error more closely. It seems only coincidence that it was the first option and threw me off. I suppose this could be more common since as you include more and more classes in your solution you might be likely to have type name conflicts. Option 1: FullnameThere is a simple solution which use full type name: But that can get really long names, so there other solution to use name of attribute on the type Option 2: Attribute for conflict
|
@commonsensesoftware, following on your pointers, I created this public class SubgroupDescriptionProvider : IApiDescriptionProvider {
private readonly ApiExplorerOptions _options;
public SubgroupDescriptionProvider(IOptions<ApiExplorerOptions> options) => _options = options.Value;
public int Order => -1; // Execute after DefaultApiVersionDescriptionProvider.OnProvidersExecuted
public void OnProvidersExecuting(ApiDescriptionProviderContext context) { }
public void OnProvidersExecuted(ApiDescriptionProviderContext context) {
var newResults = new List<ApiDescription>();
foreach (var result in context.Results) {
var apiVersion = result.GetApiVersion();
var versionGroupName = apiVersion.ToString(_options.GroupNameFormat, CurrentCulture);
if (result.GroupName == versionGroupName) {
continue; // This is a version grouping, nothing else to do
}
// Change [ApiExplorerSettings] group to show as subgroup of version grouping
result.GroupName = $"{versionGroupName}{SubgroupSeparator}{result.GroupName}";
// Add the missing version grouping as well
var newResult = result.Clone();
newResult.GroupName = versionGroupName;
newResults.Add(newResult);
}
newResults.ForEach(newResult => context.Results.Add(newResult));
}
} services.TryAddEnumerable(Transient<IApiDescriptionProvider, SubgroupDescriptionProvider >()); |
So the above will create not only the version subgroups, but the version only groups as well. |
Seems reasonable to me. Glad you got it working. |
@borota, thanks for providing the example, this is exactly what I want to achieve. My problem is, that ConfigureServices:
Configure:
Any ideas what I'm doing wrong? |
@Code1110 , services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>(); The complete set of moving parts is outlined in the Swagger Sample. |
OK, I just realized that I need to replace |
@Code1110 that's what I did as well. I could put all of it into a nuget package, if requested. |
Great! Work perfectly... :) ..but it's too slow and resource consuming... Do you have the following issue? |
@karpediemnow I'm not aware of any specific issues. There's no way around the IApiDescriptionGroupCollectionProvider as it's required to dispatch to any controllers. I've not had issues with the performance of this service, but if you have a very large number of controllers or you're frequently changing the candidate sources dynamically (ex: plug-ins), I can see how that might happen. The collection backing If there is a performance issue, that should be external inside ASP.NET Core itself. I expect you'll find that with or without this extension. If there is way to mitigate this issue, I'm willing to consider it as a future enhancement. |
I've tried to use this solution but I get a 404 Not Found when navigating to This doesn't seem to happen if I remove What am I doing wrong? |
@pfaustinopt I can't really speak to whether that solution works as it doesn't have a full repro. Honestly, I would not expect it to work as the status should appear in the form of The solution provided by @borota provides a working solution. Ultimately, the solution depends on how you want to have things appear in the OpenAPI UI. I've expanded upon that solution with some additional context and clarifying information in https://stackoverflow.com/questions/68788850/swagger-i-need-versioning-and-grouping. May that be of use to you. |
Thank you @commonsensesoftware , I'm going to take a look on that link. To provide you with more context, I'm trying to split my swagger into different groups, each one with its own versions, like this:
|
Totally reasonable. The long and short of it is that it's technically possible, but not easy to achieve without a lot of work - unfortunately. The API Explorer only has |
Ut oh! Now you've done it...
To get the result you want, do not conflate API version status with how you want to group; they are different, unrelated concepts. If you've already got a custom IApiDescriptionProvider doing what you want, then there are 2 possible solutions. Option 1Use the built-in group name metadata - as in Option 2If that solution doesn't work the way you want, then you can always create your own attribute to provide this metadata. For example, ConclusionChoosing either of those options will get you out of the scenario trying to compose crafty format transformations. Even if you could get the transformation to work correctly, it would probably not be routable. The API version values have to match up. |
Could you provide me with a little more guidance to implement Option 1? I've created a github repository with the code that I'm currently using. I've implemented the IApiDescriptionProvider you provided in your With those changes applied, this is what I get now in Swagger: All my groups are together in one Swagger definition, which is not what I intend to. I'm also curious, according to your suggestion of not splitting API versions by |
Ah, yes - that part of the discussion was missed. The number and generation of OpenAPI documents are completely separate from API Versioning. This is controlled in SwaggerDocApiVersionConfiguration.cs via Deprecated simply means that you are advertising that a particular API is going to go away - at some point. It doesn't convey any particular policy; for example, after 6 months. For now, it's something you publicly advertise or negotiate with your clients. I'm considering adding expanded support in the future which will jive with RFC 8594. Deprecated also does not mean unsupported. Once an API is completely unsupported, it simply no longer exists. I think you may be asking about how aggregation works. An API version reported by the IApiVersionDescriptionProvider is only considered deprecated all-up, if and only if, all of the APIs in that version are deprecated. If even one is not, then the version is not deprecated. Individual APIs will still correctly report and show that they are deprecated, even if there are other APIs in the same version that are supported. |
So you are saying that I need to add a
What is happening now is that I have a document for V2 Edited: I've modified the repository to support automatic |
First, you probably want IApiDescriptionGroupCollectionProvider instead of IApiVersionDescriptionProvider for your case. Something like: public class SwaggerDocApiVersionConfiguration : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiDescriptionGroupCollectionProvider provider;
public SwaggerDocApiVersionConfiguration(
IApiDescriptionGroupCollectionProvider provider) => this.provider = provider;
public void Configure(SwaggerGenOptions options)
{
var docs = options.SwaggerGeneratorOptions.SwaggerDocs;
for (var i = 0; i < provider.Items.Count)
{
var group = provider.Items[i];
// grouping expectation: name -> version, which means:
// 1. OpenApiInfo.Version does not apply
// 2. No built-in way to know if:
// a. All versions in API are deprecated
// b. A particular version is deprecated across all groups
//
// note: this can be solve by some post-processing on the descriptions
//
// the GroupName now includes name+version. you'd either need separate
// metadata to get just the name from or you 'could' extract the group
// component based on your known format.
docs.Add(group.GroupName, new() { Title = group.GroupName });
}
}
}
No, that isn't true. The challenge is that multi-level (2+) grouping is not intrinsically supported so there's no way that API Versioning can just do it for you. You could, however, build some extension methods or some other collaborator with functions that can compute these values. Keeping the group name completely separate with a piece of metadata, such as an attribute, would potentially make that way easier. For example, API Versioning stores the associated API version in the // expanding upon above
public void Configure(SwaggerGenOptions options)
{
var names = provider.Items
.Select(g => g.Items)
.SelectMany(i => i.GroupName)
.Distinct(StringComparer.OrdinalIgnoreCase);
var deprecationMap = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
foreach (var name in names)
{
var deprecated = groupProvider.Items
.Select(g => g.Items)
.SelectMany(i => i)
.Where(i => i.ApiGroupName() == name) // your extension; get group name only
.All(i => i.IsDeprecated());
deprecationMap.Add(name, deprecated);
}
var docs = options.SwaggerGeneratorOptions.SwaggerDocs;
for (var i = 0; i < provider.Items.Count)
{
var group = provider.Items[i];
var name = ExtractTitle(group.GroupName); // your function to 'extract' the name only component; could be a map
var title = name;
if (deprecationMap.TryGetValue(name, out var deprecated) && deprecated)
{
title += $"{Environment.NewLine}This API version has been deprecated";
}
docs.Add(group.GroupName, new() { Title = title });
}
} The organization of The way your solution is organized makes no difference to API Versioning. You can use a single project or many. The choice is yours. Part of the reason that the Conventions API exists is so that you can apply versioning to external controllers that you didn't author and don't have source for. For example: services.AddApiVersioning(options => options.Conventions.Controller<ThirdPartyController>().HasApiVersion(2, 1)); |
Perfect, with IApiDescriptionGroupCollectionProvider I can find all the
To find if a certain API version is deprecated, I'm following @borota advice:
|
@pfaustinopt or @commonsensesoftware - do either of you have working code with this in place? Having a hard time recreating this.... |
@danielohirsch , this question has been asked many times over the years. The same limitations exist, but the capability to merge API version while retaining a group name is now supported out-of-the-box starting in |
Yes, but this requires the use of .net 7, correct?
…On Mon, Feb 6, 2023 at 3:51 PM Chris Martinez ***@***.***> wrote:
@danielohirsch <https://github.com/danielohirsch> , this question has
been asked many times over the years. The same limitations exist, but the
capability to merge API version while retaining a group name is now
supported out-of-the-box starting in 7.0. The wiki topic for the Format
Group Name <http://../wiki/API-Explorer-Options#format-group-name> option
describes how to use it. Let me know if you have any other questions about
the feature.
—
Reply to this email directly, view it on GitHub
<#516 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AGZKTQBWD2HJRJZUVCOUVXLWWFW6BANCNFSM4ICQ5FDA>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
No, but why would you want anything else? 😛 Sometimes I forget what I backport or added and carried forward. It appears this feature has been supported since |
Oh, it's hard to move that fast in enterprise libs... :) I'll go to 7 when I can, but for now, our stuff is in 6. |
Oh, good to know. I feel like an idiot now having gone round and round on this for as long as I did. I have an api aggregator that I am trying to gen out separate docs for based on child domain entry point... Hoping what you said will work for me. |
Hi @commonsensesoftware , Is there a way to merge all versions of endpoint in one swagger page? |
Yes, it is possible, but not without some work. The Swashbuckle configuration is effectively the same as if you had no versioning (e.g. it uses the default grouping mechanisms). Omit the version-specific part and that should aspect should be covered. API Versioning automatically uses the associated the API version as the group name. There is no way to opt out of that behavior because anyone not versioning by URL segment (which violates the Uniform Interface constraint) would end up in a broken state (e.g. the Swagger UI wouldn't work by default). The group name can be cleared (e.g.
The net effect should be that there is no longer any grouping and the UI is a flat list. I hope that helps. |
@commonsensesoftware |
@danielchiangGMF Custom schema is not necessary. Step 1 - Create a custom API description providerinternal sealed class AllGroupApiDescriptionProvider : IApiDescriptionProvider
{
public int Order => int.MaxValue;
public void OnProvidersExecuted( ApiDescriptionProviderContext context )
{
foreach ( var result in context.Results )
{
result.GroupName = "all";
}
}
public void OnProvidersExecuting( ApiDescriptionProviderContext context ) { }
} Step 2 - Register the provider in DIbuilder.Services.TryAddEnumerable(
ServiceDescriptor.Transient<IApiDescriptionProvider, AllGroupApiDescriptionProvider>() ); Step 3 - Update the Swashbuckle configurationbuilder.Services.AddSwaggerGen(
options =>
{
// NOTE: your options might not be the same here
options.OperationFilter<SwaggerDefaultValues>();
var fileName = typeof( Program ).Assembly.GetName().Name + ".xml";
var filePath = Path.Combine( AppContext.BaseDirectory, fileName );
options.IncludeXmlComments( filePath );
options.SwaggerDoc(
"all",
new()
{
Title = "Example API",
Contact = new() { Name = "Daniel", Email = "[email protected]" },
License = new() { Name = "MIT", Url = new Uri( "https://opensource.org/licenses/MIT" ) },
Description = "An example application with OpenAPI, Swashbuckle, and API versioning.",
} );
} );
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI( options => options.SwaggerEndpoint( "all/swagger.json", "All" ) ); Step 4 - Build and Run Your AppProfit! 🪙🪙 🪙 |
Thank you for sharing the code. It seems like it wont work for minimal api @@ |
@danielchiangGMF You're welcome. Why not? What makes you think that Minimal APIs are somehow special or different in this context? With the exception of ordering, I don't see why it wouldn't work. |
Without a repro, it's hard to provide an exact solution. I've attached a modified version of the Minimal API with OpenAPI example, which will demonstrate how to get things working. There may be some variances to your particular case that you need to make however. Note, that model names have to be unique within the OpenAPI document, which may or may not be a problem for you. |
Morning @commonsensesoftware |
Update: We are slowly resolving our issue now. Thank you. Will let you know if we encounter sth new. |
This is a question about best practices to achieve the multi swagger generation per version rather than specific issue with library. Hope someone can help or suggest a work around.
Background:
We have existing Asp.net core service that was using hard coded url versioning such as
[Route("api/v1/app")]
. We also have certain controller actions separated into groups to generate two different swagger files for Azure APIM. These different operations needed to be billed and managed differently (throttling, quota, etc). We did this by using the group name:and then manually generating matching swagger files
Intention
We wanted to add a new version (2.0) to the app to change the urls to more closely align with the Microsoft REST guidelines and wanted to set it up in a scalable way based on the sample https://github.com/microsoft/aspnet-api-versioning/tree/master/samples/aspnetcore/SwaggerSample
this is involved changes to
AddApiVersioning
,AddVersionedApiExplorer
, and most notably how the swagger files are generated.We wanted to preserve the separate of authoring and session operates and hoped to be able to generate two files per version descriptor such as:
to achieve something like this:
However there seems to be catch-22 situation. It seems Swagger/Swashbuckle was using group name to associate operations and generate files. If we change group name to be generic
''v'VVV"
it means we can no longer use the split the actions/operations between the groups using the[ApiExplorerSettings(GroupName = "session")]
attribute since it is one group per version. If we keep the specific groups then we can't generate them dynamically from the version descriptors because the group names ("authoring" and "session" won't map to the versions "v1" and "v2" etc.Our thought was maybe there is some hack like making the version name include a slash such as
v{version}/authoring
andv{version}/session
but that would increase complexity and put us off standard sample for people to help us.Our only current idea is to go to generic versioning with single swagger per version and sepearate the authoring vs session groups by moving it to a new .net core project. That is a much larger work than we thought as we have to re-architect to share resources across them and thus creating this issue asking for help.
The text was updated successfully, but these errors were encountered: