Skip to content

Commit 20ac9f8

Browse files
authored
Merge pull request #24 from SimplifyNet/feature/lib_versions_update
[add] net10 support, Accept-Language header, Bearer security auto-detection
2 parents 475d330 + b6dbb5a commit 20ac9f8

12 files changed

Lines changed: 491 additions & 134 deletions

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,40 @@ public class GetController : Controller2
5656

5757
5. After application started go to <http://localhost:5000/swagger/index.html> or <http://localhost:5000/swagger/v1/swagger.json> to see generated Swagger
5858

59+
## Configuration
60+
61+
### Bearer security scheme
62+
63+
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:
64+
65+
```csharp
66+
x.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { ... });
67+
x.AddSimplifyWebSwagger(); // Bearer requirement is added automatically
68+
```
69+
70+
To restrict to a specific scheme name (e.g. when you have multiple definitions but want only one applied), set `SecuritySchemeName` explicitly:
71+
72+
```csharp
73+
x.AddSimplifyWebSwagger(new SimplifyWebSwaggerArgs
74+
{
75+
SecuritySchemeName = "Bearer"
76+
});
77+
```
78+
79+
### JSON property casing
80+
81+
By default, Swagger schema property names follow the `System.Text.Json` default casing (PascalCase). To use camelCase (matching `JsonSerializerDefaults.Web`), register a custom `ISerializerDataContractResolver`:
82+
83+
```csharp
84+
builder.Services
85+
.AddSingleton<ISerializerDataContractResolver>(_ =>
86+
new JsonSerializerDataContractResolver(
87+
new JsonSerializerOptions(JsonSerializerDefaults.Web)
88+
))
89+
.AddEndpointsApiExplorer()
90+
.AddSwaggerGen(x => x.AddSimplifyWebSwagger());
91+
```
92+
5993
## Example application
6094

6195
Below is the example of Swagger generated by `Simplify.Web.Swagger`:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Collections.Generic;
2+
3+
namespace Simplify.Web.Swagger;
4+
5+
/// <summary>
6+
/// Configuration for the Accept-Language header parameter added to all operations.
7+
/// </summary>
8+
public class AcceptLanguageHeaderArgs
9+
{
10+
/// <summary>
11+
/// Initializes a new instance with the specified supported languages.
12+
/// The first entry is used as the default value.
13+
/// </summary>
14+
/// <param name="languages">Supported language codes, e.g. "en-US", "ru-RU".</param>
15+
public AcceptLanguageHeaderArgs(params string[] languages)
16+
{
17+
Languages = languages;
18+
Default = languages.Length > 0 ? languages[0] : string.Empty;
19+
}
20+
21+
/// <summary>
22+
/// The list of supported language codes.
23+
/// </summary>
24+
public IReadOnlyList<string> Languages { get; }
25+
26+
/// <summary>
27+
/// The default language code (defaults to the first entry in <see cref="Languages"/>).
28+
/// </summary>
29+
public string Default { get; set; }
30+
}

src/Simplify.Web.Swagger/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,24 @@
22

33
## [1.3.0] - Unreleased
44

5+
### Added
6+
7+
- .NET 10.0 explicit support
8+
- `AcceptLanguageHeaderArgs` — configures an `Accept-Language` header parameter added to all Swagger operations
9+
- `SimplifyWebSwaggerArgs.AcceptLanguageHeader` — enables Accept-Language header with supported languages list
10+
- `SimplifyWebSwaggerArgs.SecuritySchemeName` — explicit override for security scheme name; when `null` (default), all schemes registered via `AddSecurityDefinition` are applied automatically to authorized operations
11+
- Route parameter types auto-detection from controller `Invoke`/`InvokeAsync` method signatures (typed path parameters in Swagger schema)
12+
- `AddSimplifyWebSwaggerServices` extension method — registers PascalCase `ISerializerDataContractResolver` before `AddSwaggerGen`
13+
14+
### Changed
15+
16+
- Security requirements on authorized operations are now auto-detected from registered `AddSecurityDefinition` schemes; no manual `SecuritySchemeName` configuration needed
17+
518
### Dependencies
619

