NativeLambdaRouter.SourceGenerator.OpenApi
1.7.0
dotnet add package NativeLambdaRouter.SourceGenerator.OpenApi --version 1.7.0
NuGet\Install-Package NativeLambdaRouter.SourceGenerator.OpenApi -Version 1.7.0
<PackageReference Include="NativeLambdaRouter.SourceGenerator.OpenApi" Version="1.7.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="NativeLambdaRouter.SourceGenerator.OpenApi" Version="1.7.0" />
<PackageReference Include="NativeLambdaRouter.SourceGenerator.OpenApi"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add NativeLambdaRouter.SourceGenerator.OpenApi --version 1.7.0
#r "nuget: NativeLambdaRouter.SourceGenerator.OpenApi, 1.7.0"
#:package NativeLambdaRouter.SourceGenerator.OpenApi@1.7.0
#addin nuget:?package=NativeLambdaRouter.SourceGenerator.OpenApi&version=1.7.0
#tool nuget:?package=NativeLambdaRouter.SourceGenerator.OpenApi&version=1.7.0
NativeLambdaRouter.SourceGenerator.OpenApi
Roslyn Source Generator that emits OpenAPI 3.1 YAML at compile time from
NativeLambdaRouter endpoint maps — zero runtime overhead, zero reflection.
- Version:
1.7.0 - Target:
netstandard2.0(analyzer), consumed bynet10.0projects - Generated artifact:
{OpenApiSpecName ?? AssemblyName}.Generated.GeneratedOpenApiSpec - Interface: implements
Native.OpenApi.IGeneratedOpenApiSpecwhen that package is referenced
Agent quick-reference
What the generator inspects
The generator walks the syntax + semantic model of each compilation looking for:
| Surface | Where |
|---|---|
| Route maps | routes.MapGet<TCommand, TResponse>(...), MapPost, MapPut, MapPatch, MapDelete, Map("METHOD", ...) |
| Fluent chain | .WithName, .WithSummary, .WithDescription, .WithTags, .Accepts, .Produces(contentType), .ProducesProblem(statusCode), .AllowAnonymous, .ExcludeFromDocs |
| Command attributes | [EndpointName], [EndpointSummary], [EndpointDescription], [Tags], [Accepts], [HideFromDocs], [Deprecated], [ApiExample], [ErrorCatalog] |
| Catalog attributes | [ErrorDefinition] on const string fields of the type referenced by [ErrorCatalog(typeof(T))] |
| Handler attributes | [ApiResponse] on Handle(...) of IRequestHandler<TCommand, TResponse> implementations |
| Schema types | all TCommand / TResponse types; records, classes, enums, nullable reference types, arrays, dictionaries |
What it emits
{assembly}.Generated.GeneratedOpenApiSpecsingleton (public static readonly Instance) with:YamlContent— full OpenAPI 3.1 YAMLEndpointCount— count of discovered endpoints (public + hidden)EndpointList—(Method, Path)[]of discovered endpoints
- YAML
components.schemas.SwepayProblemDetailswhenever any operation servesapplication/problem+jsonwithout a typed body (F13) - Per-operation Wave 1 extensions (F03 deprecation fields, F09 examples map, F12
x-swepay-errorsslice) plus the rootx-swepay-error-catalog
Precedence rule
Fluent chain wins over command attributes when both are present — matches ASP.NET Core Minimal APIs.
Installation
<ItemGroup>
<PackageReference Include="NativeLambdaRouter.SourceGenerator.OpenApi"
Version="1.7.0"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<PackageReference Include="NativeOpenApi" Version="1.7.0" />
</ItemGroup>
The generator is a build-time-only analyzer.
ReferenceOutputAssembly="false"prevents the analyzer DLL from being published with the Lambda binary.
Supported map methods
| Method | HTTP verb |
|---|---|
MapGet<TCommand, TResponse>(path, factory) |
GET |
MapPost<TCommand, TResponse>(path, factory) |
POST |
MapPut<TCommand, TResponse>(path, factory) |
PUT |
MapPatch<TCommand, TResponse>(path, factory) |
PATCH |
MapDelete<TCommand, TResponse>(path, factory) |
DELETE |
Map<TCommand, TResponse>("METHOD", path, factory) |
arbitrary (e.g. OPTIONS) |
Path parameters ({id}) are extracted into parameters: [{ in: path, required: true, schema: { type: string } }].
Wave 1 (v1.7.0) — UX attributes
Full reference: ../Native.OpenApi/README.md · Root: ../../README.md
| Attribute / call | Generated YAML field(s) |
|---|---|
[HideFromDocs] / .ExcludeFromDocs() |
operation omitted |
[Deprecated(sunset, alternative, reason)] |
deprecated: true, x-sunset, x-swepay-alternative, x-swepay-deprecation-reason |
[ApiExample(name, summary) { RequestJson, ResponseStatus, ResponseJson }] |
requestBody.content.*.examples.{name} and/or responses.{status}.content.*.examples.{name} (via externalValue) |
[ErrorCatalog(typeof(T))] + [ErrorDefinition] fields |
root x-swepay-error-catalog + per-op x-swepay-errors (codes filtered by declared response statuses) |
any application/problem+json response without typed body |
$ref: "#/components/schemas/SwepayProblemDetails" + canonical schema injection |
Fluent metadata — pre-Wave-1 reference
routes.MapGet<GetClientsCommand, GetClientsResponse>("/v1/clients", ctx => new GetClientsCommand())
.WithName("ListAllClients")
.WithSummary("Retrieve all clients")
.WithDescription("Returns a paginated list of registered clients.")
.WithTags("Clients", "Admin")
.ProducesProblem(422);
| Method | Effect |
|---|---|
.WithName("id") |
operationId: id |
.WithSummary("text") |
summary: text |
.WithDescription("text") |
description: text (Markdown allowed; fenced ```mermaid blocks render when the host enables F17) |
.WithTags("A", "B") |
overrides auto-generated tags |
.Accepts("contentType") |
sets request body content type (default application/json) |
.ProducesProblem(statusCode) |
adds an application/problem+json response — points at SwepayProblemDetails (F13) |
.AllowAnonymous() |
emits security: [] (OpenAPI 3.1 convention) |
.ExcludeFromDocs() |
F01 — omits this operation from the generated spec |
Attribute-based metadata on TCommand
using NativeLambdaRouter.OpenApi.Attributes;
using Native.OpenApi.Attributes;
[EndpointName("ListAllClients")]
[EndpointSummary("Retrieve all clients")]
[EndpointDescription("Returns a paginated list of registered clients.")]
[Tags("Clients", "Admin")]
[Deprecated(sunset: "2026-12-31", alternative: "GET /v2/clients", reason: "no pagination.")]
[ErrorCatalog(typeof(SwepayErrors))]
[ApiExample("happy-path", "First page of clients",
RequestJson = "examples/list-clients/happy.json",
ResponseStatus = 200,
ResponseJson = "examples/list-clients/happy-response.json")]
public sealed record GetClientsCommand;
| Namespace | Attributes |
|---|---|
NativeLambdaRouter.OpenApi.Attributes |
EndpointName, EndpointSummary, EndpointDescription, Tags, Accepts |
Native.OpenApi.Attributes |
HideFromDocs, Deprecated, ApiExample, ErrorCatalog, ErrorDefinition, ApiResponse |
[ApiResponse] on handlers (v1.6.0+)
using Native.OpenApi;
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, CreateOrderResponse>
{
[ApiResponse(201, typeof(CreateOrderResponse))]
[ApiResponse(400, typeof(ValidationProblem), "application/problem+json")]
[ApiResponse(422, null, "application/problem+json")] // → SwepayProblemDetails
[ApiResponse(500, typeof(ProblemDetails), "application/problem+json")]
public async ValueTask<CreateOrderResponse> Handle(CreateOrderCommand r, CancellationToken ct) { ... }
}
The generator discovers IRequestHandler<TCommand, TResponse> via the semantic model and merges the handler's [ApiResponse] list with the fluent-chain .ProducesProblem(...) responses. When a problem+json response overlaps the default 400/401/500 responses, the declared one replaces the default $ref.
Form-encoded bodies (v1.5.1+)
[Accepts("application/x-www-form-urlencoded")]
public sealed record RefreshTokenCommand(string RealmId, string ClientId, string RefreshToken, string? Scope);
Or via fluent:
routes.MapPost<RefreshTokenCommand, TokenResponse>("/v1/realms/{realm}/protocol/openid-connect/refresh", ...)
.Accepts("application/x-www-form-urlencoded");
The generator emits an inline schema (all fields type: string, nullables excluded from required) instead of a $ref — matches the OAuth2 token endpoint convention.
Schema introspection
| C# type | OpenAPI |
|---|---|
string |
type: string |
int, long, short, byte |
type: integer + format |
float, double, decimal |
type: number + format |
bool |
type: boolean |
DateTime, DateTimeOffset |
type: string + format: date-time |
DateOnly |
type: string + format: date |
Guid |
type: string + format: uuid |
Uri |
type: string + format: uri |
List<T>, T[], IReadOnlyList<T> |
type: array + items |
Dictionary<K, V> |
type: object |
enum |
type: string + enum: [...] |
| complex types | $ref: "#/components/schemas/{TypeName}" |
nullable reference / Nullable<T> |
excluded from required |
MSBuild properties
Shipped via build/NativeLambdaRouter.SourceGenerator.OpenApi.props (auto-imported when the package is added as a NuGet dependency).
| Property | Read by | Default | Purpose |
|---|---|---|---|
OpenApiSpecName |
generator | AssemblyName |
namespace base → {value}.Generated |
OpenApiSpecTitle |
generator | OpenApiSpecName (dots → spaces) |
info.title in YAML |
OpenApiBrandPrimaryColor / AccentColor / LogoUrl / Favicon / FontFamily / ThemeJson |
renderer host | — | F15 branding |
OpenApiFooterStatusUrl / SupportUrl / ChangelogUrl / SlaUrl / TermsUrl |
renderer host | — | F16 footer |
OpenApiEnableMermaid, OpenApiInlineAssets |
renderer host | false |
F17 Mermaid + air-gap |
OpenApiServerProduction, OpenApiServerSandbox, OpenApiDefaultAudience |
reserved | — | Wave 2/3 |
Using
ProjectReferenceinstead of NuGet?.propsis not auto-imported. Add aDirectory.Build.propswith the same<CompilerVisibleProperty Include="..." />items. See samples/MultiLambdaSample/Directory.Build.props.
AWS Lambda AssemblyName=bootstrap recipe
Multiple Lambda projects with AssemblyName=bootstrap collide on the default bootstrap.Generated namespace. Override per producer:
<PropertyGroup>
<AssemblyName>bootstrap</AssemblyName>
<OpenApiSpecName>Swepay.Functions.Admin</OpenApiSpecName>
<OpenApiSpecTitle>Admin API</OpenApiSpecTitle>
</PropertyGroup>
Accessing the generated spec
using MyProject.Generated;
string yaml = GeneratedOpenApiSpec.YamlContent;
int n = GeneratedOpenApiSpec.EndpointCount;
foreach (var (method, path) in GeneratedOpenApiSpec.EndpointList)
Console.WriteLine($"{method} {path}");
// Polymorphic — requires the NativeOpenApi package
Native.OpenApi.IGeneratedOpenApiSpec spec = GeneratedOpenApiSpec.Instance;
EndpointListexposes all discovered endpoints, including those hidden by[HideFromDocs]/.ExcludeFromDocs(). Hidden endpoints are only absent from the YAML — runtime router wiring still sees them.
Multi-project architecture
Each producer project gets its own GeneratedOpenApiSpec under its namespace:
Functions.Admin → Functions.Admin.Generated.GeneratedOpenApiSpec
Functions.Identity → Functions.Identity.Generated.GeneratedOpenApiSpec
Functions.OpenId → Functions.OpenId.Generated.GeneratedOpenApiSpec
A consolidator project merges them with NativeOpenApi:
public class ConsolidatedOpenApiDocumentLoader : OpenApiDocumentLoaderBase
{
public ConsolidatedOpenApiDocumentLoader(OpenApiResourceReader reader) : base(reader) { }
public override IReadOnlyList<OpenApiDocumentPart> LoadCommon() => new[]
{
Load("schemas", "openapi/schemas.yaml"),
Load("responses", "openapi/responses.yaml"),
Load("security", "openapi/security.yaml"),
};
public override IReadOnlyList<OpenApiDocumentPart> LoadPartials() => new[]
{
LoadFromGeneratedSpec("admin", Functions.Admin.Generated.GeneratedOpenApiSpec.Instance),
LoadFromGeneratedSpec("identity", Functions.Identity.Generated.GeneratedOpenApiSpec.Instance),
LoadFromGeneratedSpec("openid", Functions.OpenId.Generated.GeneratedOpenApiSpec.Instance),
};
}
Full working example: samples/MultiLambdaSample.
Debug: write generated .cs files to disk
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
Output path:
obj/{Configuration}/{TargetFramework}/generated/NativeLambdaRouter.SourceGenerator.OpenApi/
NativeLambdaRouter.SourceGenerator.OpenApi.OpenApiSourceGenerator/
GeneratedOpenApiSpec.g.cs
Not required for the generator to work — the generated class is injected directly into the compilation.
EmitCompilerGeneratedFilesonly produces a physical copy for inspection.
Related
- NativeOpenApi — library + renderer + attributes
- Repository root · Changelog · UX RFC
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.