OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
21.94k stars 6.59k forks source link

[BUG] csharp generichost issue with generated sort and filter query parameters #19893

Open leorg99 opened 1 month ago

leorg99 commented 1 month ago

Bug Report Checklist

Description

I am not able to specify a sort or filter query parameter in the required form using the generated code.

I am not sure if it's a bug in the generator or simply a limitation of the OpenAPI V2 Definition File or the spec. If this is the case, then maybe there is a work around that I can implement?

Background:

The API that I am working with shows that sort and filter can be used as follows:

sort_by[<field>]=<value>

where:  \<field> is the name of the field to sort by, placed within square brackets []  \<value> is asc or desc.

<filter>[<field>]=<value>

where:  \<filter> is the filter type to use;  \<field>is the name of the field to filter by, placed within square brackets [];  \<value> is the field value that the filter should apply to.

As an example, this is how the sort and filter is generated for the following method in GroupsApi.cs:

public async Task<IGetGroupsApiResponse> GetGroupsAsync(Option<string> cursor = default, Option<int> perPage = default, Option<Object> sortBy = default, Option<Object> filter = default, Option<Object> filterPrefix = default, Option<string> ids = default, System.Threading.CancellationToken cancellationToken = default)
{
  ...
     System.Collections.Specialized.NameValueCollection parseQueryStringLocalVar = System.Web.HttpUtility.ParseQueryString(string.Empty);

   if (cursor.IsSet)
       parseQueryStringLocalVar["cursor"] = ClientUtils.ParameterToString(cursor.Value);

   if (perPage.IsSet)
       parseQueryStringLocalVar["per_page"] = ClientUtils.ParameterToString(perPage.Value);

   if (sortBy.IsSet)
       parseQueryStringLocalVar["sort_by"] = ClientUtils.ParameterToString(sortBy.Value);

   if (filter.IsSet)
       parseQueryStringLocalVar["filter"] = ClientUtils.ParameterToString(filter.Value);

   if (filterPrefix.IsSet)
       parseQueryStringLocalVar["filter_prefix"] = ClientUtils.ParameterToString(filterPrefix.Value);

   if (ids.IsSet)
       parseQueryStringLocalVar["ids"] = ClientUtils.ParameterToString(ids.Value);

   uriBuilderLocalVar.Query = parseQueryStringLocalVar.ToString();

   httpRequestMessageLocalVar.RequestUri = uriBuilderLocalVar.Uri;
...
}

The OpenAPI V2 Definition File defines this method as follows:

    "/groups": {
      "get": {
        "summary": "List Groups",
        "description": "List Groups",
        "produces": [
          "application/json"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "cursor",
            "description": "Used for pagination.  When a list request has more records available, cursors are provided in the response headers `X-Files-Cursor-Next` and `X-Files-Cursor-Prev`.  Send one of those cursor value here to resume an existing list from the next available record.  Note: many of our SDKs have iterator methods that will automatically handle cursor-based pagination.",
            "type": "string",
            "required": false,
            "x-ms-summary": "Used for pagination.  When a list request has more records available, cursors are provided in the response headers `X-Files-Cursor-Next` and `X-Files-Cursor-Prev`.  Send one of those cursor value here to resume an existing list from the next available record.  Note: many of our SDKs have iterator methods that will automatically handle cursor-based pagination."
          },
          {
            "in": "query",
            "name": "per_page",
            "description": "Number of records to show per page.  (Max: 10,000, 1,000 or less is recommended).",
            "type": "integer",
            "format": "int32",
            "required": false,
            "x-ms-summary": "Number of records to show per page.  (Max: 10,000, 1,000 or less is recommended)."
          },
          {
            "in": "query",
            "name": "sort_by",
            "description": "If set, sort records by the specified field in either `asc` or `desc` direction. Valid fields are `name`.",
            "type": "object",
            "required": false,
            "x-example": {
              "name": "desc"
            },
            "x-ms-summary": "If set, sort records by the specified field in either `asc` or `desc` direction. Valid fields are `name`."
          },
          {
            "in": "query",
            "name": "filter",
            "description": "If set, return records where the specified field is equal to the supplied value. Valid fields are `name`.",
            "type": "object",
            "required": false,
            "x-example": {
              "name": "test"
            },
            "x-ms-summary": "If set, return records where the specified field is equal to the supplied value. Valid fields are `name`."
          },
          {
            "in": "query",
            "name": "filter_prefix",
            "description": "If set, return records where the specified field is prefixed by the supplied value. Valid fields are `name`.",
            "type": "object",
            "required": false,
            "x-example": {
              "name": "test"
            },
            "x-ms-summary": "If set, return records where the specified field is prefixed by the supplied value. Valid fields are `name`."
          },
          {
            "in": "query",
            "name": "ids",
            "description": "Comma-separated list of group ids to include in results.",
            "type": "string",
            "required": false,
            "x-ms-summary": "Comma-separated list of group ids to include in results."
          }
        ],
        "responses": {
          "200": {
            "description": "A list of Groups objects.",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/definitions/GroupEntity"
              }
            },
            "x-ms-summary": "A list of Groups objects."
          },
          "400": {
            "description": "Bad Request",
            "x-ms-summary": "Bad Request"
          },
          "401": {
            "description": "Unauthorized",
            "x-ms-summary": "Unauthorized"
          },
          "403": {
            "description": "Forbidden",
            "x-ms-summary": "Forbidden"
          },
          "404": {
            "description": "Not Found",
            "x-ms-summary": "Not Found"
          },
          "405": {
            "description": "Method Not Allowed",
            "x-ms-summary": "Method Not Allowed"
          },
          "409": {
            "description": "Conflict",
            "x-ms-summary": "Conflict"
          },
          "412": {
            "description": "Precondition Failed",
            "x-ms-summary": "Precondition Failed"
          },
          "422": {
            "description": "Unprocessable Entity",
            "x-ms-summary": "Unprocessable Entity"
          },
          "423": {
            "description": "Locked",
            "x-ms-summary": "Locked"
          },
          "429": {
            "description": "Too Many Requests",
            "x-ms-summary": "Too Many Requests"
          }
        },
        "tags": [
          "groups",
          "Groups"
        ],
        "operationId": "GetGroups",
        "x-authentication": [
          "folder_admin"
        ],
        "x-category": [
          "user_accounts"
        ],
        "x-included-in-ipaas": true,
        "x-sortable_columns": [
          "name"
        ],
        "x-filter_sort_combinations_with_types": {
          "filter_columns": {
            "name": {
              "type": "pattern",
              "sort": [
                "name"
              ]
            }
          },
          "filter_combinations": []
        }
      }
    },
