Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 105 additions & 33 deletions src/Swashbuckle.AspNetCore.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -41,29 +43,7 @@ public static int Main(string[] args)

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");
var commandName = args[0];

var subProcessArguments = new string[args.Length - 1];
if (subProcessArguments.Length > 0)
{
Array.Copy(args, 1, subProcessArguments, 0, subProcessArguments.Length);
}

var subProcessCommandLine = string.Format(
"exec --depsfile {0} --runtimeconfig {1} {2} _{3} {4}", // note the underscore prepended to the command name
EscapePath(depsFile),
EscapePath(runtimeConfig),
EscapePath(typeof(Program).GetTypeInfo().Assembly.Location),
commandName,
string.Join(" ", subProcessArguments.Select(x => EscapePath(x)))
);
string subProcessCommandLine = PrepareCommandLine(args, namedArgs);

var subProcess = Process.Start("dotnet", subProcessCommandLine);

Expand All @@ -84,16 +64,7 @@ public static int Main(string[] args)
c.Option("--yaml", "", true);
c.OnRun((namedArgs) =>
{
// 1) Configure host with provided startupassembly
var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
Path.Combine(Directory.GetCurrentDirectory(), namedArgs["startupassembly"]));

// 2) Build a service container that's based on the startup assembly
var serviceProvider = GetServiceProvider(startupAssembly);

// 3) Retrieve Swagger via configured provider
var swaggerProvider = serviceProvider.GetRequiredService<ISwaggerProvider>();
var swaggerOptions = serviceProvider.GetService<IOptions<SwaggerOptions>>();
SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions);
var swaggerDocumentSerializer = swaggerOptions?.Value?.CustomDocumentSerializer;
var swagger = swaggerProvider.GetSwagger(
namedArgs["swaggerdoc"],
Expand Down Expand Up @@ -156,9 +127,110 @@ public static int Main(string[] args)
});
});

// > dotnet swagger list
runner.SubCommand("list", "retrieves the list of Swagger document names from a startup assembly", c =>
{
c.Argument("startupassembly", "relative path to the application's startup assembly");
c.Option("--output", "relative path where the document names will be output, defaults to stdout");
c.OnRun((namedArgs) =>
{
string subProcessCommandLine = PrepareCommandLine(args, namedArgs);

var subProcess = Process.Start("dotnet", subProcessCommandLine);

subProcess.WaitForExit();
return subProcess.ExitCode;
});
});

// > dotnet swagger _list ... (* should only be invoked via "dotnet exec")
runner.SubCommand("_list", "", c =>
{
c.Argument("startupassembly", "");
c.Option("--output", "");
c.OnRun((namedArgs) =>
{
SetupAndRetrieveSwaggerProviderAndOptions(namedArgs, out var swaggerProvider, out var swaggerOptions);
IList<string> docNames = new List<string>();

string outputPath = namedArgs.TryGetValue("--output", out var arg1)
? Path.Combine(Directory.GetCurrentDirectory(), arg1)
: null;
bool outputViaConsole = outputPath == null;
if (!string.IsNullOrEmpty(outputPath))
{
string directoryPath = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrEmpty(directoryPath) && !Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}
}

using Stream stream = outputViaConsole ? Console.OpenStandardOutput() : File.Create(outputPath);
using StreamWriter writer = new(stream, outputViaConsole ? Console.OutputEncoding : Encoding.UTF8);

if (swaggerProvider is not ISwaggerDocumentMetadataProvider docMetaProvider)
{
writer.WriteLine($"The registered {nameof(ISwaggerProvider)} instance does not implement {nameof(ISwaggerDocumentMetadataProvider)}; unable to list the Swagger document names.");
return -1;
}

docNames = docMetaProvider.GetDocumentNames();

foreach (var name in docNames)
{
writer.WriteLine($"\"{name}\"");
}

return 0;
});
});

return runner.Run(args);
}

private static void SetupAndRetrieveSwaggerProviderAndOptions(System.Collections.Generic.IDictionary<string, string> namedArgs, out ISwaggerProvider swaggerProvider, out IOptions<SwaggerOptions> swaggerOptions)
{
// 1) Configure host with provided startupassembly
var startupAssembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(
Path.Combine(Directory.GetCurrentDirectory(), namedArgs["startupassembly"]));

// 2) Build a service container that's based on the startup assembly
var serviceProvider = GetServiceProvider(startupAssembly);

// 3) Retrieve Swagger via configured provider
swaggerProvider = serviceProvider.GetRequiredService<ISwaggerProvider>();
swaggerOptions = serviceProvider.GetService<IOptions<SwaggerOptions>>();
}

