Incremental Source Generator: OpenAPI 3.0/3.1 β gRPC + Protobuf + C# 14
SharpPortico is a compile-time source generator that converts OpenAPI specifications (YAML or JSON) into production-quality gRPC services, protobuf messages, and modern C# 14 client/server code β zero reflection, NativeAOT-safe, fully AOT compatible. An optional proxy mode turns it into a gRPCβREST gateway for legacy REST APIs.
Runs on .NET 10 and .NET 11. The generator itself is netstandard2.0, which is what Roslyn loads analyzers on, so it works in a consumer's compiler whatever that consumer targets; the generated code is emitted in C# 14 and compiles on both.
- π
IIncrementalGeneratorβ fast incremental pipeline, safe on 10k-line specs - π¦ C# 14 output β primary constructors, collection expressions, required members, file-scoped namespaces
- π§± NativeAOT / reflection-free β hand-written
IMessage<T>implementations, noActivator;samples/NativeAotExampleis a 3.1 contract published as a native image and run in CI, so the claim is a binary that runs rather than a flag - π Both server and client, and both hosts β the generated
ServiceBaseis served byGrpc.Coreor by ASP.NET Core'sMapGrpcService, alongside a modern typed client (GrpcChannel) - π Proxy mode β generated
{Service}Proxy : ServiceBaseforwards gRPC β legacy REST (X-Api-Key outbound, response cache with per-call bypass, ULID client keys, audit logging) - π Auth mapped β Bearer / API-Key / OAuth2 metadata helpers and interceptors
- π§ Smart mapping β
$ref,allOf,oneOf/anyOf, arrays βrepeated, enums, pagination detection, streaming hints (x-grpc-streaming), octet-stream βbytes - π― Presence, not defaults β a nullable scalar becomes a proto3
optionalfield withHasβ¦/Clearβ¦, so "never set" and "set to0" stay different answers on the wire, inEquals, and in the gateway (presence) - π§Ύ A CLI that answers a gate β
sharpportico generate spec.yaml --format jsonprints one deterministic document (service identity, RPC surface, counts, every diagnostic) and still exits1on a refusal, so a pipeline branches on the exit code instead of parsing text - π οΈ Two declaration styles β
<AdditionalFiles>and/or[OpenApiToGrpc]assembly attributes
flowchart LR
A[openapi.yaml / .json] -->|AdditionalFiles| G[SharpPorticoGenerator<br/>IIncrementalGenerator]
A -->|OpenApiToGrpc attribute| G
G -->|Microsoft.OpenApi| P[Parser + SchemaMapper<br/>OpenAPI 3.0/3.1]
P -->|GrpcModel IR| E[Emitters]
E --> C[Generated C# .g.cs<br/>messages + service + client + DI]
E --> PR[Generated .proto.cs<br/>proto descriptor]
E --> PX["Generated {Service}Proxy<br/>gRPC to REST gateway"]
C --> R[Google.Protobuf]
C --> S[Grpc.Core / Grpc.Net.Client]
PX --> H[HttpRestClient + IProxyCache + IKeyProvider]
| OpenAPI Concept | gRPC / Protobuf Mapping |
|---|---|
| paths + HTTP verb | Unary RPC by default; x-grpc-streaming or large POST payloads β streaming |
| path / query / header params | Combined into a single *Request message |
| request body | Nested message; application/octet-stream β bytes |
| response | *Response message + google.rpc.Status-shaped error wrapper |
| components/schemas | message definitions; $ref, allOf, oneOf/anyOf resolved |
OpenAPI 3.1 β type arrays, const, prefixItems, contentEncoding, numeric exclusive bounds, $defs, webhooks |
read through a 3.0 rewrite; each construct is mapped, and what it lost is reported (SP1002/SP1003) rather than passed over |
| arrays | repeated fields |
nullable: true (or a 3.1 ["string", "null"] type array) |
optional on a singular scalar: proto3 presence, so a value set to the default and a value never sent stay different answers |
| enums | protobuf enums |
| authentication | metadata helpers + interceptors for Bearer / API-Key / OAuth2 |
| pagination | page/limit/cursor/next_page_token detected β token-based or server-streaming |
OpenApiToGrpc attribute (assembly level):
using SharpPortico;
[assembly: OpenApiToGrpc("openapi/users.yaml", "UserService", "MyApp.Generated")]or MSBuild <AdditionalFiles>:
<ItemGroup>
<AdditionalFiles Include="openapi/**/*.yaml" />
</ItemGroup><ProjectReference Include="../../src/SharpPortico.SourceGenerator/SharpPortico.SourceGenerator.csproj"
ReferenceOutputAssembly="false"
OutputItemType="Analyzer" />using var channel = GrpcChannel.ForAddress("http://localhost:50051");
var client = UserServiceClient.Create(channel);
var user = await client.GetUserAsync(42);One service base, two hosts.
ASP.NET Core (grpc-dotnet). The base carries [BindServiceMethod(typeof(PetService), "BindService")] and the
contract carries the BindService(ServiceBinderBase, PetServiceBase) overload that grpc-dotnet's binder looks
for, so MapGrpcService finds the service:
builder.Services.AddGrpc();
builder.Services.AddSingleton<PetServiceImpl>();
var app = builder.Build();
app.MapGrpcService<PetServiceImpl>();Grpc.Core. Unchanged: PetService.BindService(new PetServiceImpl()) returns the ServerServiceDefinition
that Server takes.
An implementation overrides ListPetsAsync(request, context) either way. grpc-dotnet binds a handler by the
RPC's own name, so the base also declares ListPets(request, context), which forwards to ListPetsAsync.
NativeAOT: the messages, the contract and the client are reflection-free, but grpc-dotnet's server binds by reflection β it looks up the binder method and each handler at startup β so an ASP.NET Core gRPC server is not an AOT target. That is a property of grpc-dotnet rather than of the generated code.
The
Grpc.Corehost above is the AOT-safe server, which is whatsamples/NativeAotExamplepublishes: a native executable that serves the generated contract and drives the generated client from the same process.
Point local .NET apps at SharpPortico over gRPC while it forwards to a legacy REST service (X-Api-Key, corporate network). Host the generated {Service}Proxy : ServiceBase:
[assembly: OpenApiToGrpc("openapi/users.yaml", "UserService", "MyApp.Generated",
EnableProxyGeneration = true,
ProxyBaseUrl = "https://corporate.example",
ProxyApiKeyHeaderName = "X-Api-Key",
ProxyCacheTtlSeconds = 60,
ProxyClientKeyMode = ClientKeyMode.None,
ProxyAuditEnabled = true)]var options = new ProxyOptions { BaseUrl = "https://corporate.example", ApiKeyHeaderName = "X-Api-Key" };
server.Services.Add(UserService.BindService(
new UserServiceProxy(options, new HttpRestClient(httpClient, options.BaseUrl),
cache: new MemoryProxyCache(memoryCache), keys: keyProvider)));- Cache: GET responses are cached (TTL). Clients bypass per call via
x-portico-bypass-cachemetadata. - Client keys (
ProxyClientKeyMode):NoneΒ·Forward(client key β X-Api-Key 1:1) Β·Own(validate ULID-shaped keyx-portico-key, use configured outbound key). - Keys never hardcoded:
IKeyProvider(config / Key Vault / delegate). - Audit:
ProxyAuditEnabled = true(or injectIProxyAuditLogger) logs client, RPC, cache-hit, HTTP status.
Live end-to-end demo: samples/LegacyProxyExample. OpenAPI 3.1 + NativeAOT demo: samples/NativeAotExample.
| Document | Answers |
|---|---|
docs/README.md |
the index: which page answers which question, and how each claim is proved |
docs/SharpPortico.md |
the developer guide β packages, declaration, proxy mode, mapping rules, samples |
docs/reference.md |
every option and default, the generated surface member by member, the runtime contracts, the CLI, compatibility |
docs/presence.md |
nullable fields β proto3 optional: members, wire format, gateway behaviour, upgrading |
docs/diagnostics.md |
every SP⦠id, its severity, what to fix, and how to gate a build on it |
src/*/README.md |
the NuGet package readmes, shipped inside the packages |
SharpPortico/
βββ src/
β βββ SharpPortico.SourceGenerator/ // Incremental generator
β βββ SharpPortico.Abstractions/ // [OpenApiToGrpc] attribute + options
β βββ SharpPortico.Runtime/ // AOT-safe runtime helpers (proxy pipeline; packed)
β βββ SharpPortico.Cli/ // dotnet sharpportico generate (tool)
βββ samples/
β βββ GrpcServerExample/ // full gRPC server + client (petstore)
β βββ MinimalApiExample/ // ASP.NET Minimal API over the gRPC client
β βββ LegacyProxyExample/ // gRPCβREST proxy: cache hit + bypass demo
β βββ NativeAotExample/ // OpenAPI 3.1 contract published as a native image (PublishAot)
βββ tests/
β βββ SharpPortico.Tests/ // xunit snapshot + mapping + perf tests
βββ docs/
βββ .github/scripts/check-protos.sh // compiles every sample's emitted descriptor with protoc
dotnet tool install -g SharpPortico.Cli --version 1.5.0
# What the mapping makes of the spec: service, namespace, package, RPC surface, counts, diagnostics
sharpportico generate openapi/petstore.yaml
# The same answer as one document, for a CI job that must not parse text
sharpportico generate openapi/petstore.yaml --format json
# And the descriptor a build of it would ship
sharpportico generate openapi/petstore.yaml --out out/The descriptor comes from the emitter the generator itself calls and is LF-normalized exactly as the generated
{Service}Proto.Text const holds it, so the file and a build of the same spec cannot disagree β which is what makes it
worth handing to protoc, to a client generator or to a reviewer. The C# compiler never reads that descriptor, so CI
compiles every sample's with protoc (.github/scripts/check-protos.sh): a name protoc rejects, or an import left
out of the file, is otherwise a build that stays green.
--format json puts one document on stdout and nothing else β formatVersion, the spec path as given, the service
identity, the RPC surface, the counts and every diagnostic with its id, severity and message. A refused contract still
produces the document and still exits 1, so the exit code is the first gate and the document is the second:
sharpportico generate openapi/petstore.yaml --format json > report.json
jq -e '[.diagnostics[] | select(.severity == "error")] | length == 0' report.jsonThe bytes are deterministic β UTF-8 without a byte-order mark, \n line endings, one trailing newline β and identical
between runs and between operating systems, which is what makes the format worth diffing.
tests/SharpPortico.Tests/openapi/petstore.report.json pins it and a Linux CI step regenerates and diffs it. The
developer guide lists every member and shows a refusal.
Looking for the Java equivalent? JavaPortico is the Java sibling of SharpPortico β the same OpenAPI β gRPC + protobuf generator and gRPCβREST proxy pipeline for the Java/Maven ecosystem (JDK 25 LTS), with a matching mapping model, proxy runtime and configuration knobs.
See CHANGELOG.md for the full release history.
MIT β see LICENSE. Copyright (c) 2026 MPCoreDeveloper.
