RicoSuter / NSwag

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

NSwag Dotnet Core IFormFile #2650

Open thedwillis opened 4 years ago

thedwillis commented 4 years ago

Been struggling with this for hours and I'm hoping I can get some closure while I still have hair left. I have the following dotnet core action:

C# Code ``` [HttpPost, DisableRequestSizeLimit] [ProducesResponseType(typeof(string), (int)HttpStatusCode.Created)] [ProducesResponseType((int)HttpStatusCode.BadRequest)] [ProducesResponseType((int)HttpStatusCode.UnsupportedMediaType)] public async Task Upload([FromRoute] string authId, IFormFile file) { try { var fileName = await _storageService.UploadAsync(authId, file); return CreatedAtAction(nameof(GetUri), new { authId = authId, fileName = fileName }, fileName); } catch (UnsupportedMediaTypeException) { return new UnsupportedMediaTypeResult(); } catch (Exception ex) { _logger.LogError(LoggingEvent.Unknown, ex, LoggingEvent.Unknown.Name); return StatusCode((int)HttpStatusCode.InternalServerError); } } ```

Which is producing the following swagger file with Swashbuckle 5.0.0:

Swagger Code ``` { "openapi": "3.0.1", "info": { "title": "Documents API", "description": "API for exposing document endpoints.", "termsOfService": "http://tempuri.org/terms", "contact": { "name": "bleh", "email": "bleh@bleh.net" }, "version": "v1" }, "paths": { "/api/{authId}/timesheetsignoff/{fileName}": { "get": { "tags": [ "TimesheetSignOffs" ], "summary": "Retrieves a URI to the timesheet sign-off document.", "operationId": "TimesheetSignOffs_GetUri", "parameters": [ { "name": "authId", "in": "path", "description": "User's unique auth identifier.", "required": true, "schema": { "type": "string" } }, { "name": "fileName", "in": "path", "description": "Name of file to retrieve.", "required": true, "schema": { "type": "string" } } ], "responses": { "200": { "description": "Success", "content": { "text/plain": { "schema": { "type": "string", "format": "uri" } }, "application/json": { "schema": { "type": "string", "format": "uri" } }, "text/json": { "schema": { "type": "string", "format": "uri" } } } }, "404": { "description": "Not Found", "content": { "text/plain": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } }, "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } }, "text/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" } }, "security": [ { "OAuth2": [ "" ] } ] } }, "/api/{authId}/timesheetsignoff": { "post": { "tags": [ "TimesheetSignOffs" ], "summary": "Uploads a new timesheet sign-off document to user's storage.", "operationId": "TimesheetSignOffs_Upload", "parameters": [ { "name": "authId", "in": "path", "description": "User's unique auth identifier.", "required": true, "schema": { "type": "string" } } ], "requestBody": { "content": { "multipart/form-data": { "schema": { "type": "object", "properties": { "ContentType": { "type": "string" }, "ContentDisposition": { "type": "string" }, "Headers": { "type": "object", "additionalProperties": { "type": "array", "items": { "type": "string" } } }, "Length": { "type": "integer", "format": "int64" }, "Name": { "type": "string" }, "FileName": { "type": "string" } } }, "encoding": { "ContentType": { "style": "form" }, "ContentDisposition": { "style": "form" }, "Headers": { "style": "form" }, "Length": { "style": "form" }, "Name": { "style": "form" }, "FileName": { "style": "form" } } } } }, "responses": { "201": { "description": "Success", "content": { "text/plain": { "schema": { "type": "string" } }, "application/json": { "schema": { "type": "string" } }, "text/json": { "schema": { "type": "string" } } } }, "400": { "description": "Bad Request", "content": { "text/plain": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } }, "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } }, "text/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "415": { "description": "Client Error", "content": { "text/plain": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } }, "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } }, "text/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } } }, "401": { "description": "Unauthorized" }, "403": { "description": "Forbidden" } }, "security": [ { "OAuth2": [ "" ] } ] } } }, "components": { "schemas": { "ProblemDetails": { "type": "object", "properties": { "type": { "type": "string", "nullable": true }, "title": { "type": "string", "nullable": true }, "status": { "type": "integer", "format": "int32", "nullable": true }, "detail": { "type": "string", "nullable": true }, "instance": { "type": "string", "nullable": true } }, "additionalProperties": { "type": "object", "additionalProperties": false } } }, "securitySchemes": { "OAuth2": { "type": "oauth2", "description": "Login with your bearer authentication token, e.g. Bearer ", "flows": { "clientCredentials": { "tokenUrl": "http://localhost:5555/connect/token", "scopes": { "documents": "" } } } } } } } ```

