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

C# Code Generation generates method with return default(void) #3912

Open mikoskinen opened 2 years ago

mikoskinen commented 2 years ago

The C# generator generated client method with the following signature:

public async System.Threading.Tasks.Task AttachmentsPatchAsync(int attachmentId, AttachmentRename body, System.Threading.CancellationToken cancellationToken)

Problem is that the method tries to return default(void) which doesn't compile:

                try
                {
                    var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
                    if (response_.Content != null && response_.Content.Headers != null)
                    {
                        foreach (var item_ in response_.Content.Headers)
                            headers_[item_.Key] = item_.Value;
                    }

                    ProcessResponse(client_, response_);

                    var status_ = (int)response_.StatusCode;
                    return default(void);
                }

Instead of this I suppose plain return would work.

This is with OperationGenerationMode = OperationGenerationMode.MultipleClientsFromPathSegments.

The Open API spec is: https://dev.procountor.com/static/swagger.latest.json

crackalak commented 2 years ago

Yea this looks like a bug with default responses, think another condition needs to check for HasResult here instead of just using UnwrappedResultDefaultValue: https://github.com/RicoSuter/NSwag/blob/67cc25b63a92df49d185dfa15486ba9e60c0db39/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.liquid#L337-L342

Having said that, it's only falling back to that as you haven't specified any response types looking at your API spec, whereas they are specified on other patch endpoints.

"responses" : {
          "default" : {
            "description" : "default response",
            "content" : {
              "application/json" : { }
            }
          }
        }
PedroJesusRomeroOrtega commented 1 year ago

With responses in this way, is also returning the same return default(void);.

    "responses": {
        "200": {
            "description": "OK",
            "content": {
                "application/json": {},
                "text/json": {},
                "application/xml": {},
                "text/xml": {}
            }
        },

The Open Api spec comes from an external provider (a3laboral-services-api ) so I can not fix it in the external API. So, Is there a workaround while this issue get fixed?

Thank you in advance

crackalak commented 1 year ago

@PedroJesusRomeroOrtega what does the API actually return for the request? You could manually edit the spec after downloading it to make it match for now, and possibly contact the provider to get it fixed.

thomasf-nvm commented 1 year ago

+1 on this issue.

        "responses": {
          "200": {
            "description": "Ok",
            "content": {
              "application/vnd.mycompany.v1+json": { }
            }
          },

results in

                        var status_ = (int)response_.StatusCode;
                        if (status_ == 200)
                        {
                            return default(void);
                        }
thomasf-nvm commented 1 year ago

@RicoSuter would you accept a PR for this issue?

Cyber1000 commented 1 year ago

any new on this? https://community.openproject.org/api/v3/spec.yml seems to have something similar, though I don't know if it's a problem of nswag or openproject.

Can anyone tell me if this is the same or similar cause as yours? At least I'm getting default(void) for returncode 200, though swagger seems to return something referenced by "$ref": "#/components/examples/Views". So I should definitely get a result here, as far as I see in your examples you don't expect any result.

        "/api/v3/views": {
            "get": {
                "parameters": [
                    {
                        "description": "JSON specifying filter conditions.\nCurrently supported filters are:\n\n+ project: filters views by the project their associated query is assigned to. If the project filter is passed with the `!*` (not any) operator, global views are returned.\n\n+ id: filters views based on their id\n\n+ type: filters views based on their type",
                        "example": "[{ \"project_id\": { \"operator\": \"!*\", \"values\": null }\" }]",
                        "in": "query",
                        "name": "filters",
                        "required": false,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "content": {
                            "application/hal+json": {
                                "examples": {
                                    "Queries": {
                                        "$ref": "#/components/examples/Views"
                                    }
                                }
                            }
                        },
                        "description": "OK",
                        "headers": {}
                    }
                },
                "tags": [
                    "Views"
                ],
                "description": "Returns a collection of Views. The collection can be filtered via query parameters similar to how work packages are filtered.",
                "operationId": "List_views",
                "summary": "List views"
            }
        }
        /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
        /// <summary>
        /// List views
        /// </summary>
        /// <remarks>
        /// Returns a collection of Views. The collection can be filtered via query parameters similar to how work packages are filtered.
        /// </remarks>
        /// <param name="filters">JSON specifying filter conditions.
        /// <br/>Currently supported filters are:
        /// <br/>
        /// <br/>+ project: filters views by the project their associated query is assigned to. If the project filter is passed with the `!*` (not any) operator, global views are returned.
        /// <br/>
        /// <br/>+ id: filters views based on their id
        /// <br/>
        /// <br/>+ type: filters views based on their type</param>
        /// <returns>OK</returns>
        /// <exception cref="ApiException">A server side error occurred.</exception>
        public virtual async System.Threading.Tasks.Task ViewsAsync(string filters, System.Threading.CancellationToken cancellationToken)
        {
            var urlBuilder_ = new System.Text.StringBuilder();
            urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/v3/views?");
            if (filters != null)
            {
                urlBuilder_.Append(System.Uri.EscapeDataString("filters") + "=").Append(System.Uri.EscapeDataString(ConvertToString(filters, System.Globalization.CultureInfo.InvariantCulture))).Append("&");
            }
            urlBuilder_.Length--;

            var client_ = _httpClient;
            var disposeClient_ = false;
            try
            {
                using (var request_ = new System.Net.Http.HttpRequestMessage())
                {
                    request_.Method = new System.Net.Http.HttpMethod("GET");
                    request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/hal+json"));

                    PrepareRequest(client_, request_, urlBuilder_);

                    var url_ = urlBuilder_.ToString();
                    request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute);

                    PrepareRequest(client_, request_, url_);

                    var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
                    var disposeResponse_ = true;
                    try
                    {
                        var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
                        if (response_.Content != null && response_.Content.Headers != null)
                        {
                            foreach (var item_ in response_.Content.Headers)
                                headers_[item_.Key] = item_.Value;
                        }

                        ProcessResponse(client_, response_);

                        var status_ = (int)response_.StatusCode;
                        if (status_ == 200)
                        {
                            return default(void);
                        }
                        else
                        {
                            var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
                            throw new ApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null);
                        }
                    }
                    finally
                    {
                        if (disposeResponse_)
                            response_.Dispose();
                    }
                }
            }
            finally
            {
                if (disposeClient_)
                    client_.Dispose();
            }
        }
sungam3r commented 1 year ago

I ended up with modifying resulting generated code - code.Replace("return default(void);", "return;");. Roughly but it works.

Cyber1000 commented 1 year ago

As said for me this wouldn't work, since I'm expecting a result here. Maybe this is a bug on its own.

TheXenocide commented 1 month ago

For now I'm working around this with a custom liquid template that manually detects when the default return is default(void) but this solution is less than ideal