openapi-generator version

7.9.0

OpenAPI declaration file content or url
Generation Details

csharp.config

{
    "apiName": "FilesComApi",
    "library": "generichost",
    "netCoreProjectFile": true,
    "nullableReferenceTypes": true,
    "optionalAssemblyInfo": true,
    "packageName": "Files.Com.Core",
    "targetFramework": "net8.0",
    "sourceFolder": "src"
}
Steps to reproduce

Build

cd generated
dotnet new globaljson --sdk-version 8.0.400 --roll-forward feature
dotnet build
Related issues/PRs

19626

19892

Suggest a fix
leorg99 commented 1 month ago

Thanks in advance! @devhl-labs @wing328

devhl-labs commented 1 month ago

I don't believe this is a part of the openapi spec. To get some support for it, you may need to manually edit the spec before using it, or ask the api owners for changes.

leorg99 commented 1 month ago

@devhl-labs Yeah this isn't my API. I am just a consumer who is looking for a sane way to generate a client to consume it.

Maybe I can do it somehow with user defined templates and customizing this block? https://github.com/OpenAPITools/openapi-generator/blob/38dac13c261d26a72be78bba89ee4a681843e7b0/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api.mustache#L360-L443

I think I would try to create some kind of class models for sort and filter operations. Then, I would modify the template above to add these models as parameters and use them to generate and update the query parameters collection.

Using the example in the original post, we would end up with instead something like

public async Task<IGetGroupsApiResponse> GetGroupsAsync(...,
    Option<Sort> sortBy = default,
    Option<Filter> filter = default, 
    ...)
{
  ...
     System.Collections.Specialized.NameValueCollection parseQueryStringLocalVar = System.Web.HttpUtility.ParseQueryString(string.Empty);

   if (sortBy.IsSet)
       // sortBy.Key= "sort_by[someField]"
       // sortBy.Value = "asc" or "desc"
      parseQueryStringLocalVar[sortBy.Key] = ClientUtils.ParameterToString(sortBy.Value);

   if (filter.IsSet)
       // filter.Key= "filter[someField]"
       // filter.Value = "someValue"
       parseQueryStringLocalVar[filter.Key] = ClientUtils.ParameterToString(filter.Value);
   ...
   uriBuilderLocalVar.Query = parseQueryStringLocalVar.ToString();

   httpRequestMessageLocalVar.RequestUri = uriBuilderLocalVar.Uri;
   ...
}

If you have any suggestions or guidance on how I should go about doing this, I would very much appreciate it!

Thanks again