From 8908e1ed865796fd522be7499d9cce780c56f104 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 May 2024 10:09:02 +0100 Subject: [PATCH 01/20] Bump version (#2876) * Bump version Bump version to 6.6.2 for the next release. * Fix link Point to the correct tag. --------- Co-authored-by: github-actions[bot] Co-authored-by: Martin Costello --- Directory.Build.props | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index ff0bb45651..ded0e19ea5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -45,7 +45,7 @@ snupkg true true - 6.6.1 + 6.6.2 false diff --git a/README.md b/README.md index d396cd57cb..8128b2f930 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Once you have an API that can describe itself in Swagger, you've opened the trea | Swashbuckle Version | ASP.NET Core | Swagger / OpenAPI Spec. | swagger-ui | Redoc UI | |----------|----------|----------|----------|----------| | [CI](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/commits/master/)|>= 2.0.0 | 2.0, 3.0 | [5.x.x](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.SwaggerUI/package.json#L6) | [2.x.x](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.ReDoc/package.json#L6) | -| [6.6.1](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/v6.6.0)|>= 2.0.0 | 2.0, 3.0 | 5.17.9 | 2.1.4 | +| [6.6.1](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/v6.6.1)|>= 2.0.0 | 2.0, 3.0 | 5.17.9 | 2.1.4 | | [5.6.3](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/v5.6.3)|>= 2.0.0 | 2.0, 3.0 | 3.32.5 | 2.0.0-rc.40 | | [4.0.0](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/v4.0.0)|>= 2.0.0, < 3.0.0 | 2.0 | 3.19.5 | 1.22.2 | | [3.0.0](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/tree/v3.0.0)|>= 1.0.4, < 3.0.0 | 2.0 | 3.17.1 | 1.20.0 | From a29ba36f236084a2cc72f56aee3a5500ae467df5 Mon Sep 17 00:00:00 2001 From: Keah Peters Date: Tue, 14 May 2024 15:53:43 +0100 Subject: [PATCH 02/20] Fix to make required and nullable properties nullable in schema (#2879) Fix to make required and nullable properties nullable in schema. - Added test for type with required nullable properties. --- .../SchemaGenerator/SchemaGenerator.cs | 19 +++------- .../JsonSerializerSchemaGeneratorTests.cs | 36 ++++++++++++++++--- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs index a03b312e08..3ca1f7ddd0 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs @@ -68,24 +68,13 @@ private OpenApiSchema GenerateSchemaForMember( { var requiredAttribute = customAttributes.OfType().FirstOrDefault(); - schema.ReadOnly = dataProperty.IsReadOnly; - schema.WriteOnly = dataProperty.IsWriteOnly; - -#if NET7_0_OR_GREATER - var hasRequiredMemberAttribute = customAttributes.OfType().Any(); - schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes - ? dataProperty.IsNullable && requiredAttribute == null && !hasRequiredMemberAttribute && !memberInfo.IsNonNullableReferenceType() - : dataProperty.IsNullable && requiredAttribute == null && !hasRequiredMemberAttribute; - - schema.MinLength = modelType == typeof(string) && (hasRequiredMemberAttribute || requiredAttribute is { AllowEmptyStrings: false }) ? 1 : null; -#else - schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes - ? dataProperty.IsNullable && requiredAttribute==null && !memberInfo.IsNonNullableReferenceType() - : dataProperty.IsNullable && requiredAttribute==null; + ? dataProperty.IsNullable && requiredAttribute == null && !memberInfo.IsNonNullableReferenceType() + : dataProperty.IsNullable && requiredAttribute == null; + schema.ReadOnly = dataProperty.IsReadOnly; + schema.WriteOnly = dataProperty.IsWriteOnly; schema.MinLength = modelType == typeof(string) && requiredAttribute is { AllowEmptyStrings: false } ? 1 : null; -#endif } var defaultValueAttribute = customAttributes.OfType().FirstOrDefault(); diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index 4bc5ac4c4f..bbe9122072 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -367,9 +367,10 @@ public void GenerateSchema_SetsReadOnlyAndWriteOnlyFlags_IfPropertyIsRestricted( } #if NET7_0_OR_GREATER - public class TypeWithRequiredProperty + public class TypeWithRequiredProperties { - public required string RequiredProperty { get; set; } + public required string RequiredString { get; set; } + public required int RequiredInt { get; set; } } public class TypeWithRequiredPropertyAndValidationAttribute @@ -383,10 +384,13 @@ public void GenerateSchema_SetsRequired_IfPropertyHasRequiredKeyword() { var schemaRepository = new SchemaRepository(); - var referenceSchema = Subject().GenerateSchema(typeof(TypeWithRequiredProperty), schemaRepository); + var referenceSchema = Subject().GenerateSchema(typeof(TypeWithRequiredProperties), schemaRepository); var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; - Assert.Equal(new[] { "RequiredProperty" }, schema.Required.ToArray()); + Assert.True(schema.Properties["RequiredString"].Nullable); + Assert.Contains("RequiredString", schema.Required.ToArray()); + Assert.False(schema.Properties["RequiredInt"].Nullable); + Assert.Contains("RequiredInt", schema.Required.ToArray()); } [Fact] @@ -398,9 +402,31 @@ public void GenerateSchema_SetsRequired_IfPropertyHasRequiredKeywordAndValidatio var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; Assert.Equal(1, schema.Properties["RequiredProperty"].MinLength); - Assert.False(schema.Properties["RequiredProperty"].Nullable); + Assert.True(schema.Properties["RequiredProperty"].Nullable); Assert.Equal(new[] { "RequiredProperty" }, schema.Required.ToArray()); } + +#nullable enable + public class TypeWithNullableReferenceTypes + { + public required string? RequiredNullableString { get; set; } + public required string RequiredNonNullableString { get; set; } + } + + [Fact] + public void GenerateSchema_SetsRequiredAndNullable_IfPropertyHasRequiredKeywordAndIsNullable() + { + var schemaRepository = new SchemaRepository(); + + var referenceSchema = Subject(configureGenerator: (c) => c.SupportNonNullableReferenceTypes = true).GenerateSchema(typeof(TypeWithNullableReferenceTypes), schemaRepository); + + var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; + Assert.True(schema.Properties["RequiredNullableString"].Nullable); + Assert.Contains("RequiredNullableString", schema.Required.ToArray()); + Assert.False(schema.Properties["RequiredNonNullableString"].Nullable); + Assert.Contains("RequiredNonNullableString", schema.Required.ToArray()); + } +#nullable disable #endif [Theory] From f16fa786964d72c6ca660c7f0d5b319ccb3fffbf Mon Sep 17 00:00:00 2001 From: cremor Date: Wed, 15 May 2024 10:22:36 +0200 Subject: [PATCH 03/20] Update Swagger UI section (#2883) Update a few outdated links to the Swagger UI repository and add missing extension methods on SwaggerUIOptions. --- README.md | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8128b2f930..5a5ed3f42f 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,7 @@ The steps described above will get you up and running with minimal setup. Howeve * [Change Document Title](#change-document-title) * [List Multiple Swagger Documents](#list-multiple-swagger-documents) * [Apply swagger-ui Parameters](#apply-swagger-ui-parameters) + * [Inject Custom JavaScript](#inject-custom-javascript) * [Inject Custom CSS](#inject-custom-css) * [Customize index.html](#customize-indexhtml) * [Enable OAuth2.0 Flows](#enable-oauth20-flows) @@ -1239,7 +1240,6 @@ By default, the Swagger UI will be exposed at "/swagger". If necessary, you can app.UseSwaggerUI(c => { c.RoutePrefix = "api-docs" - ... } ``` @@ -1251,7 +1251,6 @@ By default, the Swagger UI will have a generic document title. When you have mul app.UseSwaggerUI(c => { c.DocumentTitle = "My Swagger UI"; - ... } ``` @@ -1269,7 +1268,7 @@ app.UseSwaggerUI(c => ### Apply swagger-ui Parameters ### -The swagger-ui ships with its own set of configuration parameters, all described here https://github.com/swagger-api/swagger-ui/blob/v3.8.1/docs/usage/configuration.md#display. In Swashbuckle, most of these are surfaced through the SwaggerUI middleware options: +The swagger-ui ships with its own set of configuration parameters, all described [here](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md#display). In Swashbuckle, most of these are surfaced through the SwaggerUI middleware options: ```csharp app.UseSwaggerUI(c => @@ -1282,6 +1281,8 @@ app.UseSwaggerUI(c => c.DocExpansion(DocExpansion.None); c.EnableDeepLinking(); c.EnableFilter(); + c.EnablePersistAuthorization(); + c.EnableTryItOutByDefault(); c.MaxDisplayedTags(5); c.ShowExtensions(); c.ShowCommonExtensions(); @@ -1292,6 +1293,17 @@ app.UseSwaggerUI(c => }); ``` +### Inject Custom JavaScript ### + +To tweak the behavior, you can inject additional JavaScript files by adding them to your `wwwroot` folder and specifying the relative paths in the middleware options: + +```csharp +app.UseSwaggerUI(c => +{ + c.InjectJavascript("/swagger-ui/custom.js"); +} +``` + _NOTE: The `InjectOnCompleteJavaScript` and `InjectOnFailureJavaScript` options have been removed because the latest version of swagger-ui doesn't expose the necessary hooks. Instead, it provides a [flexible customization system](https://github.com/swagger-api/swagger-ui/blob/master/docs/customization/overview.md) based on concepts and patterns from React and Redux. To leverage this, you'll need to provide a custom version of index.html as described [below](#customize-indexhtml)._ The [custom index sample app](test/WebSites/CustomUIIndex/Swagger/index.html) demonstrates this approach, using the swagger-ui plugin system provide a custom topbar, and to hide the info component. @@ -1303,7 +1315,6 @@ To tweak the look and feel, you can inject additional CSS stylesheets by adding ```csharp app.UseSwaggerUI(c => { - ... c.InjectStylesheet("/swagger-ui/custom.css"); } ``` @@ -1326,20 +1337,22 @@ _To get started, you should base your custom index.html on the [default version] The swagger-ui has built-in support to participate in OAuth2.0 authorization flows. It interacts with authorization and/or token endpoints, as specified in the Swagger JSON, to obtain access tokens for subsequent API calls. See [Adding Security Definitions and Requirements](#add-security-definitions-and-requirements) for an example of adding OAuth2.0 metadata to the generated Swagger. -If your Swagger endpoint includes the appropriate security metadata, the UI interaction should be automatically enabled. However, you can further customize OAuth support in the UI with the following settings below. See [Swagger-UI v3.10.0](https://github.com/swagger-api/swagger-ui/blob/v3.10.0/docs/usage/oauth2.md) for more info: +If your Swagger endpoint includes the appropriate security metadata, the UI interaction should be automatically enabled. However, you can further customize OAuth support in the UI with the following settings below. See [Swagger-UI documentation](https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/oauth2.md) for more info: ```csharp app.UseSwaggerUI(c => { - ... - c.OAuthClientId("test-id"); c.OAuthClientSecret("test-secret"); + c.OAuthUsername("test-user"); c.OAuthRealm("test-realm"); c.OAuthAppName("test-app"); + c.OAuth2RedirectUrl("url"); c.OAuthScopeSeparator(" "); + c.OAuthScopes("scope1", "scope2"); c.OAuthAdditionalQueryStringParams(new Dictionary { { "foo", "bar" }}); c.OAuthUseBasicAuthenticationWithAccessCodeGrant(); + c.OAuthUsePkce(); }); ``` @@ -1350,8 +1363,6 @@ To use custom interceptors on requests and responses going through swagger-ui yo ```csharp app.UseSwaggerUI(c => { - ... - c.UseRequestInterceptor("(req) => { req.headers['x-my-custom-header'] = 'MyCustomValue'; return req; }"); c.UseResponseInterceptor("(res) => { console.log('Custom interceptor intercepted response from:', res.url); return res; }"); }); @@ -1362,8 +1373,6 @@ This can be useful in a range of scenarios where you might want to append local ```csharp app.UseSwaggerUI(c => { - ... - c.UseRequestInterceptor("(req) => { req.headers['X-XSRF-Token'] = localStorage.getItem('xsrf-token'); return req; }"); }); ``` From 7a0c4e1cec719b78f8596b201fd784150e0d3de6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 09:36:02 +0000 Subject: [PATCH 04/20] Bump martincostello/update-dotnet-sdk from 3.2.1 to 3.2.2 (#2888) Bumps [martincostello/update-dotnet-sdk](https://github.com/martincostello/update-dotnet-sdk) from 3.2.1 to 3.2.2. - [Release notes](https://github.com/martincostello/update-dotnet-sdk/releases) - [Commits](https://github.com/martincostello/update-dotnet-sdk/compare/caa37bca20d50c57dc0173d77f956751bb1a1d55...68300a9120c745ca0ff120e2940b63ef1ad2c725) --- updated-dependencies: - dependency-name: martincostello/update-dotnet-sdk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/update-dotnet-sdk.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-dotnet-sdk.yml b/.github/workflows/update-dotnet-sdk.yml index 6df6eebdb9..087510f8aa 100644 --- a/.github/workflows/update-dotnet-sdk.yml +++ b/.github/workflows/update-dotnet-sdk.yml @@ -11,7 +11,7 @@ permissions: jobs: update-dotnet-sdk: - uses: martincostello/update-dotnet-sdk/.github/workflows/update-dotnet-sdk.yml@caa37bca20d50c57dc0173d77f956751bb1a1d55 # v3.2.1 + uses: martincostello/update-dotnet-sdk/.github/workflows/update-dotnet-sdk.yml@68300a9120c745ca0ff120e2940b63ef1ad2c725 # v3.2.2 permissions: contents: write pull-requests: write From 4220ffb16367b32dba79954a380748782ca86140 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 09:37:30 +0000 Subject: [PATCH 05/20] Bump codecov/codecov-action from 4.3.1 to 4.4.0 (#2889) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.3.1 to 4.4.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/5ecb98a3c6b747ed38dc09f787459979aebb39be...6d798873df2b1b8e5846dba6fb86631229fbcb17) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4544d3045c..4e0c87ac1b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -78,7 +78,7 @@ jobs: if-no-files-found: ignore - name: Upload coverage to Codecov - uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # v4.3.1 + uses: codecov/codecov-action@6d798873df2b1b8e5846dba6fb86631229fbcb17 # v4.4.0 with: files: ./artifacts/coverage/coverage.cobertura.xml flags: ${{ runner.os }} From 674eac8f3f79a7e271b007ca074d31f0c0c5669d Mon Sep 17 00:00:00 2001 From: Bogdan Shahnitsky Date: Wed, 15 May 2024 12:58:02 +0300 Subject: [PATCH 06/20] Add support for the `[Length]`attribute. (#2882) Implement support for the `[Length]` attribute. --- .../OpenApiSchemaExtensions.cs | 25 +++++++++++++++++++ .../NewtonsoftSchemaGeneratorTests.cs | 7 +++++- .../JsonSerializerSchemaGeneratorTests.cs | 7 +++++- .../Fixtures/TypeWithValidationAttributes.cs | 12 ++++++++- ...WithValidationAttributesViaMetadataType.cs | 20 ++++++++++++++- 5 files changed, 67 insertions(+), 4 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs index 16d5040cec..0d8fc813a8 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/OpenApiSchemaExtensions.cs @@ -42,6 +42,13 @@ public static void ApplyValidationAttributes(this OpenApiSchema schema, IEnumera else if (attribute is MaxLengthAttribute maxLengthAttribute) ApplyMaxLengthAttribute(schema, maxLengthAttribute); +#if NET8_0_OR_GREATER + + else if (attribute is LengthAttribute lengthAttribute) + ApplyLengthAttribute(schema, lengthAttribute); + +#endif + else if (attribute is RangeAttribute rangeAttribute) ApplyRangeAttribute(schema, rangeAttribute); @@ -146,6 +153,24 @@ private static void ApplyMaxLengthRouteConstraint(OpenApiSchema schema, MaxLengt schema.MaxLength = maxLengthRouteConstraint.MaxLength; } +#if NET8_0_OR_GREATER + + private static void ApplyLengthAttribute(OpenApiSchema schema, LengthAttribute lengthAttribute) + { + if (schema.Type == "array") + { + schema.MinItems = lengthAttribute.MinimumLength; + schema.MaxItems = lengthAttribute.MaximumLength; + } + else + { + schema.MinLength = lengthAttribute.MinimumLength; + schema.MaxLength = lengthAttribute.MaximumLength; + } + } + +#endif + private static void ApplyRangeAttribute(OpenApiSchema schema, RangeAttribute rangeAttribute) { schema.Maximum = decimal.TryParse(rangeAttribute.Maximum.ToString(), out decimal maximum) diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs index b5624af9d3..799026119e 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs @@ -302,7 +302,6 @@ public void GenerateSchema_SetsDeprecatedFlag_IfPropertyHasObsoleteAttribute() [Theory] [InlineData(typeof(TypeWithValidationAttributes))] [InlineData(typeof(TypeWithValidationAttributesViaMetadataType))] - public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAttributes(Type type) { var schemaRepository = new SchemaRepository(); @@ -315,6 +314,12 @@ public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAt Assert.Equal(3, schema.Properties["StringWithMinMaxLength"].MaxLength); Assert.Equal(1, schema.Properties["ArrayWithMinMaxLength"].MinItems); Assert.Equal(3, schema.Properties["ArrayWithMinMaxLength"].MaxItems); +#if NET8_0_OR_GREATER + Assert.Equal(1, schema.Properties["StringWithLength"].MinLength); + Assert.Equal(3, schema.Properties["StringWithLength"].MaxLength); + Assert.Equal(1, schema.Properties["ArrayWithLength"].MinItems); + Assert.Equal(3, schema.Properties["ArrayWithLength"].MaxItems); +#endif Assert.Equal(1, schema.Properties["IntWithRange"].Minimum); Assert.Equal(10, schema.Properties["IntWithRange"].Maximum); Assert.Equal("^[3-6]?\\d{12,15}$", schema.Properties["StringWithRegularExpression"].Pattern); diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index bbe9122072..6416652ee9 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -325,7 +325,6 @@ public void GenerateSchema_SetsDeprecatedFlag_IfPropertyHasObsoleteAttribute() [Theory] [InlineData(typeof(TypeWithValidationAttributes))] [InlineData(typeof(TypeWithValidationAttributesViaMetadataType))] - public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAttributes(Type type) { var schemaRepository = new SchemaRepository(); @@ -338,6 +337,12 @@ public void GenerateSchema_SetsValidationProperties_IfComplexTypeHasValidationAt Assert.Equal(3, schema.Properties["StringWithMinMaxLength"].MaxLength); Assert.Equal(1, schema.Properties["ArrayWithMinMaxLength"].MinItems); Assert.Equal(3, schema.Properties["ArrayWithMinMaxLength"].MaxItems); +#if NET8_0_OR_GREATER + Assert.Equal(1, schema.Properties["StringWithLength"].MinLength); + Assert.Equal(3, schema.Properties["StringWithLength"].MaxLength); + Assert.Equal(1, schema.Properties["ArrayWithLength"].MinItems); + Assert.Equal(3, schema.Properties["ArrayWithLength"].MaxItems); +#endif Assert.Equal(1, schema.Properties["IntWithRange"].Minimum); Assert.Equal(10, schema.Properties["IntWithRange"].Maximum); Assert.Equal("^[3-6]?\\d{12,15}$", schema.Properties["StringWithRegularExpression"].Pattern); diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs index 2780e9be37..9ee38bd305 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributes.cs @@ -13,6 +13,16 @@ public class TypeWithValidationAttributes [MinLength(1), MaxLength(3)] public string[] ArrayWithMinMaxLength { get; set; } +#if NET8_0_OR_GREATER + + [Length(1, 3)] + public string StringWithLength { get; set; } + + [Length(1, 3)] + public string[] ArrayWithLength { get; set; } + +#endif + [Range(1, 10)] public int IntWithRange { get; set; } @@ -28,4 +38,4 @@ public class TypeWithValidationAttributes [Required(AllowEmptyStrings = true)] public string StringWithRequiredAllowEmptyTrue { get; set; } } -} \ No newline at end of file +} diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs index d42f965709..603605e3b5 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithValidationAttributesViaMetadataType.cs @@ -12,6 +12,14 @@ public class TypeWithValidationAttributesViaMetadataType public string[] ArrayWithMinMaxLength { get; set; } +#if NET8_0_OR_GREATER + + public string StringWithLength { get; set; } + + public string[] ArrayWithLength { get; set; } + +#endif + public int IntWithRange { get; set; } public string StringWithRegularExpression { get; set; } @@ -34,6 +42,16 @@ public class MetadataType [MinLength(1), MaxLength(3)] public string[] ArrayWithMinMaxLength { get; set; } +#if NET8_0_OR_GREATER + + [Length(1, 3)] + public string StringWithLength { get; set; } + + [Length(1, 3)] + public string[] ArrayWithLength { get; set; } + +#endif + [Range(1, 10)] public int IntWithRange { get; set; } @@ -49,4 +67,4 @@ public class MetadataType [Required(AllowEmptyStrings = true)] public string StringWithRequiredAllowEmptyTrue { get; set; } } -} \ No newline at end of file +} From 7deb5f6b5f5bd8d6223e54a78b24d8012f5b2b0f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 10:14:12 +0000 Subject: [PATCH 07/20] Update .NET SDK (#2892) Update .NET SDK to version 8.0.300. --- updated-dependencies: - dependency-name: Microsoft.NET.Sdk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: github-actions[bot] Co-authored-by: github-actions[bot] --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 36dbe49054..6f115a660e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.204", + "version": "8.0.300", "allowPrerelease": false, "rollForward": "latestMajor" } From d793865353eaf311eba6ebf32e9b34b7610c937c Mon Sep 17 00:00:00 2001 From: Patrik Westerlund Date: Wed, 15 May 2024 18:21:52 +0800 Subject: [PATCH 08/20] Existing output file should be deleted/overwritten (#2886) The CLI currently uses `File.OpenWrite(path)` (added in #2677) to create an output stream for the resulting swagger document. This does not delete the existing contents, but instead starts writing from position 0, causing issues if the output is smaller than the previous file content. Changed to `File.Create(path)` to make sure any previous content is discarded. --- src/Swashbuckle.AspNetCore.Cli/Program.cs | 2 +- .../ToolTests.cs | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Swashbuckle.AspNetCore.Cli/Program.cs b/src/Swashbuckle.AspNetCore.Cli/Program.cs index e416e7bbae..f48a71db95 100644 --- a/src/Swashbuckle.AspNetCore.Cli/Program.cs +++ b/src/Swashbuckle.AspNetCore.Cli/Program.cs @@ -101,7 +101,7 @@ public static int Main(string[] args) ? Path.Combine(Directory.GetCurrentDirectory(), arg1) : null; - using (Stream stream = outputPath != null ? File.OpenWrite(outputPath) : Console.OpenStandardOutput()) + using (Stream stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput()) using (var streamWriter = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture)) { IOpenApiWriter writer; diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs index f71213ebca..3906c388ae 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs @@ -28,6 +28,28 @@ public void Can_Generate_Swagger_Json() Assert.True(productsPath.TryGetProperty("post", out _)); } + [Fact(Skip = "Disabled because it makes CI unstable")] + public void Overwrites_Existing_File() + { + using var temporaryDirectory = new TemporaryDirectory(); + var path = Path.Combine(temporaryDirectory.Path, "swagger.json"); + + var dummyContent = new string('x', 100_000); + File.WriteAllText(path, dummyContent); + + var args = new string[] { "tofile", "--output", path, Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), "v1" }; + Assert.Equal(0, Program.Main(args)); + + var readContent = File.ReadAllText(path); + Assert.True(readContent.Length < dummyContent.Length); + using var document = JsonDocument.Parse(readContent); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var productsPath = paths.GetProperty("/products"); + Assert.True(productsPath.TryGetProperty("post", out _)); + } + [Fact] public void CustomDocumentSerializer_Writes_Custom_V2_Document() { From e1b56e17f8fd1581f173df3bf1dbb59be02d86e8 Mon Sep 17 00:00:00 2001 From: Patrik Westerlund Date: Thu, 16 May 2024 14:16:44 +0800 Subject: [PATCH 09/20] Fix libraries being handled as test projects (#2898) --- .../Swashbuckle.AspNetCore.ApiTesting.Xunit.csproj | 4 ++++ .../Swashbuckle.AspNetCore.TestSupport.csproj | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/Swashbuckle.AspNetCore.ApiTesting.Xunit/Swashbuckle.AspNetCore.ApiTesting.Xunit.csproj b/src/Swashbuckle.AspNetCore.ApiTesting.Xunit/Swashbuckle.AspNetCore.ApiTesting.Xunit.csproj index ca56c5b78a..ef300e4abb 100644 --- a/src/Swashbuckle.AspNetCore.ApiTesting.Xunit/Swashbuckle.AspNetCore.ApiTesting.Xunit.csproj +++ b/src/Swashbuckle.AspNetCore.ApiTesting.Xunit/Swashbuckle.AspNetCore.ApiTesting.Xunit.csproj @@ -17,4 +17,8 @@ + + + + diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Swashbuckle.AspNetCore.TestSupport.csproj b/test/Swashbuckle.AspNetCore.TestSupport/Swashbuckle.AspNetCore.TestSupport.csproj index c43a30c319..4fd2d10b91 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Swashbuckle.AspNetCore.TestSupport.csproj +++ b/test/Swashbuckle.AspNetCore.TestSupport/Swashbuckle.AspNetCore.TestSupport.csproj @@ -22,4 +22,8 @@ + + + + From 77cde477897f8a2e0d03096e49f750cc5cb37ea1 Mon Sep 17 00:00:00 2001 From: Patrik Westerlund Date: Thu, 16 May 2024 15:22:25 +0800 Subject: [PATCH 10/20] Avoid competing swagger generation during test build (#2897) The previous implementation caused the concurrent builds for various frameworks to compete in writing to the output `swagger.json` file, causing exceptions during builds (since it happened in csproj). In my local development I often failed to rebuild the full solution because some projects failed to build for this reason. It has also happened in CI (https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/2886). Note that the NSwag source code generator in `NswagClientExample` still outputs everything in one spot for all frameworks, but I haven't noticed failures from that. Maybe it's just less likely to write concurrently. I left it as is. Un-skip the `Overwrites_Existing_File` test. --- .../ToolTests.cs | 2 +- .../SwaggerIntegrationTests.cs | 2 +- test/WebSites/CliExample/CliExample.csproj | 2 +- .../v1/{swagger.json => swagger_net6.0.json} | 2 +- .../wwwroot/swagger/v1/swagger_net7.0.json | 54 +++++++ .../wwwroot/swagger/v1/swagger_net8.0.json | 54 +++++++ .../CliExampleWithFactory.csproj | 2 +- .../v1/{swagger.json => swagger_net6.0.json} | 2 +- .../wwwroot/swagger/v1/swagger_net7.0.json | 54 +++++++ .../wwwroot/swagger/v1/swagger_net8.0.json | 54 +++++++ .../NswagClientExample.csproj | 4 +- .../{swagger.json => swagger_net6.0.json} | 2 +- .../NswagClientExample/swagger_net8.0.json | 140 ++++++++++++++++++ 13 files changed, 365 insertions(+), 9 deletions(-) rename test/WebSites/CliExample/wwwroot/swagger/v1/{swagger.json => swagger_net6.0.json} (99%) create mode 100644 test/WebSites/CliExample/wwwroot/swagger/v1/swagger_net7.0.json create mode 100644 test/WebSites/CliExample/wwwroot/swagger/v1/swagger_net8.0.json rename test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/{swagger.json => swagger_net6.0.json} (99%) create mode 100644 test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger_net7.0.json create mode 100644 test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger_net8.0.json rename test/WebSites/NswagClientExample/{swagger.json => swagger_net6.0.json} (99%) create mode 100644 test/WebSites/NswagClientExample/swagger_net8.0.json diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs index 3906c388ae..cc6598009a 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs @@ -28,7 +28,7 @@ public void Can_Generate_Swagger_Json() Assert.True(productsPath.TryGetProperty("post", out _)); } - [Fact(Skip = "Disabled because it makes CI unstable")] + [Fact] public void Overwrites_Existing_File() { using var temporaryDirectory = new TemporaryDirectory(); diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs index 386a5715af..eb2fcfcc6a 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs @@ -20,7 +20,7 @@ public class SwaggerIntegrationTests { [Theory] [InlineData(typeof(Basic.Startup), "/swagger/v1/swagger.json")] - [InlineData(typeof(CliExample.Startup), "/swagger/v1/swagger.json")] + [InlineData(typeof(CliExample.Startup), "/swagger/v1/swagger_net8.0.json")] [InlineData(typeof(ConfigFromFile.Startup), "/swagger/v1/swagger.json")] [InlineData(typeof(CustomUIConfig.Startup), "/swagger/v1/swagger.json")] [InlineData(typeof(CustomUIIndex.Startup), "/swagger/v1/swagger.json")] diff --git a/test/WebSites/CliExample/CliExample.csproj b/test/WebSites/CliExample/CliExample.csproj index 20d3de5abd..59092cc3b7 100644 --- a/test/WebSites/CliExample/CliExample.csproj +++ b/test/WebSites/CliExample/CliExample.csproj @@ -28,7 +28,7 @@ --> - diff --git a/test/WebSites/CliExample/wwwroot/swagger/v1/swagger.json b/test/WebSites/CliExample/wwwroot/swagger/v1/swagger_net6.0.json similarity index 99% rename from test/WebSites/CliExample/wwwroot/swagger/v1/swagger.json rename to test/WebSites/CliExample/wwwroot/swagger/v1/swagger_net6.0.json index 95aa261dd6..0e1dee7edd 100644 --- a/test/WebSites/CliExample/wwwroot/swagger/v1/swagger.json +++ b/test/WebSites/CliExample/wwwroot/swagger/v1/swagger_net6.0.json @@ -51,4 +51,4 @@ } } } -} +} \ No newline at end of file diff --git a/test/WebSites/CliExample/wwwroot/swagger/v1/swagger_net7.0.json b/test/WebSites/CliExample/wwwroot/swagger/v1/swagger_net7.0.json new file mode 100644 index 0000000000..0e1dee7edd --- /dev/null +++ b/test/WebSites/CliExample/wwwroot/swagger/v1/swagger_net7.0.json @@ -0,0 +1,54 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "CliExample", + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost:51071" + } + ], + "paths": { + "/products": { + "get": { + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/CliExample/wwwroot/swagger/v1/swagger_net8.0.json b/test/WebSites/CliExample/wwwroot/swagger/v1/swagger_net8.0.json new file mode 100644 index 0000000000..0e1dee7edd --- /dev/null +++ b/test/WebSites/CliExample/wwwroot/swagger/v1/swagger_net8.0.json @@ -0,0 +1,54 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "CliExample", + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost:51071" + } + ], + "paths": { + "/products": { + "get": { + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/CliExampleWithFactory/CliExampleWithFactory.csproj b/test/WebSites/CliExampleWithFactory/CliExampleWithFactory.csproj index 48549a7fe7..154b9d9f30 100644 --- a/test/WebSites/CliExampleWithFactory/CliExampleWithFactory.csproj +++ b/test/WebSites/CliExampleWithFactory/CliExampleWithFactory.csproj @@ -32,7 +32,7 @@ --> - diff --git a/test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger.json b/test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger_net6.0.json similarity index 99% rename from test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger.json rename to test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger_net6.0.json index eb21965c49..2778aed45a 100644 --- a/test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger.json +++ b/test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger_net6.0.json @@ -51,4 +51,4 @@ } } } -} +} \ No newline at end of file diff --git a/test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger_net7.0.json b/test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger_net7.0.json new file mode 100644 index 0000000000..2778aed45a --- /dev/null +++ b/test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger_net7.0.json @@ -0,0 +1,54 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "CliExampleWithFactory", + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost:57556/" + } + ], + "paths": { + "/products": { + "get": { + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger_net8.0.json b/test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger_net8.0.json new file mode 100644 index 0000000000..2778aed45a --- /dev/null +++ b/test/WebSites/CliExampleWithFactory/wwwroot/swagger/v1/swagger_net8.0.json @@ -0,0 +1,54 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "CliExampleWithFactory", + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost:57556/" + } + ], + "paths": { + "/products": { + "get": { + "tags": [ + "Products" + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Product" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Product": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "description": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/NswagClientExample/NswagClientExample.csproj b/test/WebSites/NswagClientExample/NswagClientExample.csproj index 6b6ea3dea4..4ee15c17f8 100644 --- a/test/WebSites/NswagClientExample/NswagClientExample.csproj +++ b/test/WebSites/NswagClientExample/NswagClientExample.csproj @@ -25,12 +25,12 @@ - - + diff --git a/test/WebSites/NswagClientExample/swagger.json b/test/WebSites/NswagClientExample/swagger_net6.0.json similarity index 99% rename from test/WebSites/NswagClientExample/swagger.json rename to test/WebSites/NswagClientExample/swagger_net6.0.json index 824dd5bc62..68e90e3375 100644 --- a/test/WebSites/NswagClientExample/swagger.json +++ b/test/WebSites/NswagClientExample/swagger_net6.0.json @@ -137,4 +137,4 @@ } } } -} +} \ No newline at end of file diff --git a/test/WebSites/NswagClientExample/swagger_net8.0.json b/test/WebSites/NswagClientExample/swagger_net8.0.json new file mode 100644 index 0000000000..68e90e3375 --- /dev/null +++ b/test/WebSites/NswagClientExample/swagger_net8.0.json @@ -0,0 +1,140 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "NswagClientExample", + "version": "1.0" + }, + "servers": [ + { + "url": "http://example.com" + } + ], + "paths": { + "/Animals": { + "post": { + "tags": [ + "Animals" + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Animal" + }, + { + "$ref": "#/components/schemas/Cat" + }, + { + "$ref": "#/components/schemas/Dog" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Animal" + }, + { + "$ref": "#/components/schemas/Cat" + }, + { + "$ref": "#/components/schemas/Dog" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/Animal" + }, + { + "$ref": "#/components/schemas/Cat" + }, + { + "$ref": "#/components/schemas/Dog" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "Animal": { + "required": [ + "animalType" + ], + "type": "object", + "properties": { + "animalType": { + "$ref": "#/components/schemas/AnimalType" + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "animalType", + "mapping": { + "Cat": "#/components/schemas/Cat", + "Dog": "#/components/schemas/Dog" + } + } + }, + "AnimalType": { + "enum": [ + "Cat", + "Dog" + ], + "type": "string" + }, + "Cat": { + "allOf": [ + { + "$ref": "#/components/schemas/Animal" + }, + { + "type": "object", + "properties": { + "catSpecificProperty": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + ] + }, + "Dog": { + "allOf": [ + { + "$ref": "#/components/schemas/Animal" + }, + { + "type": "object", + "properties": { + "dogSpecificProperty": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + } + ] + } + } + } +} \ No newline at end of file From 828b7df2ac697d4b3b513d42a17cad152ad9f2e6 Mon Sep 17 00:00:00 2001 From: Maxwell Weru Date: Thu, 16 May 2024 10:28:38 +0300 Subject: [PATCH 11/20] Replace br tags in XML comments with new lines (#2899) Replace any `
` tags with a new line instead. --- .../XmlComments/XmlCommentsTextHelper.cs | 12 ++++++++++++ .../XmlComments/XmlCommentsTextHelperTests.cs | 3 ++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs index bb70fab137..6383b3a36f 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsTextHelper.cs @@ -21,6 +21,7 @@ public static string Humanize(string text) .HumanizeCodeTags() .HumanizeMultilineCodeTags() .HumanizeParaTags() + .HumanizeBrTags() // must be called after HumanizeParaTags() so that it replaces any additional
tags .DecodeXml(); } @@ -108,6 +109,11 @@ private static string HumanizeParaTags(this string text) return ParaTag().Replace(text, (match) => "
" + match.Groups["display"].Value); } + private static string HumanizeBrTags(this string text) + { + return BrTag().Replace(text, m => Environment.NewLine); + } + private static string DecodeXml(this string text) { return WebUtility.HtmlDecode(text); @@ -118,6 +124,7 @@ private static string DecodeXml(this string text) private const string MultilineCodeTagPattern = @"(?.+?)"; private const string ParaTagPattern = @"(?.+?)"; private const string HrefPattern = @"(.*)<\/see>"; + private const string BrPattern = @"(
)"; // handles
,
,
#if NET7_0_OR_GREATER [GeneratedRegex(RefTagPattern)] @@ -134,18 +141,23 @@ private static string DecodeXml(this string text) [GeneratedRegex(HrefPattern)] private static partial Regex HrefTag(); + + [GeneratedRegex(BrPattern)] + private static partial Regex BrTag(); #else private static readonly Regex _refTag = new(RefTagPattern); private static readonly Regex _codeTag = new(CodeTagPattern); private static readonly Regex _multilineCodeTag = new(MultilineCodeTagPattern, RegexOptions.Singleline); private static readonly Regex _paraTag = new(ParaTagPattern, RegexOptions.Singleline); private static readonly Regex _hrefTag = new(HrefPattern); + private static readonly Regex _brTag = new(BrPattern); private static Regex RefTag() => _refTag; private static Regex CodeTag() => _codeTag; private static Regex MultilineCodeTag() => _multilineCodeTag; private static Regex ParaTag() => _paraTag; private static Regex HrefTag() => _hrefTag; + private static Regex BrTag() => _brTag; #endif } } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsTextHelperTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsTextHelperTests.cs index 7690cd78fd..9464de28ff 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsTextHelperTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsTextHelperTests.cs @@ -128,10 +128,11 @@ Misplaced Tab Indentation [InlineData(@" does something", "param1 does something")] [InlineData("DoWork is a method in TestClass.", "`DoWork` is a method in `TestClass`.")] [InlineData("DoWork is a method in \nTestClass\n.", "```DoWork``` is a method in ```\nTestClass\n```.")] - [InlineData("This is a paragraph.", "
This is a paragraph.")] + [InlineData("This is a paragraph.", "\r\nThis is a paragraph.")] [InlineData("GET /Todo?iscomplete=true&owner=mike", "GET /Todo?iscomplete=true&owner=mike")] [InlineData(@"Returns a item.", "Returns a null item.")] [InlineData(@"ISO currency code", "[ISO currency code](https://www.iso.org/iso-4217-currency-codes.html)")] + [InlineData("First line.
Second line.
Third line.
Fourth line.", "First line.\r\nSecond line.\r\nThird line.\r\nFourth line.")] public void Humanize_HumanizesInlineTags( string input, string expectedOutput) From 76ec39d2db343566086e29247970ca7f59017eb7 Mon Sep 17 00:00:00 2001 From: stb-co <93922970+stb-co@users.noreply.github.com> Date: Thu, 16 May 2024 14:47:23 +0200 Subject: [PATCH 12/20] Fix schema generation for C# 9 positional record with no example (#2901) * Add schema filter test for when example tag is not present * Add datetime to schema filter example tag positive test * Fix missing example property on record xmldoc param tag causing unexpected empty example string in generated schema --- .../XmlComments/XmlCommentsSchemaFilter.cs | 5 +- .../Fixtures/XmlAnnotatedRecord.cs | 4 +- .../XmlAnnotatedRecordWithoutExample.cs | 35 +++++++++ .../Fixtures/XmlAnnotatedType.cs | 6 ++ .../XmlAnnotatedTypeWithoutExample.cs | 71 +++++++++++++++++++ .../XmlCommentsSchemaFilterTests.cs | 42 +++++++++++ 6 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecordWithoutExample.cs create mode 100644 test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedTypeWithoutExample.cs diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs index 4075034e27..a246657d2c 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/XmlComments/XmlCommentsSchemaFilter.cs @@ -50,7 +50,10 @@ private void ApplyMemberTags(OpenApiSchema schema, SchemaFilterContext context) schema.Description = XmlCommentsTextHelper.Humanize(summaryNode); var example = recordDefaultConstructorProperty.GetAttribute("example", string.Empty); - TrySetExample(schema, context, example); + if (!string.IsNullOrEmpty(example)) + { + TrySetExample(schema, context, example); + } } if (fieldOrPropertyNode != null) diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecord.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecord.cs index 7b35dbc335..c337c379ed 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecord.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecord.cs @@ -4,13 +4,14 @@ namespace Swashbuckle.AspNetCore.SwaggerGen.Test { /// - /// Summary for XmlAnnotatedType + /// Summary for XmlAnnotatedRecord /// /// Summary for BoolProperty /// Summary for IntProperty /// Summary for LongProperty /// Summary for FloatProperty /// Summary for DoubleProperty + /// Summary for DateTimeProperty /// Summary for EnumProperty /// Summary for GuidProperty /// Summary for Nullable StringPropertyWithNullExample @@ -23,6 +24,7 @@ public record XmlAnnotatedRecord( long LongProperty, float FloatProperty, double DoubleProperty, + DateTime DateTimeProperty, IntEnum EnumProperty, Guid GuidProperty, string StringPropertyWithNullExample, diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecordWithoutExample.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecordWithoutExample.cs new file mode 100644 index 0000000000..2eea51841e --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedRecordWithoutExample.cs @@ -0,0 +1,35 @@ +using System; +using Swashbuckle.AspNetCore.TestSupport; + +namespace Swashbuckle.AspNetCore.SwaggerGen.Test +{ + /// + /// Summary for XmlAnnotatedRecordWithoutExample + /// + /// Summary for BoolProperty + /// Summary for IntProperty + /// Summary for LongProperty + /// Summary for FloatProperty + /// Summary for DoubleProperty + /// Summary for DateTimeProperty + /// Summary for EnumProperty + /// Summary for GuidProperty + /// Summary for Nullable StringPropertyWithNullExample + /// Summary for StringProperty + /// Summary for StringPropertyWithUri + /// Summary for ObjectProperty + public record XmlAnnotatedRecordWithoutExample( + bool BoolProperty, + int IntProperty, + long LongProperty, + float FloatProperty, + double DoubleProperty, + DateTime DateTimeProperty, + IntEnum EnumProperty, + Guid GuidProperty, + string StringPropertyWithNullExample, + string StringProperty, + string StringPropertyWithUri, + object ObjectProperty + ); +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedType.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedType.cs index a61b54bd72..4227d35ffd 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedType.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedType.cs @@ -45,6 +45,12 @@ public class XmlAnnotatedType /// 1.25 public double DoubleProperty { get; set; } + /// + /// Summary for DateTimeProperty + /// + /// 6/22/2022 12:00:00 AM + public DateTime DateTimeProperty { get; set; } + /// /// Summary for EnumProperty /// diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedTypeWithoutExample.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedTypeWithoutExample.cs new file mode 100644 index 0000000000..f06f01e840 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/XmlAnnotatedTypeWithoutExample.cs @@ -0,0 +1,71 @@ +using System; +using Swashbuckle.AspNetCore.TestSupport; + +namespace Swashbuckle.AspNetCore.SwaggerGen.Test +{ + /// + /// Summary for XmlAnnotatedTypeWithoutExample + /// + public class XmlAnnotatedTypeWithoutExample + { + /// + /// Summary for BoolProperty + /// + public bool BoolProperty { get; set; } + + /// + /// Summary for IntProperty + /// + public int IntProperty { get; set; } + + /// + /// Summary for LongProperty + /// + public long LongProperty { get; set; } + + /// + /// Summary for FloatProperty + /// + public float FloatProperty { get; set; } + + /// + /// Summary for DoubleProperty + /// + public double DoubleProperty { get; set; } + + /// + /// Summary for DateTimeProperty + /// + public DateTime DateTimeProperty { get; set; } + + /// + /// Summary for EnumProperty + /// + public IntEnum EnumProperty { get; set; } + + /// + /// Summary for GuidProperty + /// + public Guid GuidProperty { get; set; } + + /// + /// Summary for Nullable StringPropertyWithNullExample + /// + public string StringPropertyWithNullExample { get; set; } + + /// + /// Summary for StringProperty + /// + public string StringProperty { get; set; } + + /// + /// Summary for StringPropertyWithUri + /// + public string StringPropertyWithUri { get; set; } + + /// + /// Summary for ObjectProperty + /// + public object ObjectProperty { get; set; } + } +} \ No newline at end of file diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsSchemaFilterTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsSchemaFilterTests.cs index ecfba3e080..36c32db543 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsSchemaFilterTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/XmlComments/XmlCommentsSchemaFilterTests.cs @@ -62,6 +62,7 @@ public void Apply_SetsDescription_FromPropertySummaryTag( [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.LongProperty), "integer", "4294967295")] [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.FloatProperty), "number", "1.2")] [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.DoubleProperty), "number", "1.25")] + [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.DateTimeProperty), "string", "\"6/22/2022 12:00:00 AM\"")] [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.EnumProperty), "integer", "2")] [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.GuidProperty), "string", "\"d3966535-2637-48fa-b911-e3c27405ee09\"")] [InlineData(typeof(XmlAnnotatedType), nameof(XmlAnnotatedType.StringProperty), "string", "\"Example for StringProperty\"")] @@ -73,6 +74,7 @@ public void Apply_SetsDescription_FromPropertySummaryTag( [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.LongProperty), "integer", "4294967295")] [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.FloatProperty), "number", "1.2")] [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.DoubleProperty), "number", "1.25")] + [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.DateTimeProperty), "string", "\"6/22/2022 12:00:00 AM\"")] [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.EnumProperty), "integer", "2")] [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.GuidProperty), "string", "\"d3966535-2637-48fa-b911-e3c27405ee09\"")] [InlineData(typeof(XmlAnnotatedRecord), nameof(XmlAnnotatedRecord.StringProperty), "string", "\"Example for StringProperty\"")] @@ -96,6 +98,46 @@ public void Apply_SetsExample_FromPropertyExampleTag( Assert.Equal(expectedExampleAsJson, schema.Example.ToJson()); } + [Theory] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.BoolProperty), "boolean")] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.IntProperty), "integer")] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.LongProperty), "integer")] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.FloatProperty), "number")] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.DoubleProperty), "number")] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.DateTimeProperty), "string")] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.EnumProperty), "integer")] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.GuidProperty), "string")] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.StringProperty), "string")] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.ObjectProperty), "object")] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.StringPropertyWithNullExample), "string")] + [InlineData(typeof(XmlAnnotatedTypeWithoutExample), nameof(XmlAnnotatedTypeWithoutExample.StringPropertyWithUri), "string")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.BoolProperty), "boolean")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.IntProperty), "integer")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.LongProperty), "integer")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.FloatProperty), "number")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.DoubleProperty), "number")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.DateTimeProperty), "string")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.EnumProperty), "integer")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.GuidProperty), "string")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.StringProperty), "string")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.ObjectProperty), "object")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.StringPropertyWithNullExample), "string")] + [InlineData(typeof(XmlAnnotatedRecordWithoutExample), nameof(XmlAnnotatedRecordWithoutExample.StringPropertyWithUri), "string")] + [UseInvariantCulture] + public void Apply_DoesNotSetExample_WhenPropertyExampleTagIsNotProvided( + Type declaringType, + string propertyName, + string schemaType) + { + var propertyInfo = declaringType.GetProperty(propertyName); + var schema = new OpenApiSchema { Type = schemaType }; + var filterContext = new SchemaFilterContext(propertyInfo.PropertyType, null, null, memberInfo: propertyInfo); + + Subject().Apply(schema, filterContext); + + Assert.Null(schema.Example); + } + [Theory] [InlineData("en-US", 1.2F)] [InlineData("sv-SE", 1.2F)] From ecce973c9b71df4d82af798e4a4f82747c0d190c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 May 2024 13:10:34 +0000 Subject: [PATCH 13/20] Bump swagger-ui-dist in /src/Swashbuckle.AspNetCore.SwaggerUI (#2903) Bumps [swagger-ui-dist](https://github.com/swagger-api/swagger-ui) from 5.17.9 to 5.17.10. - [Release notes](https://github.com/swagger-api/swagger-ui/releases) - [Changelog](https://github.com/swagger-api/swagger-ui/blob/master/.releaserc) - [Commits](https://github.com/swagger-api/swagger-ui/compare/v5.17.9...v5.17.10) --- updated-dependencies: - dependency-name: swagger-ui-dist dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/Swashbuckle.AspNetCore.SwaggerUI/package-lock.json | 8 ++++---- src/Swashbuckle.AspNetCore.SwaggerUI/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/package-lock.json b/src/Swashbuckle.AspNetCore.SwaggerUI/package-lock.json index 7d38ffd6b9..37fe27abc0 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/package-lock.json +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/package-lock.json @@ -9,13 +9,13 @@ "version": "1.0.0", "hasInstallScript": true, "dependencies": { - "swagger-ui-dist": "5.17.9" + "swagger-ui-dist": "5.17.10" } }, "node_modules/swagger-ui-dist": { - "version": "5.17.9", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.9.tgz", - "integrity": "sha512-qhZdoXIleblFxufohnd4ir4KmVA7/uFfd/9sDTtH8A6Qm1lEK40MhrMrDqy9AygGjw1bnJpZH4yZ5wu12vW1aw==" + "version": "5.17.10", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.10.tgz", + "integrity": "sha512-fp8SYeEK216KS1/noDvursUOGojEbkvtckOpOmAGZUjlx/ma7VLD2PLQwyermjlzFrlHI5uCt1V+M1C3qBvRyQ==" } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/package.json b/src/Swashbuckle.AspNetCore.SwaggerUI/package.json index 695425ea63..3d75f0f09a 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/package.json +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { - "swagger-ui-dist": "5.17.9" + "swagger-ui-dist": "5.17.10" }, "scripts": { "postinstall": "node scripts/remove-source-map-links.js" From a0f4bb42667e4f9d24ef6e790ceed972ca0289a9 Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 15 May 2024 10:36:12 +0100 Subject: [PATCH 14/20] Only restore packages when needed Speed up local development iterations by only running `npm install` when the `node_modules` directory does not already exist. --- .../Swashbuckle.AspNetCore.ReDoc.csproj | 2 +- .../Swashbuckle.AspNetCore.SwaggerUI.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj b/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj index 90efe41080..ab861e7bf2 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj +++ b/src/Swashbuckle.AspNetCore.ReDoc/Swashbuckle.AspNetCore.ReDoc.csproj @@ -26,7 +26,7 @@ - + diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj b/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj index 3e3a285f7b..10d562b646 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/Swashbuckle.AspNetCore.SwaggerUI.csproj @@ -30,7 +30,7 @@ - + From 91c936b0e01c9b7c003563a679a00dd1413c7885 Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 15 May 2024 10:36:57 +0100 Subject: [PATCH 15/20] Fix IDE suggestions Apply code refactoring suggestions from Visual Studio. --- src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs | 2 +- src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs | 4 ++-- src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs index 0ef11d7559..d6e745daff 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs @@ -30,7 +30,7 @@ public SwaggerMiddleware( { _next = next; _options = options ?? new SwaggerOptions(); - _requestMatcher = new TemplateMatcher(TemplateParser.Parse(_options.RouteTemplate), new RouteValueDictionary()); + _requestMatcher = new TemplateMatcher(TemplateParser.Parse(_options.RouteTemplate), []); } #if !NETSTANDARD diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs index 52dac4b73e..0b3ae62bbe 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs @@ -89,7 +89,7 @@ public async Task Invoke(HttpContext httpContext) await _staticFileMiddleware.Invoke(httpContext); } - private StaticFileMiddleware CreateStaticFileMiddleware( + private static StaticFileMiddleware CreateStaticFileMiddleware( RequestDelegate next, IWebHostEnvironment hostingEnv, ILoggerFactory loggerFactory, @@ -104,7 +104,7 @@ private StaticFileMiddleware CreateStaticFileMiddleware( return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory); } - private void RespondWithRedirect(HttpResponse response, string location) + private static void RespondWithRedirect(HttpResponse response, string location) { response.StatusCode = 301; response.Headers["Location"] = location; diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs index cd9aa9df42..f4140ec572 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs @@ -162,7 +162,7 @@ public class ConfigObject public string ValidatorUrl { get; set; } = null; [JsonExtensionData] - public Dictionary AdditionalItems { get; set; } = new Dictionary(); + public Dictionary AdditionalItems { get; set; } = []; } public class UrlDescriptor @@ -233,7 +233,7 @@ public class OAuthConfigObject /// /// String array of initially selected oauth scopes, default is empty array /// - public IEnumerable Scopes { get; set; } = new string[] { }; + public IEnumerable Scopes { get; set; } = []; /// /// Additional query parameters added to authorizationUrl and tokenUrl From ac588fe23146a0aabc58eb8963da5290d08a3538 Mon Sep 17 00:00:00 2001 From: martincostello Date: Wed, 15 May 2024 10:41:52 +0100 Subject: [PATCH 16/20] Fix serialization of AdditionalItems Extend custom `JsonSerializerContext` to cover objects that might be added to the additional JSON property. Resolves #2884, --- .../SwaggerUIJsonSerializerContext.cs | 33 +++++++++++++++++++ test/WebSites/CustomUIConfig/Startup.cs | 32 +++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs index ed15c9df84..72e175fb6e 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIJsonSerializerContext.cs @@ -1,4 +1,7 @@ #if NET6_0_OR_GREATER +using System; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace Swashbuckle.AspNetCore.SwaggerUI; @@ -6,6 +9,36 @@ namespace Swashbuckle.AspNetCore.SwaggerUI; [JsonSerializable(typeof(ConfigObject))] [JsonSerializable(typeof(InterceptorFunctions))] [JsonSerializable(typeof(OAuthConfigObject))] +// These primitive types are declared for common types that may be used with ConfigObject.AdditionalItems. See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2884. +[JsonSerializable(typeof(bool))] +[JsonSerializable(typeof(byte))] +[JsonSerializable(typeof(sbyte))] +[JsonSerializable(typeof(short))] +[JsonSerializable(typeof(ushort))] +[JsonSerializable(typeof(int))] +[JsonSerializable(typeof(uint))] +[JsonSerializable(typeof(long))] +[JsonSerializable(typeof(ulong))] +[JsonSerializable(typeof(float))] +[JsonSerializable(typeof(double))] +[JsonSerializable(typeof(decimal))] +[JsonSerializable(typeof(char))] +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(DateTime))] +[JsonSerializable(typeof(DateTimeOffset))] +[JsonSerializable(typeof(TimeSpan))] +[JsonSerializable(typeof(JsonArray))] +[JsonSerializable(typeof(JsonObject))] +[JsonSerializable(typeof(JsonDocument))] +#if NET7_0_OR_GREATER +[JsonSerializable(typeof(DateOnly))] +[JsonSerializable(typeof(TimeOnly))] +#endif +#if NET8_0_OR_GREATER +[JsonSerializable(typeof(Half))] +[JsonSerializable(typeof(Int128))] +[JsonSerializable(typeof(UInt128))] +#endif [JsonSourceGenerationOptions( DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] diff --git a/test/WebSites/CustomUIConfig/Startup.cs b/test/WebSites/CustomUIConfig/Startup.cs index 9a86e2dced..abfefe10dc 100644 --- a/test/WebSites/CustomUIConfig/Startup.cs +++ b/test/WebSites/CustomUIConfig/Startup.cs @@ -1,9 +1,11 @@ +using System; +using System.Text.Json; +using System.Text.Json.Nodes; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerUI; namespace CustomUIConfig @@ -76,6 +78,34 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) c.UseResponseInterceptor("(res) => { console.log('Custom interceptor intercepted response from:', res.url); return res; }"); c.EnablePersistAuthorization(); + c.ConfigObject.AdditionalItems.Add("syntaxHighlight", false); + c.ConfigObject.AdditionalItems.Add("charProperty", 'c'); + c.ConfigObject.AdditionalItems.Add("stringProperty", "value"); + c.ConfigObject.AdditionalItems.Add("byteProperty", (byte)1); + c.ConfigObject.AdditionalItems.Add("signedByteProperty", (sbyte)1); + c.ConfigObject.AdditionalItems.Add("int16Property", (short)1); + c.ConfigObject.AdditionalItems.Add("uint16Property", (ushort)1); + c.ConfigObject.AdditionalItems.Add("int32Property", 1); + c.ConfigObject.AdditionalItems.Add("uint32Property", 1u); + c.ConfigObject.AdditionalItems.Add("int64Property", 1L); + c.ConfigObject.AdditionalItems.Add("uint64Property", 1uL); + c.ConfigObject.AdditionalItems.Add("floatProperty", 1f); + c.ConfigObject.AdditionalItems.Add("doubleProperty", 1d); + c.ConfigObject.AdditionalItems.Add("decimalProperty", 1m); + c.ConfigObject.AdditionalItems.Add("dateTimeProperty", DateTime.UtcNow); + c.ConfigObject.AdditionalItems.Add("dateTimeOffsetProperty", DateTimeOffset.UtcNow); + c.ConfigObject.AdditionalItems.Add("timeSpanProperty", new TimeSpan(12, 34, 56)); + c.ConfigObject.AdditionalItems.Add("jsonArray", new JsonArray() { "string" }); + c.ConfigObject.AdditionalItems.Add("jsonObject", new JsonObject() { ["foo"] = "bar" }); + c.ConfigObject.AdditionalItems.Add("jsonDocument", JsonDocument.Parse("""{ "foo": "bar" }""")); + +#if NET8_0_OR_GREATER + c.ConfigObject.AdditionalItems.Add("dateOnlyProperty", new DateOnly(1977, 05, 25)); + c.ConfigObject.AdditionalItems.Add("timeOnlyProperty", new TimeOnly(12, 34, 56)); + c.ConfigObject.AdditionalItems.Add("halfProperty", Half.CreateChecked(1)); + c.ConfigObject.AdditionalItems.Add("int128Property", Int128.CreateChecked(1)); + c.ConfigObject.AdditionalItems.Add("unt128Property", UInt128.CreateChecked(1)); +#endif }); } } From 26c78cbc751f5e0cdbe787a2dc328a7b67bb6bb4 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Sat, 18 May 2024 01:50:39 +0100 Subject: [PATCH 17/20] Disable parallel build (#2894) Disable parallel build to avoid file-write conflicts. Relates to #2893. --- Directory.Build.props | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Directory.Build.props b/Directory.Build.props index ded0e19ea5..4ad8addeb7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,6 +2,8 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb domaindrivendev + + false SHA256 https://github.com/domaindrivendev/Swashbuckle.AspNetCore true From 432c417ee5f7d1598de837495e4de97853e8bb4d Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Sat, 18 May 2024 07:49:19 +0100 Subject: [PATCH 18/20] Do not run IHostedService implementations (#2880) - Remove `IHostedService` implementations so the CLI doesn't run hosted services (and then hang/fail). - Refactor CLI tests to reduce duplication. - Use newer C# syntax as suggested by Visual Studio. Co-authored-by: Sjoerd van der Meer <2460430+desjoerd@users.noreply.github.com> --- Swashbuckle.AspNetCore.sln | 7 + .../CommandRunner.cs | 8 +- .../HostFactoryResolver.cs | 146 +++++++++--------- .../HostingApplication.cs | 27 +++- .../CommandRunnerTests.cs | 38 ++--- .../Swashbuckle.AspNetCore.Cli.Test.csproj | 1 + .../ToolTests.cs | 133 +++++++++++----- .../MinimalAppWithHostedServices.csproj | 18 +++ .../MinimalAppWithHostedServices/Program.cs | 34 ++++ .../Properties/launchSettings.json | 12 ++ 10 files changed, 279 insertions(+), 145 deletions(-) create mode 100644 test/WebSites/MinimalAppWithHostedServices/MinimalAppWithHostedServices.csproj create mode 100644 test/WebSites/MinimalAppWithHostedServices/Program.cs create mode 100644 test/WebSites/MinimalAppWithHostedServices/Properties/launchSettings.json diff --git a/Swashbuckle.AspNetCore.sln b/Swashbuckle.AspNetCore.sln index 59cedef3ae..7fcf8c3e97 100644 --- a/Swashbuckle.AspNetCore.sln +++ b/Swashbuckle.AspNetCore.sln @@ -113,6 +113,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "test\WebSites\Web EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.Aot", "test\WebSites\WebApi.Aot\WebApi.Aot.csproj", "{07BB09CF-6C6F-4D00-A459-93586345C921}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalAppWithHostedServices", "test\WebSites\MinimalAppWithHostedServices\MinimalAppWithHostedServices.csproj", "{D06A88E8-6F42-4F40-943A-E266C0AE6EC9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -259,6 +261,10 @@ Global {07BB09CF-6C6F-4D00-A459-93586345C921}.Debug|Any CPU.Build.0 = Debug|Any CPU {07BB09CF-6C6F-4D00-A459-93586345C921}.Release|Any CPU.ActiveCfg = Release|Any CPU {07BB09CF-6C6F-4D00-A459-93586345C921}.Release|Any CPU.Build.0 = Release|Any CPU + {D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -302,6 +308,7 @@ Global {B6037A37-4A4F-438D-B18A-0C9D1408EAB2} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} {DE1D77F8-3916-4DEE-A57D-6DDC357F64C6} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} {07BB09CF-6C6F-4D00-A459-93586345C921} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} + {D06A88E8-6F42-4F40-943A-E266C0AE6EC9} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {36FC6A67-247D-4149-8EDD-79FFD1A75F51} diff --git a/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs b/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs index 3e9384f9a5..3bccca13b4 100644 --- a/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs +++ b/src/Swashbuckle.AspNetCore.Cli/CommandRunner.cs @@ -17,10 +17,10 @@ public CommandRunner(string commandName, string commandDescription, TextWriter o { CommandName = commandName; CommandDescription = commandDescription; - _argumentDescriptors = new Dictionary(); - _optionDescriptors = new Dictionary(); - _runFunc = (namedArgs) => { return 1; }; // noop - _subRunners = new List(); + _argumentDescriptors = []; + _optionDescriptors = []; + _runFunc = (_) => 1; // no-op + _subRunners = []; _output = output; } diff --git a/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs b/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs index acc5ff59ad..9a4ec584fb 100644 --- a/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs +++ b/src/Swashbuckle.AspNetCore.Cli/HostFactoryResolver.cs @@ -89,7 +89,7 @@ private static Func ResolveFactory(Assembly assembly, string nam return null; } - return args => (T)factory.Invoke(null, new object[] { args }); + return args => (T)factory.Invoke(null, [args]); } // TReturn Factory(string[] args); @@ -153,7 +153,7 @@ public static Func ResolveServiceProviderFactory(Ass private static object Build(object builder) { var buildMethod = builder.GetType().GetMethod("Build"); - return buildMethod?.Invoke(builder, Array.Empty()); + return buildMethod?.Invoke(builder, []); } private static IServiceProvider GetServiceProvider(object host) @@ -174,13 +174,19 @@ private sealed class HostingListener : IObserver, IObserver< private readonly TimeSpan _waitTimeout; private readonly bool _stopApplication; - private readonly TaskCompletionSource _hostTcs = new TaskCompletionSource(); + private readonly TaskCompletionSource _hostTcs = new(); private IDisposable _disposable; - private Action _configure; - private Action _entrypointCompleted; - private static readonly AsyncLocal _currentListener = new AsyncLocal(); - - public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeout, bool stopApplication, Action configure, Action entrypointCompleted) + private readonly Action _configure; + private readonly Action _entrypointCompleted; + private static readonly AsyncLocal _currentListener = new(); + + public HostingListener( + string[] args, + MethodInfo entryPoint, + TimeSpan waitTimeout, + bool stopApplication, + Action configure, + Action entrypointCompleted) { _args = args; _entryPoint = entryPoint; @@ -192,84 +198,82 @@ public HostingListener(string[] args, MethodInfo entryPoint, TimeSpan waitTimeou public object CreateHost() { - using (var subscription = DiagnosticListener.AllListeners.Subscribe(this)) + using var subscription = DiagnosticListener.AllListeners.Subscribe(this); + + // Kick off the entry point on a new thread so we don't block the current one + // in case we need to timeout the execution + var thread = new Thread(() => { + Exception exception = null; - // Kick off the entry point on a new thread so we don't block the current one - // in case we need to timeout the execution - var thread = new Thread(() => + try { - Exception exception = null; + // Set the async local to the instance of the HostingListener so we can filter events that + // aren't scoped to this execution of the entry point. + _currentListener.Value = this; - try - { - // Set the async local to the instance of the HostingListener so we can filter events that - // aren't scoped to this execution of the entry point. - _currentListener.Value = this; - - var parameters = _entryPoint.GetParameters(); - if (parameters.Length == 0) - { - _entryPoint.Invoke(null, Array.Empty()); - } - else - { - _entryPoint.Invoke(null, new object[] { _args }); - } - - // Try to set an exception if the entry point returns gracefully, this will force - // build to throw - _hostTcs.TrySetException(new InvalidOperationException("Unable to build IHost")); - } - catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException) + var parameters = _entryPoint.GetParameters(); + if (parameters.Length == 0) { - // The host was stopped by our own logic + _entryPoint.Invoke(null, []); } - catch (TargetInvocationException tie) + else { - exception = tie.InnerException ?? tie; - - // Another exception happened, propagate that to the caller - _hostTcs.TrySetException(exception); + _entryPoint.Invoke(null, [_args]); } - catch (Exception ex) - { - exception = ex; - // Another exception happened, propagate that to the caller - _hostTcs.TrySetException(ex); - } - finally - { - // Signal that the entry point is completed - _entrypointCompleted?.Invoke(exception); - } - }) + // Try to set an exception if the entry point returns gracefully, this will force + // build to throw + _hostTcs.TrySetException(new InvalidOperationException("Unable to build IHost")); + } + catch (TargetInvocationException tie) when (tie.InnerException is StopTheHostException) { - // Make sure this doesn't hang the process - IsBackground = true - }; - - // Start the thread - thread.Start(); + // The host was stopped by our own logic + } + catch (TargetInvocationException tie) + { + exception = tie.InnerException ?? tie; - try + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(exception); + } + catch (Exception ex) { - // Wait before throwing an exception - if (!_hostTcs.Task.Wait(_waitTimeout)) - { - throw new InvalidOperationException("Unable to build IHost"); - } + exception = ex; + + // Another exception happened, propagate that to the caller + _hostTcs.TrySetException(ex); } - catch (AggregateException) when (_hostTcs.Task.IsCompleted) + finally { - // Lets this propagate out of the call to GetAwaiter().GetResult() + // Signal that the entry point is completed + _entrypointCompleted?.Invoke(exception); } + }) + { + // Make sure this doesn't hang the process + IsBackground = true + }; - Debug.Assert(_hostTcs.Task.IsCompleted); + // Start the thread + thread.Start(); - return _hostTcs.Task.GetAwaiter().GetResult(); + try + { + // Wait before throwing an exception + if (!_hostTcs.Task.Wait(_waitTimeout)) + { + throw new InvalidOperationException("Unable to build IHost"); + } } + catch (AggregateException) when (_hostTcs.Task.IsCompleted) + { + // Lets this propagate out of the call to GetAwaiter().GetResult() + } + + Debug.Assert(_hostTcs.Task.IsCompleted); + + return _hostTcs.Task.GetAwaiter().GetResult(); } public void OnCompleted() @@ -279,7 +283,6 @@ public void OnCompleted() public void OnError(Exception error) { - } public void OnNext(DiagnosticListener value) @@ -321,10 +324,7 @@ public void OnNext(KeyValuePair value) } } - private sealed class StopTheHostException : Exception - { - - } + private sealed class StopTheHostException : Exception; } } } diff --git a/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs b/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs index f17c6c1419..3c7685c088 100644 --- a/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs +++ b/src/Swashbuckle.AspNetCore.Cli/HostingApplication.cs @@ -4,7 +4,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http.Features; +#if NETCOREAPP3_0_OR_GREATER using Microsoft.Extensions.DependencyInjection; +#endif using Microsoft.Extensions.Hosting; namespace Swashbuckle.AspNetCore.Cli @@ -31,6 +33,19 @@ void ConfigureHostBuilder(object hostBuilder) { services.AddSingleton(); services.AddSingleton(); + + for (var i = services.Count - 1; i >= 0; i--) + { + // exclude all implementations of IHostedService + // except Microsoft.AspNetCore.Hosting.GenericWebHostService because that one will build/configure + // the WebApplication/Middleware pipeline in the case of the GenericWebHostBuilder. + var registration = services[i]; + if (registration.ServiceType == typeof(IHostedService) + && registration.ImplementationType is not { FullName: "Microsoft.AspNetCore.Hosting.GenericWebHostService" }) + { + services.RemoveAt(i); + } + } }); } @@ -69,18 +84,17 @@ void OnEntryPointExit(Exception exception) // We set the application name in the hosting environment to the startup assembly // to avoid falling back to the entry assembly (dotnet-swagger) when configuring our // application. - var services = ((IHost)factory(new[] { $"--{HostDefaults.ApplicationKey}={assemblyName}" })).Services; + var services = ((IHost)factory([$"--{HostDefaults.ApplicationKey}={assemblyName}"])).Services; // Wait for the application to start so that we know it's fully configured. This is important because // we need the middleware pipeline to be configured before we access the ISwaggerProvider in // in the IServiceProvider var applicationLifetime = services.GetRequiredService(); - using (var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(null))) - { - waitForStartTcs.Task.Wait(); - return services; - } + using var registration = applicationLifetime.ApplicationStarted.Register(() => waitForStartTcs.TrySetResult(null)); + waitForStartTcs.Task.Wait(); + + return services; } catch (InvalidOperationException) { @@ -103,7 +117,6 @@ private class NoopServer : IServer public void Dispose() { } public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; - } } } diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs index 65d0a8c311..dd01930e22 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/CommandRunnerTests.cs @@ -4,10 +4,10 @@ namespace Swashbuckle.AspNetCore.Cli.Test { - public class CommandRunnerTests + public static class CommandRunnerTests { [Fact] - public void Run_ParsesArgumentsAndExecutesCommands_AccordingToConfiguredMetadata() + public static void Run_ParsesArgumentsAndExecutesCommands_AccordingToConfiguredMetadata() { var receivedValues = new List(); var subject = new CommandRunner("test", "a test", new StringWriter()); @@ -36,23 +36,23 @@ public void Run_ParsesArgumentsAndExecutesCommands_AccordingToConfiguredMetadata }); }); - var cmd1ExitCode = subject.Run(new[] { "cmd1", "--opt1", "foo", "--opt2", "bar" }); - var cmd2ExitCode = subject.Run(new[] { "cmd2", "--opt1", "blah", "--opt2", "dblah" }); + var cmd1ExitCode = subject.Run(["cmd1", "--opt1", "foo", "--opt2", "bar"]); + var cmd2ExitCode = subject.Run(["cmd2", "--opt1", "blah", "--opt2", "dblah"]); Assert.Equal(2, cmd1ExitCode); Assert.Equal(3, cmd2ExitCode); - Assert.Equal(new[] { "foo", null, "bar", "blah", null, "dblah" }, receivedValues.ToArray()); + Assert.Equal(["foo", null, "bar", "blah", null, "dblah"], [.. receivedValues]); } [Fact] - public void Run_PrintsAvailableCommands_WhenUnexpectedCommandIsProvided() + public static void Run_PrintsAvailableCommands_WhenUnexpectedCommandIsProvided() { var output = new StringWriter(); var subject = new CommandRunner("test", "a test", output); subject.SubCommand("cmd", "does something", c => { }); - var exitCode = subject.Run(new[] { "foo" }); + var exitCode = subject.Run(["foo"]); Assert.StartsWith("a test", output.ToString()); Assert.Contains("Commands:", output.ToString()); @@ -60,14 +60,14 @@ public void Run_PrintsAvailableCommands_WhenUnexpectedCommandIsProvided() } [Fact] - public void Run_PrintsAvailableCommands_WhenHelpOptionIsProvided() + public static void Run_PrintsAvailableCommands_WhenHelpOptionIsProvided() { var output = new StringWriter(); var subject = new CommandRunner("test", "a test", output); subject.SubCommand("cmd", "does something", c => { }); - var exitCode = subject.Run(new[] { "--help" }); + var exitCode = subject.Run(["--help"]); Assert.StartsWith("a test", output.ToString()); Assert.Contains("Commands:", output.ToString()); @@ -75,15 +75,15 @@ public void Run_PrintsAvailableCommands_WhenHelpOptionIsProvided() } [Theory] - [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt2", "foo" }, true)] - [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1" }, true)] - [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1", "--opt2" }, true)] - [InlineData(new[] { "--opt1" }, new string[] { }, new[] { "cmd", "--opt1", "foo" }, false)] - [InlineData(new string[] { }, new[] { "arg1" }, new[] { "cmd" }, true)] - [InlineData(new string[] { }, new[] { "arg1" }, new[] { "cmd", "--opt1" }, true)] - [InlineData(new string[] {}, new[] { "arg1" }, new[] { "cmd", "foo", "bar" }, true)] - [InlineData(new string[] {}, new[] { "arg1" }, new[] { "cmd", "foo" }, false)] - public void Run_PrintsCommandUsage_WhenUnexpectedArgumentsAreProvided( + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt2", "foo" }, true)] + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1" }, true)] + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1", "--opt2" }, true)] + [InlineData(new[] { "--opt1" }, new string[0], new[] { "cmd", "--opt1", "foo" }, false)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd" }, true)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "--opt1" }, true)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "foo", "bar" }, true)] + [InlineData(new string[0], new[] { "arg1" }, new[] { "cmd", "foo" }, false)] + public static void Run_PrintsCommandUsage_WhenUnexpectedArgumentsAreProvided( string[] optionNames, string[] argNames, string[] providedArgs, @@ -107,4 +107,4 @@ public void Run_PrintsCommandUsage_WhenUnexpectedArgumentsAreProvided( Assert.Empty(output.ToString()); } } -} \ No newline at end of file +} diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj b/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj index 2ccd4ea7fb..66198fe569 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj +++ b/test/Swashbuckle.AspNetCore.Cli.Test/Swashbuckle.AspNetCore.Cli.Test.csproj @@ -8,6 +8,7 @@ + diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs index cc6598009a..e056a9a0f2 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs @@ -1,26 +1,32 @@ -using System.IO; +using System; +using System.IO; using System.Text.Json; using Swashbuckle.AspNetCore.TestSupport.Utilities; using Xunit; namespace Swashbuckle.AspNetCore.Cli.Test { - public class ToolTests + public static class ToolTests { [Fact] - public void Throws_When_Startup_Assembly_Does_Not_Exist() + public static void Throws_When_Startup_Assembly_Does_Not_Exist() { - var args = new string[] { "tofile", "--output", "swagger.json", "--serializeasv2", "./does_not_exist.dll", "v1" }; + string[] args = ["tofile", "--output", "swagger.json", "--serializeasv2", "./does_not_exist.dll", "v1"]; Assert.Throws(() => Program.Main(args)); } [Fact] - public void Can_Generate_Swagger_Json() + public static void Can_Generate_Swagger_Json() { - using var temporaryDirectory = new TemporaryDirectory(); - var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", "--serializeasv2", Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), "v1" }; - Assert.Equal(0, Program.Main(args)); - using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json"))); + using var document = RunApplication((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--serializeasv2", + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]); // verify one of the endpoints var paths = document.RootElement.GetProperty("paths"); @@ -29,20 +35,22 @@ public void Can_Generate_Swagger_Json() } [Fact] - public void Overwrites_Existing_File() + public static void Overwrites_Existing_File() { - using var temporaryDirectory = new TemporaryDirectory(); - var path = Path.Combine(temporaryDirectory.Path, "swagger.json"); - - var dummyContent = new string('x', 100_000); - File.WriteAllText(path, dummyContent); - - var args = new string[] { "tofile", "--output", path, Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), "v1" }; - Assert.Equal(0, Program.Main(args)); - - var readContent = File.ReadAllText(path); - Assert.True(readContent.Length < dummyContent.Length); - using var document = JsonDocument.Parse(readContent); + using var document = RunApplication((outputPath) => + { + File.WriteAllText(outputPath, new string('x', 100_000)); + + return + [ + "tofile", + "--output", + outputPath, + "--serializeasv2", + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]; + }); // verify one of the endpoints var paths = document.RootElement.GetProperty("paths"); @@ -51,13 +59,17 @@ public void Overwrites_Existing_File() } [Fact] - public void CustomDocumentSerializer_Writes_Custom_V2_Document() + public static void CustomDocumentSerializer_Writes_Custom_V2_Document() { - using var temporaryDirectory = new TemporaryDirectory(); - var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", "--serializeasv2", Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), "v1" }; - Assert.Equal(0, Program.Main(args)); - - using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json"))); + using var document = RunApplication((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--serializeasv2", + Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), + "v1" + ]); // verify that the custom serializer wrote the swagger info var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); @@ -65,34 +77,71 @@ public void CustomDocumentSerializer_Writes_Custom_V2_Document() } [Fact] - public void CustomDocumentSerializer_Writes_Custom_V3_Document() + public static void CustomDocumentSerializer_Writes_Custom_V3_Document() { - using var temporaryDirectory = new TemporaryDirectory(); - var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), "v1" }; - Assert.Equal(0, Program.Main(args)); - - using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json"))); + using var document = RunApplication((outputPath) => + [ + "tofile", + "--output", + outputPath, + Path.Combine(Directory.GetCurrentDirectory(), + "CustomDocumentSerializer.dll"), + "v1" + ]); // verify that the custom serializer wrote the swagger info var swaggerInfo = document.RootElement.GetProperty("swagger").GetString(); Assert.Equal("DocumentSerializerTest3.0", swaggerInfo); } -#if NET6_0_OR_GREATER [Fact] - public void Can_Generate_Swagger_Json_ForTopLevelApp() + public static void Can_Generate_Swagger_Json_ForTopLevelApp() { - using var temporaryDirectory = new TemporaryDirectory(); - var args = new string[] { "tofile", "--output", $"{temporaryDirectory.Path}/swagger.json", "--serializeasv2", Path.Combine(Directory.GetCurrentDirectory(), "MinimalApp.dll"), "v1" }; - Assert.Equal(0, Program.Main(args)); - - using var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(temporaryDirectory.Path, "swagger.json"))); + using var document = RunApplication((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--serializeasv2", + Path.Combine(Directory.GetCurrentDirectory(), "MinimalApp.dll"), + "v1" + ]); // verify one of the endpoints var paths = document.RootElement.GetProperty("paths"); var path = paths.GetProperty("/WeatherForecast"); Assert.True(path.TryGetProperty("get", out _)); } -#endif + + [Fact] + public static void Does_Not_Run_Crashing_HostedService() + { + using var document = RunApplication((outputPath) => + [ + "tofile", + "--output", + outputPath, + Path.Combine(Directory.GetCurrentDirectory(), "MinimalAppWithHostedServices.dll"), + "v1" + ]); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var path = paths.GetProperty("/ShouldContain"); + Assert.True(path.TryGetProperty("get", out _)); + } + + private static JsonDocument RunApplication(Func setup) + { + using var temporaryDirectory = new TemporaryDirectory(); + string outputPath = Path.Combine(temporaryDirectory.Path, "swagger.json"); + + string[] args = setup(outputPath); + + Assert.Equal(0, Program.Main(args)); + + string json = File.ReadAllText(outputPath); + return JsonDocument.Parse(json); + } } } diff --git a/test/WebSites/MinimalAppWithHostedServices/MinimalAppWithHostedServices.csproj b/test/WebSites/MinimalAppWithHostedServices/MinimalAppWithHostedServices.csproj new file mode 100644 index 0000000000..88afa5f5a3 --- /dev/null +++ b/test/WebSites/MinimalAppWithHostedServices/MinimalAppWithHostedServices.csproj @@ -0,0 +1,18 @@ + + + + net6.0;net8.0 + enable + enable + $([System.IO.Path]::Combine('$(ArtifactsPath)', 'bin', 'Swashbuckle.AspNetCore.Cli', '$(Configuration)_$(TargetFramework)', 'dotnet-swagger.dll')) + + + + + + + + + + + diff --git a/test/WebSites/MinimalAppWithHostedServices/Program.cs b/test/WebSites/MinimalAppWithHostedServices/Program.cs new file mode 100644 index 0000000000..60eade8971 --- /dev/null +++ b/test/WebSites/MinimalAppWithHostedServices/Program.cs @@ -0,0 +1,34 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new() { Title = "MinimalApp", Version = "v1" }); +}); + +builder.Services.AddHostedService(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "MinimalApp v1")); +} + +app.MapGet("/ShouldContain", () => "Hello World!"); + +app.Run(); + +class HostedService : IHostedService +{ + public Task StartAsync(CancellationToken cancellationToken) + { + throw new Exception("Crash!"); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} diff --git a/test/WebSites/MinimalAppWithHostedServices/Properties/launchSettings.json b/test/WebSites/MinimalAppWithHostedServices/Properties/launchSettings.json new file mode 100644 index 0000000000..633d47ed49 --- /dev/null +++ b/test/WebSites/MinimalAppWithHostedServices/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "MinimalAppWithHostedServices": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:57095;http://localhost:57096" + } + } +} From d5cd28e3e89971a53723fa590197752eb754b8ec Mon Sep 17 00:00:00 2001 From: martincostello Date: Thu, 16 May 2024 21:02:02 +0100 Subject: [PATCH 19/20] Fix interceptor JSON casing Fix property casing of Swagger UI interceptor function properties. Resolves #2906. --- .../SwaggerUIOptions.cs | 8 +++++--- .../SwaggerUIIntegrationTests.cs | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs index f4140ec572..a53566a08b 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs @@ -1,10 +1,10 @@ using System; -using System.IO; -using System.Reflection; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Text.Json.Serialization; +using System.Reflection; using System.Text.Json; +using System.Text.Json.Serialization; namespace Swashbuckle.AspNetCore.SwaggerUI { @@ -262,6 +262,7 @@ public class InterceptorFunctions /// Accepts one argument requestInterceptor(request) and must return the modified request, or a Promise that resolves to the modified request. /// Ex: "function (req) { req.headers['MyCustomHeader'] = 'CustomValue'; return req; }" /// + [JsonPropertyName("RequestInterceptorFunction")] public string RequestInterceptorFunction { get; set; } /// @@ -270,6 +271,7 @@ public class InterceptorFunctions /// Accepts one argument responseInterceptor(response) and must return the modified response, or a Promise that resolves to the modified response. /// Ex: "function (res) { console.log(res); return res; }" /// + [JsonPropertyName("ResponseInterceptorFunction")] public string ResponseInterceptorFunction { get; set; } } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs index bd6beab563..75ef927a41 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerUIIntegrationTests.cs @@ -71,6 +71,18 @@ public async Task IndexUrl_ReturnsCustomIndexHtml_IfConfigured() Assert.Contains("Example.com", content); } + [Fact] + public async Task IndexUrl_ReturnsInterceptors_IfConfigured() + { + var client = new TestSite(typeof(CustomUIConfig.Startup)).BuildClient(); + + var response = await client.GetAsync("/swagger/index.html"); + var content = await response.Content.ReadAsStringAsync(); + + Assert.Contains("\"RequestInterceptorFunction\":", content); + Assert.Contains("\"ResponseInterceptorFunction\":", content); + } + [Theory] [InlineData("/swagger/index.html", new [] { "Version 1.0", "Version 2.0" })] [InlineData("/swagger/1.0/index.html", new [] { "Version 1.0" })] From fe87e6d4ce56cda59474763067b929d4d92fd146 Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Tue, 21 May 2024 15:57:23 +0100 Subject: [PATCH 20/20] Fix exceptions generating default values (#2895) - Fix SwaggerGeneratorException if the type of a `[DefaultValue]` does not match the type of the property when using System.Text.Json for serialization to resolve #2885. - Fix schema generation of default values for nullable enums with System.Text.Json to resolve #2904. - Resolve some IDE refactoring suggestions. - Render the response if an integration test fails. --- Swashbuckle.AspNetCore.sln | 7 +++ .../NewtonsoftDataContractResolver.cs | 2 +- .../JsonSerializerDataContractResolver.cs | 21 ++++++--- .../SchemaGenerator/SchemaGenerator.cs | 47 +++++++++++++------ .../SwaggerIntegrationTests.cs | 18 +++++-- ...hbuckle.AspNetCore.IntegrationTests.csproj | 7 +-- .../NewtonsoftSchemaGeneratorTests.cs | 1 + .../JsonSerializerSchemaGeneratorTests.cs | 1 + .../Fixtures/TypeWithDefaultAttributes.cs | 3 ++ .../MvcWithNullable/MvcWithNullable.csproj | 20 ++++++++ test/WebSites/MvcWithNullable/Program.cs | 40 ++++++++++++++++ .../Properties/launchSettings.json | 41 ++++++++++++++++ .../appsettings.Development.json | 8 ++++ .../WebSites/MvcWithNullable/appsettings.json | 9 ++++ test/WebSites/WebApi/Program.cs | 7 ++- 15 files changed, 198 insertions(+), 34 deletions(-) create mode 100644 test/WebSites/MvcWithNullable/MvcWithNullable.csproj create mode 100644 test/WebSites/MvcWithNullable/Program.cs create mode 100644 test/WebSites/MvcWithNullable/Properties/launchSettings.json create mode 100644 test/WebSites/MvcWithNullable/appsettings.Development.json create mode 100644 test/WebSites/MvcWithNullable/appsettings.json diff --git a/Swashbuckle.AspNetCore.sln b/Swashbuckle.AspNetCore.sln index 7fcf8c3e97..98b1fc2db9 100644 --- a/Swashbuckle.AspNetCore.sln +++ b/Swashbuckle.AspNetCore.sln @@ -115,6 +115,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.Aot", "test\WebSites EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinimalAppWithHostedServices", "test\WebSites\MinimalAppWithHostedServices\MinimalAppWithHostedServices.csproj", "{D06A88E8-6F42-4F40-943A-E266C0AE6EC9}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvcWithNullable", "test\WebSites\MvcWithNullable\MvcWithNullable.csproj", "{F88B6070-BE3C-45F9-978C-2ECBA9518C24}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -265,6 +267,10 @@ Global {D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Debug|Any CPU.Build.0 = Debug|Any CPU {D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Release|Any CPU.ActiveCfg = Release|Any CPU {D06A88E8-6F42-4F40-943A-E266C0AE6EC9}.Release|Any CPU.Build.0 = Release|Any CPU + {F88B6070-BE3C-45F9-978C-2ECBA9518C24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F88B6070-BE3C-45F9-978C-2ECBA9518C24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F88B6070-BE3C-45F9-978C-2ECBA9518C24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F88B6070-BE3C-45F9-978C-2ECBA9518C24}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -309,6 +315,7 @@ Global {DE1D77F8-3916-4DEE-A57D-6DDC357F64C6} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} {07BB09CF-6C6F-4D00-A459-93586345C921} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} {D06A88E8-6F42-4F40-943A-E266C0AE6EC9} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} + {F88B6070-BE3C-45F9-978C-2ECBA9518C24} = {DB3F57FC-1472-4F03-B551-43394DA3C5EB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {36FC6A67-247D-4149-8EDD-79FFD1A75F51} diff --git a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs index b1ac8118eb..81875cc98c 100644 --- a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs @@ -143,7 +143,7 @@ private string JsonConverterFunc(object value) return JsonConvert.SerializeObject(value, _serializerSettings); } - private IEnumerable GetDataPropertiesFor(JsonObjectContract jsonObjectContract, out Type extensionDataType) + private List GetDataPropertiesFor(JsonObjectContract jsonObjectContract, out Type extensionDataType) { var dataProperties = new List(); diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs index d9487dda95..9b0b7fdc8b 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs @@ -41,12 +41,19 @@ public DataContract GetDataContractForType(Type type) var enumValues = type.GetEnumValues(); // Test to determine if the serializer will treat as string - var serializeAsString = (enumValues.Length > 0) - && JsonConverterFunc(enumValues.GetValue(0), type).StartsWith("\""); + var serializeAsString = + enumValues.Length > 0 && +#if NET5_0_OR_GREATER + JsonConverterFunc(enumValues.GetValue(0), type).StartsWith('\"'); +#else + JsonConverterFunc(enumValues.GetValue(0), type).StartsWith("\""); +#endif + + var exampleType = serializeAsString ? + typeof(string) : + type.GetEnumUnderlyingType(); - primitiveTypeAndFormat = serializeAsString - ? PrimitiveTypesAndFormats[typeof(string)] - : PrimitiveTypesAndFormats[type.GetEnumUnderlyingType()]; + primitiveTypeAndFormat = PrimitiveTypesAndFormats[exampleType]; return DataContract.ForPrimitive( underlyingType: type, @@ -144,7 +151,7 @@ public bool IsSupportedCollection(Type type, out Type itemType) return false; } - private IEnumerable GetDataPropertiesFor(Type objectType, out Type extensionDataType) + private List GetDataPropertiesFor(Type objectType, out Type extensionDataType) { extensionDataType = null; @@ -177,7 +184,7 @@ private IEnumerable GetDataPropertiesFor(Type objectType, out Type return (property.IsPubliclyReadable() || property.IsPubliclyWritable()) && - !(property.GetIndexParameters().Any()) && + !(property.GetIndexParameters().Length > 0) && !(property.HasAttribute() && isIgnoredViaNet5Attribute) && !(property.HasAttribute()) && !(_serializerOptions.IgnoreReadOnlyProperties && !property.IsPubliclyWritable()); diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs index 3ca1f7ddd0..55047933b9 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; namespace Swashbuckle.AspNetCore.SwaggerGen @@ -55,7 +56,7 @@ private OpenApiSchema GenerateSchemaForMember( if (_generatorOptions.UseAllOfToExtendReferenceSchemas && schema.Reference != null) { - schema.AllOf = new[] { new OpenApiSchema { Reference = schema.Reference } }; + schema.AllOf = [new OpenApiSchema { Reference = schema.Reference }]; schema.Reference = null; } @@ -80,8 +81,7 @@ private OpenApiSchema GenerateSchemaForMember( var defaultValueAttribute = customAttributes.OfType().FirstOrDefault(); if (defaultValueAttribute != null) { - var defaultAsJson = dataContract.JsonConverter(defaultValueAttribute.Value); - schema.Default = OpenApiAnyFactory.CreateFromJson(defaultAsJson); + schema.Default = GenerateDefaultValue(dataContract, modelType, defaultValueAttribute.Value); } var obsoleteAttribute = customAttributes.OfType().FirstOrDefault(); @@ -90,8 +90,9 @@ private OpenApiSchema GenerateSchemaForMember( schema.Deprecated = true; } - // NullableAttribute behaves diffrently for Dictionaries - if (schema.AdditionalPropertiesAllowed && modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + // NullableAttribute behaves differently for Dictionaries + if (schema.AdditionalPropertiesAllowed && modelType.IsGenericType && + modelType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { schema.AdditionalProperties.Nullable = !memberInfo.IsDictionaryValueNonNullable(); } @@ -118,7 +119,7 @@ private OpenApiSchema GenerateSchemaForParameter( if (_generatorOptions.UseAllOfToExtendReferenceSchemas && schema.Reference != null) { - schema.AllOf = new[] { new OpenApiSchema { Reference = schema.Reference } }; + schema.AllOf = [new OpenApiSchema { Reference = schema.Reference }]; schema.Reference = null; } @@ -132,8 +133,7 @@ private OpenApiSchema GenerateSchemaForParameter( if (defaultValue != null) { - var defaultAsJson = dataContract.JsonConverter(defaultValue); - schema.Default = OpenApiAnyFactory.CreateFromJson(defaultAsJson); + schema.Default = GenerateDefaultValue(dataContract, modelType, defaultValue); } schema.ApplyValidationAttributes(customAttributes); @@ -200,15 +200,15 @@ private OpenApiSchema GeneratePolymorphicSchema( }; } - private static readonly Type[] BinaryStringTypes = new[] - { + private static readonly Type[] BinaryStringTypes = + [ typeof(IFormFile), typeof(FileResult), typeof(System.IO.Stream), #if NETCOREAPP3_0_OR_GREATER typeof(System.IO.Pipelines.PipeReader), #endif - }; + ]; private OpenApiSchema GenerateConcreteSchema(DataContract dataContract, SchemaRepository schemaRepository) { @@ -277,7 +277,7 @@ private bool TryGetCustomTypeMapping(Type modelType, out Func sch || (modelType.IsConstructedGenericType && _generatorOptions.CustomTypeMappings.TryGetValue(modelType.GetGenericTypeDefinition(), out schemaFactory)); } - private OpenApiSchema CreatePrimitiveSchema(DataContract dataContract) + private static OpenApiSchema CreatePrimitiveSchema(DataContract dataContract) { var schema = new OpenApiSchema { @@ -292,7 +292,7 @@ private OpenApiSchema CreatePrimitiveSchema(DataContract dataContract) schema.Enum = dataContract.EnumValues .Select(value => JsonSerializer.Serialize(value)) .Distinct() - .Select(valueAsJson => OpenApiAnyFactory.CreateFromJson(valueAsJson)) + .Select(OpenApiAnyFactory.CreateFromJson) .ToList(); return schema; @@ -402,7 +402,7 @@ private OpenApiSchema CreateObjectSchema(DataContract dataContract, SchemaReposi foreach (var dataProperty in applicableDataProperties) { - var customAttributes = dataProperty.MemberInfo?.GetInlineAndMetadataAttributes() ?? Enumerable.Empty(); + var customAttributes = dataProperty.MemberInfo?.GetInlineAndMetadataAttributes() ?? []; if (_generatorOptions.IgnoreObsoleteProperties && customAttributes.OfType().Any()) continue; @@ -519,5 +519,24 @@ private void ApplyFilters( filter.Apply(schema, filterContext); } } + + private IOpenApiAny GenerateDefaultValue( + DataContract dataContract, + Type modelType, + object defaultValue) + { + // If the types do not match (e.g. a default which is an integer is specified for a double), + // attempt to coerce the default value to the correct type so that it can be serialized correctly. + // See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2885 and + // https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2904. + var defaultValueType = defaultValue?.GetType(); + if (defaultValueType != null && defaultValueType != modelType) + { + dataContract = GetDataContractFor(defaultValueType); + } + + var defaultAsJson = dataContract.JsonConverter(defaultValue); + return OpenApiAnyFactory.CreateFromJson(defaultAsJson); + } } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs index eb2fcfcc6a..1365214429 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs @@ -118,13 +118,21 @@ public async Task SwaggerMiddleware_CanBeConfiguredMultipleTimes( public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_For_WebApi( string swaggerRequestUri) { - await SwaggerEndpointReturnsValidSwaggerJson(swaggerRequestUri); + await SwaggerEndpointReturnsValidSwaggerJson(swaggerRequestUri); + } + + [Theory] + [InlineData("/swagger/v1/swagger.json")] + public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_For_Mvc( + string swaggerRequestUri) + { + await SwaggerEndpointReturnsValidSwaggerJson(swaggerRequestUri); } [Fact] public async Task TypesAreRenderedCorrectly() { - using var application = new TestApplication(); + using var application = new TestApplication(); using var client = application.CreateDefaultClient(); using var swaggerResponse = await client.GetFromJsonAsync("/swagger/v1/swagger.json"); @@ -153,7 +161,7 @@ public async Task TypesAreRenderedCorrectly() ]); } - private async Task SwaggerEndpointReturnsValidSwaggerJson(string swaggerRequestUri) + private static async Task SwaggerEndpointReturnsValidSwaggerJson(string swaggerRequestUri) where TEntryPoint : class { using var application = new TestApplication(); @@ -163,11 +171,11 @@ private async Task SwaggerEndpointReturnsValidSwaggerJson(string sw } #endif - private async Task AssertValidSwaggerJson(HttpClient client, string swaggerRequestUri) + private static async Task AssertValidSwaggerJson(HttpClient client, string swaggerRequestUri) { using var swaggerResponse = await client.GetAsync(swaggerRequestUri); - swaggerResponse.EnsureSuccessStatusCode(); + Assert.True(swaggerResponse.IsSuccessStatusCode, await swaggerResponse.Content.ReadAsStringAsync()); using var contentStream = await swaggerResponse.Content.ReadAsStreamAsync(); new OpenApiStreamReader().Read(contentStream, out OpenApiDiagnostic diagnostic); Assert.Empty(diagnostic.Errors); diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/Swashbuckle.AspNetCore.IntegrationTests.csproj b/test/Swashbuckle.AspNetCore.IntegrationTests/Swashbuckle.AspNetCore.IntegrationTests.csproj index 13a400f73a..d4e5ac45f8 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/Swashbuckle.AspNetCore.IntegrationTests.csproj +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/Swashbuckle.AspNetCore.IntegrationTests.csproj @@ -1,4 +1,4 @@ - + $(MSBuildThisFileDirectory)..\..\src\Swashbuckle.AspNetCore.Swagger\Swashbuckle.AspNetCore.Swagger.snk @@ -23,10 +23,7 @@ - - - - + diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs index 799026119e..518e5db3d2 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs @@ -269,6 +269,7 @@ public void GenerateSchema_SetsNullableFlag_IfPropertyIsReferenceOrNullableType( [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.LongWithDefault), "9223372036854775807")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.FloatWithDefault), "3.4028235E+38")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.DoubleWithDefault), "1.7976931348623157E+308")] + [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.DoubleWithDefaultOfDifferentType), "1")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.StringWithDefault), "\"foobar\"")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.IntArrayWithDefault), "[\n 1,\n 2,\n 3\n]")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.StringArrayWithDefault), "[\n \"foo\",\n \"bar\"\n]")] diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index 6416652ee9..d162753253 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -292,6 +292,7 @@ public void GenerateSchema_SetsNullableFlag_IfPropertyIsReferenceOrNullableType( [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.LongWithDefault), "9223372036854775807")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.FloatWithDefault), "3.4028235E+38")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.DoubleWithDefault), "1.7976931348623157E+308")] + [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.DoubleWithDefaultOfDifferentType), "1")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.StringWithDefault), "\"foobar\"")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.IntArrayWithDefault), "[\n 1,\n 2,\n 3\n]")] [InlineData(typeof(TypeWithDefaultAttributes), nameof(TypeWithDefaultAttributes.StringArrayWithDefault), "[\n \"foo\",\n \"bar\"\n]")] diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithDefaultAttributes.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithDefaultAttributes.cs index b999efd4d5..f1bdd58371 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithDefaultAttributes.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithDefaultAttributes.cs @@ -19,6 +19,9 @@ public class TypeWithDefaultAttributes [DefaultValue(double.MaxValue)] public double DoubleWithDefault { get; set; } + [DefaultValue(1)] // Repro for https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2885 + public double DoubleWithDefaultOfDifferentType { get; set; } + [DefaultValue("foobar")] public string StringWithDefault { get; set; } diff --git a/test/WebSites/MvcWithNullable/MvcWithNullable.csproj b/test/WebSites/MvcWithNullable/MvcWithNullable.csproj new file mode 100644 index 0000000000..29f5a084a3 --- /dev/null +++ b/test/WebSites/MvcWithNullable/MvcWithNullable.csproj @@ -0,0 +1,20 @@ + + + + enable + $(NoWarn);CA1050 + enable + WebApi + net8.0 + + + + + + + + + + + + diff --git a/test/WebSites/MvcWithNullable/Program.cs b/test/WebSites/MvcWithNullable/Program.cs new file mode 100644 index 0000000000..c3d808e394 --- /dev/null +++ b/test/WebSites/MvcWithNullable/Program.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.UseAllOfToExtendReferenceSchemas(); +}); + +var app = builder.Build(); +app.UseHttpsRedirection(); + +if (app.Environment.IsDevelopment()) +{ + _ = app.UseSwagger(); + _ = app.UseSwaggerUI(); +} + +app.MapControllers(); + +app.Run(); + +[ApiController] +[Route("api/[controller]")] +public class EnumController : ControllerBase +{ + [HttpGet] + public IActionResult Get(LogLevel? logLevel = LogLevel.Error) => Ok(new { logLevel }); +} + +namespace MvcWithNullable +{ + public partial class Program + { + // Expose the Program class for use with WebApplicationFactory + } +} diff --git a/test/WebSites/MvcWithNullable/Properties/launchSettings.json b/test/WebSites/MvcWithNullable/Properties/launchSettings.json new file mode 100644 index 0000000000..82bdd832be --- /dev/null +++ b/test/WebSites/MvcWithNullable/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:21394", + "sslPort": 44373 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5205", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7175;http://localhost:5205", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/test/WebSites/MvcWithNullable/appsettings.Development.json b/test/WebSites/MvcWithNullable/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/test/WebSites/MvcWithNullable/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/test/WebSites/MvcWithNullable/appsettings.json b/test/WebSites/MvcWithNullable/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/test/WebSites/MvcWithNullable/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/test/WebSites/WebApi/Program.cs b/test/WebSites/WebApi/Program.cs index 860b079886..dc5b28abe7 100644 --- a/test/WebSites/WebApi/Program.cs +++ b/test/WebSites/WebApi/Program.cs @@ -40,7 +40,10 @@ record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } -public partial class Program +namespace WebApi { - // Expose the Program class for use with WebApplicationFactory + public partial class Program + { + // Expose the Program class for use with WebApplicationFactory + } }