diff --git a/src/Swashbuckle.AspNetCore.Cli/Program.cs b/src/Swashbuckle.AspNetCore.Cli/Program.cs index 72eb5f02b0..9b06fc9e84 100644 --- a/src/Swashbuckle.AspNetCore.Cli/Program.cs +++ b/src/Swashbuckle.AspNetCore.Cli/Program.cs @@ -32,15 +32,19 @@ public static int Main(string[] args) { c.Argument("startupassembly", "relative path to the application's startup assembly"); c.Argument("swaggerdoc", "name of the swagger doc you want to retrieve, as configured in your startup class"); + c.Option("--output", "relative path where the Swagger will be output, defaults to stdout"); c.Option("--host", "a specific host to include in the Swagger output"); c.Option("--basepath", "a specific basePath to include in the Swagger output"); c.Option("--serializeasv2", "output Swagger in the V2 format rather than V3", true); c.Option("--yaml", "exports swagger in a yaml format", true); + c.OnRun((namedArgs) => { if (!File.Exists(namedArgs["startupassembly"])) + { throw new FileNotFoundException(namedArgs["startupassembly"]); + } var depsFile = namedArgs["startupassembly"].Replace(".dll", ".deps.json"); var runtimeConfig = namedArgs["startupassembly"].Replace(".dll", ".runtimeconfig.json"); @@ -110,37 +114,42 @@ public static int Main(string[] args) } } - using (Stream stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput()) - using (var streamWriter = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture)) + using Stream stream = outputPath != null ? File.Create(outputPath) : Console.OpenStandardOutput(); + using var streamWriter = new FormattingStreamWriter(stream, CultureInfo.InvariantCulture); + + IOpenApiWriter writer; + if (namedArgs.ContainsKey("--yaml")) { - IOpenApiWriter writer; - if (namedArgs.ContainsKey("--yaml")) - writer = new OpenApiYamlWriter(streamWriter); - else - writer = new OpenApiJsonWriter(streamWriter); + writer = new OpenApiYamlWriter(streamWriter); + } + else + { + writer = new OpenApiJsonWriter(streamWriter); + } - if (namedArgs.ContainsKey("--serializeasv2")) - { - if (swaggerDocumentSerializer != null) - { - swaggerDocumentSerializer.SerializeDocument(swagger, writer, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); - } - else - { - swagger.SerializeAsV2(writer); - } - } - else if (swaggerDocumentSerializer != null) + if (namedArgs.ContainsKey("--serializeasv2")) + { + if (swaggerDocumentSerializer != null) { - swaggerDocumentSerializer.SerializeDocument(swagger, writer, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0); + swaggerDocumentSerializer.SerializeDocument(swagger, writer, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); } else { - swagger.SerializeAsV3(writer); + swagger.SerializeAsV2(writer); } + } + else if (swaggerDocumentSerializer != null) + { + swaggerDocumentSerializer.SerializeDocument(swagger, writer, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0); + } + else + { + swagger.SerializeAsV3(writer); + } - if (outputPath != null) - Console.WriteLine($"Swagger JSON/YAML successfully written to {outputPath}"); + if (outputPath != null) + { + Console.WriteLine($"Swagger JSON/YAML successfully written to {outputPath}"); } return 0; @@ -152,7 +161,7 @@ public static int Main(string[] args) private static string EscapePath(string path) { - return path.Contains(" ") + return path.Contains(' ') ? "\"" + path + "\"" : path; } @@ -200,23 +209,26 @@ private static bool TryGetCustomHost( .Where(t => t.Name == factoryClassName) .ToList(); - if (!factoryTypes.Any()) + if (factoryTypes.Count == 0) { host = default; return false; } - - if (factoryTypes.Count() > 1) + else if (factoryTypes.Count > 1) + { throw new InvalidOperationException($"Multiple {factoryClassName} classes detected"); + } var factoryMethod = factoryTypes .Single() .GetMethod(factoryMethodName, BindingFlags.Public | BindingFlags.Static); if (factoryMethod == null || factoryMethod.ReturnType != typeof(THost)) + { throw new InvalidOperationException( $"{factoryClassName} class detected but does not contain a public static method " + $"called {factoryMethodName} with return type {typeof(THost).Name}"); + } host = (THost)factoryMethod.Invoke(null, null); return true; diff --git a/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs b/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs index 6f8d198b57..9d6036798d 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/IAsyncSwaggerProvider.cs @@ -10,4 +10,4 @@ Task GetSwaggerAsync( string host = null, string basePath = null); } -} \ No newline at end of file +} diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs index d6e745daff..7566107ac5 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs @@ -1,5 +1,4 @@ -using System; -using System.Globalization; +using System.Globalization; using System.IO; using System.Text; using System.Threading.Tasks; @@ -57,17 +56,23 @@ public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvid ? httpContext.Request.PathBase.Value : null; - var swagger = swaggerProvider switch + OpenApiDocument swagger; + var asyncSwaggerProvider = httpContext.RequestServices.GetService(); + + if (asyncSwaggerProvider is not null) { - IAsyncSwaggerProvider asyncSwaggerProvider => await asyncSwaggerProvider.GetSwaggerAsync( + swagger = await asyncSwaggerProvider.GetSwaggerAsync( documentName: documentName, host: null, - basePath: basePath), - _ => swaggerProvider.GetSwagger( + basePath: basePath); + } + else + { + swagger = swaggerProvider.GetSwagger( documentName: documentName, host: null, - basePath: basePath) - }; + basePath: basePath); + } // One last opportunity to modify the Swagger Document - this time with request context foreach (var filter in _options.PreSerializeFilters) @@ -123,7 +128,7 @@ private bool RequestingSwaggerDocument(HttpRequest request, out string documentN return false; } - private void RespondWithNotFound(HttpResponse response) + private static void RespondWithNotFound(HttpResponse response) { response.StatusCode = 404; } @@ -133,26 +138,25 @@ private async Task RespondWithSwaggerJson(HttpResponse response, OpenApiDocument response.StatusCode = 200; response.ContentType = "application/json;charset=utf-8"; - using (var textWriter = new StringWriter(CultureInfo.InvariantCulture)) + using var textWriter = new StringWriter(CultureInfo.InvariantCulture); + var jsonWriter = new OpenApiJsonWriter(textWriter); + + if (_options.SerializeAsV2) { - var jsonWriter = new OpenApiJsonWriter(textWriter); - if (_options.SerializeAsV2) - { - if (_options.CustomDocumentSerializer != null) - _options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); - else - swagger.SerializeAsV2(jsonWriter); - } + if (_options.CustomDocumentSerializer != null) + _options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); else - { - if (_options.CustomDocumentSerializer != null) - _options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0); - else - swagger.SerializeAsV3(jsonWriter); - } - - await response.WriteAsync(textWriter.ToString(), new UTF8Encoding(false)); + swagger.SerializeAsV2(jsonWriter); + } + else + { + if (_options.CustomDocumentSerializer != null) + _options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0); + else + swagger.SerializeAsV3(jsonWriter); } + + await response.WriteAsync(textWriter.ToString(), new UTF8Encoding(false)); } private async Task RespondWithSwaggerYaml(HttpResponse response, OpenApiDocument swagger) @@ -160,26 +164,24 @@ private async Task RespondWithSwaggerYaml(HttpResponse response, OpenApiDocument response.StatusCode = 200; response.ContentType = "text/yaml;charset=utf-8"; - using (var textWriter = new StringWriter(CultureInfo.InvariantCulture)) + using var textWriter = new StringWriter(CultureInfo.InvariantCulture); + var yamlWriter = new OpenApiYamlWriter(textWriter); + if (_options.SerializeAsV2) { - var yamlWriter = new OpenApiYamlWriter(textWriter); - if (_options.SerializeAsV2) - { - if (_options.CustomDocumentSerializer != null) - _options.CustomDocumentSerializer.SerializeDocument(swagger, yamlWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); - else - swagger.SerializeAsV2(yamlWriter); - } + if (_options.CustomDocumentSerializer != null) + _options.CustomDocumentSerializer.SerializeDocument(swagger, yamlWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); else - { - if (_options.CustomDocumentSerializer != null) - _options.CustomDocumentSerializer.SerializeDocument(swagger, yamlWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0); - else - swagger.SerializeAsV3(yamlWriter); - } - - await response.WriteAsync(textWriter.ToString(), new UTF8Encoding(false)); + swagger.SerializeAsV2(yamlWriter); } + else + { + if (_options.CustomDocumentSerializer != null) + _options.CustomDocumentSerializer.SerializeDocument(swagger, yamlWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0); + else + swagger.SerializeAsV3(yamlWriter); + } + + await response.WriteAsync(textWriter.ToString(), new UTF8Encoding(false)); } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs index 38bb98071b..29d43ca555 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSwaggerGeneratorOptions.cs @@ -68,9 +68,13 @@ public void DeepCopy(SwaggerGeneratorOptions source, SwaggerGeneratorOptions tar target.SecuritySchemes = new Dictionary(source.SecuritySchemes); target.SecurityRequirements = new List(source.SecurityRequirements); target.ParameterFilters = new List(source.ParameterFilters); + target.ParameterAsyncFilters = new List(source.ParameterAsyncFilters); target.OperationFilters = new List(source.OperationFilters); + target.OperationAsyncFilters = new List(source.OperationAsyncFilters); target.DocumentFilters = new List(source.DocumentFilters); + target.DocumentAsyncFilters = new List(source.DocumentAsyncFilters); target.RequestBodyFilters = new List(source.RequestBodyFilters); + target.RequestBodyAsyncFilters = new List(source.RequestBodyAsyncFilters); target.SecuritySchemesSelector = source.SecuritySchemesSelector; } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs index 43adb723c6..6a0cd52896 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs @@ -23,9 +23,10 @@ public static IServiceCollection AddSwaggerGen( services.AddTransient, ConfigureSwaggerGeneratorOptions>(); services.AddTransient, ConfigureSchemaGeneratorOptions>(); - // Register generator and it's dependencies - services.TryAddTransient(); - services.TryAddTransient(); + // Register generator and its dependencies + services.TryAddTransient(); + services.TryAddTransient(s => s.GetRequiredService()); + services.TryAddTransient(s => s.GetRequiredService()); services.TryAddTransient(s => s.GetRequiredService>().Value); services.TryAddTransient(); services.TryAddTransient(s => s.GetRequiredService>().Value); @@ -53,13 +54,15 @@ public static void ConfigureSwaggerGen( private sealed class JsonSerializerOptionsProvider { - private readonly IServiceProvider _serviceProvider; private JsonSerializerOptions _options; +#if !NETSTANDARD2_0 + private readonly IServiceProvider _serviceProvider; public JsonSerializerOptionsProvider(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } +#endif public JsonSerializerOptions Options => _options ??= ResolveOptions(); diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDocumentFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDocumentFilter.cs index ecac182ba4..c9ba82305e 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDocumentFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IDocumentFilter.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.OpenApi.Models; @@ -9,6 +11,11 @@ public interface IDocumentFilter void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context); } + public interface IDocumentAsyncFilter + { + Task ApplyAsync(OpenApiDocument swaggerDoc, DocumentFilterContext context, CancellationToken cancellationToken); + } + public class DocumentFilterContext { public DocumentFilterContext( diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IOperationFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IOperationFilter.cs index eb3d6820b6..91c4f7ea0d 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IOperationFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IOperationFilter.cs @@ -1,4 +1,6 @@ using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.OpenApi.Models; @@ -9,6 +11,11 @@ public interface IOperationFilter void Apply(OpenApiOperation operation, OperationFilterContext context); } + public interface IOperationAsyncFilter + { + Task ApplyAsync(OpenApiOperation operation, OperationFilterContext context, CancellationToken cancellationToken); + } + public class OperationFilterContext { public OperationFilterContext( diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IParameterFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IParameterFilter.cs index 32db63efcc..94649498a1 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IParameterFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IParameterFilter.cs @@ -1,4 +1,6 @@ using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.OpenApi.Models; @@ -9,6 +11,11 @@ public interface IParameterFilter void Apply(OpenApiParameter parameter, ParameterFilterContext context); } + public interface IParameterAsyncFilter + { + Task ApplyAsync(OpenApiParameter parameter, ParameterFilterContext context, CancellationToken cancellationToken); + } + public class ParameterFilterContext { public ParameterFilterContext( diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs index cafb5e5685..28660ba742 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/IRequestBodyFilter.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.OpenApi.Models; @@ -9,6 +11,11 @@ public interface IRequestBodyFilter void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context); } + public interface IRequestBodyAsyncFilter + { + Task ApplyAsync(OpenApiRequestBody requestBody, RequestBodyFilterContext context, CancellationToken cancellationToken); + } + public class RequestBodyFilterContext { public RequestBodyFilterContext( diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs index ed6390a355..c80f8657cb 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; @@ -45,46 +46,83 @@ public SwaggerGenerator( _authenticationSchemeProvider = authenticationSchemeProvider; } - public async Task GetSwaggerAsync(string documentName, string host = null, string basePath = null) + public async Task GetSwaggerAsync( + string documentName, + string host = null, + string basePath = null) { - var (applicableApiDescriptions, swaggerDoc, schemaRepository) = GetSwaggerDocumentWithoutFilters(documentName, host, basePath); + var (filterContext, swaggerDoc) = GetSwaggerDocumentWithoutPaths(documentName, host, basePath); - swaggerDoc.Components.SecuritySchemes = await GetSecuritySchemes(); + 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 - var filterContext = new DocumentFilterContext(applicableApiDescriptions, _schemaGenerator, schemaRepository); + foreach (var filter in _options.DocumentAsyncFilters) + { + await filter.ApplyAsync(swaggerDoc, filterContext, CancellationToken.None); + } + foreach (var filter in _options.DocumentFilters) { filter.Apply(swaggerDoc, filterContext); } - swaggerDoc.Components.Schemas = new SortedDictionary(swaggerDoc.Components.Schemas, _options.SchemaComparer); + SortSchemas(swaggerDoc); return swaggerDoc; } public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null) { - var (applicableApiDescriptions, swaggerDoc, schemaRepository) = GetSwaggerDocumentWithoutFilters(documentName, host, basePath); + try + { + var (filterContext, swaggerDoc) = GetSwaggerDocumentWithoutPaths(documentName, host, basePath); - swaggerDoc.Components.SecuritySchemes = GetSecuritySchemes().Result; + swaggerDoc.Paths = GeneratePaths(filterContext.ApiDescriptions, filterContext.SchemaRepository); + swaggerDoc.Components.SecuritySchemes = GetSecuritySchemesAsync().Result; - // NOTE: Filter processing moved here so they may effect generated security schemes - var filterContext = new DocumentFilterContext(applicableApiDescriptions, _schemaGenerator, schemaRepository); - foreach (var filter in _options.DocumentFilters) - { - filter.Apply(swaggerDoc, filterContext); + // NOTE: Filter processing moved here so they may effect generated security schemes + foreach (var filter in _options.DocumentFilters) + { + filter.Apply(swaggerDoc, filterContext); + } + + SortSchemas(swaggerDoc); + + return swaggerDoc; } + catch (AggregateException ex) + { + // Unwrap any AggregateException from using async methods to run the synchronous filters + var inner = ex.InnerException; - swaggerDoc.Components.Schemas = new SortedDictionary(swaggerDoc.Components.Schemas, _options.SchemaComparer); + while (inner is not null) + { + if (inner is AggregateException) + { + inner = inner.InnerException; + } + else + { + throw inner; + } + } - return swaggerDoc; + throw; + } } - private (IEnumerable, OpenApiDocument, SchemaRepository) GetSwaggerDocumentWithoutFilters(string documentName, string host = null, string basePath = null) + private void SortSchemas(OpenApiDocument document) + { + document.Components.Schemas = new SortedDictionary(document.Components.Schemas, _options.SchemaComparer); + } + + private (DocumentFilterContext, OpenApiDocument) GetSwaggerDocumentWithoutPaths(string documentName, string host = null, string basePath = null) { if (!_options.SwaggerDocs.TryGetValue(documentName, out OpenApiInfo info)) + { throw new UnknownSwaggerDocument(documentName, _options.SwaggerDocs.Select(d => d.Key)); + } var applicableApiDescriptions = _apiDescriptionsProvider.ApiDescriptionGroups.Items .SelectMany(group => group.Items) @@ -102,7 +140,6 @@ public OpenApiDocument GetSwagger(string documentName, string host = null, strin { Info = info, Servers = GenerateServers(host, basePath), - Paths = GeneratePaths(applicableApiDescriptions, schemaRepository), Components = new OpenApiComponents { Schemas = schemaRepository.Schemas, @@ -110,10 +147,10 @@ public OpenApiDocument GetSwagger(string documentName, string host = null, strin SecurityRequirements = new List(_options.SecurityRequirements) }; - return (applicableApiDescriptions, swaggerDoc, schemaRepository); + return (new DocumentFilterContext(applicableApiDescriptions, _schemaGenerator, schemaRepository), swaggerDoc); } - private async Task> GetSecuritySchemes() + private async Task> GetSecuritySchemesAsync() { if (!_options.InferSecuritySchemes) { @@ -122,7 +159,7 @@ private async Task> GetSecurityScheme var authenticationSchemes = (_authenticationSchemeProvider is not null) ? await _authenticationSchemeProvider.GetAllSchemesAsync() - : Enumerable.Empty(); + : []; if (_options.SecuritySchemesSelector != null) { @@ -143,19 +180,22 @@ private async Task> GetSecurityScheme }); } - private IList GenerateServers(string host, string basePath) + private List GenerateServers(string host, string basePath) { - if (_options.Servers.Any()) + if (_options.Servers.Count > 0) { return new List(_options.Servers); } return (host == null && basePath == null) - ? new List() - : new List { new OpenApiServer { Url = $"{host}{basePath}" } }; + ? [] + : [new() { Url = $"{host}{basePath}" }]; } - private OpenApiPaths GeneratePaths(IEnumerable apiDescriptions, SchemaRepository schemaRepository) + private async Task GeneratePathsAsync( + IEnumerable apiDescriptions, + SchemaRepository schemaRepository, + Func, SchemaRepository, Task>> operationsGenerator) { var apiDescriptionsByPath = apiDescriptions .OrderBy(_options.SortKeySelector) @@ -167,60 +207,115 @@ private OpenApiPaths GeneratePaths(IEnumerable apiDescriptions, paths.Add($"/{group.Key}", new OpenApiPathItem { - Operations = GenerateOperations(group, schemaRepository) + Operations = await operationsGenerator(group, schemaRepository) }); }; return paths; } - private IDictionary GenerateOperations( + private OpenApiPaths GeneratePaths(IEnumerable apiDescriptions, SchemaRepository schemaRepository) + { + return GeneratePathsAsync( + apiDescriptions, + schemaRepository, + (group, schemaRepository) => Task.FromResult(GenerateOperations(group, schemaRepository))).Result; + } + + private async Task GeneratePathsAsync( IEnumerable apiDescriptions, SchemaRepository schemaRepository) { - var apiDescriptionsByMethod = apiDescriptions + return await GeneratePathsAsync( + apiDescriptions, + schemaRepository, + GenerateOperationsAsync); + } + + private IEnumerable<(OperationType, ApiDescription)> GetOperationsGroupedByMethod( + IEnumerable apiDescriptions) + { + return apiDescriptions .OrderBy(_options.SortKeySelector) - .GroupBy(apiDesc => apiDesc.HttpMethod); + .GroupBy(apiDesc => apiDesc.HttpMethod) + .Select(PrepareGenerateOperation); + } + + private Dictionary GenerateOperations( + IEnumerable apiDescriptions, + SchemaRepository schemaRepository) + { + var apiDescriptionsByMethod = GetOperationsGroupedByMethod(apiDescriptions); + var operations = new Dictionary(); + + foreach ((var operationType, var description) in apiDescriptionsByMethod) + { + operations.Add(operationType, GenerateOperation(description, schemaRepository)); + } + return operations; + } + + private async Task> GenerateOperationsAsync( + IEnumerable apiDescriptions, + SchemaRepository schemaRepository) + { + var apiDescriptionsByMethod = GetOperationsGroupedByMethod(apiDescriptions); var operations = new Dictionary(); - foreach (var group in apiDescriptionsByMethod) + foreach ((var operationType, var description) in apiDescriptionsByMethod) { - var httpMethod = group.Key; + operations.Add(operationType, await GenerateOperationAsync(description, schemaRepository)); + } - if (httpMethod == null) - throw new SwaggerGeneratorException(string.Format( - "Ambiguous HTTP method for action - {0}. " + - "Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0", - group.First().ActionDescriptor.DisplayName)); + return operations; + } - if (group.Count() > 1 && _options.ConflictingActionsResolver == null) - 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", - httpMethod, - group.First().RelativePath, - string.Join(",", group.Select(apiDesc => apiDesc.ActionDescriptor.DisplayName)))); + private (OperationType OperationType, ApiDescription ApiDescription) PrepareGenerateOperation(IGrouping group) + { + var httpMethod = group.Key ?? throw new SwaggerGeneratorException(string.Format( + "Ambiguous HTTP method for action - {0}. " + + "Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0", + group.First().ActionDescriptor.DisplayName)); - var apiDescription = (group.Count() > 1) ? _options.ConflictingActionsResolver(group) : group.Single(); + var count = group.Count(); - var normalizedMethod = httpMethod.ToUpperInvariant(); - if (!OperationTypeMap.TryGetValue(normalizedMethod, out var operationType)) - { - // See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2600 and - // https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2740. - throw new SwaggerGeneratorException($"The \"{httpMethod}\" HTTP method is not supported."); - } + if (count > 1 && _options.ConflictingActionsResolver == null) + { + 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", + httpMethod, + group.First().RelativePath, + string.Join(",", group.Select(apiDesc => apiDesc.ActionDescriptor.DisplayName)))); + } - operations.Add(operationType, GenerateOperation(apiDescription, schemaRepository)); - }; + var apiDescription = (count > 1) ? _options.ConflictingActionsResolver(group) : group.Single(); - return operations; + var normalizedMethod = httpMethod.ToUpperInvariant(); + if (!OperationTypeMap.TryGetValue(normalizedMethod, out var operationType)) + { + // See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2600 and + // https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2740. + throw new SwaggerGeneratorException($"The \"{httpMethod}\" HTTP method is not supported."); + } + + return (operationType, apiDescription); } - private OpenApiOperation GenerateOperation(ApiDescription apiDescription, SchemaRepository schemaRepository) + private async Task GenerateOperationAsync( + ApiDescription apiDescription, + SchemaRepository schemaRepository, + Func>> parametersGenerator, + Func> bodyGenerator, + Func applyFilters) { - OpenApiOperation operation = GenerateOpenApiOperationFromMetadata(apiDescription, schemaRepository); + OpenApiOperation operation = +#if NET6_0_OR_GREATER + GenerateOpenApiOperationFromMetadata(apiDescription, schemaRepository); +#else + null; +#endif try { @@ -228,8 +323,8 @@ private OpenApiOperation GenerateOperation(ApiDescription apiDescription, Schema { Tags = GenerateOperationTags(apiDescription), OperationId = _options.OperationIdSelector(apiDescription), - Parameters = GenerateParameters(apiDescription, schemaRepository), - RequestBody = GenerateRequestBody(apiDescription, schemaRepository), + Parameters = await parametersGenerator(apiDescription, schemaRepository), + RequestBody = await bodyGenerator(apiDescription, schemaRepository), Responses = GenerateResponses(apiDescription, schemaRepository), Deprecated = apiDescription.CustomAttributes().OfType().Any(), #if NET7_0_OR_GREATER @@ -240,10 +335,8 @@ private OpenApiOperation GenerateOperation(ApiDescription apiDescription, Schema apiDescription.TryGetMethodInfo(out MethodInfo methodInfo); var filterContext = new OperationFilterContext(apiDescription, _schemaGenerator, schemaRepository, methodInfo); - foreach (var filter in _options.OperationFilters) - { - filter.Apply(operation, filterContext); - } + + await applyFilters(operation, filterContext); return operation; } @@ -255,9 +348,50 @@ private OpenApiOperation GenerateOperation(ApiDescription apiDescription, Schema } } - private OpenApiOperation GenerateOpenApiOperationFromMetadata(ApiDescription apiDescription, SchemaRepository schemaRepository) + private OpenApiOperation GenerateOperation(ApiDescription apiDescription, SchemaRepository schemaRepository) { + return GenerateOperationAsync( + apiDescription, + schemaRepository, + (description, repository) => Task.FromResult(GenerateParameters(description, repository)), + (description, repository) => Task.FromResult(GenerateRequestBody(description, repository)), + (operation, filterContext) => + { + foreach (var filter in _options.OperationFilters) + { + filter.Apply(operation, filterContext); + } + + return Task.CompletedTask; + }).Result; + } + + private async Task GenerateOperationAsync( + ApiDescription apiDescription, + SchemaRepository schemaRepository) + { + return await GenerateOperationAsync( + apiDescription, + schemaRepository, + GenerateParametersAsync, + GenerateRequestBodyAsync, + async (operation, filterContext) => + { + foreach (var filter in _options.OperationAsyncFilters) + { + await filter.ApplyAsync(operation, filterContext, CancellationToken.None); + } + + foreach (var filter in _options.OperationFilters) + { + filter.Apply(operation, filterContext); + } + }); + } + #if NET6_0_OR_GREATER + private OpenApiOperation GenerateOpenApiOperationFromMetadata(ApiDescription apiDescription, SchemaRepository schemaRepository) + { var metadata = apiDescription.ActionDescriptor?.EndpointMetadata; var operation = metadata?.OfType().SingleOrDefault(); @@ -316,42 +450,68 @@ private OpenApiOperation GenerateOpenApiOperationFromMetadata(ApiDescription api } return operation; -#else - return null; -#endif } +#endif - private IList GenerateOperationTags(ApiDescription apiDescription) + private List GenerateOperationTags(ApiDescription apiDescription) { return _options.TagsSelector(apiDescription) .Select(tagName => new OpenApiTag { Name = tagName }) .ToList(); } - private IList GenerateParameters(ApiDescription apiDescription, SchemaRepository schemaRespository) + private static async Task> GenerateParametersAsync( + ApiDescription apiDescription, + SchemaRepository schemaRespository, + Func> parameterGenerator) { if (apiDescription.ParameterDescriptions.Any(IsFromFormAttributeUsedWithIFormFile)) + { throw new SwaggerGeneratorException(string.Format( "Error reading parameter(s) for action {0} as [FromForm] attribute used with IFormFile. " + "Please refer to https://github.com/domaindrivendev/Swashbuckle.AspNetCore#handle-forms-and-file-uploads for more information", apiDescription.ActionDescriptor.DisplayName)); + } var applicableApiParameters = apiDescription.ParameterDescriptions .Where(apiParam => { - return (!apiParam.IsFromBody() && !apiParam.IsFromForm()) + return !apiParam.IsFromBody() && !apiParam.IsFromForm() && (!apiParam.CustomAttributes().OfType().Any()) && (!apiParam.CustomAttributes().OfType().Any()) && (apiParam.ModelMetadata == null || apiParam.ModelMetadata.IsBindingAllowed) && !apiParam.IsIllegalHeaderParameter(); }); - return applicableApiParameters - .Select(apiParam => GenerateParameter(apiParam, schemaRespository)) - .ToList(); + var parameters = new List(); + + foreach (var parameter in applicableApiParameters) + { + parameters.Add(await parameterGenerator(parameter, schemaRespository)); + } + + return parameters; } - private OpenApiParameter GenerateParameter( + private List GenerateParameters(ApiDescription apiDescription, SchemaRepository schemaRespository) + { + return GenerateParametersAsync( + apiDescription, + schemaRespository, + (parameter, schemaRespository) => Task.FromResult(GenerateParameter(parameter, schemaRespository))).Result; + } + + private async Task> GenerateParametersAsync( + ApiDescription apiDescription, + SchemaRepository schemaRespository) + { + return await GenerateParametersAsync( + apiDescription, + schemaRespository, + GenerateParameterAsync); + } + + private OpenApiParameter GenerateParameterWithoutFilter( ApiParameterDescription apiParameter, SchemaRepository schemaRepository) { @@ -383,7 +543,7 @@ private OpenApiParameter GenerateParameter( description = openApiSchema.Description; } - var parameter = new OpenApiParameter + return new OpenApiParameter { Name = name, In = location, @@ -391,14 +551,49 @@ private OpenApiParameter GenerateParameter( Schema = schema, Description = description }; + } - var filterContext = new ParameterFilterContext( + private (OpenApiParameter, ParameterFilterContext) GenerateParameterAndContext( + ApiParameterDescription apiParameter, + SchemaRepository schemaRepository) + { + var parameter = GenerateParameterWithoutFilter(apiParameter, schemaRepository); + + var context = new ParameterFilterContext( apiParameter, _schemaGenerator, schemaRepository, apiParameter.PropertyInfo(), apiParameter.ParameterInfo()); + return (parameter, context); + } + + private OpenApiParameter GenerateParameter( + ApiParameterDescription apiParameter, + SchemaRepository schemaRepository) + { + var (parameter, filterContext) = GenerateParameterAndContext(apiParameter, schemaRepository); + + foreach (var filter in _options.ParameterFilters) + { + filter.Apply(parameter, filterContext); + } + + return parameter; + } + + private async Task GenerateParameterAsync( + ApiParameterDescription apiParameter, + SchemaRepository schemaRepository) + { + var (parameter, filterContext) = GenerateParameterAndContext(apiParameter, schemaRepository); + + foreach (var filter in _options.ParameterAsyncFilters) + { + await filter.ApplyAsync(parameter, filterContext, CancellationToken.None); + } + foreach (var filter in _options.ParameterFilters) { filter.Apply(parameter, filterContext); @@ -426,7 +621,7 @@ private OpenApiSchema GenerateSchema( } } - private OpenApiRequestBody GenerateRequestBody( + private (OpenApiRequestBody RequestBody, RequestBodyFilterContext FilterContext) GenerateRequestBodyAndFilterContext( ApiDescription apiDescription, SchemaRepository schemaRepository) { @@ -461,6 +656,15 @@ private OpenApiRequestBody GenerateRequestBody( schemaRepository: schemaRepository); } + return (requestBody, filterContext); + } + + private OpenApiRequestBody GenerateRequestBody( + ApiDescription apiDescription, + SchemaRepository schemaRepository) + { + var (requestBody, filterContext) = GenerateRequestBodyAndFilterContext(apiDescription, schemaRepository); + if (requestBody != null) { foreach (var filter in _options.RequestBodyFilters) @@ -472,6 +676,28 @@ private OpenApiRequestBody GenerateRequestBody( return requestBody; } + private async Task GenerateRequestBodyAsync( + ApiDescription apiDescription, + SchemaRepository schemaRepository) + { + var (requestBody, filterContext) = GenerateRequestBodyAndFilterContext(apiDescription, schemaRepository); + + if (requestBody != null) + { + foreach (var filter in _options.RequestBodyAsyncFilters) + { + await filter.ApplyAsync(requestBody, filterContext, CancellationToken.None); + } + + foreach (var filter in _options.RequestBodyFilters) + { + filter.Apply(requestBody, filterContext); + } + } + + return requestBody; + } + private OpenApiRequestBody GenerateRequestBodyFromBodyParameter( ApiDescription apiDescription, SchemaRepository schemaRepository, @@ -501,7 +727,7 @@ private OpenApiRequestBody GenerateRequestBodyFromBodyParameter( }; } - private IEnumerable InferRequestContentTypes(ApiDescription apiDescription) + private static IEnumerable InferRequestContentTypes(ApiDescription apiDescription) { // If there's content types explicitly specified via ConsumesAttribute, use them var explicitContentTypes = apiDescription.CustomAttributes().OfType() @@ -522,7 +748,7 @@ private OpenApiRequestBody GenerateRequestBodyFromFormParameters( IEnumerable formParameters) { var contentTypes = InferRequestContentTypes(apiDescription); - contentTypes = contentTypes.Any() ? contentTypes : new[] { "multipart/form-data" }; + contentTypes = contentTypes.Any() ? contentTypes : ["multipart/form-data"]; var schema = GenerateSchemaFromFormParameters(formParameters, schemaRepository); @@ -620,12 +846,12 @@ private OpenApiResponse GenerateResponse( }; } - private IEnumerable InferResponseContentTypes(ApiDescription apiDescription, ApiResponseType apiResponseType) + private static IEnumerable InferResponseContentTypes(ApiDescription apiDescription, ApiResponseType apiResponseType) { // If there's no associated model type, return an empty list (i.e. no content) if (apiResponseType.ModelMetadata == null && (apiResponseType.Type == null || apiResponseType.Type == typeof(void))) { - return Enumerable.Empty(); + return []; } // If there's content types explicitly specified via ProducesAttribute, use them @@ -640,7 +866,7 @@ private IEnumerable InferResponseContentTypes(ApiDescription apiDescript .Distinct(); if (apiExplorerContentTypes.Any()) return apiExplorerContentTypes; - return Enumerable.Empty(); + return []; } private OpenApiMediaType CreateResponseMediaType(Type modelType, SchemaRepository schemaRespository) @@ -659,27 +885,27 @@ private static bool IsFromFormAttributeUsedWithIFormFile(ApiParameterDescription return fromFormAttribute != null && parameterInfo?.ParameterType == typeof(IFormFile); } - private static readonly Dictionary OperationTypeMap = new Dictionary + private static readonly Dictionary OperationTypeMap = new() { - { "GET", OperationType.Get }, - { "PUT", OperationType.Put }, - { "POST", OperationType.Post }, - { "DELETE", OperationType.Delete }, - { "OPTIONS", OperationType.Options }, - { "HEAD", OperationType.Head }, - { "PATCH", OperationType.Patch }, - { "TRACE", OperationType.Trace } + ["GET"] = OperationType.Get, + ["PUT"] = OperationType.Put, + ["POST"] = OperationType.Post, + ["DELETE"] = OperationType.Delete, + ["OPTIONS"] = OperationType.Options, + ["HEAD"] = OperationType.Head, + ["PATCH"] = OperationType.Patch, + ["TRACE"] = OperationType.Trace, }; - private static readonly Dictionary ParameterLocationMap = new Dictionary + private static readonly Dictionary ParameterLocationMap = new() { - { BindingSource.Query, ParameterLocation.Query }, - { BindingSource.Header, ParameterLocation.Header }, - { BindingSource.Path, ParameterLocation.Path } + [BindingSource.Query] = ParameterLocation.Query, + [BindingSource.Header] = ParameterLocation.Header, + [BindingSource.Path] = ParameterLocation.Path, }; - private static readonly IReadOnlyCollection> ResponseDescriptionMap = new[] - { + private static readonly IReadOnlyCollection> ResponseDescriptionMap = + [ new KeyValuePair("100", "Continue"), new KeyValuePair("101", "Switching Protocols"), new KeyValuePair("1\\d{2}", "Information"), @@ -741,16 +967,16 @@ private static bool IsFromFormAttributeUsedWithIFormFile(ApiParameterDescription new KeyValuePair("5\\d{2}", "Server Error"), new KeyValuePair("default", "Error") - }; + ]; #if NET7_0_OR_GREATER - private string GenerateSummary(ApiDescription apiDescription) => + private static string GenerateSummary(ApiDescription apiDescription) => apiDescription.ActionDescriptor?.EndpointMetadata ?.OfType() .Select(s => s.Summary) .LastOrDefault(); - private string GenerateDescription(ApiDescription apiDescription) => + private static string GenerateDescription(ApiDescription apiDescription) => apiDescription.ActionDescriptor?.EndpointMetadata ?.OfType() .Select(s => s.Description) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs index 48183d3cea..41fb349464 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGeneratorOptions.cs @@ -26,9 +26,13 @@ public SwaggerGeneratorOptions() SecuritySchemes = new Dictionary(); SecurityRequirements = new List(); ParameterFilters = new List(); + ParameterAsyncFilters = new List(); RequestBodyFilters = new List(); + RequestBodyAsyncFilters = new List(); OperationFilters = new List(); + OperationAsyncFilters = new List(); DocumentFilters = new List(); + DocumentAsyncFilters = new List(); } public IDictionary SwaggerDocs { get; set; } @@ -47,7 +51,7 @@ public SwaggerGeneratorOptions() public bool InferSecuritySchemes { get; set; } - public Func, IDictionary> SecuritySchemesSelector { get; set;} + public Func, IDictionary> SecuritySchemesSelector { get; set; } public bool DescribeAllParametersInCamelCase { get; set; } @@ -61,12 +65,20 @@ public SwaggerGeneratorOptions() public IList ParameterFilters { get; set; } + public IList ParameterAsyncFilters { get; set; } + public List RequestBodyFilters { get; set; } + public IList RequestBodyAsyncFilters { get; set; } + public List OperationFilters { get; set; } + public IList OperationAsyncFilters { get; set; } + public IList DocumentFilters { get; set; } + public IList DocumentAsyncFilters { get; set; } + private bool DefaultDocInclusionPredicate(string documentName, ApiDescription apiDescription) { return apiDescription.GroupName == null || apiDescription.GroupName == documentName; diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/ConfigureSwaggerGeneratorOptionsTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/ConfigureSwaggerGeneratorOptionsTests.cs index 2a2522b945..b16c02e894 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(18, publicProperties.Length); + Assert.Equal(22, publicProperties.Length); } [Fact] diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestDocumentFilter.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestDocumentFilter.cs index 015653daf8..5c854acf83 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestDocumentFilter.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestDocumentFilter.cs @@ -1,10 +1,12 @@ -using Microsoft.OpenApi.Any; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.TestSupport; namespace Swashbuckle.AspNetCore.SwaggerGen.Test { - public class TestDocumentFilter : IDocumentFilter + public class TestDocumentFilter : IDocumentFilter, IDocumentAsyncFilter { public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { @@ -12,5 +14,11 @@ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) swaggerDoc.Extensions.Add("X-docName", new OpenApiString(context.DocumentName)); context.SchemaGenerator.GenerateSchema(typeof(ComplexType), context.SchemaRepository); } + + public Task ApplyAsync(OpenApiDocument swaggerDoc, DocumentFilterContext context, CancellationToken cancellationToken) + { + Apply(swaggerDoc, context); + return Task.CompletedTask; + } } -} \ No newline at end of file +} diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestOperationFilter.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestOperationFilter.cs index 47a8f83483..cda3beed97 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestOperationFilter.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestOperationFilter.cs @@ -1,14 +1,22 @@ -using Microsoft.OpenApi.Any; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; namespace Swashbuckle.AspNetCore.SwaggerGen.Test { - public class TestOperationFilter : IOperationFilter + public class TestOperationFilter : IOperationFilter, IOperationAsyncFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { operation.Extensions.Add("X-foo", new OpenApiString("bar")); operation.Extensions.Add("X-docName", new OpenApiString(context.DocumentName)); } + + public Task ApplyAsync(OpenApiOperation operation, OperationFilterContext context, CancellationToken cancellationToken) + { + Apply(operation, context); + return Task.CompletedTask; + } } -} \ No newline at end of file +} diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestParameterFilter.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestParameterFilter.cs index 65e3e26a39..e48781ea2e 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestParameterFilter.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestParameterFilter.cs @@ -1,14 +1,22 @@ -using Microsoft.OpenApi.Any; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; namespace Swashbuckle.AspNetCore.SwaggerGen.Test { - public class TestParameterFilter : IParameterFilter + public class TestParameterFilter : IParameterFilter, IParameterAsyncFilter { public void Apply(OpenApiParameter parameter, ParameterFilterContext context) { parameter.Extensions.Add("X-foo", new OpenApiString("bar")); parameter.Extensions.Add("X-docName", new OpenApiString(context.DocumentName)); } + + public Task ApplyAsync(OpenApiParameter parameter, ParameterFilterContext context, CancellationToken cancellationToken) + { + Apply(parameter, context); + return Task.CompletedTask; + } } -} \ No newline at end of file +} diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestRequestBodyFilter.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestRequestBodyFilter.cs index 663f2ec0bd..67f549bdb3 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestRequestBodyFilter.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/TestRequestBodyFilter.cs @@ -1,14 +1,22 @@ -using Microsoft.OpenApi.Any; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; namespace Swashbuckle.AspNetCore.SwaggerGen.Test { - public class TestRequestBodyFilter : IRequestBodyFilter + public class TestRequestBodyFilter : IRequestBodyFilter, IRequestBodyAsyncFilter { public void Apply(OpenApiRequestBody requestBody, RequestBodyFilterContext context) { requestBody.Extensions.Add("X-foo", new OpenApiString("bar")); requestBody.Extensions.Add("X-docName", new OpenApiString(context.DocumentName)); } + + public Task ApplyAsync(OpenApiRequestBody requestBody, RequestBodyFilterContext context, CancellationToken cancellationToken) + { + Apply(requestBody, context); + return Task.CompletedTask; + } } -} \ No newline at end of file +} diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index 42be2d5451..5cb20af5a2 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -1566,6 +1566,266 @@ public void GetSwagger_SupportsOption_DocumentFilters() Assert.Contains("ComplexType", document.Components.Schemas.Keys); } + [Fact] + public async Task GetSwaggerAsync_SupportsOption_OperationFilters() + { + var subject = Subject( + apiDescriptions: new[] + { + ApiDescriptionFactory.Create( + c => nameof(c.ActionWithNoParameters), groupName: "v1", httpMethod: "POST", relativePath: "resource") + }, + options: new SwaggerGeneratorOptions + { + SwaggerDocs = new Dictionary + { + ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" } + }, + OperationFilters = new List + { + new TestOperationFilter() + } + } + ); + + var document = await subject.GetSwaggerAsync("v1"); + + var operation = document.Paths["/resource"].Operations[OperationType.Post]; + Assert.Equal(2, operation.Extensions.Count); + Assert.Equal("bar", ((OpenApiString)operation.Extensions["X-foo"]).Value); + Assert.Equal("v1", ((OpenApiString)operation.Extensions["X-docName"]).Value); + } + + [Fact] + public async Task GetSwaggerAsync_SupportsOption_OperationAsyncFilters() + { + var subject = Subject( + apiDescriptions: + [ + ApiDescriptionFactory.Create( + c => nameof(c.ActionWithNoParameters), groupName: "v1", httpMethod: "POST", relativePath: "resource") + ], + options: new SwaggerGeneratorOptions + { + SwaggerDocs = new Dictionary + { + ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" } + }, + OperationAsyncFilters = + [ + new TestOperationFilter() + ] + } + ); + + var document = await subject.GetSwaggerAsync("v1"); + + var operation = document.Paths["/resource"].Operations[OperationType.Post]; + Assert.Equal(2, operation.Extensions.Count); + Assert.Equal("bar", ((OpenApiString)operation.Extensions["X-foo"]).Value); + Assert.Equal("v1", ((OpenApiString)operation.Extensions["X-docName"]).Value); + } + + [Fact] + public async Task GetSwaggerAsync_SupportsOption_DocumentAsyncFilters() + { + var subject = Subject( + apiDescriptions: [], + options: new SwaggerGeneratorOptions + { + SwaggerDocs = new Dictionary + { + ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" } + }, + DocumentAsyncFilters = + [ + new TestDocumentFilter() + ] + } + ); + + var document = await subject.GetSwaggerAsync("v1"); + + Assert.Equal(2, document.Extensions.Count); + Assert.Equal("bar", ((OpenApiString)document.Extensions["X-foo"]).Value); + Assert.Equal("v1", ((OpenApiString)document.Extensions["X-docName"]).Value); + Assert.Contains("ComplexType", document.Components.Schemas.Keys); + } + + [Fact] + public async Task GetSwaggerAsync_SupportsOption_DocumentFilters() + { + var subject = Subject( + apiDescriptions: Array.Empty(), + options: new SwaggerGeneratorOptions + { + SwaggerDocs = new Dictionary + { + ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" } + }, + DocumentFilters = + [ + new TestDocumentFilter() + ] + } + ); + + var document = await subject.GetSwaggerAsync("v1"); + + Assert.Equal(2, document.Extensions.Count); + Assert.Equal("bar", ((OpenApiString)document.Extensions["X-foo"]).Value); + Assert.Equal("v1", ((OpenApiString)document.Extensions["X-docName"]).Value); + Assert.Contains("ComplexType", document.Components.Schemas.Keys); + } + + [Fact] + public async Task GetSwaggerAsync_SupportsOption_RequestBodyAsyncFilters() + { + var subject = Subject( + apiDescriptions: new[] + { + ApiDescriptionFactory.Create( + c => nameof(c.ActionWithParameter), + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: new [] + { + new ApiParameterDescription { Name = "param", Source = BindingSource.Body } + }) + }, + options: new SwaggerGeneratorOptions + { + SwaggerDocs = new Dictionary + { + ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" } + }, + RequestBodyAsyncFilters = + [ + new TestRequestBodyFilter() + ] + } + ); + + var document = await subject.GetSwaggerAsync("v1"); + + var operation = document.Paths["/resource"].Operations[OperationType.Post]; + Assert.Equal(2, operation.RequestBody.Extensions.Count); + Assert.Equal("bar", ((OpenApiString)operation.RequestBody.Extensions["X-foo"]).Value); + Assert.Equal("v1", ((OpenApiString)operation.RequestBody.Extensions["X-docName"]).Value); + } + + [Fact] + public async Task GetSwaggerAsync_SupportsOption_RequestBodyFilters() + { + var subject = Subject( + apiDescriptions: new[] + { + ApiDescriptionFactory.Create( + c => nameof(c.ActionWithParameter), + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: new [] + { + new ApiParameterDescription { Name = "param", Source = BindingSource.Body } + }) + }, + options: new SwaggerGeneratorOptions + { + SwaggerDocs = new Dictionary + { + ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" } + }, + RequestBodyFilters = new List + { + new TestRequestBodyFilter() + } + } + ); + + var document = await subject.GetSwaggerAsync("v1"); + + var operation = document.Paths["/resource"].Operations[OperationType.Post]; + Assert.Equal(2, operation.RequestBody.Extensions.Count); + Assert.Equal("bar", ((OpenApiString)operation.RequestBody.Extensions["X-foo"]).Value); + Assert.Equal("v1", ((OpenApiString)operation.RequestBody.Extensions["X-docName"]).Value); + } + + [Fact] + public async Task GetSwaggerAsync_SupportsOption_ParameterFilters() + { + var subject = Subject( + apiDescriptions: new[] + { + ApiDescriptionFactory.Create( + c => nameof(c.ActionWithParameter), + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: new [] + { + new ApiParameterDescription { Name = "param", Source = BindingSource.Query } + }) + }, + options: new SwaggerGeneratorOptions + { + SwaggerDocs = new Dictionary + { + ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" } + }, + ParameterFilters = new List + { + new TestParameterFilter() + } + } + ); + + var document = await subject.GetSwaggerAsync("v1"); + + var operation = document.Paths["/resource"].Operations[OperationType.Post]; + Assert.Equal(2, operation.Parameters[0].Extensions.Count); + Assert.Equal("bar", ((OpenApiString)operation.Parameters[0].Extensions["X-foo"]).Value); + Assert.Equal("v1", ((OpenApiString)operation.Parameters[0].Extensions["X-docName"]).Value); + } + + [Fact] + public async Task GetSwaggerAsync_SupportsOption_ParameterAsyncFilters() + { + var subject = Subject( + apiDescriptions: new[] + { + ApiDescriptionFactory.Create( + c => nameof(c.ActionWithParameter), + groupName: "v1", + httpMethod: "POST", + relativePath: "resource", + parameterDescriptions: new [] + { + new ApiParameterDescription { Name = "param", Source = BindingSource.Query } + }) + }, + options: new SwaggerGeneratorOptions + { + SwaggerDocs = new Dictionary + { + ["v1"] = new OpenApiInfo { Version = "V1", Title = "Test API" } + }, + ParameterAsyncFilters = + [ + new TestParameterFilter() + ] + } + ); + + var document = await subject.GetSwaggerAsync("v1"); + + var operation = document.Paths["/resource"].Operations[OperationType.Post]; + Assert.Equal(2, operation.Parameters[0].Extensions.Count); + Assert.Equal("bar", ((OpenApiString)operation.Parameters[0].Extensions["X-foo"]).Value); + Assert.Equal("v1", ((OpenApiString)operation.Parameters[0].Extensions["X-docName"]).Value); + } + [Theory] [InlineData("connect")] [InlineData("CONNECT")]