Last active
June 13, 2024 14:44
-
-
Save DanteDeRuwe/de3f745d4a3aad7065affe1fc9ed96d0 to your computer and use it in GitHub Desktop.
Swagger extensions for minimal apis
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Asp.Versioning.ApiExplorer; | |
using Microsoft.Extensions.Options; | |
using Microsoft.OpenApi.Models; | |
using Swashbuckle.AspNetCore.SwaggerGen; | |
namespace BFF.API.Extensions.OpenApi; | |
public class ConfigureSwaggerGenOptions(IApiVersionDescriptionProvider versionProvider) : IConfigureNamedOptions<SwaggerGenOptions> | |
{ | |
public void Configure(string? name, SwaggerGenOptions options) => Configure(options); | |
public void Configure(SwaggerGenOptions options) | |
{ | |
CreateDocPerVersion(options); | |
AddJwtBearerSecurity(options); | |
options.DescribeAllParametersInCamelCase(); | |
} | |
private static void AddJwtBearerSecurity(SwaggerGenOptions options) | |
{ | |
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme | |
{ | |
Description = "JWT Authorization header using the Bearer scheme", | |
Name = "Authorization", | |
In = ParameterLocation.Header, | |
Type = SecuritySchemeType.Http, | |
Scheme = "Bearer", | |
BearerFormat = "JWT" | |
}); | |
options.AddSecurityRequirement(new OpenApiSecurityRequirement | |
{ | |
{ | |
new OpenApiSecurityScheme | |
{ | |
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } | |
}, | |
[] | |
} | |
}); | |
} | |
private void CreateDocPerVersion(SwaggerGenOptions options) | |
{ | |
foreach (var versionDescription in versionProvider.ApiVersionDescriptions) | |
{ | |
var openApiInfo = new OpenApiInfo | |
{ | |
Title = "Mediahuis CIAM Customer Support BFF", | |
Version = $"v{versionDescription.ApiVersion}", | |
Contact = new OpenApiContact { Name = "Mediahuis CIAM team - AE studio" } | |
}; | |
options.SwaggerDoc(versionDescription.GroupName, openApiInfo); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Net; | |
using System.Text; | |
using Microsoft.AspNetCore.Authorization; | |
using Microsoft.AspNetCore.Authorization.Infrastructure; | |
using Microsoft.AspNetCore.Builder; | |
using Microsoft.AspNetCore.Http; | |
using Microsoft.AspNetCore.Mvc; | |
using Microsoft.AspNetCore.Routing; | |
namespace BFF.Application.Util; | |
public static class EndpointBuilderExtensions | |
{ | |
/// <summary> | |
/// Does the same thing as <see cref="OpenApiRouteHandlerBuilderExtensions.ProducesProblem"/>, but also usable on <see cref="RouteGroupBuilder"/> for example. | |
/// </summary> | |
public static TBuilder ProducesProblem<TBuilder>(this TBuilder builder, int statusCode, string contentType = "application/problem+json") | |
where TBuilder : IEndpointConventionBuilder | |
{ | |
return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, typeof(ProblemDetails), [contentType])); | |
} | |
/// <inheritdoc cref="ProducesProblem{TBuilder}(TBuilder,int,string)"/> | |
public static TBuilder ProducesProblem<TBuilder>(this TBuilder builder, HttpStatusCode statusCode, | |
string contentType = "application/problem+json") | |
where TBuilder : IEndpointConventionBuilder => builder.ProducesProblem((int)statusCode, contentType); | |
/// <summary> | |
/// Does the same thing as <see cref="OpenApiRouteHandlerBuilderExtensions.Produces"/>, but also usable on <see cref="RouteGroupBuilder"/> for example. | |
/// </summary> | |
public static TBuilder Produces<TBuilder>(this TBuilder builder, int statusCode, string contentType = "application/problem+json") | |
where TBuilder : IEndpointConventionBuilder | |
{ | |
return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, typeof(void), [contentType])); | |
} | |
/// <inheritdoc cref="Produces{TBuilder}(TBuilder,int,string)"/> | |
public static TBuilder Produces<TBuilder>(this TBuilder builder, HttpStatusCode statusCode, | |
string contentType = "application/problem+json") | |
where TBuilder : IEndpointConventionBuilder => builder.ProducesProblem((int)statusCode, contentType); | |
/// <summary> | |
/// Adds authorization like normal (<see cref="AuthorizationEndpointConventionBuilderExtensions.RequireAuthorization{TBuilder}(TBuilder)"/>), but also does the following things: | |
/// <list type="bullet"> | |
/// <item>adds a section to the beginning of the description of the endpoint that lists the authorization requirements.</item> | |
/// <item>adds a <c>ProducesProblem()</c> to the endpoint with status codes <c>Forbidden</c> (403) and <c>Unauthorized</c> (401).</item> | |
/// </list> | |
/// </summary> | |
/// <remarks>Please call this AFTER any .WithDescription() call to preserve the existing description.</remarks> | |
public static RouteHandlerBuilder RequireCustomAuthorization(this RouteHandlerBuilder builder, | |
Action<AuthorizationPolicyBuilder>? configurePolicy = null) | |
{ | |
var policyBuilder = new AuthorizationPolicyBuilder(); | |
configurePolicy?.Invoke(policyBuilder); | |
var policy = policyBuilder.Build(); | |
return builder | |
.RequireAuthorization(policy) | |
.ProducesProblem(HttpStatusCode.Forbidden) | |
.ProducesProblem(HttpStatusCode.Unauthorized) | |
.AddAuthSectionToDescription(policy.Requirements); | |
} | |
/// <summary> | |
/// Shorthand | |
/// </summary> | |
/// <param name="builder"></param> | |
/// <param name="scopes"></param> | |
/// <param name="claimName"></param> | |
/// <returns></returns> | |
public static AuthorizationPolicyBuilder RequireScope(this AuthorizationPolicyBuilder builder, | |
IEnumerable<string> scopes, string claimName = "scp") => builder.RequireClaim(claimName, scopes); | |
private static RouteHandlerBuilder AddAuthSectionToDescription(this RouteHandlerBuilder builder, | |
IEnumerable<IAuthorizationRequirement> policyRequirements) | |
{ | |
builder.Add(endpointBuilder => | |
{ | |
List<string> roles = []; | |
List<string> scopes = []; | |
foreach (var requirement in policyRequirements) | |
{ | |
switch (requirement) | |
{ | |
case RolesAuthorizationRequirement rolesRequirement: | |
roles.AddRange(rolesRequirement.AllowedRoles); | |
break; | |
case ClaimsAuthorizationRequirement { ClaimType: "scope" or "scp" } claimsRequirement: | |
scopes.AddRange(claimsRequirement.AllowedValues); | |
break; | |
} | |
} | |
var description = GetFullDescription(endpointBuilder, roles, scopes); | |
if (description is not null) endpointBuilder.Metadata.Add(new EndpointDescriptionAttribute(description)); | |
}); | |
return builder; | |
string? GetFullDescription(EndpointBuilder endpointBuilder, ICollection<string> roles, ICollection<string> scopes) | |
{ | |
// Since this method ADDS to the existing description, we need to get the existing description first. It is always the last one added. | |
var sb = endpointBuilder.Metadata.OfType<EndpointDescriptionAttribute>().LastOrDefault()?.Description is string existingDescription | |
? new StringBuilder("<h2>Description</h2>").Append(existingDescription) // Add a nice header | |
: new StringBuilder(); | |
// If no roles or scopes are required, return already here | |
if (roles.Count <= 0 && scopes.Count <= 0) return sb.ToString(); | |
// Start a new section if necessary | |
sb.AppendIfNotEmpty("<br/><br/><hr/><br/><br/>"); | |
// Add the auth roles and scopes to the existing description | |
sb.Append("<h2>Authorization</h2>"); | |
if (roles.Count > 0) AddSection("Allowed roles", roles); | |
if (scopes.Count > 0) AddSection("Allowed scopes", scopes); | |
return sb.ToString(); | |
void AddSection(string title, IEnumerable<string> items) => sb | |
.Append("<strong>").Append(title).Append("</strong> ") | |
.AppendJoin(", ", items.Select(x => $"<code>{x}</code>")) | |
.Append("<br/><br/>"); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Text; | |
namespace BFF.Application.Util; | |
public static class StringBuilderExtensions | |
{ | |
public static StringBuilder AppendIf(this StringBuilder sb, bool condition, string content) => condition ? sb.Append(content) : sb; | |
public static StringBuilder AppendIfEmpty(this StringBuilder sb, string content) => sb.AppendIf(sb.Length == 0, content); | |
public static StringBuilder AppendIfNotEmpty(this StringBuilder sb, string content) => sb.AppendIf(sb.Length > 0, content); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace BFF.API.Extensions.OpenApi; | |
public static class SwaggerExtensions | |
{ | |
/// <summary> | |
/// Adds Swagger services to the service collection | |
/// </summary> | |
public static IServiceCollection AddCustomSwagger(this IServiceCollection services) | |
{ | |
services.AddEndpointsApiExplorer(); | |
services.AddSwaggerGen(); | |
services.ConfigureOptions<ConfigureSwaggerGenOptions>(); | |
return services; | |
} | |
/// <summary> | |
/// Adds Swagger middleware to the web application pipeline | |
/// </summary> | |
/// <remarks>Call this AFTER all routes have been mapped onto the app.</remarks> | |
public static WebApplication UseCustomSwagger(this WebApplication app) | |
{ | |
app.UseSwagger(); | |
app.UseSwaggerUI(options => | |
{ | |
foreach (var versionName in app.DescribeApiVersions().Select(v => v.GroupName)) | |
{ | |
options.SwaggerEndpoint($"/swagger/{versionName}/swagger.json", versionName); | |
} | |
}); | |
return app; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment