RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.67k stars 1.23k forks source link

Generated C# compile errors with templated paths in OpenApi reference #3976

Open Trokkin opened 2 years ago

Trokkin commented 2 years ago

When swagger generates reference with templated paths, NSwag would not recognize the templates in the path and paste them in method names as is, like such:

    ...
    "/v1/Companies/{key}": {
      "get": {
        "tags": [
          "Companies"
        ],
        "operationId": "v1/Companies/{key}",
        ...

->

        ...
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public System.Threading.Tasks.Task<Company> V1_Companies({key})Async(int key)
        {
            return V1_Companies({key})Async(key, System.Threading.CancellationToken.None);
        }
        ...

Which would never be recognized by C# as a correct statement.

Also, in my case OData add end points like $count -- it's possible to filter them out of Swagger, but that won't not solve the general problem:

    ...
    "/v1/Companies/$count": {
      "get": {
        "tags": [
          "Companies"
        ],
        "operationId": "v1/Companies/$count",
        ...

->

        ...
        /// <returns>Success</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public System.Threading.Tasks.Task<System.Collections.Generic.ICollection<Company>> V1_Companies_$countAsync()
        {
            return V1_Companies_$countAsync(System.Threading.CancellationToken.None);
        }
        ...

Steps to reproduce:

With VS2022:

  1. Clone AspNetCoreOData repository
  2. Open its main solution, navigate to /samples/ODataRoutingSample
  3. To Startup.cs, add using Microsoft.OpenApi.Models; and replace services.AddSwaggerGen(); with services.AddSwaggerGen(c => c.SwaggerDoc("v1", new OpenApiInfo { Title = "OData 8.x OpenAPI", Version = "v1" }));
  4. Compile and run the example, while it is running create OData ConnectedService with Url localhost:5000/swagger/v1/swagger.json. (Both native VS variant and Unchase produce the same results)
  5. Stop the program and rebuild.
  6. See 99+ errors in swaggerClient.cs
  7. Open it and go grab yourself a coffee while VS2022 stucks at processing all the 6k errors in the file.
sgutkinMadrid commented 2 years ago

+1 same issue. Using an api with a templated path generates illegal method names for C#

Example https://docs.oracle.com/en/cloud/saas/marketing/responsys-rest-api/swagger.json

goebeler commented 2 years ago

Same here, especially the OData $count is a problem for me. I'm trying to fix it using a custom IOperationNameGenerator-decorator and I'll let you know if that works.

gowon commented 1 year ago

I ran into this issue while trying to generate a C# client from an API with OData endpoints. I believe this issue is related to #4002 and #4027, which has a proposed solution in #4041. I took that solution and tried to look for a way to inject this behavior instead of recompiling my own patch. If you are able to supply your own custom IOperationNameGenerator implementation, you can supply the logic needed for legal method names:

public class CustomSingleClientFromOperationIdOperationNameGenerator : IOperationNameGenerator
{
    public bool SupportsMultipleClients { get; } = true;

    public virtual string GetClientName(OpenApiDocument document, string path, string httpMethod,
        OpenApiOperation operation)
    {
        return string.Empty;
    }

    public virtual string GetOperationName(OpenApiDocument document, string path, string httpMethod,
        OpenApiOperation operation)
    {
        // remove unwanted symbols
        var operationName = Regex.Replace(operation.OperationId, @"(?:[^\w\.-]+(\w?))",
            match => match.Groups[1].ToString().ToUpperInvariant(), RegexOptions.None);

        // capitalize all caps verbs (ie. PATCH, DELETE)
        operationName = Regex.Replace(operationName, @"[A-Z]{2,}", match => Capitalize(match.ToString().ToLowerInvariant()), RegexOptions.None);

        // capitalize first letter
        return Capitalize(operationName);
    }

    private static string Capitalize(string input)
    {
        if (string.IsNullOrEmpty(input))
        {
            return string.Empty;
        }

        return $"{input[0].ToString().ToUpper()}{input.Substring(1)}";
    }
}

I have not yet found a way to inject this using the .NET Tool or specifying in an nswag.json config. However, I was able to create a simple console app and use NSwag.Commands where I can inject my custom operation name generator:

var nSwagDocument = await NSwagDocument.LoadWithTransformationsAsync(path, variables);
nSwagDocument.CodeGenerators.OpenApiToCSharpClientCommand.Settings.OperationNameGenerator =
    new CustomSingleClientFromOperationIdOperationNameGenerator();
var result = await nSwagDocument.ExecuteAsync();

It's not the prettiest, but gets the job done without patching anything. I think a long term solution would be to make the OperationNameGenerator extensible from the nswag.json config so that the user can supply their own criteria for name generation, rather than trying to find a hardcoded pattern that serves all needs.

Eldriann commented 1 year ago

Ran into the same problem with an operationId looking like that:

"/api/4.0/users/me": {
      "get": {
        "tags": [
          "users"
        ],
        "operationId": "Users: Me",

Resulting in

public virtual async System.Threading.Tasks.Task<UserDto_4_0> Users:_MeAsync(System.Collections.Generic.IEnumerable<string> fields, System.Threading.CancellationToken cancellationToken)