720
- Simplify.Web bump to 5.2
821
- Asp.Versioning.Mvc bump to 8.1.1
22+
- Swashbuckle.AspNetCore.SwaggerGen bump to 10.2.1 (net10.0)
923

1024
## [1.2.0] - 2025-10-10
1125

src/Simplify.Web.Swagger/ControllerAction.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
using System;
22
using System.Collections.Generic;
3+
#if NET10_0
4+
using Microsoft.OpenApi;
5+
using NetHttpMethod = System.Net.Http.HttpMethod;
6+
#else
37
using Microsoft.OpenApi.Models;
8+
#endif
49
using Simplify.Web.Controllers.Meta.Routing;
510

611
namespace Simplify.Web.Swagger;
@@ -29,13 +34,23 @@ public class ControllerAction
2934
/// </value>
3035
public IDictionary<int, OpenApiResponse> Responses { get; set; } = new Dictionary<int, OpenApiResponse>();
3136

37+
/// <summary>
38+
/// Gets or sets the route parameter types, keyed by parameter name (case-insensitive).
39+
/// </summary>
40+
public IDictionary<string, Type> RouteParameterTypes { get; set; } =
41+
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
42+
3243
/// <summary>
3344
/// Gets or sets the type.
3445
/// </summary>
3546
/// <value>
3647
/// The type.
3748
/// </value>
49+
#if NET10_0
50+
public NetHttpMethod Type { get; set; } = NetHttpMethod.Get;
51+
#else
3852
public OperationType Type { get; set; }
53+
#endif
3954

4055
/// <summary>
4156
/// Gets the path.

src/Simplify.Web.Swagger/ControllerActionsFactory.cs

Lines changed: 116 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Text.RegularExpressions;
5-
using Microsoft.OpenApi.Models;
65
using Simplify.Web.Controllers.Meta;
76
using Simplify.Web.Controllers.Meta.MetaStore;
87
using Simplify.Web.Controllers.Meta.Routing;
98
using Simplify.Web.Http;
109
using Swashbuckle.AspNetCore.SwaggerGen;
10+
#if NET10_0
11+
using Microsoft.OpenApi;
12+
using NetHttpMethod = System.Net.Http.HttpMethod;
13+
#else
14+
using Microsoft.OpenApi.Models;
15+
#endif
1116

1217
namespace Simplify.Web.Swagger;
1318

