Swashbuckle.WebApi
Swashbuckle.WebApi copied to clipboard
Issues with auto-generated operationId
Currently, the operationId is generated as a combination of controller name and action:
public static string FriendlyId(this ApiDescription apiDescription)
{
return String.Format("{0}_{1}",
apiDescription.ActionDescriptor.ControllerDescriptor.ControllerName,
apiDescription.ActionDescriptor.ActionName);
}
In certain scenarios (e.g. overloading the action name for different routes) this can result in the same operationId for multiple operations, thus breaking the Swagger 2.0 constraint that operation Id's must be unique within a Swagger document.
The obvious solution would be to instead use a combination of HTTP method and route template. However, this presents a challenge related to the second Swagger 2.0 constraint - namely that operationId should be "friendly" as it MAY be used as a method name in auto-generated clients. For example, the following would not be possible in a JavaScript client:
swaggeClient.pets.GET_pets/{id}
The following would be preferable:
swaggerClient.pets.GetPetById
So, we need to come up with a deterministic (consider complex, nested routes) approach for generating an operationId that is both "unique" and "friendly".
Suggestions welcome?
+1
Just carry on from what the azure chaps are doing?
https://azure.microsoft.com/en-us/documentation/articles/app-service-api-dotnet-swashbuckle-customize/
@domaindrivendev , we faced similar issue, so what we've come up was to generate as friendly operation IDs as we can by following a pattern: <verb><context><resource><action> :
<resource>is the resource name for which the operation is applied, e.g. document, client, company, etc<action>is any action that is applied on the resource, such as share, upload, etc<verb>is http action verb:- GET (single)-> Get
- GET (colleciton) -> List
- POST -> Create,
- Delete -> Delete
- Put -> Update
- Head -> Exists
<context>is usually any parent resources under which resource is nested.
This ends up nicely in the following auto-generated operation IDs. See below table for an example. We have this implemented and working, all covered 100% with unit tests. Obviously, there is an attribute that can be applied to override this auto-generated ID.
| Url | Verb | Operation ID |
|---|---|---|
| /v1/documents/{documentId}/share | post | ShareDocument |
| /v1/documents/{documentId}/content | get | GetDocumentContent |
| /v1/documents/{documentId}/content | put | UpdateDocumentContent |
| /v1/documents/{documentId} | get | GetDocument |
| /v1/documents/{documentId} | post | CreateDocument |
| /v1/documents/{documentId} | delete | DeleteDocument |
| /v1/documents/{documentId} | put | UpdateDocument |
| /v1/documents/statistics | get | GetDocumentStatistics |
| /v1/documents | get | ListDocuments |
| /v1/documents | post | CreateDocuments |
| /v1/clients/{clientId}/documents/{documentId}/content | get | GetClientDocumentContent |
| /v1/clients/{clientId}/documents/{documentId} | get | GetClientDocument |
| /v1/clients/{clientId}/documents | get | ListClientDocuments |
Great project and thanks for your hard work!
I am in a position where I am consuming an API, over which I have limited control; that does this, and the non-unique operationIds are causing difficulty using a swagger-js-codegen client library. I've added a pull request #756 that resolves this by appending a number if the operationId has been previously used and therefore satisfies the requirement for uniqueness.
This doesn't necessarily invalidate the above two suggestions around improving friendliness; but it's a simple solution that goes to ensuring Swashbuckle always generates valid swagger.
@dariusdamalakas Any chance you're willing to share the implementation (e.g. sourcecode)?
@RobThree , we have that as an internal project at the moment, it's quite a bit of a task to get the operation ids right, so its not just a single file that could be shared. Maybe we'll be able to publish this as an open source project, but i wouldn't count on this to be done quickly.
@dariusdamalakas ok, no problem. I'll have a look at implementing this myself then.
operationId MUST be unique according to the Swagger 2.0 specification.
- Unique string used to identify the operation. The id MUST be unique among all operations described in the API. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, it is recommended to follow common programming naming conventions.
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#operation-object
@moander yep; i've got a PR #756 that resolves this by appending an incrementing number to the name of any overloaded methods.
For those interested; another option is just to get rid of the OperationId:
public class NoOperationIdFilter : IOperationFilter
{
public void Apply(Operation operation, OperationFilterContext context)
{
operation.OperationId = null;
}
}
This will get rid of the OperationId element in the swagger document.
I recently ran into the same problem and I solved it using Swashbuckle Operation Filter. The following code generates operation ids based on controller and action names, adding a numeric suffix to the operationId if one with the same id already exists:
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Api.Swagger
{
[UsedImplicitly]
public class SwaggerOperationIdFilter : IOperationFilter
{
private readonly Dictionary<string, string> _swaggerOperationIds = new Dictionary<string, string>();
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (!(context.ApiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor))
{
return;
}
if (_swaggerOperationIds.ContainsKey(controllerActionDescriptor.Id))
{
operation.OperationId = _swaggerOperationIds[controllerActionDescriptor.Id];
}
else
{
var operationIdBaseName = $"{controllerActionDescriptor.ControllerName}_{controllerActionDescriptor.ActionName}";
var operationId = operationIdBaseName;
var suffix = 2;
while (_swaggerOperationIds.Values.Contains(operationId))
{
operationId = $"{operationIdBaseName}{suffix++}";
}
_swaggerOperationIds[controllerActionDescriptor.Id] = operationId;
operation.OperationId = operationId;
}
}
}
}
services.AddSwaggerGen(options =>
{
options.OperationFilter<SwaggerOperationIdFilter>();
...
https://g-mariano.medium.com/generate-readable-apis-clients-by-setting-unique-and-meaningful-operationid-in-swagger-63d404f32ff8