From 5dd46c42745ae5f77ab3425eb77d492c5d5c60ea Mon Sep 17 00:00:00 2001 From: Adam Stachowicz Date: Tue, 26 Nov 2024 09:15:45 +0100 Subject: [PATCH] Add PathGroupSelector (#3152) Add ability to configure how paths are grouped. --- Directory.Build.props | 2 +- .../ConfigureSwaggerGeneratorOptions.cs | 1 + .../PublicAPI/PublicAPI.Unshipped.txt | 2 ++ .../ApiDescriptionExtensions.cs | 6 ++-- .../ApiParameterDescriptionExtensions.cs | 8 ++--- .../SwaggerGenerator/SwaggerGenerator.cs | 8 ++--- .../SwaggerGeneratorOptions.cs | 8 +++++ .../ConfigureSwaggerGeneratorOptionsTests.cs | 2 +- .../Fixtures/FakeController.cs | 3 ++ .../SwaggerGenerator/SwaggerGeneratorTests.cs | 32 ++++++++++++++++++- 10 files changed, 57 insertions(+), 15 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index b7ce71541e..431849fe9b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -42,7 +42,7 @@ snupkg true true - 7.1.1 + 7.2.0 false diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs index f8e26cda12..f30fc03ae8 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs @@ -116,6 +116,7 @@ public void DeepCopy(SwaggerGeneratorOptions source, SwaggerGeneratorOptions tar target.RequestBodyFilters = new List(source.RequestBodyFilters); target.RequestBodyAsyncFilters = new List(source.RequestBodyAsyncFilters); target.SecuritySchemesSelector = source.SecuritySchemesSelector; + target.PathGroupSelector = source.PathGroupSelector; } private TFilter GetOrCreateFilter(FilterDescriptor filterDescriptor) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt index 2736a7c349..fb4f007a9d 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/PublicAPI/PublicAPI.Unshipped.txt @@ -6,3 +6,5 @@ static Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.Docu static Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.OperationAsyncFilter(this Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions swaggerGenOptions, params object[] arguments) -> void static Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.ParameterAsyncFilter(this Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions swaggerGenOptions, params object[] arguments) -> void static Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.RequestBodyAsyncFilter(this Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions swaggerGenOptions, params object[] arguments) -> void +Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.PathGroupSelector.get -> System.Func +Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.PathGroupSelector.set -> void diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs index c5c3673f4f..bb8e3939fb 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiDescriptionExtensions.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Controllers; @@ -41,7 +41,7 @@ public static IEnumerable CustomAttributes(this ApiDescription apiDescri .Union(methodInfo.DeclaringType.GetCustomAttributes(true)); } - return Enumerable.Empty(); + return []; } [Obsolete("Use TryGetMethodInfo() and CustomAttributes() instead")] @@ -57,7 +57,7 @@ public static void GetAdditionalMetadata(this ApiDescription apiDescription, return; } - customAttributes = Enumerable.Empty(); + customAttributes = []; } internal static string RelativePathSansParameterConstraints(this ApiDescription apiDescription) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs index d2143bcf10..f3ecd7438e 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/ApiParameterDescriptionExtensions.cs @@ -72,9 +72,7 @@ public static PropertyInfo PropertyInfo(this ApiParameterDescription apiParamete { var modelMetadata = apiParameter.ModelMetadata; - return (modelMetadata?.ContainerType != null) - ? modelMetadata.ContainerType.GetProperty(modelMetadata.PropertyName) - : null; + return modelMetadata?.ContainerType?.GetProperty(modelMetadata.PropertyName); } public static IEnumerable CustomAttributes(this ApiParameterDescription apiParameter) @@ -103,12 +101,12 @@ internal static void GetAdditionalMetadata( internal static bool IsFromPath(this ApiParameterDescription apiParameter) { - return (apiParameter.Source == BindingSource.Path); + return apiParameter.Source == BindingSource.Path; } internal static bool IsFromBody(this ApiParameterDescription apiParameter) { - return (apiParameter.Source == BindingSource.Body); + return apiParameter.Source == BindingSource.Body; } internal static bool IsFromForm(this ApiParameterDescription apiParameter) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index b709141643..d4c692154c 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -56,7 +56,7 @@ public async Task GetSwaggerAsync( swaggerDoc.Paths = await GeneratePathsAsync(filterContext.ApiDescriptions, filterContext.SchemaRepository); swaggerDoc.Components.SecuritySchemes = await GetSecuritySchemesAsync(); - // NOTE: Filter processing moved here so they may effect generated security schemes + // NOTE: Filter processing moved here so they may affect generated security schemes foreach (var filter in _options.DocumentAsyncFilters) { await filter.ApplyAsync(swaggerDoc, filterContext, CancellationToken.None); @@ -81,7 +81,7 @@ public OpenApiDocument GetSwagger(string documentName, string host = null, strin swaggerDoc.Paths = GeneratePaths(filterContext.ApiDescriptions, filterContext.SchemaRepository); swaggerDoc.Components.SecuritySchemes = GetSecuritySchemesAsync().Result; - // NOTE: Filter processing moved here so they may effect generated security schemes + // NOTE: Filter processing moved here so they may affect generated security schemes foreach (var filter in _options.DocumentFilters) { filter.Apply(swaggerDoc, filterContext); @@ -199,7 +199,7 @@ private async Task GeneratePathsAsync( { var apiDescriptionsByPath = apiDescriptions .OrderBy(_options.SortKeySelector) - .GroupBy(apiDesc => apiDesc.RelativePathSansParameterConstraints()); + .GroupBy(_options.PathGroupSelector); var paths = new OpenApiPaths(); foreach (var group in apiDescriptionsByPath) @@ -284,7 +284,7 @@ private async Task> GenerateOperatio { throw new SwaggerGeneratorException(string.Format( "Conflicting method/path combination \"{0} {1}\" for actions - {2}. " + - "Actions require a unique method/path combination for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a workaround", + "Actions require a unique method/path combination for Swagger/OpenAPI 2.0 and 3.0. Use ConflictingActionsResolver as a workaround or provide your own implementation of PathGroupSelector.", httpMethod, group.First().RelativePath, string.Join(",", group.Select(apiDesc => apiDesc.ActionDescriptor.DisplayName)))); diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs index 41fb349464..0acde836f9 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs @@ -20,6 +20,7 @@ public SwaggerGeneratorOptions() OperationIdSelector = DefaultOperationIdSelector; TagsSelector = DefaultTagsSelector; SortKeySelector = DefaultSortKeySelector; + PathGroupSelector = DefaultPathGroupSelector; SecuritySchemesSelector = null; SchemaComparer = StringComparer.Ordinal; Servers = new List(); @@ -49,6 +50,8 @@ public SwaggerGeneratorOptions() public Func SortKeySelector { get; set; } + public Func PathGroupSelector { get; set; } + public bool InferSecuritySchemes { get; set; } public Func, IDictionary> SecuritySchemesSelector { get; set; } @@ -120,5 +123,10 @@ private string DefaultSortKeySelector(ApiDescription apiDescription) { return TagsSelector(apiDescription).First(); } + + private static string DefaultPathGroupSelector(ApiDescription apiDescription) + { + return apiDescription.RelativePathSansParameterConstraints(); + } } } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/ConfigureSwaggerGeneratorOptionsTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/ConfigureSwaggerGeneratorOptionsTests.cs index 7b3892906a..bb599457f9 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/ConfigureSwaggerGeneratorOptionsTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/ConfigureSwaggerGeneratorOptionsTests.cs @@ -18,7 +18,7 @@ public static void DeepCopy_Copies_All_Properties() // If this assertion fails, it means that a new property has been added // to SwaggerGeneratorOptions and ConfigureSwaggerGeneratorOptions.DeepCopy() needs to be updated - Assert.Equal(22, publicProperties.Length); + Assert.Equal(23, publicProperties.Length); } [Fact] diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs index f87e18a6e1..cc6b90a977 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs @@ -50,6 +50,9 @@ public void ActionWithIntParameterWithDefaultValue(int param = 1) public void ActionWithIntParameterWithDefaultValueAttribute([DefaultValue(3)] int param) { } + public void ActionWithIntFromQueryParameter([FromQuery] int param) + { } + public void ActionWithIntParameterWithRequiredAttribute([Required] int param) { } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index dded79835d..c7e63e3d20 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -1139,7 +1139,37 @@ public void GetSwagger_ThrowsSwaggerGeneratorException_IfActionsHaveConflictingH "Conflicting method/path combination \"POST resource\" for actions - " + "Swashbuckle.AspNetCore.SwaggerGen.Test.FakeController.ActionWithNoParameters (Swashbuckle.AspNetCore.SwaggerGen.Test)," + "Swashbuckle.AspNetCore.SwaggerGen.Test.FakeController.ActionWithNoParameters (Swashbuckle.AspNetCore.SwaggerGen.Test). " + - "Actions require a unique method/path combination for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a workaround", + "Actions require a unique method/path combination for Swagger/OpenAPI 2.0 and 3.0. Use ConflictingActionsResolver as a workaround or provide your own implementation of PathGroupSelector.", + exception.Message); + } + + [Fact] + public void GetSwagger_ThrowsSwaggerGeneratorException_IfActionsHaveConflictingHttpMethodAndPathWithDifferentParameters() + { + var subject = Subject( + apiDescriptions: + [ + ApiDescriptionFactory.Create( + c => nameof(c.ActionWithNoParameters), groupName: "v1", httpMethod: "GET", relativePath: "resource"), + + ApiDescriptionFactory.Create( + c => nameof(c.ActionWithIntFromQueryParameter), groupName: "v1", httpMethod: "GET", relativePath: "resource", new ApiParameterDescription[] + { + new() + { + Name = "id", + Source = BindingSource.Query, + } + }), + ] + ); + + var exception = Assert.Throws(() => subject.GetSwagger("v1")); + Assert.Equal( + "Conflicting method/path combination \"GET resource\" for actions - " + + "Swashbuckle.AspNetCore.SwaggerGen.Test.FakeController.ActionWithNoParameters (Swashbuckle.AspNetCore.SwaggerGen.Test)," + + "Swashbuckle.AspNetCore.SwaggerGen.Test.FakeController.ActionWithIntFromQueryParameter (Swashbuckle.AspNetCore.SwaggerGen.Test). " + + "Actions require a unique method/path combination for Swagger/OpenAPI 2.0 and 3.0. Use ConflictingActionsResolver as a workaround or provide your own implementation of PathGroupSelector.", exception.Message); }