@team-supercharge/oasg 18.1.0 → 18.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1277,19 +1277,31 @@ with **System.Text.Json**, targeting **.NET 10** and marked
1277
1277
  > implements each `I{ApiName}` and calls `app.MapAll{PackageName}Endpoints()`; the
1278
1278
  > abstract MVC controllers are suppressed.
1279
1279
  >
1280
- > The model/DTO layer is also AOT-clean: each model emits a source-generated
1281
- > `JsonSerializerContext` and its `ToJson()` serializes through it (no Newtonsoft,
1282
- > no reflection `JsonSerializer`). Minimal APIs are Native-AOT compatible (unlike
1283
- > MVC), so combined with the source-generated models this server can be
1284
- > Native-AOT published.
1280
+ > The package builds AOT-clean: it sets `<EnableRequestDelegateGenerator>` so the
1281
+ > minimal-API `Map*()` calls are statically generated (no reflection-based request
1282
+ > delegates), each model emits a source-generated `JsonSerializerContext` used by
1283
+ > its `ToJson()` (no Newtonsoft, no reflection `JsonSerializer`), and there is no
1284
+ > reflection-based enum converter. Building the package under the AOT analyzer
1285
+ > (`<IsAotCompatible>`) raises no IL2026/IL3050 warnings.
1285
1286
  >
1286
- > The host must also register the package's JSON options so enums deserialize by
1287
- > their `[EnumMember]` wire value (e.g. `"available"`), which the minimal-API STJ
1288
- > pipeline does not do out of the box:
1287
+ > Enums (de)serialize by their wire value (e.g. `"available"`): each generated enum
1288
+ > carries `[JsonConverter(typeof(JsonStringEnumConverter<T>))]` with per-member
1289
+ > `[JsonStringEnumMemberName]`, which the source generator honours.
1290
+ >
1291
+ > **JSON registration.** The package generates a `{PackageName}JsonSerializerContext`
1292
+ > (a source-generated `JsonSerializerContext` over every model, request/response type
1293
+ > and inline enum) plus an `Add{PackageName}JsonOptions()` extension that registers it
1294
+ > on the minimal-API JSON resolver chain. Call it in Program.cs:
1289
1295
  >
1290
1296
  > ```csharp
1291
1297
  > builder.Services.Add{PackageName}JsonOptions();
1292
1298
  > ```
1299
+ >
1300
+ > This is **required when the host is published with Native AOT** (`PublishAot=true`),
1301
+ > because AOT removes the reflection JSON fallback and the endpoints must resolve every
1302
+ > DTO through source generation. For a reflection-based (JIT) host it is harmless (the
1303
+ > reflection resolver already covers everything), but registering it still gives faster,
1304
+ > allocation-free metadata, so calling it unconditionally is recommended.
1293
1305
 
1294
1306
  ##### `withWolverineImplementation`
1295
1307
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-supercharge/oasg",
3
- "version": "18.1.0",
3
+ "version": "18.2.0",
4
4
  "description": "Node-based tool to lint OpenAPI documents and generate clients, servers and documentation from them",
5
5
  "author": "Supercharge",
6
6
  "license": "MIT",
@@ -31,6 +31,12 @@ Everything lives in a single `EndpointRouteBuilderExtensions.cs`:
31
31
  - **`ConfigureWolverineMessaging(this WolverineOptions, string localQueueName = "MyApi")`** —
32
32
  routes every request type in the assembly to a local queue.
33
33
 
34
+ A separate `ServiceCollectionExtensions.cs` holds a source-generated
35
+ `{PackageName}JsonSerializerContext` (covering every model, request/response type
36
+ and inline enum) and an **`Add{PackageName}JsonOptions(this IServiceCollection)`**
37
+ extension that registers it on the minimal-API JSON resolver chain — call it in
38
+ Program.cs (required when publishing the host with Native AOT; harmless otherwise).
39
+
34
40
  The `WolverineFx` package reference is added automatically.
35
41
 
36
42
  ---
@@ -61,6 +67,7 @@ using MyApi; // generated Map{ApiName}Endpoints / MapAllMyApiEndpoints / Configu
61
67
 
62
68
  var builder = WebApplication.CreateBuilder(args);
63
69
  builder.Services.AddHttpContextAccessor(); // see "Authentication" below
70
+ builder.Services.AddMyApiJsonOptions(); // register the source-gen JSON context (required for Native AOT)
64
71
 
