Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions Swashbuckle.AspNetCore.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
<Project Path="test/WebSites/TodoApp/TodoApp.csproj" />
<Project Path="test/WebSites/TopLevelSwaggerDoc/TopLevelSwaggerDoc.csproj" />
<Project Path="test/WebSites/WebApi.Aot/WebApi.Aot.csproj" />
<Project Path="test/WebSites/WebApi.Map/WebApi.Map.csproj" />
<Project Path="test/WebSites/WebApi/WebApi.csproj" />
</Folder>
</Solution>
22 changes: 22 additions & 0 deletions docs/configure-and-customize-swaggerui.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,25 @@
<!-- markdownlint-enable MD031 MD033 -->

[swagger-ui]: https://github.com/swagger-api/swagger-ui

## Use `MapSwaggerUI` with endpoint routing

`MapSwaggerUI` is an endpoint-routing alternative to `UseSwaggerUI`. The options API is the same, so all customization examples in this page also apply when using `MapSwaggerUI`.
Comment thread
martincostello marked this conversation as resolved.
Outdated

```cs
app.MapSwagger();
app.MapSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "V1 Docs");
});
```
Comment thread
martincostello marked this conversation as resolved.
Outdated

The main differences are:

- **Use**SwaggerUI adds middleware directly to the request pipeline and returns `IApplicationBuilder`.
- **Map**SwaggerUI maps the Swagger UI via endpoint routing and returns `IEndpointConventionBuilder`.