private static string PrepareCommandLine(string[] args, System.Collections.Generic.IDictionary<string, string> 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");
var commandName = args[0];

var subProcessArguments = new string[args.Length - 1];
if (subProcessArguments.Length > 0)
{
Array.Copy(args, 1, subProcessArguments, 0, subProcessArguments.Length);
}

var subProcessCommandLine = string.Format(
"exec --depsfile {0} --runtimeconfig {1} {2} _{3} {4}", // note the underscore prepended to the command name
EscapePath(depsFile),
EscapePath(runtimeConfig),
EscapePath(typeof(Program).GetTypeInfo().Assembly.Location),
commandName,
string.Join(" ", subProcessArguments.Select(x => EscapePath(x)))
);
return subProcessCommandLine;
}

private static string EscapePath(string path)
{
return path.Contains(' ')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Collections.Generic;

namespace Swashbuckle.AspNetCore.Swagger
{
public interface ISwaggerDocumentMetadataProvider
{
IList<string> GetDocumentNames();
}
}
2 changes: 1 addition & 1 deletion src/Swashbuckle.AspNetCore.Swagger/ISwaggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ public UnknownSwaggerDocument(string documentName, IEnumerable<string> knownDocu
string.Join(",", knownDocuments?.Select(x => $"\"{x}\""))))
{}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentMetadataProvider
Swashbuckle.AspNetCore.Swagger.ISwaggerDocumentMetadataProvider.GetDocumentNames() -> System.Collections.Generic.IList<string>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
static Swashbuckle.AspNetCore.SwaggerGen.OpenApiAnyFactory.CreateFromJson(string json, System.Text.Json.JsonSerializerOptions options) -> Microsoft.OpenApi.Any.IOpenApiAny
static Swashbuckle.AspNetCore.SwaggerGen.XmlCommentsTextHelper.Humanize(string text, string xmlCommentEndOfLine) -> string
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetDocumentNames() -> System.Collections.Generic.IList<string>
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.XmlCommentEndOfLine.get -> string
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorOptions.XmlCommentEndOfLine.set -> void
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

namespace Swashbuckle.AspNetCore.SwaggerGen
{
public class SwaggerGenerator : ISwaggerProvider, IAsyncSwaggerProvider
public class SwaggerGenerator : ISwaggerProvider, IAsyncSwaggerProvider, ISwaggerDocumentMetadataProvider
{
private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider;
private readonly ISchemaGenerator _schemaGenerator;
Expand Down Expand Up @@ -112,6 +112,8 @@ public OpenApiDocument GetSwagger(string documentName, string host = null, strin
}
}

public IList<string> GetDocumentNames() => _options.SwaggerDocs.Keys.ToList();

private void SortSchemas(OpenApiDocument document)
{
document.Components.Schemas = new SortedDictionary<string, OpenApiSchema>(document.Components.Schemas, _options.SchemaComparer);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
static Microsoft.AspNetCore.Builder.SwaggerUIOptionsExtensions.EnableSwaggerDocumentUrlsEndpoint(this Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions options) -> void
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.ExposeSwaggerDocumentUrlsRoute.get -> bool
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.ExposeSwaggerDocumentUrlsRoute.set -> void
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.SwaggerDocumentUrlsPath.get -> string
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions.SwaggerDocumentUrlsPath.set -> void
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#if NET6_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
Expand All @@ -8,6 +9,7 @@ namespace Swashbuckle.AspNetCore.SwaggerUI;

[JsonSerializable(typeof(ConfigObject))]
[JsonSerializable(typeof(InterceptorFunctions))]
[JsonSerializable(typeof(List<UrlDescriptor>))]
[JsonSerializable(typeof(OAuthConfigObject))]
// These primitive types are declared for common types that may be used with ConfigObject.AdditionalItems. See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2884.
[JsonSerializable(typeof(bool))]
Expand Down
41 changes: 40 additions & 1 deletion src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -88,6 +90,13 @@ public async Task Invoke(HttpContext httpContext)
await RespondWithFile(httpContext.Response, match.Groups[1].Value);
return;
}

var pattern = $"^/?{Regex.Escape(_options.RoutePrefix)}/{_options.SwaggerDocumentUrlsPath}/?$";
if (Regex.IsMatch(path, pattern, RegexOptions.IgnoreCase))
{
await RespondWithDocumentUrls(httpContext.Response);
return;
}
}

