Skip to content

Commit b9e7595

Browse files
authored
Add nullable annotation support to AspNetCoreServer (#9620)
* Add nullable annotation support to AspNetCoreServer * Adjust naming for compatability with PR #9235 by @dehl-labs
1 parent 0f51662 commit b9e7595

File tree

4 files changed

+147
-1
lines changed

4 files changed

+147
-1
lines changed

docs/generators/aspnetcore.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
1919
|licenseUrl|The URL of the license| |http://localhost|
2020
|modelClassModifier|Model Class Modifier can be nothing or partial| |partial|
2121
|newtonsoftVersion|Version for Microsoft.AspNetCore.Mvc.NewtonsoftJson for ASP.NET Core 3.0+| |3.0.0|
22+
|nullableReferenceTypes|Annotate Project with <Nullable>annotations</Nullable> and use ? annotation on all nullable attributes. Only supported on C# 8 / ASP.NET Core 3.0 or newer.| |false|
2223
|operationIsAsync|Set methods to async or sync (default).| |false|
2324
|operationModifier|Operation Modifier can be virtual or abstract|<dl><dt>**virtual**</dt><dd>Keep method virtual</dd><dt>**abstract**</dt><dd>Make method abstract</dd></dl>|virtual|
2425
|operationResultTask|Set methods result to Task&lt;&gt;.| |false|

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

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public class AspNetCoreServerCodegen extends AbstractCSharpCodegen {
6060
public static final String USE_NEWTONSOFT = "useNewtonsoft";
6161
public static final String USE_DEFAULT_ROUTING = "useDefaultRouting";
6262
public static final String NEWTONSOFT_VERSION = "newtonsoftVersion";
63+
public static final String NULLABLE_REFERENCE_TYPES = "nullableReferenceTypes";
6364

6465
private String packageGuid = "{" + randomUUID().toString().toUpperCase(Locale.ROOT) + "}";
6566
private String userSecretsGuid = randomUUID().toString();
@@ -84,6 +85,7 @@ public class AspNetCoreServerCodegen extends AbstractCSharpCodegen {
8485
private boolean useFrameworkReference = false;
8586
private boolean useNewtonsoft = true;
8687
private boolean useDefaultRouting = true;
88+
private boolean nullableReferenceTypes = false;
8789
private String newtonsoftVersion = "3.0.0";
8890

8991
public AspNetCoreServerCodegen() {
@@ -249,6 +251,11 @@ public AspNetCoreServerCodegen() {
249251
"Use default routing for the ASP.NET Core version.",
250252
useDefaultRouting);
251253

254+
addSwitch(NULLABLE_REFERENCE_TYPES,
255+
"Annotate Project with <Nullable>annotations</Nullable> and use ? annotation on all nullable attributes. " +
256+
"Only supported on C# 8 / ASP.NET Core 3.0 or newer.",
257+
nullableReferenceTypes);
258+
252259
addOption(CodegenConstants.ENUM_NAME_SUFFIX,
253260
CodegenConstants.ENUM_NAME_SUFFIX_DESC,
254261
enumNameSuffix);
@@ -366,6 +373,7 @@ public void processOpts() {
366373
setIsFramework();
367374
setUseNewtonsoft();
368375
setUseEndpointRouting();
376+
setNullableReferenceTypes();
369377

370378
supportingFiles.add(new SupportingFile("build.sh.mustache", "", "build.sh"));
371379
supportingFiles.add(new SupportingFile("build.bat.mustache", "", "build.bat"));
@@ -520,7 +528,7 @@ public String toRegularExpression(String pattern) {
520528
@Override
521529
public String getNullableType(Schema p, String type) {
522530
if (languageSpecificPrimitives.contains(type)) {
523-
if (isSupportNullable() && ModelUtils.isNullable(p) && nullableType.contains(type)) {
531+
if (isSupportNullable() && ModelUtils.isNullable(p) && (nullableType.contains(type) || nullableReferenceTypes)) {
524532
return type + "?";
525533
} else {
526534
return type;
@@ -649,6 +657,19 @@ private void setUseSwashbuckle() {
649657
}
650658
}
651659

660+
private void setNullableReferenceTypes() {
661+
if (additionalProperties.containsKey(NULLABLE_REFERENCE_TYPES)) {
662+
if (aspnetCoreVersion.getOptValue().startsWith("2.")) {
663+
LOGGER.warn("Nullable annotation are not supported in ASP.NET core version 2. Setting " + NULLABLE_REFERENCE_TYPES + " to false");
664+
additionalProperties.put(NULLABLE_REFERENCE_TYPES, false);
665+
} else {
666+
nullableReferenceTypes = convertPropertyToBooleanAndWriteBack(NULLABLE_REFERENCE_TYPES);
667+
}
668+
} else {
669+
additionalProperties.put(NULLABLE_REFERENCE_TYPES, nullableReferenceTypes);
670+
}
671+
}
672+
652673
private void setOperationIsAsync() {
653674
if (isLibrary) {
654675
operationIsAsync = false;

modules/openapi-generator/src/main/resources/aspnetcore/3.0/Project.csproj.mustache

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<PreserveCompilationContext>true</PreserveCompilationContext>
99
<Version>{{packageVersion}}</Version>
10+
{{#nullableReferenceTypes}}
11+
<Nullable>annotations</Nullable>
12+
{{/nullableReferenceTypes}}
1013
{{#isLibrary}}
1114
<OutputType>Library</OutputType>
1215
{{/isLibrary}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/csharp/CSharpModelTest.java

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.openapitools.codegen.CodegenProperty;
2626
import org.openapitools.codegen.DefaultCodegen;
2727
import org.openapitools.codegen.TestUtils;
28+
import org.openapitools.codegen.languages.AspNetCoreServerCodegen;
2829
import org.openapitools.codegen.languages.CSharpClientCodegen;
2930
import org.testng.Assert;
3031
import org.testng.annotations.Test;
@@ -264,6 +265,126 @@ public void nullablePropertyTest() {
264265
Assert.assertTrue(property3.isPrimitiveType);
265266
}
266267

268+
@Test(description = "convert a model with a nullable property without nullable annotation")
269+
public void nullablePropertyWithoutNullableReferenceTypesTest() {
270+
final Schema model = new Schema()
271+
.description("a sample model")
272+
.addProperties("id", new IntegerSchema().format(SchemaTypeUtil.INTEGER64_FORMAT).nullable(true))
273+
.addProperties("urls", new ArraySchema()
274+
.items(new StringSchema()).nullable(true))
275+
.addProperties("name", new StringSchema().nullable(true))
276+
.addProperties("subObject", new Schema().addProperties("name", new StringSchema()).nullable(true))
277+
.addRequiredItem("id");
278+
final DefaultCodegen codegen = new AspNetCoreServerCodegen();
279+
OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", model);
280+
codegen.setOpenAPI(openAPI);
281+
final CodegenModel cm = codegen.fromModel("sample", model);
282+
283+
Assert.assertEquals(cm.name, "sample");
284+
Assert.assertEquals(cm.classname, "Sample");
285+
Assert.assertEquals(cm.description, "a sample model");
286+
Assert.assertEquals(cm.vars.size(), 4);
287+
288+
final CodegenProperty property1 = cm.vars.get(0);
289+
Assert.assertEquals(property1.baseName, "id");
290+
Assert.assertEquals(property1.dataType, "long?");
291+
Assert.assertEquals(property1.name, "Id");
292+
Assert.assertNull(property1.defaultValue);
293+
Assert.assertEquals(property1.baseType, "long?");
294+
Assert.assertTrue(property1.required);
295+
Assert.assertTrue(property1.isPrimitiveType);
296+
297+
final CodegenProperty property2 = cm.vars.get(1);
298+
Assert.assertEquals(property2.baseName, "urls");
299+
Assert.assertEquals(property2.dataType, "List<string>");
300+
Assert.assertEquals(property2.name, "Urls");
301+
Assert.assertNull(property2.defaultValue);
302+
Assert.assertEquals(property2.baseType, "List");
303+
Assert.assertEquals(property2.containerType, "array");
304+
Assert.assertFalse(property2.required);
305+
Assert.assertTrue(property2.isPrimitiveType);
306+
Assert.assertTrue(property2.isContainer);
307+
308+
final CodegenProperty property3 = cm.vars.get(2);
309+
Assert.assertEquals(property3.baseName, "name");
310+
Assert.assertEquals(property3.dataType, "string");
311+
Assert.assertEquals(property3.name, "Name");
312+
Assert.assertNull(property3.defaultValue);
313+
Assert.assertEquals(property3.baseType, "string");
314+
Assert.assertFalse(property3.required);
315+
Assert.assertTrue(property3.isPrimitiveType);
316+
317+
final CodegenProperty property4 = cm.vars.get(3);
318+
Assert.assertEquals(property4.baseName, "subObject");
319+
Assert.assertEquals(property4.dataType, "Object");
320+
Assert.assertEquals(property4.name, "SubObject");
321+
Assert.assertNull(property4.defaultValue);
322+
Assert.assertEquals(property4.baseType, "Object");
323+
Assert.assertFalse(property4.required);
324+
Assert.assertTrue(property4.isPrimitiveType);
325+
}
326+
327+
@Test(description = "convert a model with a nullable property using nullable annotation")
328+
public void nullablePropertyWithNullableReferenceTypesTest() {
329+
final Schema model = new Schema()
330+
.description("a sample model")
331+
.addProperties("id", new IntegerSchema().format(SchemaTypeUtil.INTEGER64_FORMAT).nullable(true))
332+
.addProperties("urls", new ArraySchema()
333+
.items(new StringSchema()).nullable(true))
334+
.addProperties("name", new StringSchema().nullable(true))
335+
.addProperties("subObject", new Schema().addProperties("name", new StringSchema()).nullable(true))
336+
.addRequiredItem("id");
337+
final DefaultCodegen codegen = new AspNetCoreServerCodegen();
338+
codegen.additionalProperties().put(AspNetCoreServerCodegen.NULLABLE_REFERENCE_TYPES, true);
339+
codegen.processOpts();
340+
OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", model);
341+
codegen.setOpenAPI(openAPI);
342+
final CodegenModel cm = codegen.fromModel("sample", model);
343+
344+
Assert.assertEquals(cm.name, "sample");
345+
Assert.assertEquals(cm.classname, "Sample");
346+
Assert.assertEquals(cm.description, "a sample model");
347+
Assert.assertEquals(cm.vars.size(), 4);
348+
349+
final CodegenProperty property1 = cm.vars.get(0);
350+
Assert.assertEquals(property1.baseName, "id");
351+
Assert.assertEquals(property1.dataType, "long?");
352+
Assert.assertEquals(property1.name, "Id");
353+
Assert.assertNull(property1.defaultValue);
354+
Assert.assertEquals(property1.baseType, "long?");
355+
Assert.assertTrue(property1.required);
356+
Assert.assertTrue(property1.isPrimitiveType);
357+
358+
final CodegenProperty property2 = cm.vars.get(1);
359+
Assert.assertEquals(property2.baseName, "urls");
360+
Assert.assertEquals(property2.dataType, "List<string>");
361+
Assert.assertEquals(property2.name, "Urls");
362+
Assert.assertNull(property2.defaultValue);
363+
Assert.assertEquals(property2.baseType, "List?");
364+
Assert.assertEquals(property2.containerType, "array");
365+
Assert.assertFalse(property2.required);
366+
Assert.assertTrue(property2.isPrimitiveType);
367+
Assert.assertTrue(property2.isContainer);
368+
369+
final CodegenProperty property3 = cm.vars.get(2);
370+
Assert.assertEquals(property3.baseName, "name");
371+
Assert.assertEquals(property3.dataType, "string?");
372+
Assert.assertEquals(property3.name, "Name");
373+
Assert.assertNull(property3.defaultValue);
374+
Assert.assertEquals(property3.baseType, "string?");
375+
Assert.assertFalse(property3.required);
376+
Assert.assertFalse(property3.isPrimitiveType);
377+
378+
final CodegenProperty property4 = cm.vars.get(3);
379+
Assert.assertEquals(property4.baseName, "subObject");
380+
Assert.assertEquals(property4.dataType, "Object?");
381+
Assert.assertEquals(property4.name, "SubObject");
382+
Assert.assertNull(property4.defaultValue);
383+
Assert.assertEquals(property4.baseType, "Object?");
384+
Assert.assertFalse(property4.required);
385+
Assert.assertFalse(property4.isPrimitiveType);
386+
}
387+
267388
@Test(description = "convert a model with list property")
268389
public void listPropertyTest() {
269390
final Schema model = new Schema()

0 commit comments

Comments
 (0)