65
72
  builder.Host.UseWolverine(opts =>
66
73
  {
@@ -26,7 +26,7 @@ if [ -f "$endpointsFile" ]; then
26
26
  sed -i.bak 's/\.MapHttp/.Map/g' "$destFile" && rm -f "$destFile.bak"
27
27
  fi
28
28
 
29
- # Same for the JSON-options service-collection extension (SupportingFiles file).
29
+ # Same for the JSON source-gen context / service-collection extension (SupportingFiles file).
30
30
  serviceExtFile="out/$targetId/ServiceCollectionExtensions.cs"
31
31
  if [ -f "$serviceExtFile" ]; then
32
32
  mv "$serviceExtFile" "out/$targetId/src/$packageName/ServiceCollectionExtensions.cs"
@@ -12,7 +12,7 @@
12
12
  "nullableReferenceTypes": true,
13
13
  "operationResultTask": true,
14
14
  "useNewtonsoft": false,
15
- "enumNameSuffix": "",
15
+ "enumNameSuffix": "Enum",
16
16
  "enumValueSuffix": "",
17
17
  "wolverineVersion": "6.10.0"
18
18
  },
@@ -5,6 +5,11 @@
5
5
  <Authors>{{packageAuthors}}</Authors>
6
6
  <TargetFramework>net10.0</TargetFramework>
7
7
  <IsAotCompatible>true</IsAotCompatible>
8
+ <!-- Statically generate the minimal-API request delegates (interceptors) so the
9
+ Map*() calls are trim-/AOT-safe instead of reflection-based at runtime.
10
+ Without this the [RequiresDynamicCode]/[RequiresUnreferencedCode] Map*
11
+ overloads raise IL3050/IL2026 under the AOT analyzer enabled above. -->
12
+ <EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
8
13
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
9
14
  <PreserveCompilationContext>true</PreserveCompilationContext>
10
15
  <Version>{{packageVersion}}</Version>
@@ -38,9 +43,6 @@
38
43
  {{^useFrameworkReference}}
39
44
  <PackageReference Include="Microsoft.AspNetCore.App" />
40
45
  {{/useFrameworkReference}}
41
- {{^useSeparateModelProject}}
42
- <PackageReference Include="Microsoft.Extensions.Configuration.Json" {{#usePackageVersions}}Version="{{aspnetCoreVersion}}.0" {{/usePackageVersions}}/>
43
- {{/useSeparateModelProject}}
44
46
  {{#useSwashbuckle}}
45
47
  <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" {{#usePackageVersions}}Version="1.10.8" {{/usePackageVersions}}/>
46
48
  {{#useNewtonsoft}}
@@ -57,7 +59,6 @@
57
59
  <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" {{#usePackageVersions}}Version="{{newtonsoftVersion}}" {{/usePackageVersions}}/>
58
60
  {{/useNewtonsoft}}
59
61
  {{/useSwashbuckle}}
60
- <PackageReference Include="JsonSubTypes" {{#usePackageVersions}}Version="1.8.0" {{/usePackageVersions}}/>
61
62
  </ItemGroup>
62
63
  <ItemGroup>
63
64
  <!--<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="{{aspnetCoreVersion}}.0" />-->
@@ -0,0 +1,24 @@
1
+
2
+ /// <summary>
3
+ /// {{^description}}Gets or Sets {{{name}}}{{/description}}{{{description}}}
4
+ /// </summary>
5
+ {{#description}}
6
+ /// <value>{{{.}}}</value>
7
+ {{/description}}
8
+ {{! AOT-safe enum (de)serialization: the source-generated, generic }}
9
+ {{! JsonStringEnumConverter<T> (no runtime MakeGenericType/Activator) plus the }}
10
+ {{! per-member JsonStringEnumMemberName below, which maps each value to its wire }}
11
+ {{! form. The attribute travels with the type, so enums round-trip as strings }}
12
+ {{! for reflection-based (JIT) hosts and source-generated (AOT) hosts alike. }}
13
+ [JsonConverter(typeof(JsonStringEnumConverter<{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}>))]
14
+ public enum {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}
15
+ {
16
+ {{#allowableValues}}{{#enumVars}}
17
+ /// <summary>
18
+ /// Enum {{name}} for {{{value}}}
19
+ /// </summary>
20
+ {{#isString}}[EnumMember(Value = "{{{value}}}")]
21
+ [JsonStringEnumMemberName("{{{value}}}")]{{/isString}}
22
+ {{name}}{{^isString}} = {{{value}}}{{/isString}}{{#isString}} = {{-index}}{{/isString}}{{^-last}},
23
+ {{/-last}}{{/enumVars}}{{/allowableValues}}
24
+ }
@@ -1,5 +1,6 @@
1
1
  // <auto-generated/>
2
2
  #nullable enable
3
+ using System;
3
4
  using System.Collections.Generic;
4
5
  using System.Threading;
5
6
  using System.Threading.Tasks;
@@ -25,6 +26,7 @@ namespace {{packageName}}
25
26
  {
26
27
  {{#operations}}
27
28
  {{#operation}}
29
+ /// <summary>Application logic for the {{operationId}} operation.</summary>
28
30
  Task<IResult> {{operationId}}({{#allParams}}{{>paramType}} {{paramName}}, {{/allParams}}CancellationToken cancellationToken);
29
31
  {{/operation}}
30
32
  {{/operations}}
@@ -76,7 +78,7 @@ namespace {{packageName}}
76
78
  {{/apis}}
77
79
  {{/apiInfo}}
78
80
  /// <summary>Maps the endpoints of every generated API.</summary>
79
- public static IEndpointRouteBuilder MapAll{{packageName}}Endpoints(this IEndpointRouteBuilder endpoints)
81
+ public static IEndpointRouteBuilder MapAll{{#lambda.pascalcase}}{{packageName}}{{/lambda.pascalcase}}Endpoints(this IEndpointRouteBuilder endpoints)
80
82
  {
81
83
  {{#apiInfo}}
82
84
  {{#apis}}
@@ -23,7 +23,6 @@ using Swashbuckle.AspNetCore.Annotations;
23
23
  {{/discriminator}}
24
24
  {{/model}}
25
25
  {{/models}}
26
- using {{packageName}}.Converters;
27
26
 
28
27
  {{#models}}
29
28
  {{#model}}
@@ -1,66 +1,90 @@
1
1
  // <auto-generated/>
2
+ #nullable enable
2
3
  using System;
3
4
  using System.Collections.Generic;
4
- using System.Reflection;
5
- using System.Runtime.Serialization;
6
- using System.Text.Json;
7
5
  using System.Text.Json.Serialization;
8
- using Microsoft.AspNetCore.Http.Json;
9
6
  using Microsoft.Extensions.DependencyInjection;
7
+ using {{packageName}}.Models;
10
8
 
11
9
  namespace {{packageName}}
12
10
  {
13
11
  /// <summary>
14
- /// Registers the System.Text.Json options required by this generated package.
15
- /// Call <c>builder.Services.Add{{packageName}}JsonOptions()</c> in Program.cs.
12
+ /// System.Text.Json source-generation context covering every type this package
13
+ /// (de)serializes through its minimal-API endpoints: each generated model, each
14
+ /// operation request/response type, and each inline enum. Registering it (see
15
+ /// <see cref="{{#lambda.pascalcase}}{{packageName}}{{/lambda.pascalcase}}ServiceCollectionExtensions" />)
16
+ /// lets the endpoints resolve JSON metadata via source generation, which is what
17
+ /// makes the package safe to trim and to publish with Native AOT — the pipeline
18
+ /// never falls back to reflection.
16
19
  /// </summary>
17
- public static class {{packageName}}ServiceCollectionExtensions
20
+ /// <remarks>
21
+ /// Duplicate <c>[JsonSerializable]</c> entries are coalesced by the generator, so
22
+ /// request/response types that are also models are harmless. Inline enums are
23
+ /// registered with an explicit <c>TypeInfoPropertyName</c> because two models can
24
+ /// declare equally-named nested enums (e.g. <c>Order.StatusEnum</c> and
25
+ /// <c>Pet.StatusEnum</c>), which would otherwise collide under SYSLIB1031.
26
+ /// </remarks>
27
+ [JsonSerializable(typeof(string))]
28
+ {{#models}}
29
+ {{#model}}
30
+ [JsonSerializable(typeof({{classname}}))]
31
+ {{#vars}}
32
+ {{#isEnum}}
33
+ {{^complexType}}
34
+ [JsonSerializable(typeof({{classname}}.{{datatypeWithEnum}}), TypeInfoPropertyName = "{{classname}}{{datatypeWithEnum}}")]
35
+ {{/complexType}}
36
+ {{/isEnum}}
37
+ {{#items.isEnum}}
38
+ {{#items}}
39
+ {{^complexType}}
40
+ [JsonSerializable(typeof({{classname}}.{{datatypeWithEnum}}), TypeInfoPropertyName = "{{classname}}{{datatypeWithEnum}}")]
41
+ {{/complexType}}
42
+ {{/items}}
43
+ {{/items.isEnum}}
44
+ {{/vars}}
45
+ {{/model}}
46
+ {{/models}}
47
+ {{#apiInfo}}
48
+ {{#apis}}
49
+ {{#operations}}
50
+ {{#operation}}
51
+ {{^isResponseFile}}
52
+ {{#returnType}}
53
+ [JsonSerializable(typeof({{{returnType}}}))]
54
+ {{/returnType}}
55
+ {{/isResponseFile}}
56
+ {{#bodyParam}}
57
+ {{^isBinary}}
58
+ [JsonSerializable(typeof({{#isArray}}List<{{{items.dataType}}}>{{/isArray}}{{^isArray}}{{{baseType}}}{{/isArray}}))]
59
+ {{/isBinary}}
60
+ {{/bodyParam}}
61
+ {{/operation}}
62
+ {{/operations}}
63
+ {{/apis}}
64
+ {{/apiInfo}}
65
+ public partial class {{#lambda.pascalcase}}{{packageName}}{{/lambda.pascalcase}}JsonSerializerContext : JsonSerializerContext
18
66
  {
19
- public static IServiceCollection Add{{packageName}}JsonOptions(this IServiceCollection services)
20
- {
21
- services.Configure<JsonOptions>(opts =>
22
- opts.SerializerOptions.Converters.Add(new EnumMemberJsonConverterFactory()));
23
- return services;
24
- }
25
67
  }
26
68
 
27
69
  /// <summary>
28
- /// Converts enums using their <see cref="EnumMemberAttribute" /> wire values (falling back to
29
- /// the member name), so JSON like <c>"available"</c> round-trips to <c>StatusEnum.Available</c>.
70
+ /// Registers the System.Text.Json source-generation context for this generated
71
+ /// package. Call <c>builder.Services.Add{{#lambda.pascalcase}}{{packageName}}{{/lambda.pascalcase}}JsonOptions()</c>
72
+ /// in Program.cs so the minimal-API endpoints resolve JSON metadata via source
73
+ /// generation — required when the host is published with Native AOT, harmless
74
+ /// (a no-op the reflection resolver already covers) for a JIT host.
30
75
  /// </summary>
31
- internal sealed class EnumMemberJsonConverterFactory : JsonConverterFactory
76
+ public static class {{#lambda.pascalcase}}{{packageName}}{{/lambda.pascalcase}}ServiceCollectionExtensions
32
77
  {
33
- public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum;
34
-
35
- public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) =>
36
- (JsonConverter?)Activator.CreateInstance(
37
- typeof(EnumMemberJsonConverter<>).MakeGenericType(typeToConvert));
38
- }
39
-
40
- internal sealed class EnumMemberJsonConverter<T> : JsonConverter<T> where T : struct, Enum
41
- {
42
- private static readonly Dictionary<string, T> _read;
43
- private static readonly Dictionary<T, string> _write;
44
-
45
- static EnumMemberJsonConverter()
78
+ /// <summary>
79
+ /// Inserts <see cref="{{#lambda.pascalcase}}{{packageName}}{{/lambda.pascalcase}}JsonSerializerContext" />
80
+ /// at the front of the minimal-API JSON type-info resolver chain.
81
+ /// </summary>
82
+ public static IServiceCollection Add{{#lambda.pascalcase}}{{packageName}}{{/lambda.pascalcase}}JsonOptions(this IServiceCollection services)
46
83
  {
47
- _read = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
48
- _write = new Dictionary<T, string>();
49
- foreach (var field in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static))
50
- {
51
- var wireValue = field.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? field.Name;
52
- var enumValue = (T)field.GetValue(null)!;
53
- _read[wireValue] = enumValue;
54
- _write[enumValue] = wireValue;
55
- }
84
+ services.ConfigureHttpJsonOptions(options =>
85
+ options.SerializerOptions.TypeInfoResolverChain.Insert(
86
+ 0, {{#lambda.pascalcase}}{{packageName}}{{/lambda.pascalcase}}JsonSerializerContext.Default));
87
+ return services;
56
88
  }
57
-
58
- public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
59
- _read.TryGetValue(reader.GetString() ?? string.Empty, out var value)
60
- ? value
61
- : throw new JsonException($"Unknown value '{reader.GetString()}' for {typeof(T).Name}");
62
-
63
- public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) =>
64
- writer.WriteStringValue(_write.TryGetValue(value, out var s) ? s : value.ToString());
65
89
  }
66
90
  }
@@ -0,0 +1,11 @@
1
+ // <auto-generated/>
2
+ //
3
+ // Intentionally empty for the System.Text.Json / Native-AOT target.
4
+ //
5
+ // The default aspnetcore template emits an MVC `CustomEnumConverter<T>`
6
+ // (a TypeConverter whose body calls the reflection-based
7
+ // JsonSerializer.Deserialize<T>, which raises IL2026/IL3050 under the AOT
8
+ // analyzer). This target suppresses MVC controllers and (de)serializes enums
9
+ // via [JsonConverter(typeof(JsonStringEnumConverter<T>))] + [JsonStringEnumMemberName]
10
+ // on each generated enum, so the converter is unused — and is omitted here to
11
+ // keep the package AOT-clean.