diff --git a/README.md b/README.md index a186bda..e16f1a4 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,40 @@ public class GetController : Controller2 5. After application started go to or to see generated Swagger +## Configuration + +### Bearer security scheme + +When your controllers use authorization (`[Authorize]`), `Simplify.Web.Swagger` automatically adds security requirements to the corresponding operations using **all security schemes registered via `AddSecurityDefinition`** — no extra configuration needed: + +```csharp +x.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { ... }); +x.AddSimplifyWebSwagger(); // Bearer requirement is added automatically +``` + +To restrict to a specific scheme name (e.g. when you have multiple definitions but want only one applied), set `SecuritySchemeName` explicitly: + +```csharp +x.AddSimplifyWebSwagger(new SimplifyWebSwaggerArgs +{ + SecuritySchemeName = "Bearer" +}); +``` + +### JSON property casing + +By default, Swagger schema property names follow the `System.Text.Json` default casing (PascalCase). To use camelCase (matching `JsonSerializerDefaults.Web`), register a custom `ISerializerDataContractResolver`: + +```csharp +builder.Services + .AddSingleton(_ => + new JsonSerializerDataContractResolver( + new JsonSerializerOptions(JsonSerializerDefaults.Web) + )) + .AddEndpointsApiExplorer() + .AddSwaggerGen(x => x.AddSimplifyWebSwagger()); +``` + ## Example application Below is the example of Swagger generated by `Simplify.Web.Swagger`: diff --git a/src/Simplify.Web.Swagger/AcceptLanguageHeaderArgs.cs b/src/Simplify.Web.Swagger/AcceptLanguageHeaderArgs.cs new file mode 100644 index 0000000..70911c3 --- /dev/null +++ b/src/Simplify.Web.Swagger/AcceptLanguageHeaderArgs.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace Simplify.Web.Swagger; + +/// +/// Configuration for the Accept-Language header parameter added to all operations. +/// +public class AcceptLanguageHeaderArgs +{ + /// + /// Initializes a new instance with the specified supported languages. + /// The first entry is used as the default value. + /// + /// Supported language codes, e.g. "en-US", "ru-RU". + public AcceptLanguageHeaderArgs(params string[] languages) + { + Languages = languages; + Default = languages.Length > 0 ? languages[0] : string.Empty; + } + + /// + /// The list of supported language codes. + /// + public IReadOnlyList Languages { get; } + + /// + /// The default language code (defaults to the first entry in ). + /// + public string Default { get; set; } +} \ No newline at end of file diff --git a/src/Simplify.Web.Swagger/CHANGELOG.md b/src/Simplify.Web.Swagger/CHANGELOG.md index dba7cee..1ca41be 100644 --- a/src/Simplify.Web.Swagger/CHANGELOG.md +++ b/src/Simplify.Web.Swagger/CHANGELOG.md @@ -2,10 +2,24 @@ ## [1.3.0] - Unreleased +### Added + +- .NET 10.0 explicit support +- `AcceptLanguageHeaderArgs` — configures an `Accept-Language` header parameter added to all Swagger operations +- `SimplifyWebSwaggerArgs.AcceptLanguageHeader` — enables Accept-Language header with supported languages list +- `SimplifyWebSwaggerArgs.SecuritySchemeName` — explicit override for security scheme name; when `null` (default), all schemes registered via `AddSecurityDefinition` are applied automatically to authorized operations +- Route parameter types auto-detection from controller `Invoke`/`InvokeAsync` method signatures (typed path parameters in Swagger schema) +- `AddSimplifyWebSwaggerServices` extension method — registers PascalCase `ISerializerDataContractResolver` before `AddSwaggerGen` + +### Changed + +- Security requirements on authorized operations are now auto-detected from registered `AddSecurityDefinition` schemes; no manual `SecuritySchemeName` configuration needed + ### Dependencies - Simplify.Web bump to 5.2 - Asp.Versioning.Mvc bump to 8.1.1 +- Swashbuckle.AspNetCore.SwaggerGen bump to 10.2.1 (net10.0) ## [1.2.0] - 2025-10-10 diff --git a/src/Simplify.Web.Swagger/ControllerAction.cs b/src/Simplify.Web.Swagger/ControllerAction.cs index 8e1984b..f665274 100644 --- a/src/Simplify.Web.Swagger/ControllerAction.cs +++ b/src/Simplify.Web.Swagger/ControllerAction.cs @@ -1,6 +1,11 @@ using System; using System.Collections.Generic; +#if NET10_0 +using Microsoft.OpenApi; +using NetHttpMethod = System.Net.Http.HttpMethod; +#else using Microsoft.OpenApi.Models; +#endif using Simplify.Web.Controllers.Meta.Routing; namespace Simplify.Web.Swagger; @@ -29,13 +34,23 @@ public class ControllerAction /// public IDictionary Responses { get; set; } = new Dictionary(); + /// + /// Gets or sets the route parameter types, keyed by parameter name (case-insensitive). + /// + public IDictionary RouteParameterTypes { get; set; } = + new Dictionary(StringComparer.OrdinalIgnoreCase); + /// /// Gets or sets the type. /// /// /// The type. /// +#if NET10_0 + public NetHttpMethod Type { get; set; } = NetHttpMethod.Get; +#else public OperationType Type { get; set; } +#endif /// /// Gets the path. diff --git a/src/Simplify.Web.Swagger/ControllerActionsFactory.cs b/src/Simplify.Web.Swagger/ControllerActionsFactory.cs index d172eb2..e64a497 100644 --- a/src/Simplify.Web.Swagger/ControllerActionsFactory.cs +++ b/src/Simplify.Web.Swagger/ControllerActionsFactory.cs @@ -2,12 +2,17 @@ using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using Microsoft.OpenApi.Models; using Simplify.Web.Controllers.Meta; using Simplify.Web.Controllers.Meta.MetaStore; using Simplify.Web.Controllers.Meta.Routing; using Simplify.Web.Http; using Swashbuckle.AspNetCore.SwaggerGen; +#if NET10_0 +using Microsoft.OpenApi; +using NetHttpMethod = System.Net.Http.HttpMethod; +#else +using Microsoft.OpenApi.Models; +#endif namespace Simplify.Web.Swagger; @@ -16,18 +21,17 @@ namespace Simplify.Web.Swagger; /// public static class ControllerActionsFactory { - private static readonly IReadOnlyCollection> ResponseDescriptionMap = + private static readonly IReadOnlyCollection< + KeyValuePair + > ResponseDescriptionMap = [ new KeyValuePair("1\\d{2}", "Information"), - new KeyValuePair("201", "Created"), new KeyValuePair("202", "Accepted"), new KeyValuePair("204", "No Content"), new KeyValuePair("2\\d{2}", "Success"), - new KeyValuePair("304", "Not Modified"), new KeyValuePair("3\\d{2}", "Redirect"), - new KeyValuePair("400", "Bad Request"), new KeyValuePair("401", "Unauthorized"), new KeyValuePair("403", "Forbidden"), @@ -38,9 +42,8 @@ public static class ControllerActionsFactory new KeyValuePair("409", "Conflict"), new KeyValuePair("429", "Too Many Requests"), new KeyValuePair("4\\d{2}", "Client Error"), - new KeyValuePair("5\\d{2}", "Server Error"), - new KeyValuePair("default", "Error") + new KeyValuePair("default", "Error"), ]; /// @@ -49,26 +52,33 @@ public static class ControllerActionsFactory /// /// The remove prefixes. /// - public static IList RemovePrefixes { get; } = - [ - "Controllers.", - "Api.v1." - ]; + public static IList RemovePrefixes { get; } = ["Controllers.", "Api.v1."]; /// /// Creates the controller actions from controllers metadata. /// /// The context. - public static IEnumerable CreateControllerActionsFromControllersMetaData(DocumentFilterContext context) => - ControllersMetaStore.Current.RoutedControllers - .SelectMany(item => CreateControllerActions(item, context)); - - private static IEnumerable CreateControllerActions(IControllerMetadata item, DocumentFilterContext context) => - item.ExecParameters! - .Routes - .Select(x => CreateControllerAction(x.Key, x.Value, item, context)); - - private static ControllerAction CreateControllerAction(HttpMethod method, IControllerRoute route, IControllerMetadata item, DocumentFilterContext context) => + public static IEnumerable CreateControllerActionsFromControllersMetaData( + DocumentFilterContext context + ) => + ControllersMetaStore.Current.RoutedControllers.SelectMany(item => + CreateControllerActions(item, context) + ); + + private static IEnumerable CreateControllerActions( + IControllerMetadata item, + DocumentFilterContext context + ) => + item.ExecParameters!.Routes.Select(x => + CreateControllerAction(x.Key, x.Value, item, context) + ); + + private static ControllerAction CreateControllerAction( + HttpMethod method, + IControllerRoute route, + IControllerMetadata item, + DocumentFilterContext context + ) => new() { Type = HttpMethodToOperationType(method), @@ -76,11 +86,15 @@ private static ControllerAction CreateControllerAction(HttpMethod method, IContr Names = CreateNames(item.ControllerType), Responses = CreateResponses(item.ControllerType, context), RequestBody = CreateRequestBody(item.ControllerType, context), - IsAuthorizationRequired = item.Security is { IsAuthorizationRequired: true } + IsAuthorizationRequired = item.Security is { IsAuthorizationRequired: true }, + RouteParameterTypes = GetRouteParameterTypes(item.ControllerType), }; private static ControllerActionNames CreateNames(Type controllerType) => - CreateNames(controllerType.FullName ?? throw new InvalidOperationException("controllerType.FullName is null")); + CreateNames( + controllerType.FullName + ?? throw new InvalidOperationException("controllerType.FullName is null") + ); private static ControllerActionNames CreateNames(string name) { @@ -113,19 +127,36 @@ private static string FormatNameSource(string str) return str; } - private static OperationType HttpMethodToOperationType(HttpMethod method) => +#if NET10_0 + private static NetHttpMethod HttpMethodToOperationType(HttpMethod method) => method switch { - HttpMethod.Get => OperationType.Get, - HttpMethod.Post => OperationType.Post, - HttpMethod.Put => OperationType.Put, - HttpMethod.Patch => OperationType.Patch, - HttpMethod.Delete => OperationType.Delete, - HttpMethod.Options => OperationType.Options, - _ => OperationType.Get + HttpMethod.Get => NetHttpMethod.Get, + HttpMethod.Post => NetHttpMethod.Post, + HttpMethod.Put => NetHttpMethod.Put, + HttpMethod.Patch => NetHttpMethod.Patch, + HttpMethod.Delete => NetHttpMethod.Delete, + HttpMethod.Options => NetHttpMethod.Options, + _ => NetHttpMethod.Get, }; - - private static OpenApiRequestBody CreateRequestBody(Type controllerType, DocumentFilterContext context) +#else + private static OperationType HttpMethodToOperationType(HttpMethod method) => + method switch + { + HttpMethod.Get => OperationType.Get, + HttpMethod.Post => OperationType.Post, + HttpMethod.Put => OperationType.Put, + HttpMethod.Patch => OperationType.Patch, + HttpMethod.Delete => OperationType.Delete, + HttpMethod.Options => OperationType.Options, + _ => OperationType.Get, + }; +#endif + + private static OpenApiRequestBody CreateRequestBody( + Type controllerType, + DocumentFilterContext context + ) { var request = new OpenApiRequestBody(); var attributes = controllerType.GetCustomAttributes(typeof(RequestBodyAttribute), false); @@ -137,31 +168,73 @@ private static OpenApiRequestBody CreateRequestBody(Type controllerType, Documen request.Content = new Dictionary { - [item.ContentType] = new() { Schema = context.SchemaGenerator.GenerateSchema(item.Model, context.SchemaRepository) } + [item.ContentType] = new() + { + Schema = context.SchemaGenerator.GenerateSchema( + item.Model, + context.SchemaRepository + ), + }, }; return request; } - private static IDictionary CreateResponses(Type controllerType, DocumentFilterContext context) => - controllerType.GetCustomAttributes(typeof(ProducesResponseAttribute), false) + private static IDictionary CreateResponses( + Type controllerType, + DocumentFilterContext context + ) => + controllerType + .GetCustomAttributes(typeof(ProducesResponseAttribute), false) .Cast() .ToDictionary(item => item.StatusCode, item => CreateResponse(item, context)); - private static OpenApiResponse CreateResponse(ProducesResponseAttribute producesResponse, DocumentFilterContext context) + private static OpenApiResponse CreateResponse( + ProducesResponseAttribute producesResponse, + DocumentFilterContext context + ) { var response = new OpenApiResponse { Description = ResponseDescriptionMap - .FirstOrDefault((entry) => Regex.IsMatch(producesResponse.StatusCode.ToString(), entry.Key)) - .Value + .FirstOrDefault( + (entry) => Regex.IsMatch(producesResponse.StatusCode.ToString(), entry.Key) + ) + .Value, }; foreach (var item in producesResponse.ContentTypes.Distinct()) - response.Content.Add(item, producesResponse.Type is null - ? new OpenApiMediaType() - : new OpenApiMediaType { Schema = context.SchemaGenerator.GenerateSchema(producesResponse.Type, context.SchemaRepository) }); + { +#if NET10_0 + response.Content ??= new Dictionary(); +#endif + response.Content.Add( + item, + producesResponse.Type is null + ? new OpenApiMediaType() + : new OpenApiMediaType + { + Schema = context.SchemaGenerator.GenerateSchema( + producesResponse.Type, + context.SchemaRepository + ), + } + ); + } return response; } + + private static IDictionary GetRouteParameterTypes(Type controllerType) + { + var method = controllerType.GetMethod("Invoke") ?? controllerType.GetMethod("InvokeAsync"); + + if (method is null) + return new Dictionary(StringComparer.OrdinalIgnoreCase); + + return method + .GetParameters() + .Where(p => p.Name != null) + .ToDictionary(p => p.Name!, p => p.ParameterType, StringComparer.OrdinalIgnoreCase); + } } \ No newline at end of file diff --git a/src/Simplify.Web.Swagger/EnumNamesSchemaFilter.cs b/src/Simplify.Web.Swagger/EnumNamesSchemaFilter.cs index 7d49d5b..87f1802 100644 --- a/src/Simplify.Web.Swagger/EnumNamesSchemaFilter.cs +++ b/src/Simplify.Web.Swagger/EnumNamesSchemaFilter.cs @@ -1,8 +1,14 @@ using System; using System.Linq; +using Swashbuckle.AspNetCore.SwaggerGen; +#if NET10_0 +using System.Collections.Generic; +using System.Text.Json.Nodes; +using Microsoft.OpenApi; +#else using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; +#endif namespace Simplify.Web.Swagger; @@ -12,18 +18,44 @@ namespace Simplify.Web.Swagger; public class EnumNamesSchemaFilter : ISchemaFilter { /// +#if NET10_0 + public void Apply(IOpenApiSchema schema, SchemaFilterContext context) + { + if (!context.Type.IsEnum) + return; + + if (schema is not OpenApiSchema concreteSchema) + return; + + var names = Enum.GetNames(context.Type); + var values = Enum.GetValues(context.Type).Cast().ToArray(); + + var varnames = new JsonArray(); + foreach (var name in names) + varnames.Add(name); + + concreteSchema.Extensions ??= new Dictionary(); + concreteSchema.Extensions["x-varnames"] = new JsonNodeExtension(varnames); + concreteSchema.Description = BuildDescription(names, values); + } +#else public void Apply(OpenApiSchema schema, SchemaFilterContext context) { if (!context.Type.IsEnum) return; var names = Enum.GetNames(context.Type); - var values = Enum.GetValues(context.Type).Cast().ToArray(); + var values = Enum.GetValues(context.Type).Cast().ToArray(); var varnames = new OpenApiArray(); foreach (var name in names) varnames.Add(new OpenApiString(name)); - schema.Extensions["names"] = varnames; + schema.Extensions["x-varnames"] = varnames; + schema.Description = BuildDescription(names, values); } +#endif + + private static string BuildDescription(string[] names, object[] values) => + string.Join(", ", names.Select((name, i) => $"{Convert.ToInt64(values[i])} = {name}")); } \ No newline at end of file diff --git a/src/Simplify.Web.Swagger/Simplify.Web.Swagger.csproj b/src/Simplify.Web.Swagger/Simplify.Web.Swagger.csproj index b4e32be..aa644c6 100644 --- a/src/Simplify.Web.Swagger/Simplify.Web.Swagger.csproj +++ b/src/Simplify.Web.Swagger/Simplify.Web.Swagger.csproj @@ -1,6 +1,6 @@  - net9.0;net8.0 + net10.0;net9.0;net8.0 latest enable nullable @@ -24,12 +24,17 @@ See https://github.com/SimplifyNet/Simplify.Web.Swagger/tree/master/src/Simplify.Web.Swagger/CHANGELOG.md for details - + + + + + + + - diff --git a/src/Simplify.Web.Swagger/SimplifyWebDocumentFilter.cs b/src/Simplify.Web.Swagger/SimplifyWebDocumentFilter.cs index f976a5b..fe21a07 100644 --- a/src/Simplify.Web.Swagger/SimplifyWebDocumentFilter.cs +++ b/src/Simplify.Web.Swagger/SimplifyWebDocumentFilter.cs @@ -1,8 +1,14 @@ +using System; using System.Collections.Generic; using System.Linq; -using Microsoft.OpenApi.Models; using Simplify.Web.Controllers.Meta.Routing; using Swashbuckle.AspNetCore.SwaggerGen; +#if NET10_0 +using Microsoft.OpenApi; +using JsonNode = System.Text.Json.Nodes.JsonNode; +#else +using Microsoft.OpenApi.Models; +#endif namespace Simplify.Web.Swagger; @@ -17,9 +23,7 @@ public class SimplifyWebDocumentFilter : IDocumentFilter /// /// Initializes an instance of . /// - public SimplifyWebDocumentFilter() - { - } + public SimplifyWebDocumentFilter() { } /// /// Initializes an instance of . @@ -34,50 +38,126 @@ public SimplifyWebDocumentFilter() /// The context public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { - foreach (var item in ControllerActionsFactory.CreateControllerActionsFromControllersMetaData(context) - .GroupBy(x => x.Path) - .Select(x => new KeyValuePair(x.Key, CreatePathItem(x)))) - swaggerDoc?.Paths.Add(item.Key, item.Value); + var controllerActions = ControllerActionsFactory + .CreateControllerActionsFromControllersMetaData(context) + .ToList(); + + foreach ( + var item in controllerActions + .GroupBy(x => x.Path) + .Select(x => new KeyValuePair( + x.Key, + CreatePathItem(x, swaggerDoc, context) + )) + ) + swaggerDoc.Paths.Add(item.Key, item.Value); + + PopulateDocumentTags(swaggerDoc, controllerActions); + } + +#if NET10_0 + private static IList CreateParameters(ControllerAction item, DocumentFilterContext context) => + item.ControllerRoute.Items + .OfType() + .Select(x => (IOpenApiParameter)CreatePathParameter(x, item, context)) + .ToList(); +#else + private static IList CreateParameters(ControllerAction item, DocumentFilterContext context) => + item.ControllerRoute.Items + .OfType() + .Select(x => CreatePathParameter(x, item, context)) + .ToList(); +#endif + + private static OpenApiParameter CreatePathParameter(PathParameter pathParam, ControllerAction item, DocumentFilterContext context) + { + var param = new OpenApiParameter + { + Name = pathParam.Name, + In = ParameterLocation.Path, + Required = true, + AllowEmptyValue = false, + }; + + if (item.RouteParameterTypes.TryGetValue(pathParam.Name, out var type)) + param.Schema = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository); + + return param; } - private static IList CreateParameters(IControllerRoute path) => - path.Items - .Where(x => x is PathParameter) - .Cast() - .Select(x => new OpenApiParameter - { - Name = x.Name, - In = ParameterLocation.Path, - AllowEmptyValue = false - }).ToList(); - - private OpenApiPathItem CreatePathItem(IEnumerable actions) + private OpenApiPathItem CreatePathItem( + IEnumerable actions, + OpenApiDocument swaggerDoc, + DocumentFilterContext context + ) { var pathItem = new OpenApiPathItem(); foreach (var item in actions) - pathItem.AddOperation(item.Type, CreateOperation(item)); + pathItem.AddOperation(item.Type, CreateOperation(item, swaggerDoc, context)); return pathItem; } - private OpenApiOperation CreateOperation(ControllerAction item) + private OpenApiOperation CreateOperation(ControllerAction item, OpenApiDocument swaggerDoc, DocumentFilterContext context) { var operation = new OpenApiOperation(); - operation.Tags.Add(new OpenApiTag - { - Name = item.Names.GroupName - }); +#if NET10_0 + operation.Tags ??= new HashSet(); + operation.Tags.Add(new OpenApiTagReference(item.Names.GroupName)); +#else + operation.Tags.Add(new OpenApiTag { Name = item.Names.GroupName }); +#endif if (item.Names.Summary != null) operation.Summary = item.Names.Summary; foreach (var response in item.Responses) + { +#if NET10_0 + operation.Responses ??= new OpenApiResponses(); +#endif operation.Responses.Add(response.Key.ToString(), response.Value); + } - operation.Parameters = CreateParameters(item.ControllerRoute); - operation.RequestBody = item.RequestBody; + operation.Parameters = CreateParameters(item, context); + + if (item.RequestBody.Content is { Count: > 0 }) + operation.RequestBody = item.RequestBody; + + if (item.IsAuthorizationRequired) + { + var schemeNames = ResolveSecuritySchemeNames(swaggerDoc); + +#if NET10_0 + if (schemeNames.Count > 0) + operation.Security = schemeNames + .Select(name => new OpenApiSecurityRequirement + { + [new OpenApiSecuritySchemeReference(name, swaggerDoc)] = [], + }) + .ToList(); +#else + if (schemeNames.Count > 0) + operation.Security = schemeNames + .Select(name => new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = name, + }, + }, + new List() + }, + }) + .ToList(); +#endif + } if (_args == null) return operation; @@ -85,6 +165,73 @@ private OpenApiOperation CreateOperation(ControllerAction item) foreach (var parameter in _args.Parameters) operation.Parameters.Add(parameter); + if (_args.AcceptLanguageHeader is { } acceptLang) + operation.Parameters.Add(CreateAcceptLanguageParameter(acceptLang)); + return operation; } + + private IReadOnlyList ResolveSecuritySchemeNames(OpenApiDocument swaggerDoc) + { + if (_args?.SecuritySchemeName is { } explicitName) + return [explicitName]; + + return swaggerDoc.Components?.SecuritySchemes?.Keys?.ToList() ?? []; + } + + private static OpenApiParameter CreateAcceptLanguageParameter(AcceptLanguageHeaderArgs args) + { + var param = new OpenApiParameter + { + Name = "Accept-Language", + In = ParameterLocation.Header, + Description = "Language preference for the response.", + Required = true, + AllowEmptyValue = true, + }; + +#if NET10_0 + param.Example = args.Default; + param.Schema = new OpenApiSchema + { + Default = args.Default, + Enum = args.Languages.Select(l => (JsonNode)l).ToList() + }; +#else + param.Example = new Microsoft.OpenApi.Any.OpenApiString(args.Default); + param.Schema = new OpenApiSchema + { + Default = new Microsoft.OpenApi.Any.OpenApiString(args.Default), + Enum = args.Languages.Select(l => (Microsoft.OpenApi.Any.IOpenApiAny)new Microsoft.OpenApi.Any.OpenApiString(l)).ToList() + }; +#endif + + return param; + } + + private static void PopulateDocumentTags( + OpenApiDocument swaggerDoc, + IEnumerable actions + ) + { +#if NET10_0 + var existingNames = + swaggerDoc.Tags?.Select(t => t.Name).ToHashSet(StringComparer.Ordinal) ?? []; +#else + var existingNames = swaggerDoc.Tags.Select(t => t.Name).ToHashSet(StringComparer.Ordinal); +#endif + + foreach ( + var name in actions + .Select(x => x.Names.GroupName) + .Distinct() + .Where(n => !existingNames.Contains(n)) + ) + { +#if NET10_0 + swaggerDoc.Tags ??= new HashSet(); +#endif + swaggerDoc.Tags.Add(new OpenApiTag { Name = name }); + } + } } \ No newline at end of file diff --git a/src/Simplify.Web.Swagger/SimplifyWebSwaggerArgs.cs b/src/Simplify.Web.Swagger/SimplifyWebSwaggerArgs.cs index 785d283..54353d9 100644 --- a/src/Simplify.Web.Swagger/SimplifyWebSwaggerArgs.cs +++ b/src/Simplify.Web.Swagger/SimplifyWebSwaggerArgs.cs @@ -1,5 +1,9 @@ using System.Collections.Generic; +#if NET10_0 +using Microsoft.OpenApi; +#else using Microsoft.OpenApi.Models; +#endif namespace Simplify.Web.Swagger; @@ -12,4 +16,16 @@ public class SimplifyWebSwaggerArgs /// Open Api Parameters /// public IList Parameters { get; } = []; + + /// + /// When set, adds an Accept-Language header parameter to every operation. + /// + public AcceptLanguageHeaderArgs? AcceptLanguageHeader { get; set; } + + /// + /// The security scheme name to apply to authorized operations (e.g. "Bearer"). + /// When null (default), all security schemes registered via AddSecurityDefinition + /// are applied automatically. Set to an explicit name to restrict to a single scheme. + /// + public string? SecuritySchemeName { get; set; } } \ No newline at end of file diff --git a/src/Simplify.Web.Swagger/SimplifyWebSwaggerExtensions.cs b/src/Simplify.Web.Swagger/SimplifyWebSwaggerExtensions.cs index 6e66d33..abfc9db 100644 --- a/src/Simplify.Web.Swagger/SimplifyWebSwaggerExtensions.cs +++ b/src/Simplify.Web.Swagger/SimplifyWebSwaggerExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Swashbuckle.AspNetCore.SwaggerGen; +using System.Text.Json; namespace Simplify.Web.Swagger; @@ -8,6 +9,24 @@ namespace Simplify.Web.Swagger; /// public static class SimplifyWebSwaggerServiceCollectionExtensions { + /// + /// Registers Simplify.Web Swagger services on the service collection. + /// Defaults schema property naming to PascalCase to match Simplify.Web default serialization. + /// Must be called before AddSwaggerGen. + /// + /// The service collection. + public static IServiceCollection AddSimplifyWebSwaggerServices(this IServiceCollection services) + { + // Swashbuckle resolves JsonSerializerOptions from Mvc.JsonOptions or HttpJsonOptions, both of which + // default to CamelCase via JsonSerializerDefaults.Web. Simplify.Web serializes independently using + // PascalCase by default. Register a custom resolver with PropertyNamingPolicy = null (= PascalCase) + // before AddSwaggerGen so that Swashbuckle's TryAddSingleton is skipped. + services.AddSingleton(_ => + new JsonSerializerDataContractResolver(new JsonSerializerOptions { PropertyNamingPolicy = null })); + + return services; + } + /// /// Add Simplify.Web controllers to Swagger documentation generation /// diff --git a/src/TesterApp/Program.cs b/src/TesterApp/Program.cs index 0b5e52d..b8f1bd9 100644 --- a/src/TesterApp/Program.cs +++ b/src/TesterApp/Program.cs @@ -1,73 +1,44 @@ -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; using Simplify.DI; using Simplify.Web; using Simplify.Web.Swagger; using TesterApp.Setup; +#if NET10_0 +using Microsoft.OpenApi; +#else +using Microsoft.OpenApi.Models; +#endif var builder = WebApplication.CreateBuilder(args); // DI -DIContainer.Current - .RegisterAll() - .Verify(); +DIContainer.Current.RegisterAll().Verify(); // Swagger -builder.Services.AddEndpointsApiExplorer() +builder + .Services.AddSimplifyWebSwaggerServices() + .AddEndpointsApiExplorer() .AddSwaggerGen(x => { - x.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme - { - Name = "Authorization", - In = ParameterLocation.Header, - Type = SecuritySchemeType.ApiKey, - Scheme = "Bearer", - BearerFormat = "JWT", - Description = "Input your Bearer token in this format - Bearer {your token here} to access this API", - }); - - x.AddSecurityRequirement(new OpenApiSecurityRequirement - { + x.AddSecurityDefinition( + "Bearer", + new OpenApiSecurityScheme { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = "Bearer", - }, - Scheme = "Bearer", - Name = "Bearer", - In = ParameterLocation.Header, - }, - new List() + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer", + BearerFormat = "JWT", + Description = + "Input your Bearer token in this format - Bearer {your token here} to access this API", } - }); + ); - var args = new SimplifyWebSwaggerArgs(); - - var parameter = new OpenApiParameter + var swaggerArgs = new SimplifyWebSwaggerArgs { - Name = "Accept-Language", - In = ParameterLocation.Header, - Description = "Language preference for the response.", - Required = true, - AllowEmptyValue = true, - Example = new OpenApiString("en-US"), - Schema = new OpenApiSchema - { - Default = new OpenApiString("en-US"), - Enum = - [ - new OpenApiString("en-US"), - new OpenApiString("ru-RU") - ] - } + AcceptLanguageHeader = new AcceptLanguageHeaderArgs("en-US", "ru-RU", "kk-KZ"), }; - args.Parameters.Add(parameter); - - x.AddSimplifyWebSwagger(args); + x.AddSimplifyWebSwagger(swaggerArgs); }); // App @@ -75,6 +46,7 @@ var app = builder.Build(); app.UseSwagger(); + app.UseSwaggerUI(); app.UseSimplifyWeb(); diff --git a/src/TesterApp/TesterApp.csproj b/src/TesterApp/TesterApp.csproj index 42781a3..55cb1f5 100644 --- a/src/TesterApp/TesterApp.csproj +++ b/src/TesterApp/TesterApp.csproj @@ -1,13 +1,13 @@ - + net10.0 enable - + + - - + - \ No newline at end of file +