- Because `MapSwaggerUI` returns an endpoint convention builder, you can attach endpoint metadata/conventions (for example, authorization policies) directly to the mapped UI endpoint.
Comment thread
Strepto marked this conversation as resolved.
Outdated
```csharp

Check failure on line 266 in docs/configure-and-customize-swaggerui.md

View workflow job for this annotation

GitHub Actions / lint

Fenced code blocks should be surrounded by blank lines

docs/configure-and-customize-swaggerui.md:266 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```csharp"] https://github.com/DavidAnson/markdownlint/blob/v0.40.0/doc/md031.md
app.MapSwaggerUI().RequireAuthorization();
```
Comment thread
martincostello marked this conversation as resolved.
Outdated
Comment thread
martincostello marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1 +1 @@

static Microsoft.AspNetCore.Builder.SwaggerUIBuilderExtensions.MapSwaggerUI(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, System.Action<Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIOptions> setupAction = null) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder
64 changes: 54 additions & 10 deletions src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerUI;
Expand All @@ -20,20 +21,63 @@ public static IApplicationBuilder UseSwaggerUI(
this IApplicationBuilder app,
Action<SwaggerUIOptions> setupAction = null)
{
SwaggerUIOptions options;
using (var scope = app.ApplicationServices.CreateScope())
{
options = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<SwaggerUIOptions>>().Value;
setupAction?.Invoke(options);
}
var options = ResolveOptions(app.ApplicationServices, setupAction);
var hostingEnv = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();

EnsureDefaultUrl(options, hostingEnv.ApplicationName);

return app.UseSwaggerUI(options);
}

/// <summary>
/// Maps the SwaggerUI middleware to the specified endpoint route.
/// </summary>
/// <remarks>
/// This is a convenience extension method that combines the registration of the SwaggerUI middleware with endpoint
/// routing. It allows you to specify the route pattern for the SwaggerUI and optionally configure the SwaggerUIOptions.
Comment thread
Strepto marked this conversation as resolved.
Outdated
/// </remarks>
/// <param name="endpoints">Endpoint route builder to which the SwaggerUI middleware will be mapped.</param>
/// <param name="setupAction">Optional setup action to configure the SwaggerUIOptions.</param>
Comment thread
Strepto marked this conversation as resolved.
Outdated
/// <returns>An <see cref="IEndpointConventionBuilder"/> that can be used to further configure the endpoint.</returns>
public static IEndpointConventionBuilder MapSwaggerUI(
this IEndpointRouteBuilder endpoints,
Action<SwaggerUIOptions> setupAction = null)
{
var options = ResolveOptions(endpoints.ServiceProvider, setupAction);
var hostingEnv = endpoints.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
Comment thread
martincostello marked this conversation as resolved.
EnsureDefaultUrl(options, hostingEnv.ApplicationName);

var pipeline = endpoints.CreateApplicationBuilder()
.UseSwaggerUI(options)
.Build();

return endpoints.Map(GetRoutePattern(options.RoutePrefix), pipeline);
}

private static SwaggerUIOptions ResolveOptions(IServiceProvider serviceProvider, Action<SwaggerUIOptions> setupAction)
{
// To simplify the common case, use a default that will work with the SwaggerMiddleware defaults
if (options.ConfigObject.Urls == null)
using var scope = serviceProvider.CreateScope();
var options = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<SwaggerUIOptions>>().Value;
setupAction?.Invoke(options);
return options;
}

private static void EnsureDefaultUrl(SwaggerUIOptions options, string applicationName)
Comment thread
martincostello marked this conversation as resolved.
{
if (options.ConfigObject.Urls != null)
{
var hostingEnv = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
options.ConfigObject.Urls = [new UrlDescriptor { Name = $"{hostingEnv.ApplicationName} v1", Url = "v1/swagger.json" }];
return;
}

return app.UseSwaggerUI(options);
options.ConfigObject.Urls = [new UrlDescriptor { Name = $"{applicationName} v1", Url = "v1/swagger.json" }];
}

private static string GetRoutePattern(string routePrefix)
{
var sanitizedRoutePrefix = routePrefix?.Trim('/');
return string.IsNullOrEmpty(sanitizedRoutePrefix)
? "{**path}"
: $"{sanitizedRoutePrefix}/{{**path}}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,19 @@ public async Task SwaggerDocWithoutSubdirectory(string path, string mediaType)
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(mediaType, response.Content.Headers.ContentType?.MediaType);
}

[Theory]
[InlineData("/swagger/index.html", "text/html")]
[InlineData("/swagger/v1/swagger.json", "application/json")]
[InlineData("/swagger/v1/swagger.yaml", "text/yaml")]
[InlineData("/swagger/v1/swagger.yml", "text/yaml")]
public async Task MapSwaggerUI_And_MapSwagger_ReturnExpectedEndpoints(string path, string mediaType)
{
var client = new WebApplicationFactory<WebApi.Map.Program>().CreateClient();

var response = await client.GetAsync(path, TestContext.Current.CancellationToken);

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal(mediaType, response.Content.Headers.ContentType?.MediaType);
}
}
62 changes: 62 additions & 0 deletions test/WebSites/WebApi.Map/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Reflection;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.Json;

// This exists just to test and verify that the MapSwagger and MapSwaggerUI works as expected.

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<JsonOptions>(
options => options.SerializerOptions.Converters.Add(new JsonStringEnumConverter())
);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new() { Title = "WebApi", Version = "v1" });
});

var app = builder.Build();

app.MapSwagger();
app.MapSwaggerUI();

app.UseHttpsRedirection();

string[] summaries =
[
"Freezing",
"Bracing",
"Chilly",
"Cool",
"Mild",
"Warm",
"Balmy",
"Hot",
"Sweltering",
"Scorching",
];

app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new {
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}
)
.ToArray();
return forecast;
});

app.Run();

namespace WebApi.Map
{
/// <summary>
/// Expose the Program class for use with <c>WebApplicationFactory</c>
/// </summary>
public partial class Program
{
}
}
41 changes: 41 additions & 0 deletions test/WebSites/WebApi.Map/Properties/launchSettings.json
Comment thread
martincostello marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:21394",
"sslPort": 44373
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5205",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7175;http://localhost:5205",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
16 changes: 16 additions & 0 deletions test/WebSites/WebApi.Map/WebApi.Map.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>WebApi.Map</RootNamespace>
<TargetFrameworks>$(DefaultTargetFrameworks)</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Swashbuckle.AspNetCore.Annotations\Swashbuckle.AspNetCore.Annotations.csproj" />
<ProjectReference Include="..\..\..\src\Swashbuckle.AspNetCore.SwaggerUI\Swashbuckle.AspNetCore.SwaggerUI.csproj" />
</ItemGroup>

</Project>
4 changes: 4 additions & 0 deletions test/WebSites/WebApi.Map/WebApi.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@WebApi_HostAddress = http://localhost:5205

GET {{WebApi_HostAddress}}/weatherforecast/
Accept: application/json
8 changes: 8 additions & 0 deletions test/WebSites/WebApi.Map/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions test/WebSites/WebApi.Map/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Loading