From 740c8aeab49ccceaa932800624c286e741cc208b Mon Sep 17 00:00:00 2001 From: martincostello Date: Tue, 4 Mar 2025 09:58:49 +0000 Subject: [PATCH 1/2] Deprecate SerializeAsV2 As a path towards OpenAPI v3.1 support, deprecate the `--serializeasv2`/`SerializeAsV2` options in favour of a new `--openapiversion`/`OpenApiVersion` option that accepts the specification version to use. --- README.md | 2 +- src/Swashbuckle.AspNetCore.Cli/Program.cs | 91 +++++++++++----- .../SwaggerBuilderExtensions.cs | 18 ++-- .../PublicAPI/PublicAPI.Shipped.txt | 4 - .../PublicAPI/PublicAPI.Unshipped.txt | 4 + .../PublicAPI/net6.0/PublicAPI.Shipped.txt | 4 + .../PublicAPI/net6.0/PublicAPI.Unshipped.txt | 0 .../PublicAPI/net8.0/PublicAPI.Shipped.txt | 4 + .../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 0 .../PublicAPI/net9.0/PublicAPI.Shipped.txt | 4 + .../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 0 .../netstandard2.0/PublicAPI.Shipped.txt | 4 + .../netstandard2.0/PublicAPI.Unshipped.txt | 0 .../SwaggerEndpointOptions.cs | 22 +++- .../SwaggerMiddleware.cs | 82 +++++++------- .../SwaggerOptions.cs | 22 +++- .../Swashbuckle.AspNetCore.Swagger.csproj | 4 + .../DependencyInjection/DocumentProvider.cs | 27 ++--- .../ToolTests.cs | 101 ++++++++++++++++-- .../CustomDocumentSerializerTests.cs | 17 ++- .../SwaggerIntegrationTests.cs | 8 +- test/WebSites/Basic/Startup.cs | 13 +-- .../CustomDocumentSerializer/Startup.cs | 9 +- test/WebSites/ReDoc/Startup.cs | 2 +- 24 files changed, 311 insertions(+), 131 deletions(-) create mode 100644 src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net6.0/PublicAPI.Shipped.txt create mode 100644 src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net6.0/PublicAPI.Unshipped.txt create mode 100644 src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net8.0/PublicAPI.Shipped.txt create mode 100644 src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net8.0/PublicAPI.Unshipped.txt create mode 100644 src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net9.0/PublicAPI.Shipped.txt create mode 100644 src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net9.0/PublicAPI.Unshipped.txt create mode 100644 src/Swashbuckle.AspNetCore.Swagger/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt create mode 100644 src/Swashbuckle.AspNetCore.Swagger/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt diff --git a/README.md b/README.md index 5b636066a0..230c14b75b 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,7 @@ By default, Swashbuckle will generate and expose Swagger JSON in version 3.0 of ```csharp app.UseSwagger(c => { - c.SerializeAsV2 = true; + c.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0; }); ``` diff --git a/src/Swashbuckle.AspNetCore.Cli/Program.cs b/src/Swashbuckle.AspNetCore.Cli/Program.cs index 5b22a4ff87..48940ebb1f 100644 --- a/src/Swashbuckle.AspNetCore.Cli/Program.cs +++ b/src/Swashbuckle.AspNetCore.Cli/Program.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using Microsoft.OpenApi; using Microsoft.OpenApi.Writers; using Swashbuckle.AspNetCore.Swagger; @@ -19,6 +20,9 @@ namespace Swashbuckle.AspNetCore.Cli { internal class Program { + private const string OpenApiVersionOption = "--openapiversion"; + private const string SerializeAsV2Flag = "--serializeasv2"; + public static int Main(string[] args) { // Helper to simplify command line parsing etc. @@ -30,7 +34,7 @@ public static int Main(string[] args) // startupassembly and it's transitive dependencies. See https://github.com/dotnet/coreclr/issues/13277 for more. // > dotnet swagger tofile ... - runner.SubCommand("tofile", "retrieves Swagger from a startup assembly, and writes to file ", c => + runner.SubCommand("tofile", "retrieves Swagger from a startup assembly, and writes to file", c => { 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"); @@ -38,17 +42,20 @@ public static int Main(string[] args) 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(OpenApiVersionOption, "output Swagger in the specified version, defaults to 3.0"); c.Option("--yaml", "exports swagger in a yaml format", true); + // TODO Remove this option in the major version that adds support for OpenAPI 3.1 + c.Option(SerializeAsV2Flag, "output Swagger in the V2 format rather than V3 [deprecated]", true); + c.OnRun((namedArgs) => { string subProcessCommandLine = PrepareCommandLine(args, namedArgs); - var subProcess = Process.Start("dotnet", subProcessCommandLine); + using var child = Process.Start("dotnet", subProcessCommandLine); - subProcess.WaitForExit(); - return subProcess.ExitCode; + child.WaitForExit(); + return child.ExitCode; }); }); @@ -60,8 +67,12 @@ public static int Main(string[] args) c.Option("--output", ""); c.Option("--host", ""); c.Option("--basepath", ""); - c.Option("--serializeasv2", "", true); + c.Option(OpenApiVersionOption, ""); c.Option("--yaml", "", true); + + // TODO Remove this option in the major version that adds support for OpenAPI 3.1 + c.Option(SerializeAsV2Flag, "", true); + c.OnRun((namedArgs) => { SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions); @@ -98,24 +109,40 @@ public static int Main(string[] args) writer = new OpenApiJsonWriter(streamWriter); } - if (namedArgs.ContainsKey("--serializeasv2")) + OpenApiSpecVersion specVersion = OpenApiSpecVersion.OpenApi3_0; + + if (namedArgs.TryGetValue(OpenApiVersionOption, out var versionArg)) { - if (swaggerDocumentSerializer != null) + specVersion = versionArg switch { - swaggerDocumentSerializer.SerializeDocument(swagger, writer, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); - } - else - { - swagger.SerializeAsV2(writer); - } + "2.0" => OpenApiSpecVersion.OpenApi2_0, + "3.0" => OpenApiSpecVersion.OpenApi3_0, + _ => throw new NotSupportedException($"The specified OpenAPI version \"{versionArg}\" is not supported."), + }; } - else if (swaggerDocumentSerializer != null) + else if (namedArgs.ContainsKey(SerializeAsV2Flag)) + { + specVersion = OpenApiSpecVersion.OpenApi2_0; + WriteSerializeAsV2DeprecationWarning(); + } + + if (swaggerDocumentSerializer != null) { - swaggerDocumentSerializer.SerializeDocument(swagger, writer, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0); + swaggerDocumentSerializer.SerializeDocument(swagger, writer, specVersion); } else { - swagger.SerializeAsV3(writer); + switch (specVersion) + { + case OpenApiSpecVersion.OpenApi2_0: + swagger.SerializeAsV2(writer); + break; + + case OpenApiSpecVersion.OpenApi3_0: + default: + swagger.SerializeAsV3(writer); + break; + } } if (outputPath != null) @@ -136,10 +163,10 @@ public static int Main(string[] args) { string subProcessCommandLine = PrepareCommandLine(args, namedArgs); - var subProcess = Process.Start("dotnet", subProcessCommandLine); + using var child = Process.Start("dotnet", subProcessCommandLine); - subProcess.WaitForExit(); - return subProcess.ExitCode; + child.WaitForExit(); + return child.ExitCode; }); }); @@ -151,7 +178,7 @@ public static int Main(string[] args) c.OnRun((namedArgs) => { SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions); - IList docNames = new List(); + IList docNames = []; string outputPath = namedArgs.TryGetValue("--output", out var arg1) ? Path.Combine(Directory.GetCurrentDirectory(), arg1) @@ -189,7 +216,7 @@ public static int Main(string[] args) return runner.Run(args); } - private static void SetupAndRetrieveSwaggerProviderAndOptions(System.Collections.Generic.IDictionary namedArgs, out ISwaggerProvider swaggerProvider, out IOptions swaggerOptions) + private static void SetupAndRetrieveSwaggerProviderAndOptions(IDictionary namedArgs, out ISwaggerProvider swaggerProvider, out IOptions swaggerOptions) { // 1) Configure host with provided startupassembly var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath( @@ -203,7 +230,7 @@ private static void SetupAndRetrieveSwaggerProviderAndOptions(System.Collections swaggerOptions = serviceProvider.GetService>(); } - private static string PrepareCommandLine(string[] args, System.Collections.Generic.IDictionary namedArgs) + private static string PrepareCommandLine(string[] args, IDictionary namedArgs) { if (!File.Exists(namedArgs["startupassembly"])) { @@ -226,7 +253,7 @@ private static string PrepareCommandLine(string[] args, System.Collections.Gener EscapePath(runtimeConfig), EscapePath(typeof(Program).GetTypeInfo().Assembly.Location), commandName, - string.Join(" ", subProcessArguments.Select(x => EscapePath(x))) + string.Join(" ", subProcessArguments.Select(EscapePath)) ); return subProcessCommandLine; } @@ -305,5 +332,21 @@ private static bool TryGetCustomHost( host = (THost)factoryMethod.Invoke(null, null); return true; } + + private static void WriteSerializeAsV2DeprecationWarning() + { + const string AppName = "Swashbuckle.AspNetCore.Cli"; + + string message = $"The {SerializeAsV2Flag} flag will be removed in a future version of {AppName}. Use the {OpenApiVersionOption} option instead."; + + Console.WriteLine(message); + + // See https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-a-warning-message + // and https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables + if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is "true") + { + Console.WriteLine($"::warning title={AppName}::{message}"); + } + } } } diff --git a/src/Swashbuckle.AspNetCore.Swagger/DependencyInjection/SwaggerBuilderExtensions.cs b/src/Swashbuckle.AspNetCore.Swagger/DependencyInjection/SwaggerBuilderExtensions.cs index be1f3945a2..505830ff8f 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/DependencyInjection/SwaggerBuilderExtensions.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/DependencyInjection/SwaggerBuilderExtensions.cs @@ -55,22 +55,22 @@ public static IEndpointConventionBuilder MapSwagger( throw new ArgumentException("Pattern must contain '{documentName}' parameter", nameof(pattern)); } - Action endpointSetupAction = options => + var pipeline = endpoints.CreateApplicationBuilder() + .UseSwagger(Configure) + .Build(); + + return endpoints.MapGet(pattern, pipeline); + + void Configure(SwaggerOptions options) { var endpointOptions = new SwaggerEndpointOptions(); setupAction?.Invoke(endpointOptions); options.RouteTemplate = pattern; - options.SerializeAsV2 = endpointOptions.SerializeAsV2; + options.OpenApiVersion = endpointOptions.OpenApiVersion; options.PreSerializeFilters.AddRange(endpointOptions.PreSerializeFilters); - }; - - var pipeline = endpoints.CreateApplicationBuilder() - .UseSwagger(endpointSetupAction) - .Build(); - - return endpoints.MapGet(pattern, pipeline); + } } #endif } diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Shipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Shipped.txt index 5252affd44..9fb8c793a8 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Shipped.txt +++ b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Shipped.txt @@ -13,8 +13,6 @@ Swashbuckle.AspNetCore.Swagger.ISwaggerProvider Swashbuckle.AspNetCore.Swagger.ISwaggerProvider.GetSwagger(string documentName, string host = null, string basePath = null) -> Microsoft.OpenApi.Models.OpenApiDocument Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.PreSerializeFilters.get -> System.Collections.Generic.List> -Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.SerializeAsV2.get -> bool -Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.SerializeAsV2.set -> void Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.SwaggerEndpointOptions() -> void Swashbuckle.AspNetCore.Swagger.SwaggerOptions Swashbuckle.AspNetCore.Swagger.SwaggerOptions.CustomDocumentSerializer.get -> Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentSerializer @@ -22,8 +20,6 @@ Swashbuckle.AspNetCore.Swagger.SwaggerOptions.CustomDocumentSerializer.set -> vo Swashbuckle.AspNetCore.Swagger.SwaggerOptions.PreSerializeFilters.get -> System.Collections.Generic.List> Swashbuckle.AspNetCore.Swagger.SwaggerOptions.RouteTemplate.get -> string Swashbuckle.AspNetCore.Swagger.SwaggerOptions.RouteTemplate.set -> void -Swashbuckle.AspNetCore.Swagger.SwaggerOptions.SerializeAsV2.get -> bool -Swashbuckle.AspNetCore.Swagger.SwaggerOptions.SerializeAsV2.set -> void Swashbuckle.AspNetCore.Swagger.SwaggerOptions.SwaggerOptions() -> void Swashbuckle.AspNetCore.Swagger.UnknownSwaggerDocument Swashbuckle.AspNetCore.Swagger.UnknownSwaggerDocument.UnknownSwaggerDocument(string documentName, System.Collections.Generic.IEnumerable knownDocuments) -> void diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt index 97812dd87b..3297f323a3 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt @@ -1,2 +1,6 @@ Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentMetadataProvider Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentMetadataProvider.GetDocumentNames() -> System.Collections.Generic.IList +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.OpenApiVersion.get -> Microsoft.OpenApi.OpenApiSpecVersion +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.OpenApiVersion.set -> void +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.OpenApiVersion.get -> Microsoft.OpenApi.OpenApiSpecVersion +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.OpenApiVersion.set -> void diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net6.0/PublicAPI.Shipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net6.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..262c06994a --- /dev/null +++ b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net6.0/PublicAPI.Shipped.txt @@ -0,0 +1,4 @@ +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.SerializeAsV2.get -> bool +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.SerializeAsV2.set -> void +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.SerializeAsV2.get -> bool +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.SerializeAsV2.set -> void diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net6.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net8.0/PublicAPI.Shipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net8.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..262c06994a --- /dev/null +++ b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net8.0/PublicAPI.Shipped.txt @@ -0,0 +1,4 @@ +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.SerializeAsV2.get -> bool +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.SerializeAsV2.set -> void +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.SerializeAsV2.get -> bool +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.SerializeAsV2.set -> void diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net8.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net9.0/PublicAPI.Shipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net9.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..262c06994a --- /dev/null +++ b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net9.0/PublicAPI.Shipped.txt @@ -0,0 +1,4 @@ +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.SerializeAsV2.get -> bool +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.SerializeAsV2.set -> void +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.SerializeAsV2.get -> bool +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.SerializeAsV2.set -> void diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/net9.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..262c06994a --- /dev/null +++ b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/netstandard2.0/PublicAPI.Shipped.txt @@ -0,0 +1,4 @@ +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.SerializeAsV2.get -> bool +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.SerializeAsV2.set -> void +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.SerializeAsV2.get -> bool +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.SerializeAsV2.set -> void diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs index 4c8acf4004..ad4b3c24e6 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Text; using Microsoft.AspNetCore.Http; +using Microsoft.OpenApi; using Microsoft.OpenApi.Models; namespace Swashbuckle.AspNetCore.Swagger @@ -10,15 +10,27 @@ public class SwaggerEndpointOptions { public SwaggerEndpointOptions() { - PreSerializeFilters = new List>(); - SerializeAsV2 = false; + PreSerializeFilters = []; + OpenApiVersion = OpenApiSpecVersion.OpenApi3_0; } + /// + /// Return Swagger JSON/YAML in the V2.0 format rather than V3.0. + /// + [Obsolete($"This property will be removed in a future version of Swashbuckle.AspNetCore. Use the {nameof(OpenApiVersion)} property instead.")] + public bool SerializeAsV2 + { + get => OpenApiVersion == OpenApiSpecVersion.OpenApi2_0; + set => OpenApiVersion = value ? OpenApiSpecVersion.OpenApi2_0 : OpenApiSpecVersion.OpenApi3_0; + } /// - /// Return Swagger JSON in the V2 format rather than V3 + /// Gets or sets the OpenAPI (Swagger) document version to use. /// - public bool SerializeAsV2 { get; set; } + /// + /// The default value is . + /// + public OpenApiSpecVersion OpenApiVersion { get; set; } /// /// Actions that can be applied SwaggerDocument's before they're serialized to JSON. diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs index 8f30d68ac6..a2e9f9a77e 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs @@ -16,6 +16,8 @@ namespace Swashbuckle.AspNetCore.Swagger { internal sealed class SwaggerMiddleware { + private static readonly Encoding UTF8WithoutBom = new UTF8Encoding(false); + private readonly RequestDelegate _next; private readonly SwaggerOptions _options; private readonly TemplateMatcher _requestMatcher; @@ -92,7 +94,7 @@ public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvid } catch (UnknownSwaggerDocument) { - RespondWithNotFound(httpContext.Response); + httpContext.Response.StatusCode = 404; } } @@ -133,60 +135,66 @@ private bool RequestingSwaggerDocument(HttpRequest request, out string documentN return false; } - private static void RespondWithNotFound(HttpResponse response) - { - response.StatusCode = 404; - } - private async Task RespondWithSwaggerJson(HttpResponse response, OpenApiDocument swagger) { - response.StatusCode = 200; - response.ContentType = "application/json;charset=utf-8"; + string json; - using var textWriter = new StringWriter(CultureInfo.InvariantCulture); - 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); - } - else + using (var textWriter = new StringWriter(CultureInfo.InvariantCulture)) { - if (_options.CustomDocumentSerializer != null) - _options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0); - else - swagger.SerializeAsV3(jsonWriter); + var openApiWriter = new OpenApiJsonWriter(textWriter); + + SerializeDocument(swagger, openApiWriter); + + json = textWriter.ToString(); } - await response.WriteAsync(textWriter.ToString(), new UTF8Encoding(false)); + response.StatusCode = 200; + response.ContentType = "application/json;charset=utf-8"; + + await response.WriteAsync(json, UTF8WithoutBom); } private async Task RespondWithSwaggerYaml(HttpResponse response, OpenApiDocument swagger) { + string yaml; + + using (var textWriter = new StringWriter(CultureInfo.InvariantCulture)) + { + var openApiWriter = new OpenApiYamlWriter(textWriter); + + SerializeDocument(swagger, openApiWriter); + + yaml = textWriter.ToString(); + } + response.StatusCode = 200; response.ContentType = "text/yaml;charset=utf-8"; - using var textWriter = new StringWriter(CultureInfo.InvariantCulture); - var yamlWriter = new OpenApiYamlWriter(textWriter); - if (_options.SerializeAsV2) + await response.WriteAsync(yaml, UTF8WithoutBom); + } + + private void SerializeDocument( + OpenApiDocument document, + IOpenApiWriter writer) + { + if (_options.CustomDocumentSerializer != null) { - if (_options.CustomDocumentSerializer != null) - _options.CustomDocumentSerializer.SerializeDocument(swagger, yamlWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); - else - swagger.SerializeAsV2(yamlWriter); + _options.CustomDocumentSerializer.SerializeDocument(document, writer, _options.OpenApiVersion); } else { - if (_options.CustomDocumentSerializer != null) - _options.CustomDocumentSerializer.SerializeDocument(swagger, yamlWriter, Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0); - else - swagger.SerializeAsV3(yamlWriter); + switch (_options.OpenApiVersion) + { + case Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0: + document.SerializeAsV2(writer); + break; + + case Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0: + default: + document.SerializeAsV3(writer); + break; + } } - - await response.WriteAsync(textWriter.ToString(), new UTF8Encoding(false)); } } } diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs index bc5fcd1516..47cb46e1b4 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Http; +using Microsoft.OpenApi; using Microsoft.OpenApi.Models; namespace Swashbuckle.AspNetCore.Swagger @@ -11,8 +12,8 @@ public class SwaggerOptions public SwaggerOptions() { - PreSerializeFilters = new List>(); - SerializeAsV2 = false; + PreSerializeFilters = []; + OpenApiVersion = OpenApiSpecVersion.OpenApi3_0; } /// @@ -21,9 +22,22 @@ public SwaggerOptions() public string RouteTemplate { get; set; } = DefaultRouteTemplate; /// - /// Return Swagger JSON/YAML in the V2 format rather than V3 + /// Return Swagger JSON/YAML in the V2.0 format rather than V3.0. /// - public bool SerializeAsV2 { get; set; } + [Obsolete($"This property will be removed in a future version of Swashbuckle.AspNetCore. Use the {nameof(OpenApiVersion)} property instead.")] + public bool SerializeAsV2 + { + get => OpenApiVersion == OpenApiSpecVersion.OpenApi2_0; + set => OpenApiVersion = value ? OpenApiSpecVersion.OpenApi2_0 : OpenApiSpecVersion.OpenApi3_0; + } + + /// + /// Gets or sets the OpenAPI (Swagger) document version to use. + /// + /// + /// The default value is . + /// + public OpenApiSpecVersion OpenApiVersion { get; set; } /// /// Gets or sets an optional custom implementation to use to serialize Swagger documents. diff --git a/src/Swashbuckle.AspNetCore.Swagger/Swashbuckle.AspNetCore.Swagger.csproj b/src/Swashbuckle.AspNetCore.Swagger/Swashbuckle.AspNetCore.Swagger.csproj index 377aff8931..22e830a35e 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/Swashbuckle.AspNetCore.Swagger.csproj +++ b/src/Swashbuckle.AspNetCore.Swagger/Swashbuckle.AspNetCore.Swagger.csproj @@ -38,5 +38,9 @@ + + + + diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs index 401eabba6d..1442009d6e 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Writers; using Swashbuckle.AspNetCore.Swagger; @@ -47,19 +45,24 @@ public async Task GenerateAsync(string documentName, TextWriter writer) // Let UnknownSwaggerDocument or other exception bubble up to caller. var swagger = await _swaggerProvider.GetSwaggerAsync(documentName, host: null, basePath: null); var jsonWriter = new OpenApiJsonWriter(writer); - if (_options.SerializeAsV2) + + if (_options.CustomDocumentSerializer != null) { - if (_options.CustomDocumentSerializer != null) - _options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, OpenApi.OpenApiSpecVersion.OpenApi2_0); - else - swagger.SerializeAsV2(jsonWriter); + _options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, _options.OpenApiVersion); } else { - if (_options.CustomDocumentSerializer != null) - _options.CustomDocumentSerializer.SerializeDocument(swagger, jsonWriter, OpenApi.OpenApiSpecVersion.OpenApi3_0); - else - swagger.SerializeAsV3(jsonWriter); + switch (_options.OpenApiVersion) + { + case OpenApi.OpenApiSpecVersion.OpenApi2_0: + swagger.SerializeAsV2(jsonWriter); + break; + + default: + case OpenApi.OpenApiSpecVersion.OpenApi3_0: + swagger.SerializeAsV3(jsonWriter); + break; + } } } } diff --git a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs index 343c0c5900..a2b7766249 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs @@ -26,12 +26,53 @@ public static void Can_Output_Swagger_Document_Names() [Fact] public static void Throws_When_Startup_Assembly_Does_Not_Exist() { - string[] args = ["tofile", "--output", "swagger.json", "--serializeasv2", "./does_not_exist.dll", "v1"]; + string[] args = ["tofile", "--output", "swagger.json", "--openapiversion", "2.0", "./does_not_exist.dll", "v1"]; Assert.Throws(() => Program.Main(args)); } + [Theory] + [InlineData("a")] + [InlineData("1.9")] + [InlineData("3.2")] + public static void Error_When_OpenApiVersion_Is_Not_Supported(string version) + { + string[] args = + [ + "tofile", + "--output", + "swagger.json", + "--openapiversion", + version, + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]; + Assert.NotEqual(0, Program.Main(args)); + } + [Fact] - public static void Can_Generate_Swagger_Json() + public static void Can_Generate_Swagger_Json_v2_OpenApiVersion() + { + using var document = RunToJsonCommand((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--openapiversion", + "2.0", + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]); + + Assert.Equal("2.0", document.RootElement.GetProperty("swagger").GetString()); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var productsPath = paths.GetProperty("/products"); + Assert.True(productsPath.TryGetProperty("post", out _)); + } + + [Fact] + public static void Can_Generate_Swagger_Json_v2_SerializeAsV2() { using var document = RunToJsonCommand((outputPath) => [ @@ -43,6 +84,50 @@ public static void Can_Generate_Swagger_Json() "v1" ]); + Assert.Equal("2.0", document.RootElement.GetProperty("swagger").GetString()); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var productsPath = paths.GetProperty("/products"); + Assert.True(productsPath.TryGetProperty("post", out _)); + } + + [Fact] + public static void Can_Generate_Swagger_Json_v3() + { + using var document = RunToJsonCommand((outputPath) => + [ + "tofile", + "--output", + outputPath, + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]); + + Assert.StartsWith("3.0.", document.RootElement.GetProperty("openapi").GetString()); + + // verify one of the endpoints + var paths = document.RootElement.GetProperty("paths"); + var productsPath = paths.GetProperty("/products"); + Assert.True(productsPath.TryGetProperty("post", out _)); + } + + [Fact] + public static void Can_Generate_Swagger_Json_v3_OpenApiVersion() + { + using var document = RunToJsonCommand((outputPath) => + [ + "tofile", + "--output", + outputPath, + "--openapiversion", + "3.0", + Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), + "v1" + ]); + + Assert.StartsWith("3.0.", document.RootElement.GetProperty("openapi").GetString()); + // verify one of the endpoints var paths = document.RootElement.GetProperty("paths"); var productsPath = paths.GetProperty("/products"); @@ -61,7 +146,8 @@ public static void Overwrites_Existing_File() "tofile", "--output", outputPath, - "--serializeasv2", + "--openapiversion", + "2.0", Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), "v1" ]; @@ -81,7 +167,8 @@ public static void CustomDocumentSerializer_Writes_Custom_V2_Document() "tofile", "--output", outputPath, - "--serializeasv2", + "--openapiversion", + "2.0", Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), "v1" ]); @@ -117,7 +204,8 @@ public static void Can_Generate_Swagger_Json_ForTopLevelApp() "tofile", "--output", outputPath, - "--serializeasv2", + "--openapiversion", + "2.0", Path.Combine(Directory.GetCurrentDirectory(), "MinimalApp.dll"), "v1" ]); @@ -154,7 +242,8 @@ public static void Creates_New_Folder_Path() "tofile", "--output", outputPath, - "--serializeasv2", + "--openapiversion", + "2.0", Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), "v1" ], GenerateRandomString(5)); diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/CustomDocumentSerializerTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/CustomDocumentSerializerTests.cs index 0809dba42d..74d83798e0 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/CustomDocumentSerializerTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/CustomDocumentSerializerTests.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -74,6 +75,17 @@ public async Task DocumentProvider_Writes_Custom_V3_Document() [Fact] public async Task DocumentProvider_Writes_Custom_V2_Document() + { + await DocumentProviderWritesCustomV2Document( + (options) => options.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); + } + + [Obsolete] + [Fact] + public async Task DocumentProvider_Writes_Custom_V2_Document_SerializeAsV2() + => await DocumentProviderWritesCustomV2Document((options) => options.SerializeAsV2 = true); + + private static async Task DocumentProviderWritesCustomV2Document(Action configure) { var testSite = new TestSite(typeof(CustomDocumentSerializer.Startup)); var server = testSite.BuildServer(); @@ -81,7 +93,8 @@ public async Task DocumentProvider_Writes_Custom_V2_Document() var documentProvider = services.GetService(); var options = services.GetService>(); - options.Value.SerializeAsV2 = true; + + configure(options.Value); using var stream = new MemoryStream(); diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs index 35ee4af447..768fd04c5a 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs @@ -2,10 +2,8 @@ using System.Globalization; using System.Linq; using System.Net.Http; -using System.Reflection; -#if NET8_0_OR_GREATER using System.Net.Http.Json; -#endif +using System.Reflection; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -126,11 +124,9 @@ public async Task SwaggerMiddleware_CanBeConfiguredMultipleTimes( [Theory] [InlineData(typeof(MinimalApp.Program), "/swagger/v1/swagger.json")] [InlineData(typeof(TopLevelSwaggerDoc.Program), "/swagger/v1.json")] -#if NET8_0_OR_GREATER [InlineData(typeof(MvcWithNullable.Program), "/swagger/v1/swagger.json")] [InlineData(typeof(WebApi.Program), "/swagger/v1/swagger.json")] [InlineData(typeof(WebApi.Aot.Program), "/swagger/v1/swagger.json")] -#endif public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_Without_Startup( Type entryPointType, string swaggerRequestUri) @@ -138,7 +134,6 @@ public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_Without_Startup( await SwaggerEndpointReturnsValidSwaggerJson(entryPointType, swaggerRequestUri); } -#if NET8_0_OR_GREATER [Fact] public async Task TypesAreRenderedCorrectly() { @@ -170,7 +165,6 @@ public async Task TypesAreRenderedCorrectly() () => Assert.True(properties.GetProperty("temperatureF").GetProperty("readOnly").GetBoolean()), ]); } -#endif private static async Task SwaggerEndpointReturnsValidSwaggerJson(Type entryPointType, string swaggerRequestUri) { diff --git a/test/WebSites/Basic/Startup.cs b/test/WebSites/Basic/Startup.cs index f0d44a170d..fd0bd7be54 100644 --- a/test/WebSites/Basic/Startup.cs +++ b/test/WebSites/Basic/Startup.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Localization; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; @@ -14,14 +13,6 @@ namespace Basic { public class Startup { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); @@ -72,11 +63,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); - // Expose Swagger/OpenAPI JSON in new (v3) and old (v2) formats + // Expose Swagger/OpenAPI JSON in different formats endpoints.MapSwagger("swagger/{documentName}/swagger.json"); endpoints.MapSwagger("swagger/{documentName}/swaggerv2.json", c => { - c.SerializeAsV2 = true; + c.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0; }); }); diff --git a/test/WebSites/CustomDocumentSerializer/Startup.cs b/test/WebSites/CustomDocumentSerializer/Startup.cs index 5ff5964356..196d301c24 100644 --- a/test/WebSites/CustomDocumentSerializer/Startup.cs +++ b/test/WebSites/CustomDocumentSerializer/Startup.cs @@ -2,13 +2,6 @@ namespace CustomDocumentSerializer; public class Startup { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - public void ConfigureServices(IServiceCollection services) { services.AddControllers(); @@ -33,7 +26,7 @@ public void Configure(IApplicationBuilder app) { endpoints.MapControllers(); endpoints.MapSwagger("swagger/{documentName}/swagger.json"); - endpoints.MapSwagger("swagger/{documentName}/swaggerv2.json", c => c.SerializeAsV2 = true); + endpoints.MapSwagger("swagger/{documentName}/swaggerv2.json", c => c.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0); }); } } diff --git a/test/WebSites/ReDoc/Startup.cs b/test/WebSites/ReDoc/Startup.cs index 6b63f24afc..31067648d1 100644 --- a/test/WebSites/ReDoc/Startup.cs +++ b/test/WebSites/ReDoc/Startup.cs @@ -46,7 +46,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseSwagger(c => { - c.SerializeAsV2 = true; + c.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0; }); app.UseReDoc(c => From ef5ab5345299f00f3d9b8305e8bd4de862e453ff Mon Sep 17 00:00:00 2001 From: martincostello Date: Tue, 18 Mar 2025 16:16:48 +0000 Subject: [PATCH 2/2] Fix merge Fix using statements and Public API members that went missing in merge with default branch. --- .../PublicAPI/PublicAPI.Unshipped.txt | 4 ++++ src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs | 1 + src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs | 1 + 3 files changed, 6 insertions(+) diff --git a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt index e69de29bb2..7d95464edb 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Unshipped.txt @@ -0,0 +1,4 @@ +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.OpenApiVersion.get -> Microsoft.OpenApi.OpenApiSpecVersion +Swashbuckle.AspNetCore.Swagger.SwaggerEndpointOptions.OpenApiVersion.set -> void +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.OpenApiVersion.get -> Microsoft.OpenApi.OpenApiSpecVersion +Swashbuckle.AspNetCore.Swagger.SwaggerOptions.OpenApiVersion.set -> void diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs index 04dcf23a9e..258a4214aa 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerEndpointOptions.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using Microsoft.OpenApi; using Microsoft.OpenApi.Models; namespace Swashbuckle.AspNetCore.Swagger diff --git a/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs b/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs index 3b894ddf0b..0e0fc264fd 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerOptions.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using Microsoft.OpenApi; using Microsoft.OpenApi.Models; namespace Swashbuckle.AspNetCore.Swagger