Using the following NSwag config I have successfully produced a client lib, and although the GET action works perfectly, the upload fails miserably; the action is called but neither the auth id, or file param is set:

NSwag Config ``` { "runtime": "NetCore30", "defaultVariables": null, "documentGenerator": { "fromDocument": { "url": "http://localhost:5003/swagger/v1/swagger.json", "output": null } }, "codeGenerators": { "openApiToCSharpClient": { "namespace": "SeaComply.Documents.Client", "clientBaseClass": "ClientBase", "generateClientInterfaces": true, "useHttpClientCreationMethod": false, "useHttpRequestMessageCreationMethod": true, "clientClassAccessModifier": "internal", "typeAccessModifier": "public", "generateContractsOutput": true, "contractsNamespace": "SeaComply.Documents.Client.Contracts", "contractsOutputFilePath": "Contracts.g.cs", "operationGenerationMode": "SingleClientFromOperationId", "className": "DocumentsClient", "output": "Client.g.cs" } } } ```

The NSwag output for this function is as follows:

NSwag Lib ``` public async System.Threading.Tasks.Task TimesheetSignOffs_UploadAsync(string authId, System.IO.Stream body, System.Threading.CancellationToken cancellationToken) { if (authId == null) throw new System.ArgumentNullException("authId"); var urlBuilder_ = new System.Text.StringBuilder(); urlBuilder_.Append(BaseUrl != null ? BaseUrl.TrimEnd('/') : "").Append("/api/{authId}/timesheetsignoff"); urlBuilder_.Replace("{authId}", System.Uri.EscapeDataString(ConvertToString(authId, System.Globalization.CultureInfo.InvariantCulture))); var client_ = _httpClient; try { using (var request_ = await CreateHttpRequestMessageAsync(cancellationToken).ConfigureAwait(false)) { var content_ = new System.Net.Http.StreamContent(body); content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("multipart/form-data"); request_.Content = content_; request_.Method = new System.Net.Http.HttpMethod("POST"); request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain")); 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); 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).ToString(); if (status_ == "201") { var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); return objectResponse_.Object; } else if (status_ == "400") { var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); throw new ApiException("Bad Request", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); } else if (status_ == "415") { var objectResponse_ = await ReadObjectResponseAsync(response_, headers_).ConfigureAwait(false); throw new ApiException("Client Error", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null); } else if (status_ == "401") { string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); throw new ApiException("Unauthorized", (int)response_.StatusCode, responseText_, headers_, null); } else if (status_ == "403") { string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); throw new ApiException("Forbidden", (int)response_.StatusCode, responseText_, headers_, null); } else if (status_ != "200" && status_ != "204") { 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 (" + (int)response_.StatusCode + ").", (int)response_.StatusCode, responseData_, headers_, null); } return default(string); } finally { if (response_ != null) response_.Dispose(); } } } finally { } } ````

I've sniffed the packet with Wireshark to see what is being communicated which produced the following result:

Wireshark ![alt text](https://i.imgur.com/ePaNa9U.png "Logo Title Text 1")

Looking back through previous issues I can see previous problems with IFormFiles, but from what I can tell they have been resolved for months so I'm totally lost about why the issue is happening to me

thedwillis commented 4 years ago

I should also add that this all works perfectly using Swagger UI.

sm-g commented 4 years ago
  1. hide methods:

    ProtectedMethods = new[] {
                    "YourClient.IndexPostAsync"
                },
  2. create partial interface and class for IndexPostAsync (with probably more parameters like "fileName")

  3. copy implementation from generated code and replace request_.Content with https://github.com/RicoSuter/NSwag/issues/2419#issuecomment-547075981