await _staticFileMiddleware.Invoke(httpContext);
Expand Down Expand Up @@ -150,6 +159,36 @@ private async Task RespondWithFile(HttpResponse response, string fileName)
}
}

#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage(
"AOT",
"IL2026:RequiresUnreferencedCode",
Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")]
[UnconditionalSuppressMessage(
"AOT",
"IL3050:RequiresDynamicCode",
Justification = "Method is only called if the user provides their own custom JsonSerializerOptions.")]
#endif
private async Task RespondWithDocumentUrls(HttpResponse response)
{
response.StatusCode = 200;

response.ContentType = "application/javascript;charset=utf-8";
string json = "[]";

#if NET6_0_OR_GREATER
if (_jsonSerializerOptions is null)
{
var l = new List<UrlDescriptor>(_options.ConfigObject.Urls);
json = JsonSerializer.Serialize(l, SwaggerUIOptionsJsonContext.Default.ListUrlDescriptor);
}
#endif

json ??= JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions);

await response.WriteAsync(json, Encoding.UTF8);
}

#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage(
"AOT",
Expand Down
12 changes: 12 additions & 0 deletions src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ public class SwaggerUIOptions
/// Gets or sets the path or URL to the Swagger UI CSS file.
/// </summary>
public string StylesPath { get; set; } = "./swagger-ui.css";

/// <summary>
/// Gets or sets whether to expose the <c><see cref="SwaggerUIOptions.ConfigObject">ConfigObject</see>.Urls</c> object via an
/// HTTP endpoint with the URL specified by <see cref="SwaggerDocumentUrlsPath"/>
/// so that external code can auto-discover all Swagger documents.
/// </summary>
public bool ExposeSwaggerDocumentUrlsRoute { get; set; } = false;

/// <summary>
/// Gets or sets the relative URL path to the route that exposes the values of the configured <see cref="ConfigObject.Urls"/> values.
/// </summary>
public string SwaggerDocumentUrlsPath { get; set; } = "documentUrls";
}

public class ConfigObject
Expand Down
14 changes: 12 additions & 2 deletions src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIOptionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ public static void ValidatorUrl(this SwaggerUIOptions options, string url)

/// <summary>
/// You can use this parameter to enable the swagger-ui's built-in validator (badge) functionality
/// Setting it to null will disable validation
/// Setting it to null will disable validation
/// </summary>
/// <param name="options"></param>
/// <param name="url"></param>
Expand Down Expand Up @@ -239,7 +239,7 @@ public static void OAuthUsername(this SwaggerUIOptions options, string value)
/// <param name="value"></param>
/// <remarks>Setting this exposes the client secrets in inline javascript in the swagger-ui generated html.</remarks>
public static void OAuthClientSecret(this SwaggerUIOptions options, string value)
{
{
options.OAuthConfigObject.ClientSecret = value;
}

Expand Down Expand Up @@ -333,5 +333,15 @@ public static void UseResponseInterceptor(this SwaggerUIOptions options, string
{
options.Interceptors.ResponseInterceptorFunction = value;
}

/// <summary>
/// Function to enable the <see cref="SwaggerUIOptions.ExposeSwaggerDocumentUrlsRoute"/> option to expose the available
/// Swagger document urls to external parties.
/// </summary>
/// <param name="options"></param>
public static void EnableSwaggerDocumentUrlsEndpoint(this SwaggerUIOptions options)
{
options.ExposeSwaggerDocumentUrlsRoute = true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ProjectReference Include="..\WebSites\MinimalAppWithHostedServices\MinimalAppWithHostedServices.csproj" />
<ProjectReference Include="..\WebSites\MinimalApp\MinimalApp.csproj" />
<ProjectReference Include="..\..\src\Swashbuckle.AspNetCore.Cli\Swashbuckle.AspNetCore.Cli.csproj" />
<ProjectReference Include="..\WebSites\MultipleVersions\MultipleVersions.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading