Skip to content

Commit bb9c142

Browse files
authored
[csharp][generichost] Better file support (#22806)
* better file support * build tests * use copilot to write manual tests * use copilot to write manual tests * use copilot to write manual tests * address issue one * address issue two * address issue three * address issue four, regenerate tests * rebuild .net standard tests * addressed additional bot comments * address more bot comments * address more bot comments * address more bot comments * added content-type
1 parent 0c31459 commit bb9c142

253 files changed

Lines changed: 14181 additions & 1912 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CSharpClientCodegen.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,7 @@ public void addGenericHostSupportingFiles(final String clientPackageDir, final S
11261126
supportingFiles.add(new SupportingFile("JsonSerializerOptionsProvider.mustache", clientPackageDir, "JsonSerializerOptionsProvider.cs"));
11271127
supportingFiles.add(new SupportingFile("CookieContainer.mustache", clientPackageDir, "CookieContainer.cs"));
11281128
supportingFiles.add(new SupportingFile("Option.mustache", clientPackageDir, "Option.cs"));
1129+
supportingFiles.add(new SupportingFile("FileParameter.mustache", clientPackageDir, "FileParameter.cs"));
11291130

11301131
supportingFiles.add(new SupportingFile("IApi.mustache", sourceFolder + File.separator + packageName + File.separator + apiPackage(), getInterfacePrefix() + "Api.cs"));
11311132

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// <auto-generated>
2+
{{>partial_header}}
3+
4+
{{#nrt}}
5+
#nullable enable
6+
7+
{{/nrt}}
8+
namespace {{packageName}}.{{clientPackage}}
9+
{
10+
/// <summary>
11+
/// Represents a file to be uploaded as part of a multipart/form-data request.
12+
/// </summary>
13+
{{>visibility}} sealed class FileParameter
14+
{
15+
/// <summary>
16+
/// The file content stream.
17+
/// </summary>
18+
public global::System.IO.Stream Content { get; }
19+
20+
/// <summary>
21+
/// The filename sent in the Content-Disposition header.
22+
/// When null the parameter name from the spec is used as a fallback.
23+
/// </summary>
24+
public string{{nrt?}} FileName { get; }
25+
26+
/// <summary>
27+
/// The MIME type sent in the Content-Type header of the part.
28+
/// Defaults to <c>application/octet-stream</c>.
29+
/// </summary>
30+
public string ContentType { get; }
31+
32+
/// <summary>
33+
/// Creates a new <see cref="FileParameter"/>.
34+
/// </summary>
35+
/// <param name="content">The file content stream.</param>
36+
/// <param name="fileName">Optional filename for the Content-Disposition header.</param>
37+
/// <param name="contentType">Optional MIME type for the Content-Type header of the part. Defaults to <c>application/octet-stream</c>.</param>
38+
public FileParameter(global::System.IO.Stream content, string{{nrt?}} fileName = null, string contentType = "application/octet-stream")
39+
{
40+
Content = content;
41+
FileName = fileName;
42+
ContentType = contentType;
43+
}
44+
}
45+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{{#isFile}}{{#isContainer}}List<{{packageName}}.{{clientPackage}}.FileParameter>{{>NullConditionalParameter}}{{/isContainer}}{{^isContainer}}{{packageName}}.{{clientPackage}}.FileParameter{{>NullConditionalParameter}}{{/isContainer}}{{/isFile}}{{^isFile}}{{{dataType}}}{{>NullConditionalParameter}}{{/isFile}}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#lambda.joinWithComma}}{{#allParams}}{{#required}}{{{dataType}}}{{>NullConditionalParameter}}{{/required}}{{^required}}Option<{{{dataType}}}{{>NullConditionalParameter}}>{{/required}} {{paramName}}{{#notRequiredOrIsNullable}} = default{{/notRequiredOrIsNullable}} {{/allParams}}System.Threading.CancellationToken cancellationToken = default{{^netstandard20OrLater}}(global::System.Threading.CancellationToken){{/netstandard20OrLater}}{{/lambda.joinWithComma}}
1+
{{#lambda.joinWithComma}}{{#allParams}}{{#required}}{{>OperationDataType}}{{/required}}{{^required}}Option<{{>OperationDataType}}>{{/required}} {{paramName}}{{#notRequiredOrIsNullable}} = default{{/notRequiredOrIsNullable}} {{/allParams}}System.Threading.CancellationToken cancellationToken = default{{^netstandard20OrLater}}(global::System.Threading.CancellationToken){{/netstandard20OrLater}}{{/lambda.joinWithComma}}

modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api.mustache

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ namespace {{packageName}}.{{apiPackage}}
246246

247247
{{#allParams}}
248248
{{#-first}}
249-
partial void Format{{operationId}}({{#allParams}}{{#isPrimitiveType}}ref {{/isPrimitiveType}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}});
249+
partial void Format{{operationId}}({{#allParams}}{{#isPrimitiveType}}ref {{/isPrimitiveType}}{{^required}}Option<{{/required}}{{>OperationDataType}}{{^required}}>{{/required}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}});
250250

251251
{{/-first}}
252252
{{/allParams}}
@@ -258,7 +258,7 @@ namespace {{packageName}}.{{apiPackage}}
258258
/// <param name="{{paramName}}"></param>
259259
{{/vendorExtensions.x-not-nullable-reference-types}}
260260
/// <returns></returns>
261-
private void Validate{{operationId}}({{#vendorExtensions.x-not-nullable-reference-types}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-not-nullable-reference-types}})
261+
private void Validate{{operationId}}({{#vendorExtensions.x-not-nullable-reference-types}}{{^required}}Option<{{/required}}{{>OperationDataType}}{{^required}}>{{/required}} {{paramName}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-not-nullable-reference-types}})
262262
{
263263
{{#lambda.trimTrailingWithNewLine}}
264264
{{#vendorExtensions.x-not-nullable-reference-types}}
@@ -288,7 +288,7 @@ namespace {{packageName}}.{{apiPackage}}
288288
{{#allParams}}
289289
/// <param name="{{paramName}}"></param>
290290
{{/allParams}}
291-
private void After{{operationId}}DefaultImplementation({{#lambda.joinWithComma}}{{interfacePrefix}}{{operationId}}ApiResponse apiResponseLocalVar {{#allParams}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}})
291+
private void After{{operationId}}DefaultImplementation({{#lambda.joinWithComma}}{{interfacePrefix}}{{operationId}}ApiResponse apiResponseLocalVar {{#allParams}}{{^required}}Option<{{/required}}{{>OperationDataType}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}})
292292
{
293293
bool suppressDefaultLog = false;
294294
After{{operationId}}({{#lambda.joinWithComma}}ref suppressDefaultLog apiResponseLocalVar {{#allParams}}{{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
@@ -303,7 +303,7 @@ namespace {{packageName}}.{{apiPackage}}
303303
{{#allParams}}
304304
/// <param name="{{paramName}}"></param>
305305
{{/allParams}}
306-
partial void After{{operationId}}({{#lambda.joinWithComma}}ref bool suppressDefaultLog {{interfacePrefix}}{{operationId}}ApiResponse apiResponseLocalVar {{#allParams}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
306+
partial void After{{operationId}}({{#lambda.joinWithComma}}ref bool suppressDefaultLog {{interfacePrefix}}{{operationId}}ApiResponse apiResponseLocalVar {{#allParams}}{{^required}}Option<{{/required}}{{>OperationDataType}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
307307

308308
/// <summary>
309309
/// Logs exceptions that occur while retrieving the server response
@@ -314,7 +314,7 @@ namespace {{packageName}}.{{apiPackage}}
314314
{{#allParams}}
315315
/// <param name="{{paramName}}"></param>
316316
{{/allParams}}
317-
private void OnError{{operationId}}DefaultImplementation({{#lambda.joinWithComma}}Exception exceptionLocalVar string pathFormatLocalVar string pathLocalVar {{#allParams}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}})
317+
private void OnError{{operationId}}DefaultImplementation({{#lambda.joinWithComma}}Exception exceptionLocalVar string pathFormatLocalVar string pathLocalVar {{#allParams}}{{^required}}Option<{{/required}}{{>OperationDataType}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}})
318318
{
319319
bool suppressDefaultLogLocalVar = false;
320320
OnError{{operationId}}({{#lambda.joinWithComma}}ref suppressDefaultLogLocalVar exceptionLocalVar pathFormatLocalVar pathLocalVar {{#allParams}}{{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
@@ -332,7 +332,7 @@ namespace {{packageName}}.{{apiPackage}}
332332
{{#allParams}}
333333
/// <param name="{{paramName}}"></param>
334334
{{/allParams}}
335-
partial void OnError{{operationId}}({{#lambda.joinWithComma}}ref bool suppressDefaultLogLocalVar Exception exceptionLocalVar string pathFormatLocalVar string pathLocalVar {{#allParams}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
335+
partial void OnError{{operationId}}({{#lambda.joinWithComma}}ref bool suppressDefaultLogLocalVar Exception exceptionLocalVar string pathFormatLocalVar string pathLocalVar {{#allParams}}{{^required}}Option<{{/required}}{{>OperationDataType}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
336336

337337
/// <summary>
338338
/// {{summary}} {{notes}}
@@ -471,35 +471,94 @@ namespace {{packageName}}.{{apiPackage}}
471471
{{/headerParams}}
472472
{{#formParams}}
473473
{{#-first}}
474-
MultipartContent multipartContentLocalVar = new MultipartContent();
474+
{{#isMultipart}}
475+
MultipartFormDataContent multipartContentLocalVar = new MultipartFormDataContent();
475476

476477
httpRequestMessageLocalVar.Content = multipartContentLocalVar;
477478

478-
List<KeyValuePair<string{{nrt?}}, string{{nrt?}}>> formParameterLocalVars = new List<KeyValuePair<string{{nrt?}}, string{{nrt?}}>>();
479+
{{/isMultipart}}
480+
List<KeyValuePair<string, string{{nrt?}}>> formParameterLocalVars = new List<KeyValuePair<string, string{{nrt?}}>>();
479481

480-
multipartContentLocalVar.Add(new FormUrlEncodedContent(formParameterLocalVars));{{/-first}}{{^isFile}}{{#required}}
481-
482-
formParameterLocalVars.Add(new KeyValuePair<string{{nrt?}}, string{{nrt?}}>("{{baseName}}", ClientUtils.ParameterToString({{paramName}})));
482+
{{/-first}}
483+
{{^isFile}}
484+
{{#required}}
485+
formParameterLocalVars.Add(new KeyValuePair<string, string{{nrt?}}>("{{baseName}}", ClientUtils.ParameterToString({{paramName}})));
483486

484487
{{/required}}
485488
{{^required}}
486489
if ({{paramName}}.IsSet)
487-
formParameterLocalVars.Add(new KeyValuePair<string{{nrt?}}, string{{nrt?}}>("{{baseName}}", ClientUtils.ParameterToString({{paramName}}.Value)));
490+
formParameterLocalVars.Add(new KeyValuePair<string, string{{nrt?}}>("{{baseName}}", ClientUtils.ParameterToString({{paramName}}.Value)));
488491

489492
{{/required}}
490493
{{/isFile}}
491494
{{#isFile}}
495+
{{^isMultipart}}
496+
{{! application/x-www-form-urlencoded encodes everything as text key=value pairs and has no mechanism to carry binary data. File params require multipart/form-data.}}
492497
{{#required}}
493-
multipartContentLocalVar.Add(new StreamContent({{paramName}}));
498+
throw new NotSupportedException("File parameters cannot be sent with application/x-www-form-urlencoded. Change the operation's content type to multipart/form-data.");
494499

495500
{{/required}}
496501
{{^required}}
497502
if ({{paramName}}.IsSet)
498-
multipartContentLocalVar.Add(new StreamContent({{paramName}}.Value));
503+
throw new NotSupportedException("File parameters cannot be sent with application/x-www-form-urlencoded. Change the operation's content type to multipart/form-data.");
499504

500505
{{/required}}
506+
{{/isMultipart}}
507+
{{#isMultipart}}
508+
{{#required}}
509+
{{#isContainer}}
510+
foreach ({{packageName}}.{{clientPackage}}.FileParameter fileParameterLocalVar in {{paramName}})
511+
{
512+
var streamContentLocalVar = new StreamContent(fileParameterLocalVar.Content);
513+
streamContentLocalVar.Headers.ContentType = new MediaTypeHeaderValue(fileParameterLocalVar.ContentType);
514+
multipartContentLocalVar.Add(streamContentLocalVar, "{{baseName}}", fileParameterLocalVar.FileName ?? "{{baseName}}");
515+
}
516+
517+
{{/isContainer}}
518+
{{^isContainer}}
519+
{
520+
var streamContentLocalVar = new StreamContent({{paramName}}.Content);
521+
streamContentLocalVar.Headers.ContentType = new MediaTypeHeaderValue({{paramName}}.ContentType);
522+
multipartContentLocalVar.Add(streamContentLocalVar, "{{baseName}}", {{paramName}}.FileName ?? "{{baseName}}");
523+
}
524+
525+
{{/isContainer}}
526+
{{/required}}
527+
{{^required}}
528+
if ({{paramName}}.IsSet)
529+
{
530+
{{#isContainer}}
531+
foreach ({{packageName}}.{{clientPackage}}.FileParameter fileParameterLocalVar in {{paramName}}.Value)
532+
{
533+
var streamContentLocalVar = new StreamContent(fileParameterLocalVar.Content);
534+
streamContentLocalVar.Headers.ContentType = new MediaTypeHeaderValue(fileParameterLocalVar.ContentType);
535+
multipartContentLocalVar.Add(streamContentLocalVar, "{{baseName}}", fileParameterLocalVar.FileName ?? "{{baseName}}");
536+
}
537+
{{/isContainer}}
538+
{{^isContainer}}
539+
var streamContentLocalVar = new StreamContent({{paramName}}.Value.Content);
540+
streamContentLocalVar.Headers.ContentType = new MediaTypeHeaderValue({{paramName}}.Value.ContentType);
541+
multipartContentLocalVar.Add(streamContentLocalVar, "{{baseName}}", {{paramName}}.Value.FileName ?? "{{baseName}}");
542+
{{/isContainer}}
543+
}
544+
545+
{{/required}}
546+
{{/isMultipart}}
501547
{{/isFile}}
502548
{{/formParams}}
549+
{{#formParams}}
550+
{{#-first}}
551+
{{#isMultipart}}
552+
foreach (var formParamLocalVar in formParameterLocalVars)
553+
multipartContentLocalVar.Add(new StringContent(formParamLocalVar.Value ?? string.Empty), formParamLocalVar.Key);
554+
{{/isMultipart}}
555+
{{^isMultipart}}
556+
if (formParameterLocalVars.Count > 0)
557+
httpRequestMessageLocalVar.Content = new FormUrlEncodedContent(formParameterLocalVars);
558+
{{/isMultipart}}
559+
560+
{{/-first}}
561+
{{/formParams}}
503562
{{#bodyParam}}
504563
{{#required}}
505564
httpRequestMessageLocalVar.Content = ({{paramName}}{{^required}}.Value{{/required}} as object) is System.IO.Stream stream
@@ -587,10 +646,12 @@ namespace {{packageName}}.{{apiPackage}}
587646
{{#consumes}}
588647
{{#-first}}
589648

649+
{{^formParams}}
590650
string{{nrt?}} contentTypeLocalVar = ClientUtils.SelectHeaderContentType(contentTypes);
591651

592652
if (contentTypeLocalVar != null && httpRequestMessageLocalVar.Content != null)
593653
httpRequestMessageLocalVar.Content.Headers.ContentType = new MediaTypeHeaderValue(contentTypeLocalVar);
654+
{{/formParams}}
594655

595656
{{/-first}}
596657
{{/consumes}}

modules/openapi-generator/src/main/resources/csharp/libraries/generichost/api_test.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ namespace {{packageName}}.Test.{{apiPackage}}
3535
public async Task {{operationId}}AsyncTest()
3636
{
3737
{{#allParams}}
38-
{{^required}}Client.Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}} = default{{nrt!}};
38+
{{^required}}Client.Option<{{/required}}{{>OperationDataType}}{{^required}}>{{/required}} {{paramName}} = default{{nrt!}};
3939
{{/allParams}}
4040
{{#responses}}
4141
{{#-first}}

modules/openapi-generator/src/test/resources/3_0/csharp/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,46 @@ paths:
316316
description: file to upload
317317
type: string
318318
format: binary
319+
'/pet/{petId}/uploadImages':
320+
post:
321+
tags:
322+
- pet
323+
summary: uploads an images
324+
description: ''
325+
operationId: uploadFiles
326+
parameters:
327+
- name: petId
328+
in: path
329+
description: ID of pet to update
330+
required: true
331+
schema:
332+
type: integer
333+
format: int64
334+
responses:
335+
'200':
336+
description: successful operation
337+
content:
338+
application/json:
339+
schema:
340+
$ref: '#/components/schemas/ApiResponse'
341+
security:
342+
- petstore_auth:
343+
- 'write:pets'
344+
- 'read:pets'
345+
requestBody:
346+
required: true
347+
content:
348+
multipart/form-data:
349+
schema:
350+
type: object
351+
properties:
352+
files:
353+
type: array
354+
items:
355+
type: string
356+
format: binary
357+
required:
358+
- files
319359
/store/inventory:
320360
get:
321361
tags:

samples/client/petstore/csharp/generichost/latest/ComposedEnum/.openapi-generator/FILES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ src/Org.OpenAPITools/Client/DateOnlyNullableJsonConverter.cs
2323
src/Org.OpenAPITools/Client/DateTimeJsonConverter.cs
2424
src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs
2525
src/Org.OpenAPITools/Client/ExceptionEventArgs.cs
26+
src/Org.OpenAPITools/Client/FileParameter.cs
2627
src/Org.OpenAPITools/Client/HostConfiguration.cs
2728
src/Org.OpenAPITools/Client/JsonSerializerOptionsProvider.cs
2829
src/Org.OpenAPITools/Client/Option.cs
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// <auto-generated>
2+
/*
3+
* OpenAPI
4+
*
5+
* OpenAPI
6+
*
7+
* The version of the OpenAPI document: 0.0.1
8+
* Generated by: https://github.com/openapitools/openapi-generator.git
9+
*/
10+
11+
#nullable enable
12+
13+
namespace Org.OpenAPITools.Client
14+
{
15+
/// <summary>
16+
/// Represents a file to be uploaded as part of a multipart/form-data request.
17+
/// </summary>
18+
public sealed class FileParameter
19+
{
20+
/// <summary>
21+
/// The file content stream.
22+
/// </summary>
23+
public global::System.IO.Stream Content { get; }
24+
25+
/// <summary>
26+
/// The filename sent in the Content-Disposition header.
27+
/// When null the parameter name from the spec is used as a fallback.
28+
/// </summary>
29+
public string? FileName { get; }
30+
31+
/// <summary>
32+
/// The MIME type sent in the Content-Type header of the part.
33+
/// Defaults to <c>application/octet-stream</c>.
34+
/// </summary>
35+
public string ContentType { get; }
36+
37+
/// <summary>
38+
/// Creates a new <see cref="FileParameter"/>.
39+
/// </summary>
40+
/// <param name="content">The file content stream.</param>
41+
/// <param name="fileName">Optional filename for the Content-Disposition header.</param>
42+
/// <param name="contentType">Optional MIME type for the Content-Type header of the part. Defaults to <c>application/octet-stream</c>.</param>
43+
public FileParameter(global::System.IO.Stream content, string? fileName = null, string contentType = "application/octet-stream")
44+
{
45+
Content = content;
46+
FileName = fileName;
47+
ContentType = contentType;
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)