From 3c2802f22afd392fc9b70bdd31ed816faa44c557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Garc=C3=ADa=20de=20la=20Noceda=20Arg=C3=BCelles?= Date: Fri, 11 Oct 2024 12:11:50 +0200 Subject: [PATCH] Types with TryParse must be set with type string --- .../SwaggerGenerator/SwaggerGenerator.cs | 8 +- ...r_WebApi_swaggerRequestUri=v1.verified.txt | 29 +++++++ ...est.TypesAreRenderedCorrectly.verified.txt | 29 +++++++ .../Fixtures/FakeController.cs | 3 + .../SwaggerGenerator/SwaggerGeneratorTests.cs | 32 +++++++ ..._AndModelMetadataTypeIsString.verified.txt | 84 +++++++++++++++++++ .../SwaggerGeneratorVerifyTests.cs | 29 +++++++ .../WebApi/EndPoints/OpenApiEndpoints.cs | 19 +++++ 8 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.GetSwagger_Works_As_Expected_When_TypeIsEnum_AndModelMetadataTypeIsString.verified.txt diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index fa6f178cc5..b9c099a4a3 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -590,14 +590,14 @@ private OpenApiParameter GenerateParameterWithoutFilter( var isRequired = apiParameter.IsRequiredParameter(); - var type = apiParameter.Type; + var type = apiParameter.ModelMetadata?.ModelType; if (type is not null && type == typeof(string) - && apiParameter.ModelMetadata?.ModelType is not null - && apiParameter.ModelMetadata.ModelType != type) + && apiParameter.Type is not null + && (Nullable.GetUnderlyingType(apiParameter.Type) ?? apiParameter.Type).IsEnum) { - type = apiParameter.ModelMetadata.ModelType; + type = apiParameter.Type; } var schema = (type != null) diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt index 0201aeb342..cd123b8a51 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi_swaggerRequestUri=v1.verified.txt @@ -170,6 +170,35 @@ } } }, + "/TypeWithTryParse/{tryParse}": { + "get": { + "tags": [ + "WebApi" + ], + "parameters": [ + { + "name": "tryParse", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/WithOpenApi/weatherforecast": { "get": { "tags": [ diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt index 0201aeb342..cd123b8a51 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerVerifyIntegrationTest.TypesAreRenderedCorrectly.verified.txt @@ -170,6 +170,35 @@ } } }, + "/TypeWithTryParse/{tryParse}": { + "get": { + "tags": [ + "WebApi" + ], + "parameters": [ + { + "name": "tryParse", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/WithOpenApi/weatherforecast": { "get": { "tags": [ diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs index f29ddcbaa0..43aea08890 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.SwaggerGen.Test.Fixtures; +using Swashbuckle.AspNetCore.TestSupport; namespace Swashbuckle.AspNetCore.SwaggerGen.Test { @@ -109,5 +110,7 @@ public void ActionHavingFromFormAttributeButNotWithIFormFile([FromForm] string p { } public void ActionHavingFromFormAttributeWithSwaggerIgnore([FromForm] SwaggerIngoreAnnotatedType param1) { } + public void ActionHavingEnum(IntEnum param1) + { } } } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index f4a4cb397d..576caf65d1 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -2019,6 +2019,38 @@ public void GetSwagger_Works_As_Expected_When_FromForm_Attribute_With_SwaggerIgn Assert.Equal(new[] { nameof(SwaggerIngoreAnnotatedType.NotIgnoredString) }, mediaType.Encoding.Keys); } + [Fact] + public void GetSwagger_Works_As_Expected_When_TypeIsEnum_AndModelMetadataTypeIsString() + { + var subject = Subject( + apiDescriptions: + [ + ApiDescriptionFactory.Create( + c => nameof(c.ActionHavingEnum), + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: + [ + new ApiParameterDescription + { + Name = "param1", + Source = BindingSource.Query, + Type = typeof(IntEnum), + ModelMetadata = ModelMetadataFactory.CreateForType(typeof(string)) + } + ]) + ] + ); + var document = subject.GetSwagger("v1"); + + var operation = document.Paths["/resource"].Operations[OperationType.Post]; + Assert.Equal("param1", operation.Parameters[0].Name); + Assert.NotNull(operation.Parameters[0].Schema); + Assert.NotNull(operation.Parameters[0].Schema.Reference); + Assert.Equal(typeof(IntEnum).Name, operation.Parameters[0].Schema.Reference.Id); + } + [Fact] public void GetSwagger_Copies_Description_From_GeneratedSchema() { diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.GetSwagger_Works_As_Expected_When_TypeIsEnum_AndModelMetadataTypeIsString.verified.txt b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.GetSwagger_Works_As_Expected_When_TypeIsEnum_AndModelMetadataTypeIsString.verified.txt new file mode 100644 index 0000000000..1376d80d26 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.GetSwagger_Works_As_Expected_When_TypeIsEnum_AndModelMetadataTypeIsString.verified.txt @@ -0,0 +1,84 @@ +{ + Info: { + Title: Test API, + Version: V1 + }, + Paths: { + /resource: { + Operations: { + Post: { + Tags: [ + { + Name: Fake, + UnresolvedReference: false + } + ], + Parameters: [ + { + UnresolvedReference: false, + Name: param1, + In: Query, + Required: false, + Deprecated: false, + AllowEmptyValue: false, + Style: Form, + Explode: true, + AllowReserved: false, + Schema: { + ReadOnly: false, + WriteOnly: false, + AdditionalPropertiesAllowed: true, + Nullable: false, + Deprecated: false, + UnresolvedReference: false, + Reference: { + IsFragrament: false, + Type: Schema, + Id: IntEnum, + IsExternal: false, + IsLocal: true, + ReferenceV3: #/components/schemas/IntEnum, + ReferenceV2: #/definitions/IntEnum + } + } + } + ], + Responses: { + 200: { + Description: OK, + UnresolvedReference: false + } + }, + Deprecated: false + } + }, + UnresolvedReference: false + } + }, + Components: { + Schemas: { + IntEnum: { + Type: integer, + Format: int32, + ReadOnly: false, + WriteOnly: false, + AdditionalPropertiesAllowed: true, + Enum: [ + { + Value: 2 + }, + { + Value: 4 + }, + { + Value: 8 + } + ], + Nullable: false, + Deprecated: false, + UnresolvedReference: false + } + } + }, + HashCode: F2B8FE01A8D273C628EBD9F65C7F1E80622E3C5CBBB8F150E247A34C4558F617F1D056F7B1113ABE978F9E433A9FBCA1C65A6387FB918AF00AFB54E6F5A47C5D +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.cs index 09f9e44b58..f900ec39dc 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGeneratorVerifyTests/SwaggerGeneratorVerifyTests.cs @@ -1104,6 +1104,35 @@ public Task ActionHavingFromFormAttributeWithSwaggerIgnore() return Verifier.Verify(document); } + [Fact] + public Task GetSwagger_Works_As_Expected_When_TypeIsEnum_AndModelMetadataTypeIsString() + { + var subject = Subject( + apiDescriptions: + [ + ApiDescriptionFactory.Create( + c => nameof(c.ActionHavingEnum), + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: + [ + new ApiParameterDescription + { + Name = "param1", + Source = BindingSource.Query, + Type = typeof(IntEnum), + ModelMetadata = ModelMetadataFactory.CreateForType(typeof(string)) + } + ]) + ] + ); + + var document = subject.GetSwagger("v1"); + + return Verifier.Verify(document); + } + [Fact] public Task GetSwagger_Copies_Description_From_GeneratedSchema() { diff --git a/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs index 9f5f7e44b9..3407ba4fb2 100644 --- a/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs +++ b/test/WebSites/WebApi/EndPoints/OpenApiEndpoints.cs @@ -70,6 +70,11 @@ public static IEndpointRouteBuilder MapWithOpenApiEndpoints(this IEndpointRouteB return $"{file.FileName}{tags}"; }).WithOpenApi(); + app.MapGet("/TypeWithTryParse/{tryParse}", (TypeWithTryParse tryParse) => + { + return tryParse.Name; + }).WithOpenApi(); + return app; } } @@ -82,4 +87,18 @@ record class Person(string FirstName, string LastName); record class Address(string Street, string City, string State, string ZipCode); sealed record OrganizationCustomExchangeRatesDto([property: JsonRequired] CurrenciesRate[] CurrenciesRates); sealed record CurrenciesRate([property: JsonRequired] string CurrencyFrom, [property: JsonRequired] string CurrencyTo, double Rate); + record TypeWithTryParse(string Name) + { + public static bool TryParse(string value, out TypeWithTryParse? result) + { + if (value is null) + { + result = null; + return false; + } + + result = new TypeWithTryParse(value); + return true; + } + } }