@@ -16,18 +21,17 @@ namespace Simplify.Web.Swagger;
1621
/// </summary>
1722
public static class ControllerActionsFactory
1823
{
19-
private static readonly IReadOnlyCollection<KeyValuePair<string, string>> ResponseDescriptionMap =
24+
private static readonly IReadOnlyCollection<
25+
KeyValuePair<string, string>
26+
> ResponseDescriptionMap =
2027
[
2128
new KeyValuePair<string, string>("1\\d{2}", "Information"),
22-
2329
new KeyValuePair<string, string>("201", "Created"),
2430
new KeyValuePair<string, string>("202", "Accepted"),
2531
new KeyValuePair<string, string>("204", "No Content"),
2632
new KeyValuePair<string, string>("2\\d{2}", "Success"),
27-
2833
new KeyValuePair<string, string>("304", "Not Modified"),
2934
new KeyValuePair<string, string>("3\\d{2}", "Redirect"),
30-
3135
new KeyValuePair<string, string>("400", "Bad Request"),
3236
new KeyValuePair<string, string>("401", "Unauthorized"),
3337
new KeyValuePair<string, string>("403", "Forbidden"),
@@ -38,9 +42,8 @@ public static class ControllerActionsFactory
3842
new KeyValuePair<string, string>("409", "Conflict"),
3943
new KeyValuePair<string, string>("429", "Too Many Requests"),
4044
new KeyValuePair<string, string>("4\\d{2}", "Client Error"),
41-
4245
new KeyValuePair<string, string>("5\\d{2}", "Server Error"),
43-
new KeyValuePair<string, string>("default", "Error")
46+
new KeyValuePair<string, string>("default", "Error"),
4447
];
4548

4649
/// <summary>
@@ -49,38 +52,49 @@ public static class ControllerActionsFactory
4952
/// <value>
5053
/// The remove prefixes.
5154
/// </value>
52-
public static IList<string> RemovePrefixes { get; } =
53-
[
54-
"Controllers.",
55-
"Api.v1."
56-
];
55+
public static IList<string> RemovePrefixes { get; } = ["Controllers.", "Api.v1."];
5756

5857
/// <summary>
5958
/// Creates the controller actions from controllers metadata.
6059
/// </summary>
6160
/// <param name="context">The context.</param>
62-
public static IEnumerable<ControllerAction> CreateControllerActionsFromControllersMetaData(DocumentFilterContext context) =>
63-
ControllersMetaStore.Current.RoutedControllers
64-
.SelectMany(item => CreateControllerActions(item, context));
65-
66-
private static IEnumerable<ControllerAction> CreateControllerActions(IControllerMetadata item, DocumentFilterContext context) =>
67-
item.ExecParameters!
68-
.Routes
69-
.Select(x => CreateControllerAction(x.Key, x.Value, item, context));
70-
71-
private static ControllerAction CreateControllerAction(HttpMethod method, IControllerRoute route, IControllerMetadata item, DocumentFilterContext context) =>
61+
public static IEnumerable<ControllerAction> CreateControllerActionsFromControllersMetaData(
62+
DocumentFilterContext context
63+
) =>
64+
ControllersMetaStore.Current.RoutedControllers.SelectMany(item =>
65+
CreateControllerActions(item, context)
66+
);
67+
68+
private static IEnumerable<ControllerAction> CreateControllerActions(
69+
IControllerMetadata item,
70+
DocumentFilterContext context
71+
) =>
72+
item.ExecParameters!.Routes.Select(x =>
73+
CreateControllerAction(x.Key, x.Value, item, context)
74+
);
75+
76+
private static ControllerAction CreateControllerAction(
77+
HttpMethod method,
78+
IControllerRoute route,
79+
IControllerMetadata item,
80+
DocumentFilterContext context
81+
) =>
7282
new()
7383
{
7484
Type = HttpMethodToOperationType(method),
7585
ControllerRoute = route,
7686
Names = CreateNames(item.ControllerType),
7787
Responses = CreateResponses(item.ControllerType, context),
7888
RequestBody = CreateRequestBody(item.ControllerType, context),
79-
IsAuthorizationRequired = item.Security is { IsAuthorizationRequired: true }
89+
IsAuthorizationRequired = item.Security is { IsAuthorizationRequired: true },
90+
RouteParameterTypes = GetRouteParameterTypes(item.ControllerType),
8091
};
8192

8293
private static ControllerActionNames CreateNames(Type controllerType) =>
83-
CreateNames(controllerType.FullName ?? throw new InvalidOperationException("controllerType.FullName is null"));
94+
CreateNames(
95+
controllerType.FullName
96+
?? throw new InvalidOperationException("controllerType.FullName is null")
97+
);
8498

8599
private static ControllerActionNames CreateNames(string name)
86100
{
@@ -113,19 +127,36 @@ private static string FormatNameSource(string str)
113127
return str;
114128
}
115129

116-
private static OperationType HttpMethodToOperationType(HttpMethod method) =>
130+
#if NET10_0
131+
private static NetHttpMethod HttpMethodToOperationType(HttpMethod method) =>
117132
method switch
118133
{
119-
HttpMethod.Get => OperationType.Get,
120-
HttpMethod.Post => OperationType.Post,
121-
HttpMethod.Put => OperationType.Put,
122-
HttpMethod.Patch => OperationType.Patch,
123-
HttpMethod.Delete => OperationType.Delete,
124-
HttpMethod.Options => OperationType.Options,
125-
_ => OperationType.Get
134+
HttpMethod.Get => NetHttpMethod.Get,
135+
HttpMethod.Post => NetHttpMethod.Post,
136+
HttpMethod.Put => NetHttpMethod.Put,
137+
HttpMethod.Patch => NetHttpMethod.Patch,
138+
HttpMethod.Delete => NetHttpMethod.Delete,
139+
HttpMethod.Options => NetHttpMethod.Options,
140+
_ => NetHttpMethod.Get,
126141
};
127-
128-
private static OpenApiRequestBody CreateRequestBody(Type controllerType, DocumentFilterContext context)
142+
#else
143+
private static OperationType HttpMethodToOperationType(HttpMethod method) =>
144+
method switch
145+
{
146+
HttpMethod.Get => OperationType.Get,
147+
HttpMethod.Post => OperationType.Post,
148+
HttpMethod.Put => OperationType.Put,
149+
HttpMethod.Patch => OperationType.Patch,
150+
HttpMethod.Delete => OperationType.Delete,
151+
HttpMethod.Options => OperationType.Options,
152+
_ => OperationType.Get,
153+
};
154+
#endif
155+
156+
private static OpenApiRequestBody CreateRequestBody(
157+
Type controllerType,
158+
DocumentFilterContext context
159+
)
129160
{
130161
var request = new OpenApiRequestBody();
131162
var attributes = controllerType.GetCustomAttributes(typeof(RequestBodyAttribute), false);
@@ -137,31 +168,73 @@ private static OpenApiRequestBody CreateRequestBody(Type controllerType, Documen
137168

138169
request.Content = new Dictionary<string, OpenApiMediaType>
139170
{
140-
[item.ContentType] = new() { Schema = context.SchemaGenerator.GenerateSchema(item.Model, context.SchemaRepository) }
171+
[item.ContentType] = new()
172+
{
173+
Schema = context.SchemaGenerator.GenerateSchema(
174+
item.Model,
175+
context.SchemaRepository
176+
),
177+
},
141178
};
142179

143180
return request;
144181
}
145182

146-
private static IDictionary<int, OpenApiResponse> CreateResponses(Type controllerType, DocumentFilterContext context) =>
147-
controllerType.GetCustomAttributes(typeof(ProducesResponseAttribute), false)
183+
private static IDictionary<int, OpenApiResponse> CreateResponses(
184+
Type controllerType,
185+
DocumentFilterContext context
186+
) =>
187+
controllerType
188+
.GetCustomAttributes(typeof(ProducesResponseAttribute), false)
148189
.Cast<ProducesResponseAttribute>()
149190
.ToDictionary(item => item.StatusCode, item => CreateResponse(item, context));
150191

151-
private static OpenApiResponse CreateResponse(ProducesResponseAttribute producesResponse, DocumentFilterContext context)
192+
private static OpenApiResponse CreateResponse(
193+
ProducesResponseAttribute producesResponse,
194+
DocumentFilterContext context
195+
)
152196
{
153197
var response = new OpenApiResponse
154198
{
155199
Description = ResponseDescriptionMap
156-
.FirstOrDefault((entry) => Regex.IsMatch(producesResponse.StatusCode.ToString(), entry.Key))
157-
.Value
200+
.FirstOrDefault(
201+
(entry) => Regex.IsMatch(producesResponse.StatusCode.ToString(), entry.Key)
202+
)
203+
.Value,
158204
};
159205

160206
foreach (var item in producesResponse.ContentTypes.Distinct())
161-
response.Content.Add(item, producesResponse.Type is null
162-
? new OpenApiMediaType()
163-
: new OpenApiMediaType { Schema = context.SchemaGenerator.GenerateSchema(producesResponse.Type, context.SchemaRepository) });
207+
{
208+
#if NET10_0
209+
response.Content ??= new Dictionary<string, OpenApiMediaType>();
210+
#endif
211+
response.Content.Add(
212+
item,
213+
producesResponse.Type is null
214+
? new OpenApiMediaType()
215+
: new OpenApiMediaType
216+
{
217+
Schema = context.SchemaGenerator.GenerateSchema(
218+
producesResponse.Type,
219+
context.SchemaRepository
220+
),
221+
}
222+
);
223+
}
164224

165225
return response;
166226
}
227+
228+
private static IDictionary<string, Type> GetRouteParameterTypes(Type controllerType)
229+
{
230+
var method = controllerType.GetMethod("Invoke") ?? controllerType.GetMethod("InvokeAsync");
231+
232+
if (method is null)
233+
return new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
234+
235+
return method
236+
.GetParameters()
237+
.Where(p => p.Name != null)
238+
.ToDictionary(p => p.Name!, p => p.ParameterType, StringComparer.OrdinalIgnoreCase);
239+
}
167240
}

0 commit comments

Comments
 (0)