diff --git a/Swashbuckle.AspNetCore.slnx b/Swashbuckle.AspNetCore.slnx index 5ba846053c..a6e0f06356 100644 --- a/Swashbuckle.AspNetCore.slnx +++ b/Swashbuckle.AspNetCore.slnx @@ -80,6 +80,7 @@ + diff --git a/docs/configure-and-customize-redoc.md b/docs/configure-and-customize-redoc.md index 82f24a8166..965a125bb6 100644 --- a/docs/configure-and-customize-redoc.md +++ b/docs/configure-and-customize-redoc.md @@ -1,137 +1,169 @@ -# Configuration and Customization of `Swashbuckle.AspNetCore.ReDoc` - -## Change Relative Path to the UI - -By default, the Redoc UI will be exposed at `/api-docs`. If necessary, you can alter this when enabling the Redoc middleware: - - - - -```cs -app.UseReDoc(options => -{ - options.RoutePrefix = "docs"; -}); -``` -snippet source | anchor - - - -## Change Document Title - -By default, the Redoc UI will have a generic document title. You can alter this when enabling the Redoc middleware: - - - - -```cs -app.UseReDoc(options => -{ - options.DocumentTitle = "My API Docs"; -}); -``` -snippet source | anchor - - - -## Apply Redoc Parameters - -Redoc ships with its own set of configuration parameters, all described [in the Redoc documentation][redoc-options]. -In Swashbuckle.AspNetCore, most of these are surfaced through the Redoc middleware options: - - - - -```cs -app.UseReDoc(options => -{ - options.SpecUrl("/v1/swagger.json"); - options.EnableUntrustedSpec(); - options.ScrollYOffset(10); - options.HideHostname(); - options.HideDownloadButton(); - options.ExpandResponses("200,201"); - options.RequiredPropsFirst(); - options.NoAutoAuth(); - options.PathInMiddlePanel(); - options.HideLoading(); - options.NativeScrollbars(); - options.DisableSearch(); - options.OnlyRequiredInSamples(); - options.SortPropsAlphabetically(); -}); -``` -snippet source | anchor - - - -> [!NOTE] -> Using `options.SpecUrl("/v1/swagger.json")` multiple times within the same `UseReDoc(...)` will not add multiple URLs. - -## Inject Custom CSS - -To tweak the look and feel, you can inject additional CSS stylesheets by adding them to your `wwwroot` folder and specifying -the relative paths in the middleware options: - - - - -```cs -app.UseReDoc(options => -{ - options.InjectStylesheet("/redoc/custom.css"); -}); -``` -snippet source | anchor - - - -It is also possible to modify the theme by using the `AdditionalItems` property. More information can be found -[in the Redoc documentation][redoc-options]. - - - - -```cs -app.UseReDoc(options => -{ - options.ConfigObject.AdditionalItems = new Dictionary - { - // Configured additional options - }; -}); -``` -snippet source | anchor - - - -## Customize index.html - -To customize the UI beyond the basic options listed above, you can provide your own version of the Redoc `index.html` page: - - - - -```cs -app.UseReDoc(options => -{ - options.IndexStream = () => typeof(Program).Assembly - .GetManifestResourceStream("CustomIndex.ReDoc.index.html"); // Requires file to be added as an embedded resource -}); -``` -snippet source | anchor - - - -```xml - - - - - -``` - -> [!TIP] -> To get started, you should base your custom `index.html` on the [default version](../src/Swashbuckle.AspNetCore.ReDoc/index.html). - -[redoc-options]: https://github.com/Redocly/redoc/blob/main/docs/deployment/html.md#the-redoc-object +# Configuration and Customization of `Swashbuckle.AspNetCore.ReDoc` + +## Change Relative Path to the UI + +By default, the Redoc UI will be exposed at `/api-docs`. If necessary, you can alter this when enabling the Redoc middleware: + + + + +```cs +app.UseReDoc(options => +{ + options.RoutePrefix = "docs"; +}); +``` +snippet source | anchor + + + +## Change Document Title + +By default, the Redoc UI will have a generic document title. You can alter this when enabling the Redoc middleware: + + + + +```cs +app.UseReDoc(options => +{ + options.DocumentTitle = "My API Docs"; +}); +``` +snippet source | anchor + + + +## Apply Redoc Parameters + +Redoc ships with its own set of configuration parameters, all described [in the Redoc documentation][redoc-options]. +In Swashbuckle.AspNetCore, most of these are surfaced through the Redoc middleware options: + + + + +```cs +app.UseReDoc(options => +{ + options.SpecUrl("/v1/swagger.json"); + options.EnableUntrustedSpec(); + options.ScrollYOffset(10); + options.HideHostname(); + options.HideDownloadButton(); + options.ExpandResponses("200,201"); + options.RequiredPropsFirst(); + options.NoAutoAuth(); + options.PathInMiddlePanel(); + options.HideLoading(); + options.NativeScrollbars(); + options.DisableSearch(); + options.OnlyRequiredInSamples(); + options.SortPropsAlphabetically(); +}); +``` +snippet source | anchor + + + +> [!NOTE] +> Using `options.SpecUrl("/v1/swagger.json")` multiple times within the same `UseReDoc(...)` will not add multiple URLs. + +## Inject Custom CSS + +To tweak the look and feel, you can inject additional CSS stylesheets by adding them to your `wwwroot` folder and specifying +the relative paths in the middleware options: + + + + +```cs +app.UseReDoc(options => +{ + options.InjectStylesheet("/redoc/custom.css"); +}); +``` +snippet source | anchor + + + +It is also possible to modify the theme by using the `AdditionalItems` property. More information can be found +[in the Redoc documentation][redoc-options]. + + + + +```cs +app.UseReDoc(options => +{ + options.ConfigObject.AdditionalItems = new Dictionary + { + // Configured additional options + }; +}); +``` +snippet source | anchor + + + +## Customize index.html + +To customize the UI beyond the basic options listed above, you can provide your own version of the Redoc `index.html` page: + + + + +```cs +app.UseReDoc(options => +{ + options.IndexStream = () => typeof(Program).Assembly + .GetManifestResourceStream("CustomIndex.ReDoc.index.html"); // Requires file to be added as an embedded resource +}); +``` +snippet source | anchor + + + +```xml + + + + + +``` + +> [!TIP] +> To get started, you should base your custom `index.html` on the [default version](../src/Swashbuckle.AspNetCore.ReDoc/index.html). + +[redoc-options]: https://github.com/Redocly/redoc/blob/main/docs/deployment/html.md#the-redoc-object + +## Use `MapReDoc` with endpoint routing + +`MapReDoc` is an endpoint-routing alternative to `UseReDoc`. The options API is the same, so all customization examples on this page also apply when using `MapReDoc`. + + + + +```cs +app.MapReDoc(); +``` +snippet source | anchor + + + +The main differences are: + +- **Use**ReDoc adds middleware directly to the request pipeline and returns `IApplicationBuilder`. +- **Map**ReDoc maps ReDoc via endpoint routing and returns `IEndpointConventionBuilder`. + +- Because `MapReDoc` returns an endpoint convention builder, you can attach endpoint metadata/conventions (for example, authorization policies) directly to the mapped ReDoc endpoint. + + + + +```cs +app.MapReDoc("redoc-auth") + .RequireAuthorization(); // Remember to also add RequireAuthorization to MapSwagger +``` +snippet source | anchor + + diff --git a/docs/configure-and-customize-swaggerui.md b/docs/configure-and-customize-swaggerui.md index 8dfcf90efa..f248fd0a95 100644 --- a/docs/configure-and-customize-swaggerui.md +++ b/docs/configure-and-customize-swaggerui.md @@ -1,246 +1,279 @@ -# Configuration and Customization of `Swashbuckle.AspNetCore.SwaggerUI` - -## Change Relative Path to the UI - -By default, the Swagger UI will be exposed at `/swagger`. If necessary, you can alter this when enabling the SwaggerUI middleware: - - - - -```cs -app.UseSwaggerUI(options => -{ - options.RoutePrefix = "api-docs"; -}); -``` -snippet source | anchor - - - -## Change Document Title - -By default, the Swagger UI will have a generic document title. When you have multiple OpenAPI documents open, it can be difficult to -tell them apart. You can alter this when enabling the SwaggerUI middleware: - - - - -```cs -app.UseSwaggerUI(options => -{ - options.DocumentTitle = "My Swagger UI"; -}); -``` -snippet source | anchor - - - -## Change CSS or JS Paths - -By default, the Swagger UI includes default CSS and JavaScript, but if you wish to change the path or URL (for example to use a CDN) -you can override the defaults as shown below: - - - - -```cs -app.UseSwaggerUI(options => -{ - options.StylesPath = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.29.1/swagger-ui.min.css"; - options.ScriptBundlePath = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.29.1/swagger-ui-bundle.min.js"; - options.ScriptPresetsPath = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.29.1/swagger-ui-standalone-preset.min.js"; -}); -``` -snippet source | anchor - - - -## List Multiple OpenAPI Documents - -When enabling the middleware, you're required to specify one or more OpenAPI endpoints (fully qualified or relative to the UI page) to -power the UI. If you provide multiple endpoints, they'll be listed in the top right corner of the page, allowing users to toggle between -the different documents. For example, the following configuration could be used to document different versions of an API. - - - - -```cs -app.UseSwaggerUI(options => -{ - options.SwaggerEndpoint("/swagger/v1/swagger.json", "V1 Docs"); - options.SwaggerEndpoint("/swagger/v2/swagger.json", "V2 Docs"); -}); -``` -snippet source | anchor - - - -## Apply swagger-ui Parameters - -[swagger-ui][swagger-ui] ships with its own set of configuration parameters, all described -[by the swagger-ui Configuration](https://github.com/swagger-api/swagger-ui/blob/HEAD/docs/usage/configuration.md#display). -In Swashbuckle.AspNetCore, most of these are surfaced through the SwaggerUI middleware options: - - - - -```cs -app.UseSwaggerUI(options => -{ - options.DefaultModelExpandDepth(2); - options.DefaultModelRendering(ModelRendering.Model); - options.DefaultModelsExpandDepth(-1); - options.DisplayOperationId(); - options.DisplayRequestDuration(); - options.DocExpansion(DocExpansion.None); - options.EnableDeepLinking(); - options.EnableFilter(); - options.EnablePersistAuthorization(); - options.EnableTryItOutByDefault(); - options.MaxDisplayedTags(5); - options.ShowExtensions(); - options.ShowCommonExtensions(); - options.EnableValidator(); - options.SupportedSubmitMethods(SubmitMethod.Get, SubmitMethod.Head); - options.UseRequestInterceptor("(request) => { return request; }"); - options.UseResponseInterceptor("(response) => { return response; }"); -}); -``` -snippet source | anchor - - - -## Inject Custom JavaScript - -To tweak the behavior, you can inject additional JavaScript files by adding them to your `wwwroot` folder and specifying -the relative paths in the middleware options: - - - - -```cs -app.UseSwaggerUI(options => -{ - options.InjectJavascript("/swagger-ui/custom.js"); -}); -``` -snippet source | anchor - - - -## Inject Custom CSS - -To tweak the look and feel, you can inject additional CSS stylesheets by adding them to your `wwwroot` folder and specifying the -relative paths in the middleware options: - - - - -```cs -app.UseSwaggerUI(options => -{ - options.InjectStylesheet("/swagger-ui/custom.css"); -}); -``` -snippet source | anchor - - - -## Customize index.html - -To customize the UI beyond the basic options listed above, you can provide your own version of the swagger-ui `index.html` page: - - - - -```cs -app.UseSwaggerUI(options => -{ - options.IndexStream = () => typeof(Program).Assembly - .GetManifestResourceStream("CustomUIIndex.Swagger.index.html"); // Requires file to be added as an embedded resource -}); -``` -snippet source | anchor - - - -```xml - - - - - -``` - -> [!TIP] -> To get started, you should base your custom `index.html` on the [built-in version](../src/Swashbuckle.AspNetCore.SwaggerUI/index.html). - -## Enable OAuth2.0 Flows - -[swagger-ui][swagger-ui] has built-in support to participate in OAuth2.0 authorization flows. It interacts with authorization and/or token -endpoints, as specified in the OpenAPI JSON, to obtain access tokens for subsequent API calls. See -[Adding Security Definitions and Requirements](configure-and-customize-swaggergen.md#add-security-definitions-and-requirements) for -an example of adding OAuth2.0 metadata to the generated Swagger. - -If your OpenAPI endpoint includes the appropriate security metadata, the UI interaction should be automatically enabled. However, you -can further customize OAuth support in the UI with the following settings below. See the -[Swagger-UI documentation](https://github.com/swagger-api/swagger-ui/blob/HEAD/docs/usage/oauth2.md) for more information. - - - - -```cs -app.UseSwaggerUI(options => -{ - options.OAuthClientId("test-id"); - options.OAuthClientSecret("test-secret"); - options.OAuthUsername("test-user"); - options.OAuthRealm("test-realm"); - options.OAuthAppName("test-app"); - options.OAuth2RedirectUrl("url"); - options.OAuthScopeSeparator(" "); - options.OAuthScopes("scope1", "scope2"); - options.OAuthAdditionalQueryStringParams(new Dictionary { ["foo"] = "bar" }); - options.OAuthUseBasicAuthenticationWithAccessCodeGrant(); - options.OAuthUsePkce(); -}); -``` -snippet source | anchor - - - -## Use client-side request and response interceptors - -To use custom interceptors on requests and responses going through swagger-ui you can define them as JavaScript functions -in the configuration: - - - - -```cs -app.UseSwaggerUI(options => -{ - options.UseRequestInterceptor("(req) => { req.headers['x-my-custom-header'] = 'MyCustomValue'; return req; }"); - options.UseResponseInterceptor("(res) => { console.log('Custom interceptor intercepted response from:', res.url); return res; }"); -}); -``` -snippet source | anchor - - - -This can be useful in a range of scenarios where you might want to append local XSRF tokens to all requests, for example: - - - - -```cs -app.UseSwaggerUI(options => -{ - options.UseRequestInterceptor("(req) => { req.headers['X-XSRF-Token'] = localStorage.getItem('xsrf-token'); return req; }"); -}); -``` -snippet source | anchor - - - -[swagger-ui]: https://github.com/swagger-api/swagger-ui +# Configuration and Customization of `Swashbuckle.AspNetCore.SwaggerUI` + +## Change Relative Path to the UI + +By default, the Swagger UI will be exposed at `/swagger`. If necessary, you can alter this when enabling the SwaggerUI middleware: + + + + +```cs +app.UseSwaggerUI(options => +{ + options.RoutePrefix = "api-docs"; +}); +``` +snippet source | anchor + + + +## Change Document Title + +By default, the Swagger UI will have a generic document title. When you have multiple OpenAPI documents open, it can be difficult to +tell them apart. You can alter this when enabling the SwaggerUI middleware: + + + + +```cs +app.UseSwaggerUI(options => +{ + options.DocumentTitle = "My Swagger UI"; +}); +``` +snippet source | anchor + + + +## Change CSS or JS Paths + +By default, the Swagger UI includes default CSS and JavaScript, but if you wish to change the path or URL (for example to use a CDN) +you can override the defaults as shown below: + + + + +```cs +app.UseSwaggerUI(options => +{ + options.StylesPath = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.29.1/swagger-ui.min.css"; + options.ScriptBundlePath = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.29.1/swagger-ui-bundle.min.js"; + options.ScriptPresetsPath = "https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.29.1/swagger-ui-standalone-preset.min.js"; +}); +``` +snippet source | anchor + + + +## List Multiple OpenAPI Documents + +When enabling the middleware, you're required to specify one or more OpenAPI endpoints (fully qualified or relative to the UI page) to +power the UI. If you provide multiple endpoints, they'll be listed in the top right corner of the page, allowing users to toggle between +the different documents. For example, the following configuration could be used to document different versions of an API. + + + + +```cs +app.UseSwaggerUI(options => +{ + options.SwaggerEndpoint("/swagger/v1/swagger.json", "V1 Docs"); + options.SwaggerEndpoint("/swagger/v2/swagger.json", "V2 Docs"); +}); +``` +snippet source | anchor + + + +## Apply swagger-ui Parameters + +[swagger-ui][swagger-ui] ships with its own set of configuration parameters, all described +[by the swagger-ui Configuration](https://github.com/swagger-api/swagger-ui/blob/HEAD/docs/usage/configuration.md#display). +In Swashbuckle.AspNetCore, most of these are surfaced through the SwaggerUI middleware options: + + + + +```cs +app.UseSwaggerUI(options => +{ + options.DefaultModelExpandDepth(2); + options.DefaultModelRendering(ModelRendering.Model); + options.DefaultModelsExpandDepth(-1); + options.DisplayOperationId(); + options.DisplayRequestDuration(); + options.DocExpansion(DocExpansion.None); + options.EnableDeepLinking(); + options.EnableFilter(); + options.EnablePersistAuthorization(); + options.EnableTryItOutByDefault(); + options.MaxDisplayedTags(5); + options.ShowExtensions(); + options.ShowCommonExtensions(); + options.EnableValidator(); + options.SupportedSubmitMethods(SubmitMethod.Get, SubmitMethod.Head); + options.UseRequestInterceptor("(request) => { return request; }"); + options.UseResponseInterceptor("(response) => { return response; }"); +}); +``` +snippet source | anchor + + + +## Inject Custom JavaScript + +To tweak the behavior, you can inject additional JavaScript files by adding them to your `wwwroot` folder and specifying +the relative paths in the middleware options: + + + + +```cs +app.UseSwaggerUI(options => +{ + options.InjectJavascript("/swagger-ui/custom.js"); +}); +``` +snippet source | anchor + + + +## Inject Custom CSS + +To tweak the look and feel, you can inject additional CSS stylesheets by adding them to your `wwwroot` folder and specifying the +relative paths in the middleware options: + + + + +```cs +app.UseSwaggerUI(options => +{ + options.InjectStylesheet("/swagger-ui/custom.css"); +}); +``` +snippet source | anchor + + + +## Customize index.html + +To customize the UI beyond the basic options listed above, you can provide your own version of the swagger-ui `index.html` page: + + + + +```cs +app.UseSwaggerUI(options => +{ + options.IndexStream = () => typeof(Program).Assembly + .GetManifestResourceStream("CustomUIIndex.Swagger.index.html"); // Requires file to be added as an embedded resource +}); +``` +snippet source | anchor + + + +```xml + + + + + +``` + +> [!TIP] +> To get started, you should base your custom `index.html` on the [built-in version](../src/Swashbuckle.AspNetCore.SwaggerUI/index.html). + +## Enable OAuth2.0 Flows + +[swagger-ui][swagger-ui] has built-in support to participate in OAuth2.0 authorization flows. It interacts with authorization and/or token +endpoints, as specified in the OpenAPI JSON, to obtain access tokens for subsequent API calls. See +[Adding Security Definitions and Requirements](configure-and-customize-swaggergen.md#add-security-definitions-and-requirements) for +an example of adding OAuth2.0 metadata to the generated Swagger. + +If your OpenAPI endpoint includes the appropriate security metadata, the UI interaction should be automatically enabled. However, you +can further customize OAuth support in the UI with the following settings below. See the +[Swagger-UI documentation](https://github.com/swagger-api/swagger-ui/blob/HEAD/docs/usage/oauth2.md) for more information. + + + + +```cs +app.UseSwaggerUI(options => +{ + options.OAuthClientId("test-id"); + options.OAuthClientSecret("test-secret"); + options.OAuthUsername("test-user"); + options.OAuthRealm("test-realm"); + options.OAuthAppName("test-app"); + options.OAuth2RedirectUrl("url"); + options.OAuthScopeSeparator(" "); + options.OAuthScopes("scope1", "scope2"); + options.OAuthAdditionalQueryStringParams(new Dictionary { ["foo"] = "bar" }); + options.OAuthUseBasicAuthenticationWithAccessCodeGrant(); + options.OAuthUsePkce(); +}); +``` +snippet source | anchor + + + +## Use client-side request and response interceptors + +To use custom interceptors on requests and responses going through swagger-ui you can define them as JavaScript functions +in the configuration: + + + + +```cs +app.UseSwaggerUI(options => +{ + options.UseRequestInterceptor("(req) => { req.headers['x-my-custom-header'] = 'MyCustomValue'; return req; }"); + options.UseResponseInterceptor("(res) => { console.log('Custom interceptor intercepted response from:', res.url); return res; }"); +}); +``` +snippet source | anchor + + + +This can be useful in a range of scenarios where you might want to append local XSRF tokens to all requests, for example: + + + + +```cs +app.UseSwaggerUI(options => +{ + options.UseRequestInterceptor("(req) => { req.headers['X-XSRF-Token'] = localStorage.getItem('xsrf-token'); return req; }"); +}); +``` +snippet source | anchor + + + +[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 on this page also apply when using `MapSwaggerUI`. + + + + +```cs +app.MapSwagger(); +app.MapSwaggerUI("swagger", options => options.SwaggerEndpoint("/swagger/v1/swagger.json", "V1 Docs")); +``` +snippet source | anchor + + + +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. + + + + +```cs +app.MapSwaggerUI("swagger-auth") + .RequireAuthorization(); // Remember to also add RequireAuthorization to MapSwagger. +``` +snippet source | anchor + + diff --git a/src/Swashbuckle.AspNetCore.ReDoc/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.ReDoc/PublicAPI/PublicAPI.Unshipped.txt index 5f282702bb..d36ebb3ba0 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.ReDoc/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1 @@ - \ No newline at end of file +static Microsoft.AspNetCore.Builder.ReDocBuilderExtensions.MapReDoc(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string routePrefix = null, System.Action setupAction = null) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder diff --git a/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs b/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs index cb89a7748d..7d6d894069 100644 --- a/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs +++ b/src/Swashbuckle.AspNetCore.ReDoc/ReDocBuilderExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Swashbuckle.AspNetCore.ReDoc; @@ -21,16 +22,58 @@ public static IApplicationBuilder UseReDoc( this IApplicationBuilder app, Action setupAction = null) { - ReDocOptions options; - using (var scope = app.ApplicationServices.CreateScope()) - { - options = scope.ServiceProvider.GetRequiredService>().Value; - setupAction?.Invoke(options); - } + var options = ResolveOptions(app.ApplicationServices, setupAction); + EnsureDefaultSpecUrl(options); + return app.UseReDoc(options); + } + + /// + /// Maps the Redoc middleware to the specified endpoint route. + /// + /// Endpoint route builder to which the Redoc middleware will be mapped. + /// Optional route prefix for the Redoc endpoint. If not provided, the value is used. + /// Optional setup action to configure the Redoc options. + /// An that can be used to further configure the endpoint. + public static IEndpointConventionBuilder MapReDoc( + this IEndpointRouteBuilder endpoints, + string routePrefix = null, + Action setupAction = null) + { + var options = ResolveOptions(endpoints.ServiceProvider, setupAction); + + if (routePrefix != null) + { + options.RoutePrefix = routePrefix; + } + EnsureDefaultSpecUrl(options); + + var pipeline = endpoints.CreateApplicationBuilder() + .UseReDoc(options) + .Build(); + + return endpoints.Map(GetRoutePattern(options.RoutePrefix), pipeline); + } + + private static ReDocOptions ResolveOptions(IServiceProvider serviceProvider, Action setupAction) + { + using var scope = serviceProvider.CreateScope(); + var options = scope.ServiceProvider.GetRequiredService>().Value; + setupAction?.Invoke(options); + return options; + } + + private static void EnsureDefaultSpecUrl(ReDocOptions options) + { // To simplify the common case, use a default that will work with the SwaggerMiddleware defaults options.SpecUrl ??= "../swagger/v1/swagger.json"; + } - return app.UseReDoc(options); + private static string GetRoutePattern(string routePrefix) + { + var sanitizedRoutePrefix = routePrefix?.Trim('/'); + return string.IsNullOrEmpty(sanitizedRoutePrefix) + ? "{**path}" + : $"{sanitizedRoutePrefix}/{{**path}}"; } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt b/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt index 5f282702bb..4bc6a2889c 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/PublicAPI/PublicAPI.Unshipped.txt @@ -1 +1 @@ - \ No newline at end of file +static Microsoft.AspNetCore.Builder.SwaggerUIBuilderExtensions.MapSwaggerUI(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string routePrefix = null, System.Action setupAction = null) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder diff --git a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs index 8855eb65dd..044297c18a 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerUI/SwaggerUIBuilderExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Swashbuckle.AspNetCore.SwaggerUI; @@ -20,20 +21,66 @@ public static IApplicationBuilder UseSwaggerUI( this IApplicationBuilder app, Action setupAction = null) { - SwaggerUIOptions options; - using (var scope = app.ApplicationServices.CreateScope()) - { - options = scope.ServiceProvider.GetRequiredService>().Value; - setupAction?.Invoke(options); - } + var options = ResolveOptions(app.ApplicationServices, setupAction); + var hostingEnv = app.ApplicationServices.GetRequiredService(); - // To simplify the common case, use a default that will work with the SwaggerMiddleware defaults - if (options.ConfigObject.Urls == null) - { - var hostingEnv = app.ApplicationServices.GetRequiredService(); - options.ConfigObject.Urls = [new UrlDescriptor { Name = $"{hostingEnv.ApplicationName} v1", Url = "v1/swagger.json" }]; - } + EnsureDefaultUrl(options, hostingEnv.ApplicationName); return app.UseSwaggerUI(options); } + + /// + /// Maps the SwaggerUI middleware to the specified endpoint route. + /// + /// + /// 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 . + /// + /// Endpoint route builder to which the SwaggerUI middleware will be mapped. + /// Optional: The route to register SwaggerUI on. Defaults to the value. + /// Optional setup action to configure the . + /// An that can be used to further configure the endpoint. + public static IEndpointConventionBuilder MapSwaggerUI( + this IEndpointRouteBuilder endpoints, + string routePrefix = null, + Action setupAction = null) + { + var options = ResolveOptions(endpoints.ServiceProvider, setupAction); + + if (routePrefix != null) + { + options.RoutePrefix = routePrefix; + } + + var hostingEnv = endpoints.ServiceProvider.GetRequiredService(); + 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 setupAction) + { + using var scope = serviceProvider.CreateScope(); + var options = scope.ServiceProvider.GetRequiredService>().Value; + setupAction?.Invoke(options); + return options; + } + + private static void EnsureDefaultUrl(SwaggerUIOptions options, string applicationName) + { + // To simplify the common case, use a default that will work with the SwaggerMiddleware defaults + 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}}"; + } } diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs index 186665fdb2..9257dc805a 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/ReDocIntegrationTests.cs @@ -3,6 +3,7 @@ using System.Net.Http.Headers; using System.Security.Cryptography; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc.Testing; using Swashbuckle.AspNetCore.ReDoc; using ReDocApp = ReDoc; @@ -53,6 +54,32 @@ static void AssertResource(HttpResponseMessage response) } } + + [Theory] + [InlineData("/swagger/v1/swagger.json", "application/json")] + [InlineData("/swagger/v1/swagger.yaml", "text/yaml")] + [InlineData("/swagger/v1/swagger.yml", "text/yaml")] + [InlineData("/api-docs/index.html", "text/html")] + public async Task MapSwaggerAndMapReDoc_ReturnExpectedEndpoints(string path, string mediaType) + { + var client = new WebApplicationFactory().CreateClient(); + + var response = await client.GetAsync(path, TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(mediaType, response.Content.Headers.ContentType?.MediaType); + } + + [Theory] + [InlineData("/redoc-auth/index.html")] + public async Task MapReDoc_RequireAuthorization_ReturnUnauthorized(string path) + { + var client = new WebApplicationFactory().CreateClient(); + var response = await client.GetAsync(path, TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] public async Task RedocMiddleware_ReturnsInitializerScript() { diff --git a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerAndSwaggerUIIntegrationTests.cs b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerAndSwaggerUIIntegrationTests.cs index ad3e8d9400..1300fc25ea 100644 --- a/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerAndSwaggerUIIntegrationTests.cs +++ b/test/Swashbuckle.AspNetCore.IntegrationTests/SwaggerAndSwaggerUIIntegrationTests.cs @@ -19,4 +19,28 @@ 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/v1/swagger.json", "application/json")] + [InlineData("/swagger/v1/swagger.yaml", "text/yaml")] + [InlineData("/swagger/v1/swagger.yml", "text/yaml")] + [InlineData("/swagger/index.html", "text/html")] + public async Task Map_Methods_ReturnExpectedEndpoints(string path, string mediaType) + { + var client = new WebApplicationFactory().CreateClient(); + + var response = await client.GetAsync(path, TestContext.Current.CancellationToken); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(mediaType, response.Content.Headers.ContentType?.MediaType); + } + + [Theory] + [InlineData("/swagger-auth/index.html")] + public async Task MapSwaggerUI_And_MapReDoc_RequireAuthorization_ReturnUnauthorized(string path) + { + var client = new WebApplicationFactory().CreateClient(); + var response = await client.GetAsync(path, TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } } diff --git a/test/WebSites/WebApi.Map/Program.cs b/test/WebSites/WebApi.Map/Program.cs new file mode 100644 index 0000000000..de2c440824 --- /dev/null +++ b/test/WebSites/WebApi.Map/Program.cs @@ -0,0 +1,82 @@ +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(options => options.SerializerOptions.Converters.Add(new JsonStringEnumConverter())); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => options.SwaggerDoc("v1", new() { Title = "WebApi", Version = "v1" })); + +// Authentication and Authorization are added to verify that MapSwaggerUI and MapReDoc work as expected when RequireAuthorization is used. +builder.Services.AddAuthentication().AddBearerToken(); +builder.Services.AddAuthorization(); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +// begin-snippet: SwaggerUI-MapSwaggerUI +app.MapSwagger(); +app.MapSwaggerUI("swagger", options => options.SwaggerEndpoint("/swagger/v1/swagger.json", "V1 Docs")); +// end-snippet + +// begin-snippet: SwaggerUI-MapSwaggerUI-RequireAuthorization +app.MapSwaggerUI("swagger-auth") + .RequireAuthorization(); // Remember to also add RequireAuthorization to MapSwagger. +// end-snippet + + +// Also Map redoc in the same file +// begin-snippet: Redoc-MapReDoc +app.MapReDoc(); +// end-snippet + +// begin-snippet: Redoc-MapReDoc-RequireAuthorization +app.MapReDoc("redoc-auth") + .RequireAuthorization(); // Remember to also add RequireAuthorization to MapSwagger +// end-snippet + +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 +{ + /// + /// Expose the Program class for use with WebApplicationFactory + /// + public partial class Program + { + } +} diff --git a/test/WebSites/WebApi.Map/Properties/launchSettings.json b/test/WebSites/WebApi.Map/Properties/launchSettings.json new file mode 100644 index 0000000000..c003abfed9 --- /dev/null +++ b/test/WebSites/WebApi.Map/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5154", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/test/WebSites/WebApi.Map/WebApi.Map.csproj b/test/WebSites/WebApi.Map/WebApi.Map.csproj new file mode 100644 index 0000000000..ccee2d6d9e --- /dev/null +++ b/test/WebSites/WebApi.Map/WebApi.Map.csproj @@ -0,0 +1,17 @@ + + + + enable + enable + WebApi.Map + $(DefaultTargetFrameworks) + true + + + + + + + + + diff --git a/test/WebSites/WebApi.Map/WebApi.http b/test/WebSites/WebApi.Map/WebApi.http new file mode 100644 index 0000000000..abc2f6184b --- /dev/null +++ b/test/WebSites/WebApi.Map/WebApi.http @@ -0,0 +1,4 @@ +@WebApi_HostAddress = http://localhost:5205 + +GET {{WebApi_HostAddress}}/weatherforecast/ +Accept: application/json diff --git a/test/WebSites/WebApi.Map/appsettings.Development.json b/test/WebSites/WebApi.Map/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/test/WebSites/WebApi.Map/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/test/WebSites/WebApi.Map/appsettings.json b/test/WebSites/WebApi.Map/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/test/WebSites/WebApi.Map/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}