Skip to content

Commit

Permalink
Merge branch 'master' into gh-2320-contenttype-with-fileresult
Browse files Browse the repository at this point in the history
  • Loading branch information
IGx89 committed Apr 29, 2024
2 parents d6ccc59 + 05409d5 commit dc1521c
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Net.Http.Headers;

namespace Swashbuckle.AspNetCore.SwaggerGen
{
Expand All @@ -21,6 +22,13 @@ public static class ApiParameterDescriptionExtensions
#endif
};

private static readonly HashSet<string> IllegalHeaderParameters = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
HeaderNames.Accept,
HeaderNames.Authorization,
HeaderNames.ContentType
};

public static bool IsRequiredParameter(this ApiParameterDescription apiParameter)
{
// From the OpenAPI spec:
Expand Down Expand Up @@ -111,5 +119,12 @@ internal static bool IsFromForm(this ApiParameterDescription apiParameter)
return (source == BindingSource.Form || source == BindingSource.FormFile)
|| (elementType != null && typeof(IFormFile).IsAssignableFrom(elementType));
}

internal static bool IsIllegalHeaderParameter(this ApiParameterDescription apiParameter)
{
// Certain header parameters are not allowed and should be described using the corresponding OpenAPI keywords
// https://swagger.io/docs/specification/describing-parameters/#header-parameters
return apiParameter.Source == BindingSource.Header && IllegalHeaderParameters.Contains(apiParameter.Name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -267,7 +269,7 @@ private OpenApiOperation GenerateOpenApiOperationFromMetadata(ApiDescription api
// Schemas will be generated via Swashbuckle by default.
foreach (var parameter in operation.Parameters)
{
var apiParameter = apiDescription.ParameterDescriptions.SingleOrDefault(desc => desc.Name == parameter.Name && !desc.IsFromBody() && !desc.IsFromForm());
var apiParameter = apiDescription.ParameterDescriptions.SingleOrDefault(desc => desc.Name == parameter.Name && !desc.IsFromBody() && !desc.IsFromForm() && !desc.IsIllegalHeaderParameter());
if (apiParameter is not null)
{
parameter.Schema = GenerateSchema(
Expand Down Expand Up @@ -328,13 +330,20 @@ private IList<OpenApiTag> GenerateOperationTags(ApiDescription apiDescription)

private IList<OpenApiParameter> 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 =>
{
return (!apiParam.IsFromBody() && !apiParam.IsFromForm())
&& (!apiParam.CustomAttributes().OfType<BindNeverAttribute>().Any())
&& (!apiParam.CustomAttributes().OfType<SwaggerIgnoreAttribute>().Any())
&& (apiParam.ModelMetadata == null || apiParam.ModelMetadata.IsBindingAllowed);
&& (apiParam.ModelMetadata == null || apiParam.ModelMetadata.IsBindingAllowed)
&& !apiParam.IsIllegalHeaderParameter();
});

return applicableApiParameters
Expand Down Expand Up @@ -629,6 +638,14 @@ private OpenApiMediaType CreateResponseMediaType(Type modelType, SchemaRepositor
};
}

private static bool IsFromFormAttributeUsedWithIFormFile(ApiParameterDescription apiParameter)
{
var parameterInfo = apiParameter.ParameterInfo();
var fromFormAttribute = parameterInfo?.GetCustomAttribute<FromFormAttribute>();

return fromFormAttribute != null && parameterInfo?.ParameterType == typeof(IFormFile);
}

private static readonly Dictionary<string, OperationType> OperationTypeMap = new Dictionary<string, OperationType>
{
{ "GET", OperationType.Get },
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -53,6 +54,15 @@ public void ActionWithIntParameterWithRequiredAttribute([Required]int param)
public void ActionWithIntParameterWithSwaggerIgnoreAttribute([SwaggerIgnore] int param)
{ }

public void ActionWithAcceptFromHeaderParameter([FromHeader] string accept, string param)
{ }

public void ActionWithContentTypeFromHeaderParameter([FromHeader(Name = "Content-Type")] string contentType, string param)
{ }

public void ActionWithAuthorizationFromHeaderParameter([FromHeader] string authorization, string param)
{ }

public void ActionWithObjectParameter(XmlAnnotatedType param)
{ }

Expand Down Expand Up @@ -90,5 +100,11 @@ public FileContentResult ActionWithFileResult()
[SwaggerIgnore]
public void ActionWithSwaggerIgnoreAttribute()
{ }

public void ActionHavingIFormFileParamWithFromFormAtribute([FromForm] IFormFile fileUpload)
{ }

public void ActionHavingFromFormAtributeButNotWithIFormFile([FromForm] string param1, IFormFile param2)
{ }
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Reflection;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Http;

Check failure on line 14 in test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs

View workflow job for this annotation

GitHub Actions / code-ql

The using directive for 'Microsoft.AspNetCore.Http' appeared previously in this namespace

Check failure on line 14 in test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs

View workflow job for this annotation

GitHub Actions / code-ql

The using directive for 'Microsoft.AspNetCore.Http' appeared previously in this namespace

Check failure on line 14 in test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs

View workflow job for this annotation

GitHub Actions / code-ql

The using directive for 'Microsoft.AspNetCore.Http' appeared previously in this namespace

Check failure on line 14 in test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs

View workflow job for this annotation

GitHub Actions / code-ql

The using directive for 'Microsoft.AspNetCore.Http' appeared previously in this namespace

Check failure on line 14 in test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The using directive for 'Microsoft.AspNetCore.Http' appeared previously in this namespace

Check failure on line 14 in test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs

View workflow job for this annotation

GitHub Actions / windows-latest

The using directive for 'Microsoft.AspNetCore.Http' appeared previously in this namespace
using Microsoft.AspNetCore.Routing;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
Expand Down Expand Up @@ -502,6 +505,117 @@ public void GetSwagger_IgnoresParameters_IfActionParameterHasSwaggerIgnoreAttrib
Assert.Empty(operation.Parameters);
}

[Theory]
[InlineData(nameof(FakeController.ActionWithAcceptFromHeaderParameter))]
[InlineData(nameof(FakeController.ActionWithContentTypeFromHeaderParameter))]
[InlineData(nameof(FakeController.ActionWithAuthorizationFromHeaderParameter))]
public void GetSwagger_IgnoresParameters_IfActionParameterIsIllegalHeaderParameter(string action)
{
var illegalParameter = typeof(FakeController).GetMethod(action).GetParameters()[0];
var fromHeaderAttribute = illegalParameter.GetCustomAttribute<FromHeaderAttribute>();

var subject = Subject(
new[]
{
ApiDescriptionFactory.Create<FakeController>(
c => action,
groupName: "v1",
httpMethod: "GET",
relativePath: "resource",
parameterDescriptions: new[]
{
new ApiParameterDescription
{
Name = fromHeaderAttribute?.Name ?? illegalParameter.Name,
Source = BindingSource.Header,
ModelMetadata = ModelMetadataFactory.CreateForParameter(illegalParameter)
},
new ApiParameterDescription
{
Name = "param",
Source = BindingSource.Header
}
}
)
}
);

var document = subject.GetSwagger("v1");

var operation = document.Paths["/resource"].Operations[OperationType.Get];
var parameter = Assert.Single(operation.Parameters);
Assert.Equal("param", parameter.Name);
}

[Theory]
[InlineData(nameof(FakeController.ActionWithAcceptFromHeaderParameter))]
[InlineData(nameof(FakeController.ActionWithContentTypeFromHeaderParameter))]
[InlineData(nameof(FakeController.ActionWithAuthorizationFromHeaderParameter))]
public void GetSwagger_GenerateParametersSchemas_IfActionParameterIsIllegalHeaderParameterWithProvidedOpenApiOperation(string action)
{
var illegalParameter = typeof(FakeController).GetMethod(action).GetParameters()[0];
var fromHeaderAttribute = illegalParameter.GetCustomAttribute<FromHeaderAttribute>();
var illegalParameterName = fromHeaderAttribute?.Name ?? illegalParameter.Name;
var methodInfo = typeof(FakeController).GetMethod(action);
var actionDescriptor = new ActionDescriptor
{
EndpointMetadata = new List<object>()
{
new OpenApiOperation
{
OperationId = "OperationIdSetInMetadata",
Parameters = new List<OpenApiParameter>()
{
new OpenApiParameter
{
Name = illegalParameterName,
},
new OpenApiParameter
{
Name = "param",
}
}
}
},
RouteValues = new Dictionary<string, string>
{
["controller"] = methodInfo.DeclaringType.Name.Replace("Controller", string.Empty)
}
};
var subject = Subject(
apiDescriptions: new[]
{
ApiDescriptionFactory.Create(
actionDescriptor,
methodInfo,
groupName: "v1",
httpMethod: "GET",
relativePath: "resource",
parameterDescriptions: new[]
{
new ApiParameterDescription
{
Name = illegalParameterName,
Source = BindingSource.Header,
ModelMetadata = ModelMetadataFactory.CreateForParameter(illegalParameter)
},
new ApiParameterDescription
{
Name = "param",
Source = BindingSource.Header,
ModelMetadata = ModelMetadataFactory.CreateForType(typeof(string))
}
}),
}
);

var document = subject.GetSwagger("v1");

var operation = document.Paths["/resource"].Operations[OperationType.Get];
Assert.Null(operation.Parameters.Single(p => p.Name == illegalParameterName).Schema);
Assert.NotNull(operation.Parameters.Single(p => p.Name == "param").Schema);
}

[Fact]
public void GetSwagger_SetsParameterRequired_IfApiParameterIsBoundToPath()
{
Expand Down Expand Up @@ -1468,6 +1582,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<FakeController>(
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<SwaggerGeneratorException>(() => 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<FakeController>(
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<ApiDescription> apiDescriptions,
SwaggerGeneratorOptions options = null,
Expand Down
18 changes: 18 additions & 0 deletions test/WebSites/Basic/Controllers/FromHeaderParamsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Mvc;

namespace Basic.Controllers
{
[Produces("application/json")]
public class FromHeaderParamsController
{
[HttpGet("country/validate")]
public IActionResult Get(
[FromHeader]string accept,
[FromHeader(Name = "Content-Type")] string contentType,
[FromHeader] string authorization,
[FromQuery] string country)
{
return new NoContentResult();
}
}
}

0 comments on commit dc1521c

Please sign in to comment.