Skip to content

Commit 00fcaa1

Browse files
authored
[csharp][generichost] Add Option struct to enable better validation (#15977)
* add Option struct to enable better validation * use kebab case
1 parent e2f5997 commit 00fcaa1

119 files changed

Lines changed: 2836 additions & 2253 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/AbstractCSharpCodegen.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.io.IOException;
4141
import java.io.Writer;
4242
import java.util.*;
43+
import java.util.stream.Collectors;
4344

4445
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
4546
import static org.openapitools.codegen.utils.StringUtils.camelize;
@@ -835,6 +836,9 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
835836
}
836837
}
837838

839+
Set<CodegenParameter> referenceTypes = operation.allParams.stream().filter(p -> p.vendorExtensions.get("x-is-value-type") == null && !p.isNullable).collect(Collectors.toSet());
840+
operation.vendorExtensions.put("x-not-nullable-reference-types", referenceTypes);
841+
operation.vendorExtensions.put("x-has-not-nullable-reference-types", referenceTypes.size() > 0);
838842
processOperation(operation);
839843
}
840844
}

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
@@ -1028,6 +1028,7 @@ public void addGenericHostSupportingFiles(final String clientPackageDir, final S
10281028
supportingFiles.add(new SupportingFile("ApiResponseEventArgs.mustache", clientPackageDir, "ApiResponseEventArgs.cs"));
10291029
supportingFiles.add(new SupportingFile("JsonSerializerOptionsProvider.mustache", clientPackageDir, "JsonSerializerOptionsProvider.cs"));
10301030
supportingFiles.add(new SupportingFile("CookieContainer.mustache", clientPackageDir, "CookieContainer.cs"));
1031+
supportingFiles.add(new SupportingFile("Option.mustache", clientPackageDir, "Option.cs"));
10311032

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

modules/openapi-generator/src/main/java/org/openapitools/codegen/templating/mustache/TrimTrailingWhiteSpaceLambda.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,6 @@
3838
public class TrimTrailingWhiteSpaceLambda implements Mustache.Lambda {
3939
@Override
4040
public void execute(Fragment fragment, Writer writer) throws IOException {
41-
writer.write(fragment.execute().stripTrailing());
41+
writer.write(fragment.execute().stripTrailing() + "\n");
4242
}
4343
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#notRequiredOrIsNullable}}{{nrt?}}{{^nrt}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}}{{/nrt}}{{/notRequiredOrIsNullable}}
1+
{{#isNullable}}{{nrt?}}{{^nrt}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}}{{/nrt}}{{/isNullable}}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ namespace {{packageName}}.Test.{{apiPackage}}
5757
{{#hasOAuthMethods}}
5858
string oauthTokenValue = context.Configuration["<token>"] ?? throw new Exception("Token not found.");
5959
OAuthToken oauthToken = new{{^net70OrLater}} OAuthToken{{/net70OrLater}}(oauthTokenValue, timeout: TimeSpan.FromSeconds(1));
60-
options.AddTokens(oauthToken);{{/hasOAuthMethods}}{{/lambda.trimTrailingWhiteSpace}}
60+
options.AddTokens(oauthToken);
61+
{{/hasOAuthMethods}}
62+
{{/lambda.trimTrailingWhiteSpace}}
6163
});
6264
}
6365
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,16 @@
503503
{{^isDate}}
504504
{{^isDateTime}}
505505
writer.WritePropertyName("{{baseName}}");
506-
JsonSerializer.Serialize(writer, {{#lambda.camelcase_param}}{{classname}}{{/lambda.camelcase_param}}.{{name}}, jsonSerializerOptions);{{/isDateTime}}{{/isDate}}{{/isNumeric}}{{/isBoolean}}{{/isString}}{{/isEnum}}{{/isUuid}}{{/allVars}}{{/lambda.trimLineBreaks}}{{/lambda.trimTrailingWhiteSpace}}
506+
JsonSerializer.Serialize(writer, {{#lambda.camelcase_param}}{{classname}}{{/lambda.camelcase_param}}.{{name}}, jsonSerializerOptions);
507+
{{/isDateTime}}
508+
{{/isDate}}
509+
{{/isNumeric}}
510+
{{/isBoolean}}
511+
{{/isString}}
512+
{{/isEnum}}
513+
{{/isUuid}}
514+
{{/allVars}}
515+
{{/lambda.trimLineBreaks}}
516+
{{/lambda.trimTrailingWhiteSpace}}
507517
}
508518
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{{#lambda.joinWithComma}}{{#allParams}}{{{dataType}}}{{>NullConditionalParameter}} {{paramName}}{{#notRequiredOrIsNullable}} = null{{/notRequiredOrIsNullable}} {{/allParams}}System.Threading.CancellationToken cancellationToken = default{{^netstandard20OrLater}}(System.Threading.CancellationToken){{/netstandard20OrLater}}{{/lambda.joinWithComma}}
1+
{{#lambda.joinWithComma}}{{#allParams}}{{#required}}{{{dataType}}}{{>NullConditionalParameter}}{{/required}}{{^required}}Option<{{{dataType}}}{{>NullConditionalParameter}}>{{/required}} {{paramName}}{{#notRequiredOrIsNullable}} = default{{/notRequiredOrIsNullable}} {{/allParams}}System.Threading.CancellationToken cancellationToken = default{{^netstandard20OrLater}}(System.Threading.CancellationToken){{/netstandard20OrLater}}{{/lambda.joinWithComma}}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// <auto-generated>
2+
{{>partial_header}}
3+
{{#nrt}}
4+
#nullable enable
5+
6+
{{/nrt}}
7+
8+
namespace {{packageName}}.{{clientPackage}}
9+
{
10+
/// <summary>
11+
/// A wrapper for operation parameters which are not required
12+
/// </summary>
13+
public struct Option<TType>
14+
{
15+
/// <summary>
16+
/// The value to send to the server
17+
/// </summary>
18+
public TType Value { get; }
19+
20+
/// <summary>
21+
/// When true the value will be sent to the server
22+
/// </summary>
23+
internal bool IsSet { get; }
24+
25+
/// <summary>
26+
/// A wrapper for operation parameters which are not required
27+
/// </summary>
28+
/// <param name="value"></param>
29+
public Option(TType value)
30+
{
31+
IsSet = true;
32+
Value = value;
33+
}
34+
}
35+
}

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

Lines changed: 49 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -128,61 +128,49 @@ namespace {{packageName}}.{{apiPackage}}
128128

129129
{{#allParams}}
130130
{{#-first}}
131-
partial void Format{{operationId}}({{#allParams}}{{#isPrimitiveType}}ref {{/isPrimitiveType}}{{{dataType}}}{{>NullConditionalParameter}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}});
131+
partial void Format{{operationId}}({{#allParams}}{{#isPrimitiveType}}ref {{/isPrimitiveType}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}});
132132

133133
{{/-first}}
134134
{{/allParams}}
135-
{{#notNullableParams}}
136-
{{#-first}}
135+
{{#vendorExtensions.x-has-not-nullable-reference-types}}
137136
/// <summary>
138137
/// Validates the request parameters
139138
/// </summary>
140-
{{/-first}}
139+
{{#vendorExtensions.x-not-nullable-reference-types}}
141140
/// <param name="{{paramName}}"></param>
142-
{{#-last}}
141+
{{/vendorExtensions.x-not-nullable-reference-types}}
143142
/// <returns></returns>
144-
private void Validate{{operationId}}({{#notNullableParams}}{{{dataType}}}{{>NullConditionalParameter}} {{paramName}}{{^-last}}, {{/-last}}{{/notNullableParams}})
143+
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}})
145144
{
146-
{{#notNullableParams}}
147-
{{#-first}}
148-
#pragma warning disable CS0472 // The result of the expression is always the same since a value of this type is never equal to 'null'
149-
#pragma warning disable CS8073 // The result of the expression is always the same since a value of this type is never equal to 'null'
150-
151-
{{/-first}}
152-
{{#nrt}}
153-
if ({{paramName}} == null)
154-
throw new ArgumentNullException(nameof({{paramName}}));
155-
156-
{{/nrt}}
157-
{{^nrt}}
145+
{{#lambda.trimTrailingWhiteSpace}}
146+
{{#vendorExtensions.x-not-nullable-reference-types}}
147+
{{#required}}
158148
{{^vendorExtensions.x-is-value-type}}
159149
if ({{paramName}} == null)
160150
throw new ArgumentNullException(nameof({{paramName}}));
161151

162152
{{/vendorExtensions.x-is-value-type}}
163-
{{#vendorExtensions.x-is-value-type}}
164-
if ({{paramName}} == null)
153+
{{/required}}
154+
{{^required}}
155+
{{^vendorExtensions.x-is-value-type}}
156+
if ({{paramName}}.IsSet && {{paramName}}.Value == null)
165157
throw new ArgumentNullException(nameof({{paramName}}));
166158

167159
{{/vendorExtensions.x-is-value-type}}
168-
{{/nrt}}
169-
{{#-last}}
170-
#pragma warning restore CS0472 // The result of the expression is always the same since a value of this type is never equal to 'null'
171-
#pragma warning restore CS8073 // The result of the expression is always the same since a value of this type is never equal to 'null'
172-
{{/-last}}
173-
{{/notNullableParams}}
160+
{{/required}}
161+
{{/vendorExtensions.x-not-nullable-reference-types}}
162+
{{/lambda.trimTrailingWhiteSpace}}
174163
}
175164

176-
{{/-last}}
177-
{{/notNullableParams}}
165+
{{/vendorExtensions.x-has-not-nullable-reference-types}}
178166
/// <summary>
179167
/// Processes the server response
180168
/// </summary>
181169
/// <param name="apiResponseLocalVar"></param>
182170
{{#allParams}}
183171
/// <param name="{{paramName}}"></param>
184172
{{/allParams}}
185-
private void After{{operationId}}DefaultImplementation({{#lambda.joinWithComma}}ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}> apiResponseLocalVar {{#allParams}}{{{dataType}}}{{>NullConditionalParameter}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}})
173+
private void After{{operationId}}DefaultImplementation({{#lambda.joinWithComma}}ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}> apiResponseLocalVar {{#allParams}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}})
186174
{
187175
bool suppressDefaultLog = false;
188176
After{{operationId}}({{#lambda.joinWithComma}}ref suppressDefaultLog apiResponseLocalVar {{#allParams}}{{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
@@ -197,7 +185,7 @@ namespace {{packageName}}.{{apiPackage}}
197185
{{#allParams}}
198186
/// <param name="{{paramName}}"></param>
199187
{{/allParams}}
200-
partial void After{{operationId}}({{#lambda.joinWithComma}}ref bool suppressDefaultLog ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}> apiResponseLocalVar {{#allParams}}{{{dataType}}}{{>NullConditionalParameter}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
188+
partial void After{{operationId}}({{#lambda.joinWithComma}}ref bool suppressDefaultLog ApiResponse<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}object{{/returnType}}> apiResponseLocalVar {{#allParams}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
201189

202190
/// <summary>
203191
/// Logs exceptions that occur while retrieving the server response
@@ -208,7 +196,7 @@ namespace {{packageName}}.{{apiPackage}}
208196
{{#allParams}}
209197
/// <param name="{{paramName}}"></param>
210198
{{/allParams}}
211-
private void OnError{{operationId}}DefaultImplementation({{#lambda.joinWithComma}}Exception exception string pathFormat string path {{#allParams}}{{{dataType}}}{{>NullConditionalParameter}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}})
199+
private void OnError{{operationId}}DefaultImplementation({{#lambda.joinWithComma}}Exception exception string pathFormat string path {{#allParams}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}})
212200
{
213201
{{>OnErrorDefaultImplementation}}
214202
OnError{{operationId}}({{#lambda.joinWithComma}}exception pathFormat path {{#allParams}}{{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
@@ -223,7 +211,7 @@ namespace {{packageName}}.{{apiPackage}}
223211
{{#allParams}}
224212
/// <param name="{{paramName}}"></param>
225213
{{/allParams}}
226-
partial void OnError{{operationId}}({{#lambda.joinWithComma}}Exception exception string pathFormat string path {{#allParams}}{{{dataType}}}{{>NullConditionalParameter}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
214+
partial void OnError{{operationId}}({{#lambda.joinWithComma}}Exception exception string pathFormat string path {{#allParams}}{{^required}}Option<{{/required}}{{{dataType}}}{{>NullConditionalParameter}}{{^required}}>{{/required}} {{paramName}} {{/allParams}}{{/lambda.joinWithComma}});
227215

228216
/// <summary>
229217
/// {{summary}} {{notes}}
@@ -261,12 +249,10 @@ namespace {{packageName}}.{{apiPackage}}
261249

262250
try
263251
{
264-
{{#notNullableParams}}
265-
{{#-first}}
266-
Validate{{operationId}}({{#notNullableParams}}{{paramName}}{{^-last}}, {{/-last}}{{/notNullableParams}});
252+
{{#vendorExtensions.x-has-not-nullable-reference-types}}
253+
Validate{{operationId}}({{#vendorExtensions.x-not-nullable-reference-types}}{{paramName}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-not-nullable-reference-types}});
267254

268-
{{/-first}}
269-
{{/notNullableParams}}
255+
{{/vendorExtensions.x-has-not-nullable-reference-types}}
270256
{{#allParams}}
271257
{{#-first}}
272258
Format{{operationId}}({{#allParams}}{{#isPrimitiveType}}ref {{/isPrimitiveType}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}});
@@ -295,8 +281,8 @@ namespace {{packageName}}.{{apiPackage}}
295281
{{/required}}
296282
{{^required}}
297283

298-
if ({{paramName}} != null)
299-
uriBuilderLocalVar.Path = uriBuilderLocalVar.Path + $"/{ Uri.EscapeDataString({{paramName}}).ToString()) }";
284+
if ({{paramName}}.IsSet)
285+
uriBuilderLocalVar.Path = uriBuilderLocalVar.Path + $"/{ Uri.EscapeDataString({{paramName}}.Value).ToString()) }";
300286
{{#-last}}
301287

302288
{{/-last}}
@@ -325,14 +311,14 @@ namespace {{packageName}}.{{apiPackage}}
325311
{{/-first}}
326312
{{/required}}
327313
{{#required}}
328-
parseQueryStringLocalVar["{{baseName}}"] = {{paramName}}.ToString();
314+
parseQueryStringLocalVar["{{baseName}}"] = {{paramName}}{{#isNullable}}{{nrt?}}{{^nrt}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}}{{/nrt}}{{/isNullable}}.ToString();
329315
{{/required}}
330316
{{/queryParams}}
331317

332318
{{#queryParams}}
333319
{{^required}}
334-
if ({{paramName}} != null)
335-
parseQueryStringLocalVar["{{baseName}}"] = {{paramName}}.ToString();
320+
if ({{paramName}}.IsSet)
321+
parseQueryStringLocalVar["{{baseName}}"] = {{paramName}}.Value{{#isNullable}}{{nrt?}}{{^nrt}}{{#vendorExtensions.x-is-value-type}}?{{/vendorExtensions.x-is-value-type}}{{/nrt}}{{/isNullable}}.ToString();
336322

337323
{{/required}}
338324
{{#-last}}
@@ -346,8 +332,8 @@ namespace {{packageName}}.{{apiPackage}}
346332

347333
{{/required}}
348334
{{^required}}
349-
if ({{paramName}} != null)
350-
httpRequestMessageLocalVar.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}));
335+
if ({{paramName}}.IsSet)
336+
httpRequestMessageLocalVar.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}.Value));
351337

352338
{{/required}}
353339
{{/headerParams}}
@@ -365,8 +351,8 @@ namespace {{packageName}}.{{apiPackage}}
365351

366352
{{/required}}
367353
{{^required}}
368-
if ({{paramName}} != null)
369-
formParameterLocalVars.Add(new KeyValuePair<string{{nrt?}}, string{{nrt?}}>("{{baseName}}", ClientUtils.ParameterToString({{paramName}})));
354+
if ({{paramName}}.IsSet)
355+
formParameterLocalVars.Add(new KeyValuePair<string{{nrt?}}, string{{nrt?}}>("{{baseName}}", ClientUtils.ParameterToString({{paramName}}.Value)));
370356

371357
{{/required}}
372358
{{/isFile}}
@@ -376,16 +362,24 @@ namespace {{packageName}}.{{apiPackage}}
376362

377363
{{/required}}
378364
{{^required}}
379-
if ({{paramName}} != null)
380-
multipartContentLocalVar.Add(new StreamContent({{paramName}}));
365+
if ({{paramName}}.IsSet)
366+
multipartContentLocalVar.Add(new StreamContent({{paramName}}.Value));
381367

382368
{{/required}}
383369
{{/isFile}}
384370
{{/formParams}}
385371
{{#bodyParam}}
386-
httpRequestMessageLocalVar.Content = ({{paramName}} as object) is System.IO.Stream stream
372+
{{#required}}
373+
httpRequestMessageLocalVar.Content = ({{paramName}}{{^required}}.Value{{/required}} as object) is System.IO.Stream stream
387374
? httpRequestMessageLocalVar.Content = new StreamContent(stream)
388-
: httpRequestMessageLocalVar.Content = new StringContent(JsonSerializer.Serialize({{paramName}}, _jsonSerializerOptions));
375+
: httpRequestMessageLocalVar.Content = new StringContent(JsonSerializer.Serialize({{paramName}}{{^required}}.Value{{/required}}, _jsonSerializerOptions));
376+
{{/required}}
377+
{{^required}}
378+
if ({{paramName}}.IsSet)
379+
httpRequestMessageLocalVar.Content = ({{paramName}}{{^required}}.Value{{/required}} as object) is System.IO.Stream stream
380+
? httpRequestMessageLocalVar.Content = new StreamContent(stream)
381+
: httpRequestMessageLocalVar.Content = new StringContent(JsonSerializer.Serialize({{paramName}}{{^required}}.Value{{/required}}, _jsonSerializerOptions));
382+
{{/required}}
389383

390384
{{/bodyParam}}
391385
{{#authMethods}}
@@ -442,9 +436,11 @@ namespace {{packageName}}.{{apiPackage}}
442436

443437
tokenBaseLocalVars.Add(httpSignatureTokenLocalVar);
444438

445-
string requestBodyLocalVar = await httpRequestMessageLocalVar.Content{{nrt!}}.ReadAsStringAsync({{#net60OrLater}}cancellationToken{{/net60OrLater}}).ConfigureAwait(false);
439+
if (httpRequestMessageLocalVar.Content != null) {
440+
string requestBodyLocalVar = await httpRequestMessageLocalVar.Content.ReadAsStringAsync({{#net60OrLater}}cancellationToken{{/net60OrLater}}).ConfigureAwait(false);
446441

447-
httpSignatureTokenLocalVar.UseInHeader(httpRequestMessageLocalVar, requestBodyLocalVar, cancellationToken);
442+
httpSignatureTokenLocalVar.UseInHeader(httpRequestMessageLocalVar, requestBodyLocalVar, cancellationToken);
443+
}
448444
{{/isHttpSignature}}
449445
{{/authMethods}}
450446
{{#consumes}}
@@ -462,7 +458,7 @@ namespace {{packageName}}.{{apiPackage}}
462458

463459
string{{nrt?}} contentTypeLocalVar = ClientUtils.SelectHeaderContentType(contentTypes);
464460

465-
if (contentTypeLocalVar != null)
461+
if (contentTypeLocalVar != null && httpRequestMessageLocalVar.Content != null)
466462
httpRequestMessageLocalVar.Content.Headers.ContentType = new MediaTypeHeaderValue(contentTypeLocalVar);
467463

468464
{{/-first}}

0 commit comments

Comments
 (0)