diff --git a/README.md b/README.md index 60db15fd5b..59a2552fdb 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 377acb5240..fe516fbc7d 100644 --- a/src/Swashbuckle.AspNetCore.Cli/Program.cs +++ b/src/Swashbuckle.AspNetCore.Cli/Program.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; +using Microsoft.OpenApi; using Microsoft.OpenApi.Writers; using Swashbuckle.AspNetCore.Swagger; @@ -15,6 +16,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. @@ -26,7 +30,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"); @@ -34,17 +38,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; }); }); @@ -56,8 +63,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); @@ -94,24 +105,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) @@ -132,10 +159,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; }); }); @@ -147,7 +174,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) @@ -185,7 +212,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( @@ -199,7 +226,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"])) { @@ -222,7 +249,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; } @@ -301,5 +328,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 f381631052..db9c813e47 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/DependencyInjection/SwaggerBuilderExtensions.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/DependencyInjection/SwaggerBuilderExtensions.cs @@ -53,22 +53,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 0b8e3fa25f..60897ad016 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Shipped.txt +++ b/src/Swashbuckle.AspNetCore.Swagger/PublicAPI/PublicAPI.Shipped.txt @@ -15,8 +15,6 @@ Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentMetadataProvider Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentMetadataProvider.GetDocumentNames() -> System.Collections.Generic.IList 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 @@ -24,8 +22,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 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/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 a6d60c483c..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.AspNetCore.Http; +using Microsoft.OpenApi; using Microsoft.OpenApi.Models; namespace Swashbuckle.AspNetCore.Swagger @@ -7,15 +8,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 80a48f9958..1e63306458 100644 --- a/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs +++ b/src/Swashbuckle.AspNetCore.Swagger/SwaggerMiddleware.cs @@ -14,6 +14,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; @@ -90,7 +92,7 @@ public async Task Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvid } catch (UnknownSwaggerDocument) { - RespondWithNotFound(httpContext.Response); + httpContext.Response.StatusCode = 404; } } @@ -131,60 +133,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 212fc7a123..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.AspNetCore.Http; +using Microsoft.OpenApi; using Microsoft.OpenApi.Models; namespace Swashbuckle.AspNetCore.Swagger @@ -9,8 +10,8 @@ public class SwaggerOptions public SwaggerOptions() { - PreSerializeFilters = new List>(); - SerializeAsV2 = false; + PreSerializeFilters = []; + OpenApiVersion = OpenApiSpecVersion.OpenApi3_0; } /// @@ -19,9 +20,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 4108282405..4a69b96afd 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 f3a2867361..c0bf9d89ae 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/DocumentProvider.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Microsoft.OpenApi.Writers; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; @@ -42,19 +42,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 127cdc598e..f26c4920b8 100644 --- a/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs +++ b/test/Swashbuckle.AspNetCore.Cli.Test/ToolTests.cs @@ -24,12 +24,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) => [ @@ -41,6 +82,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"); @@ -59,7 +144,8 @@ public static void Overwrites_Existing_File() "tofile", "--output", outputPath, - "--serializeasv2", + "--openapiversion", + "2.0", Path.Combine(Directory.GetCurrentDirectory(), "Basic.dll"), "v1" ]; @@ -79,7 +165,8 @@ public static void CustomDocumentSerializer_Writes_Custom_V2_Document() "tofile", "--output", outputPath, - "--serializeasv2", + "--openapiversion", + "2.0", Path.Combine(Directory.GetCurrentDirectory(), "CustomDocumentSerializer.dll"), "v1" ]); @@ -115,7 +202,8 @@ public static void Can_Generate_Swagger_Json_ForTopLevelApp() "tofile", "--output", outputPath, - "--serializeasv2", + "--openapiversion", + "2.0", Path.Combine(Directory.GetCurrentDirectory(), "MinimalApp.dll"), "v1" ]); @@ -152,7 +240,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 9cb4f0522d..9dfbbc5c7d 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/CustomDocumentSerializerTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/CustomDocumentSerializerTests.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using System.Text.Json; using Microsoft.Extensions.ApiDescriptions; using Microsoft.Extensions.DependencyInjection; @@ -71,6 +71,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(); @@ -78,7 +89,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 0428ad1c71..03aa71304f 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerIntegrationTests.cs @@ -119,11 +119,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) @@ -131,7 +129,6 @@ public async Task SwaggerEndpoint_ReturnsValidSwaggerJson_Without_Startup( await SwaggerEndpointReturnsValidSwaggerJson(entryPointType, swaggerRequestUri); } -#if NET8_0_OR_GREATER [Fact] public async Task TypesAreRenderedCorrectly() { @@ -169,7 +166,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 da29c4b90d..54c8abc7c7 100644 --- a/test/WebSites/Basic/Startup.cs +++ b/test/WebSites/Basic/Startup.cs @@ -8,14 +8,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(); @@ -66,11 +58,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 06e321c72b..4c392502c4 100644 --- a/test/WebSites/ReDoc/Startup.cs +++ b/test/WebSites/ReDoc/Startup.cs @@ -40,7 +40,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseSwagger(c => { - c.SerializeAsV2 = true; + c.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi2_0; }); app.UseReDoc(c =>