From 1088d706004bfc060d0ae5cd27176634a61533ab Mon Sep 17 00:00:00 2001 From: Nikunj <99412420+nikunjbhargava@users.noreply.github.com> Date: Sat, 27 Apr 2024 16:38:22 +0100 Subject: [PATCH] Throw when [FromForm] used with IFormFile (#2840) Throw an error when a user uses `[FromForm]` with `IFormFile` in controller methods. --- .../SwaggerGenerator/SwaggerGenerator.cs | 16 ++++ .../Fixtures/FakeController.cs | 7 ++ .../SwaggerGenerator/SwaggerGeneratorTests.cs | 80 +++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index 03c586e95..0cfa1c418 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -5,12 +5,14 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.Swagger; + #if NET7_0_OR_GREATER using Microsoft.AspNetCore.Http.Metadata; #endif @@ -328,6 +330,12 @@ private IList GenerateOperationTags(ApiDescription apiDescription) private IList GenerateParameters(ApiDescription apiDescription, SchemaRepository schemaRespository) { + if (apiDescription.ParameterDescriptions.Any(IsFromFormAttributeUsedWithIFormFile)) + throw new SwaggerGeneratorException(string.Format( + "Error reading parameter(s) for action {0} as [FromForm] attribute used with IFormFile. " + + "Please refer to https://github.com/domaindrivendev/Swashbuckle.AspNetCore#handle-forms-and-file-uploads for more information", + apiDescription.ActionDescriptor.DisplayName)); + var applicableApiParameters = apiDescription.ParameterDescriptions .Where(apiParam => { @@ -626,6 +634,14 @@ private OpenApiMediaType CreateResponseMediaType(ModelMetadata modelMetadata, Sc }; } + private static bool IsFromFormAttributeUsedWithIFormFile(ApiParameterDescription apiParameter) + { + var parameterInfo = apiParameter.ParameterInfo(); + var fromFormAttribute = parameterInfo?.GetCustomAttribute(); + + return fromFormAttribute != null && parameterInfo?.ParameterType == typeof(IFormFile); + } + private static readonly Dictionary OperationTypeMap = new Dictionary { { "GET", OperationType.Get }, diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs index 78937d154..856bbf457 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Swashbuckle.AspNetCore.Annotations; @@ -84,5 +85,11 @@ public int ActionWithProducesAttribute() [SwaggerIgnore] public void ActionWithSwaggerIgnoreAttribute() { } + + public void ActionHavingIFormFileParamWithFromFormAtribute([FromForm] IFormFile fileUpload) + { } + + public void ActionHavingFromFormAtributeButNotWithIFormFile([FromForm] string param1, IFormFile param2) + { } } } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index 6b6d64ea1..e2d5e187c 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Routing; using Microsoft.OpenApi.Any; @@ -1433,6 +1434,85 @@ public void GetSwagger_GeneratesSwaggerDocument_ThrowsIfHttpMethodNotSupported(s Assert.Equal($"The \"{httpMethod}\" HTTP method is not supported.", exception.Message); } + [Fact] + public void GetSwagger_Throws_Exception_When_FromForm_Attribute_Used_With_IFormFile() + { + var parameterInfo = typeof(FakeController) + .GetMethod(nameof(FakeController.ActionHavingIFormFileParamWithFromFormAtribute)) + .GetParameters()[0]; + + var subject = Subject( + apiDescriptions: new[] + { + ApiDescriptionFactory.Create( + c => nameof(c.ActionHavingIFormFileParamWithFromFormAtribute), + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: new[] + { + new ApiParameterDescription + { + Name = "fileUpload", // Name of the parameter + Type = typeof(IFormFile), // Type of the parameter + ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = parameterInfo } + } + }) + } + ); + + Assert.Throws(() => subject.GetSwagger("v1")); + } + + [Fact] + public void GetSwagger_Works_As_Expected_When_FromForm_Attribute_Not_Used_With_IFormFile() + { + var paraminfo = typeof(FakeController) + .GetMethod(nameof(FakeController.ActionHavingFromFormAtributeButNotWithIFormFile)) + .GetParameters()[0]; + + var fileUploadParameterInfo = typeof(FakeController) + .GetMethod(nameof(FakeController.ActionHavingFromFormAtributeButNotWithIFormFile)) + .GetParameters()[1]; + + var subject = Subject( + apiDescriptions: new[] + { + ApiDescriptionFactory.Create( + c => nameof(c.ActionHavingFromFormAtributeButNotWithIFormFile), + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: new[] + { + new ApiParameterDescription + { + Name = "param1", // Name of the parameter + Type = typeof(string), // Type of the parameter + ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = paraminfo } + }, + new ApiParameterDescription + { + Name = "param2", // Name of the parameter + Type = typeof(IFormFile), // Type of the parameter + ParameterDescriptor = new ControllerParameterDescriptor { ParameterInfo = fileUploadParameterInfo } + } + }) + } + ); + + var document = subject.GetSwagger("v1"); + Assert.Equal("V1", document.Info.Version); + Assert.Equal("Test API", document.Info.Title); + Assert.Equal(new[] { "/resource" }, document.Paths.Keys.ToArray()); + + var operation = document.Paths["/resource"].Operations[OperationType.Post]; + Assert.NotNull(operation.Parameters); + Assert.Equal(2, operation.Parameters.Count); + Assert.Equal("param1", operation.Parameters[0].Name); + Assert.Equal("param2", operation.Parameters[1].Name); + } + private static SwaggerGenerator Subject( IEnumerable apiDescriptions, SwaggerGeneratorOptions options = null,