diff --git a/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/FileUploadController.cs b/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/FileUploadController.cs index f1034a734..e946478ce 100644 --- a/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/FileUploadController.cs +++ b/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/FileUploadController.cs @@ -30,9 +30,25 @@ public Task UploadAttachment( public class CaseAttachmentModel { - public string Description { get; set; } + public string? Description { get; set; } - public IFormFile Contents { get; set; } + public IFormFile? Contents { get; set; } + } + + [HttpPost("UploadAttachment2")] + public Task UploadAttachment2( + [FromForm][Required] CaseAttachmentModel2 model, + [Required] IFormFile contents) + { + return Task.FromResult(Ok()); + } + + public class CaseAttachmentModel2 + { + [Required] + public string Title { get; init; } + + public int? MessageId { get; set; } } } } \ No newline at end of file diff --git a/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/HeaderParametersController.cs b/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/HeaderParametersController.cs index 932542324..0cedb1f51 100644 --- a/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/HeaderParametersController.cs +++ b/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/HeaderParametersController.cs @@ -7,7 +7,7 @@ namespace NSwag.Generation.AspNetCore.Tests.Web.Controllers.Parameters public class HeaderParametersController : Controller { [HttpGet] - public ActionResult MyAction([FromHeader] string required, [FromHeader] string optional = null) + public ActionResult MyAction([FromHeader] string required, [FromHeader] string? optional = null) { return Ok(); } diff --git a/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/SimpleQueryParametersController.cs b/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/SimpleQueryParametersController.cs index 97c93f95b..25093331c 100644 --- a/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/SimpleQueryParametersController.cs +++ b/src/NSwag.Generation.AspNetCore.Tests.Web/Controllers/Parameters/SimpleQueryParametersController.cs @@ -8,7 +8,7 @@ namespace NSwag.Generation.AspNetCore.Tests.Web.Controllers.Parameters public class SimpleQueryParametersController : Controller { [HttpGet] - public ActionResult GetList(int? required, int optional = 10, [BindRequired, FromQuery] string requiredString = null) + public ActionResult GetList(int requiredBecauseNonNullable, int? optionalBecauseNullable, int? optionalBecauseNullableWithDefaultValue = 10, [BindRequired, FromQuery] string? requiredBecauseBindRequired = null) { return Ok(); } diff --git a/src/NSwag.Generation.AspNetCore.Tests/Parameters/FormDataTests.cs b/src/NSwag.Generation.AspNetCore.Tests/Parameters/FormDataTests.cs index 58bf93895..277cb8036 100644 --- a/src/NSwag.Generation.AspNetCore.Tests/Parameters/FormDataTests.cs +++ b/src/NSwag.Generation.AspNetCore.Tests/Parameters/FormDataTests.cs @@ -31,6 +31,39 @@ public async Task WhenOperationHasFormDataFile_ThenItIsInRequestBody() Assert.Equal(JsonObjectType.String, schema.Properties["file"].Type); Assert.Equal(JsonObjectType.String, schema.Properties["test"].Type); + Assert.Contains(@" ""/api/FileUpload/UploadFiles"": { + ""post"": { + ""tags"": [ + ""FileUpload"" + ], + ""operationId"": ""FileUpload_UploadFiles"", + ""requestBody"": { + ""content"": { + ""multipart/form-data"": { + ""schema"": { + ""required"": [ + ""files"", + ""test"" + ], + ""properties"": { + ""files"": { + ""type"": ""array"", + ""nullable"": false, + ""items"": { + ""type"": ""string"", + ""format"": ""binary"" + } + }, + ""test"": { + ""type"": ""string"", + ""nullable"": false + } + } + } + } + } + },".Replace("\r", ""), json.Replace("\r", "")); + await VerifyHelper.Verify(json); } @@ -56,5 +89,56 @@ public async Task WhenOperationHasFormDataComplex_ThenItIsInRequestBody() Assert.NotNull(operation); await VerifyHelper.Verify(json); } + + [Fact] + public async Task WhenOperationHasFormDataComplexWithRequiredProperties_ThenItIsInRequestBody() + { + // Arrange + var settings = new AspNetCoreOpenApiDocumentGeneratorSettings + { + SchemaSettings = new NewtonsoftJsonSchemaGeneratorSettings + { + SchemaType = SchemaType.OpenApi3 + } + }; + + // Act + var document = await GenerateDocumentAsync(settings, typeof(FileUploadController)); + var json = document.ToJson(); + + // Assert + var operation = document.Operations.First(o => o.Operation.OperationId == "FileUpload_UploadAttachment2").Operation; + + Assert.NotNull(operation); + Assert.Contains(@"""requestBody"": { + ""content"": { + ""multipart/form-data"": { + ""schema"": { + ""type"": ""object"", + ""required"": [ + ""Title"", + ""contents"" + ], + ""properties"": { + ""Title"": { + ""type"": ""string"", + ""nullable"": false + }, + ""MessageId"": { + ""type"": ""integer"", + ""format"": ""int32"", + ""nullable"": true + }, + ""contents"": { + ""type"": ""string"", + ""format"": ""binary"", + ""nullable"": false + } + } + } + } + } + },".Replace("\r", ""), json.Replace("\r", "")); + } } } \ No newline at end of file diff --git a/src/NSwag.Generation.AspNetCore.Tests/Parameters/QueryParametersTests.cs b/src/NSwag.Generation.AspNetCore.Tests/Parameters/QueryParametersTests.cs index f0ff2f19b..8bbd1f38b 100644 --- a/src/NSwag.Generation.AspNetCore.Tests/Parameters/QueryParametersTests.cs +++ b/src/NSwag.Generation.AspNetCore.Tests/Parameters/QueryParametersTests.cs @@ -52,10 +52,11 @@ public async Task When_simple_query_parameters_are_nullable_and_set_to_null_they // Assert var operation = document.Operations.First().Operation; - Assert.Equal(3, operation.ActualParameters.Count); + Assert.Equal(4, operation.ActualParameters.Count); Assert.True(operation.ActualParameters.Skip(0).First().IsRequired); - Assert.False(operation.ActualParameters.Skip(1).First().IsRequired); - Assert.True(operation.ActualParameters.Skip(2).First().IsRequired); + Assert.True(operation.ActualParameters.Skip(1).First().IsRequired); + Assert.False(operation.ActualParameters.Skip(2).First().IsRequired); + Assert.True(operation.ActualParameters.Skip(3).First().IsRequired); } [Fact] @@ -74,10 +75,11 @@ public async Task When_simple_query_parameter_has_BindRequiredAttribute_then_it_ // Assert var operation = document.Operations.First().Operation; - Assert.Equal(3, operation.ActualParameters.Count); - Assert.False(operation.ActualParameters.Skip(0).First().IsRequired); + Assert.Equal(4, operation.ActualParameters.Count); + Assert.True(operation.ActualParameters.Skip(0).First().IsRequired); Assert.False(operation.ActualParameters.Skip(1).First().IsRequired); - Assert.True(operation.ActualParameters.Skip(2).First().IsRequired); + Assert.False(operation.ActualParameters.Skip(2).First().IsRequired); + Assert.True(operation.ActualParameters.Skip(3).First().IsRequired); } [Fact] diff --git a/src/NSwag.Generation.AspNetCore/Processors/OperationParameterProcessor.cs b/src/NSwag.Generation.AspNetCore/Processors/OperationParameterProcessor.cs index 6ee6a5847..0b6145982 100644 --- a/src/NSwag.Generation.AspNetCore/Processors/OperationParameterProcessor.cs +++ b/src/NSwag.Generation.AspNetCore/Processors/OperationParameterProcessor.cs @@ -345,10 +345,18 @@ private static JsonSchema CreateOrGetFormDataSchema(OperationProcessorContext co return value.Schema ??= new JsonSchema(); } - private static JsonSchemaProperty CreateFormDataProperty(OperationProcessorContext context, ExtendedApiParameterDescription extendedApiParameter, JsonSchema schema) + private JsonSchemaProperty CreateFormDataProperty(OperationProcessorContext context, ExtendedApiParameterDescription extendedApiParameter, JsonSchema schema) { - return context.SchemaGenerator.GenerateWithReferenceAndNullability( + var formDataProperty = context.SchemaGenerator.GenerateWithReferenceAndNullability( extendedApiParameter.ApiParameter.Type.ToContextualType(extendedApiParameter.Attributes), context.SchemaResolver); + + var contextualPropertyType = extendedApiParameter.ParameterType.ToContextualType(); + var typeDescription = _settings.SchemaSettings.ReflectionService.GetDescription(contextualPropertyType, _settings.SchemaSettings); + var isRequired = extendedApiParameter.IsRequired(_settings.RequireParametersWithoutDefault); + formDataProperty.IsRequired = isRequired; + formDataProperty.IsNullableRaw = _settings.AllowNullableBodyParameters && !isRequired && typeDescription.IsNullable; + + return formDataProperty; } private bool IsFileArray(Type type, JsonTypeDescription typeInfo) @@ -524,7 +532,7 @@ public bool IsRequired(bool requireParametersWithoutDefault) // available in asp.net core >= 2.2 if (ApiParameter.HasProperty("IsRequired")) { - isRequired = ApiParameter.TryGetPropertyValue("IsRequired", false); + isRequired = ApiParameter.TryGetPropertyValue("IsRequired", false) || ApiParameter.ModelMetadata?.IsRequired == true; } else {