Swashbuckle.WebApi icon indicating copy to clipboard operation
Swashbuckle.WebApi copied to clipboard

Issues with auto-generated operationId

Open domaindrivendev opened this issue 10 years ago • 11 comments

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?

domaindrivendev avatar Apr 27 '15 19:04 domaindrivendev

+1

wbreza avatar Dec 14 '15 17:12 wbreza

Just carry on from what the azure chaps are doing?

https://azure.microsoft.com/en-us/documentation/articles/app-service-api-dotnet-swashbuckle-customize/

whippet-rider avatar Dec 14 '15 22:12 whippet-rider

@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> :

  1. <resource> is the resource name for which the operation is applied, e.g. document, client, company, etc
  2. <action> is any action that is applied on the resource, such as share, upload, etc
  3. <verb> is http action verb:
    • GET (single)-> Get
    • GET (colleciton) -> List
    • POST -> Create,
    • Delete -> Delete
    • Put -> Update
    • Head -> Exists
  4. <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

gerektoolhy avatar Apr 16 '16 10:04 gerektoolhy

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.

sjmelia avatar May 10 '16 21:05 sjmelia

@dariusdamalakas Any chance you're willing to share the implementation (e.g. sourcecode)?

RobThree avatar Jun 07 '16 13:06 RobThree

@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.

gerektoolhy avatar Jun 07 '16 13:06 gerektoolhy

@dariusdamalakas ok, no problem. I'll have a look at implementing this myself then.

RobThree avatar Jun 07 '16 13:06 RobThree

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 avatar Aug 18 '16 17:08 moander

@moander yep; i've got a PR #756 that resolves this by appending an incrementing number to the name of any overloaded methods.

sjmelia avatar Aug 19 '16 07:08 sjmelia

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.

RobThree avatar Jan 21 '19 13:01 RobThree

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

gmariano avatar May 03 '21 15:05 gmariano