Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
040b231
feat: add contract model and C# generator
elzalem Mar 2, 2026
b7fd799
refactor(csharp): enrich contract model and add direct coverage
elzalem Mar 3, 2026
ab7103c
test(csharp): expand golden coverage and address lint
elzalem Mar 3, 2026
ff61cb5
fix(httpgen): avoid unsafe rune to byte conversion
elzalem Mar 3, 2026
66f63fb
test: deduplicate golden diff helper
elzalem Mar 3, 2026
060a788
feat(csharp): honor numeric and string encoding annotations
elzalem Mar 3, 2026
9f58edf
feat(csharp): reflect structural contract annotations
elzalem Mar 3, 2026
3c11d85
test(csharp): refresh simple contract golden
elzalem Mar 3, 2026
85f7fdc
build: always rebuild generator binaries
elzalem Mar 3, 2026
e44afd9
test(csharp): cover map enum and message contracts
elzalem Mar 3, 2026
a4c6b33
docs(csharp): add generator guide and discovery links
elzalem Mar 3, 2026
e847bdb
examples(csharp): add contract generation demo
elzalem Mar 3, 2026
4ff4b79
feat(csharp): generate HttpClient service clients
elzalem Mar 3, 2026
5484e98
feat(csharp): honor unwrap and bytes encoding at runtime
elzalem Mar 3, 2026
291785a
docs(csharp): describe generated HttpClient clients
elzalem Mar 3, 2026
56cbd4a
feat(csharp): honor empty behavior at runtime
elzalem Mar 3, 2026
65e0de3
feat(csharp): validate oneof discriminator runtime semantics
elzalem Mar 3, 2026
61b4256
test(csharp): cover advanced unwrap combinations
elzalem Mar 3, 2026
a1b8a29
test(csharp): expand edge-case normalization coverage
elzalem Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ help:
@echo ""
@echo "Current binaries to build: $(BINARIES)"

# Build all binaries
# Build all binaries. This always rebuilds because generator implementations live
# under internal/*, not just cmd/*, and stale binaries break golden tests.
.PHONY: build
build: $(BINARY_PATHS)

# Pattern rule to build each binary
$(BIN_DIR)/%: $(CMD_DIR)/%/*.go | $(BIN_DIR)
@echo "Building $*..."
@go build -o $@ ./$(CMD_DIR)/$*
build: | $(BIN_DIR)
@for binary in $(BINARIES); do \
echo "Building $$binary..."; \
go build -o $(BIN_DIR)/$$binary ./$(CMD_DIR)/$$binary; \
done

# Create bin directory
$(BIN_DIR):
Expand Down Expand Up @@ -285,4 +285,4 @@ ci-validate:
else \
echo "actionlint not found. Install with: go install github.com/rhysd/actionlint/cmd/actionlint@latest"; \
fi; \
done
done
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ This starts a working HTTP API with JSON endpoints and OpenAPI docs - all genera
|-----------|--------|
| `protoc-gen-go-http` | Go HTTP servers with routing, request binding, validation, and error handling |
| `protoc-gen-go-client` | Go HTTP clients with type safety, header helpers, and per-call options |
| `protoc-gen-csharp-http` | C# contracts and `HttpClient` service clients for typed SDKs and integrations |
| `protoc-gen-ts-client` | TypeScript HTTP clients with type safety, header helpers, and per-call options |
| `protoc-gen-ts-server` | TypeScript HTTP servers with routing, request binding, validation, and error handling — runs on Node, Deno, Bun, Cloudflare Workers |
| `protoc-gen-openapiv3` | OpenAPI v3.1 specs that stay in sync with your code, one file per service |
Expand Down Expand Up @@ -135,6 +136,7 @@ UserService.openapi.yaml
# Install the tools
go install github.com/SebastienMelki/sebuf/cmd/protoc-gen-go-http@latest
go install github.com/SebastienMelki/sebuf/cmd/protoc-gen-go-client@latest
go install github.com/SebastienMelki/sebuf/cmd/protoc-gen-csharp-http@latest
go install github.com/SebastienMelki/sebuf/cmd/protoc-gen-openapiv3@latest
go install github.com/SebastienMelki/sebuf/cmd/protoc-gen-ts-client@latest
go install github.com/SebastienMelki/sebuf/cmd/protoc-gen-ts-server@latest
Expand Down Expand Up @@ -180,6 +182,7 @@ sebuf is used at [Sarwa](https://www.sarwa.co/), the fastest-growing investment

- **[Complete Tutorial](./examples/simple-api/)** - Full walkthrough with working code
- **[Documentation](./docs/)** - Comprehensive guides and API reference
- **[C# Contract Generation](./docs/csharp-generation.md)** - C# plugin options, supported annotations, and examples
- **[More Examples](./docs/examples/)** - Additional patterns and use cases

## Built on Great Tools
Expand Down
17 changes: 17 additions & 0 deletions cmd/protoc-gen-csharp-http/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/types/pluginpb"

"github.com/SebastienMelki/sebuf/internal/csharpgen"
)

func main() {
options, cfg := csharpgen.NewOptions()
options.Run(func(plugin *protogen.Plugin) error {
plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)
gen := csharpgen.New(plugin, *cfg)
return gen.Generate()
})
}
143 changes: 143 additions & 0 deletions docs/csharp-generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# C# HTTP Client Generation

> **Generate C# contracts and `HttpClient` service clients from protobuf services**

`protoc-gen-csharp-http` generates C# contract types and `HttpClient`-based service clients from annotated protobuf packages. It is designed for SDKs, typed API integrations, and shared contracts where C# needs the same JSON-facing shape and HTTP calling surface as other sebuf generators.

## Quick Start

### Installation

```bash
go install github.com/SebastienMelki/sebuf/cmd/protoc-gen-csharp-http@latest
```

### Buf Configuration

Add the plugin to `buf.gen.yaml`:

```yaml
version: v2
plugins:
- local: protoc-gen-csharp-http
out: gen/csharp
opt:
- namespace=Acme.Contracts
- json_lib=system_text_json
```

### Protoc Usage

```bash
protoc \
--plugin=protoc-gen-csharp-http="$(go env GOPATH)/bin/protoc-gen-csharp-http" \
--csharp-http_out=gen/csharp \
--csharp-http_opt=namespace=Acme.Contracts,json_lib=newtonsoft \
--proto_path=. \
--proto_path=./proto \
proto/example/v1/service.proto
```

## Generated Output

For each generated package, the plugin emits one `Contracts.g.cs` file containing:

- C# `enum` types for protobuf enums
- C# classes for protobuf messages
- `I{Service}Client` and `{Service}Client` types built on `HttpClient`
- `{Service}ClientOptions` and `{Service}CallOptions` for headers and transport configuration
- `ApiException` for non-success responses
- `ServiceContracts` metadata with service name, base path, HTTP method, route, request type, and response type per RPC

Nested protobuf messages and enums are flattened into idiomatic C# names such as `WidgetProfile` and `WidgetState`.

## Supported Options

### Generator Options

- `namespace`
Sets the C# namespace. Default: `Sebuf.Generated`
- `json_lib`
Chooses JSON attributes and converters. Supported values:
- `newtonsoft`
- `system_text_json`

## JSON Contract Behavior

The generator reflects the JSON-facing contract shape for the supported annotations below.

### Field and Message Shape

- `flatten`
Flattens child message fields into the parent contract, honoring `flatten_prefix`
- `oneof_config`
Emits discriminator properties and flattened discriminated-union fields when configured
- `unwrap`
Root unwrap messages generate collection-shaped contracts such as `List<T>`, and map-value unwrap is preserved during client request/response serialization
- `nullable`
Uses nullable C# reference/value types where the JSON contract can be `null`
- `empty_behavior`
Uses nullable contract fields for `NULL` and `OMIT` empty-message behavior

### Value Encoding

- `int64_encoding`
Maps `int64` JSON number encoding to `long`; otherwise uses `string`
- `enum_encoding`
Supports numeric enums or string enums with JSON converters
- `enum_value`
Applies custom string values via `[EnumMember(Value = "...")]`
- `timestamp_format`
Maps timestamp fields to `string` or `long` depending on configured format
- `bytes_encoding`
Represents bytes fields as `byte[]` and re-encodes on the wire for `hex`, `base64_raw`, `base64url`, and `base64url_raw`

## Example

Proto:

```proto
message Widget {
optional string display_name = 1 [(sebuf.http.nullable) = true];
Profile profile = 2 [(sebuf.http.flatten) = true, (sebuf.http.flatten_prefix) = "meta_"];

message Profile {
string note = 1;
}
}
```

Generated C#:

```csharp
public sealed class Widget
{
[JsonProperty("display_name")]
public string? DisplayName { get; set; }

[JsonProperty("meta_note")]
public string? MetaNote { get; set; }
}
```

## Client Runtime

For each protobuf service, the generator emits:

- `IWidgetServiceClient`
- `WidgetServiceClient`
- `WidgetServiceClientOptions`
- `WidgetServiceCallOptions`

Generated clients:

- use `HttpClient`
- build paths from annotated route params
- add annotated query params for `GET` / `DELETE`
- apply service-level and method-level headers
- serialize request bodies as JSON
- deserialize JSON responses into generated contracts
- preserve `unwrap` and `bytes_encoding` wire behavior
- throw `ApiException` for non-2xx responses

See [examples/csharp-contracts-demo](../examples/csharp-contracts-demo/) for a working generation example.
11 changes: 11 additions & 0 deletions docs/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This starts a working HTTP API with user management, authentication, and OpenAPI
| **[nested-resources](../../examples/nested-resources/)** | Organization hierarchy API | Deep path nesting (3 levels), multiple path params per endpoint |
| **[multi-service-api](../../examples/multi-service-api/)** | Multi-tenant platform | Multiple services, different auth levels, service/method headers |
| **[market-data-unwrap](../../examples/market-data-unwrap/)** | Financial market data API | Unwrap annotation for map values, JSON/protobuf compatibility |
| **[csharp-contracts-demo](../../examples/csharp-contracts-demo/)** | C# contract generation demo | C# contracts, flattened fields, oneof discriminator metadata, root unwrap |
| **[ts-client-demo](../../examples/ts-client-demo/)** | TypeScript client demo | TypeScript HTTP client, CRUD API, query params, headers, error handling |
| **[ts-fullstack-demo](../../examples/ts-fullstack-demo/)** | TypeScript full-stack demo | TS client + TS server from same proto, CRUD, unwrap, custom errors |

Expand Down Expand Up @@ -139,6 +140,16 @@ cd examples/ts-client-demo && make demo

**Prerequisites**: Node.js (for the TypeScript client)

### csharp-contracts-demo
HTTP client generation example for `protoc-gen-csharp-http`.
- Generates C# contracts plus `HttpClient` service clients
- Shows `flatten`, `nullable`, `oneof_config`, `unwrap`, `bytes_encoding`, and service route metadata
- Supports both `newtonsoft` and `System.Text.Json` output

```bash
cd examples/csharp-contracts-demo && make generate
```

### ts-fullstack-demo
Full TypeScript stack: both client and server generated from the same proto.
- Generated TypeScript server from `protoc-gen-ts-server` (Web Fetch API)
Expand Down
1 change: 1 addition & 0 deletions examples/csharp-contracts-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
gen/
25 changes: 25 additions & 0 deletions examples/csharp-contracts-demo/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
PROTO_DIR := proto
OUT_DIR := gen

.PHONY: generate
generate:
@mkdir -p $(OUT_DIR)/newtonsoft $(OUT_DIR)/system-text-json
@protoc \
--plugin=protoc-gen-csharp-http=../../bin/protoc-gen-csharp-http \
--proto_path=$(PROTO_DIR) \
--proto_path=../../proto \
--csharp-http_out=$(OUT_DIR)/newtonsoft \
--csharp-http_opt=namespace=Demo.Contracts,json_lib=newtonsoft \
$(PROTO_DIR)/contracts.proto
@protoc \
--plugin=protoc-gen-csharp-http=../../bin/protoc-gen-csharp-http \
--proto_path=$(PROTO_DIR) \
--proto_path=../../proto \
--csharp-http_out=$(OUT_DIR)/system-text-json \
--csharp-http_opt=namespace=Demo.Contracts,json_lib=system_text_json \
$(PROTO_DIR)/contracts.proto
@echo "Generated C# contracts into $(OUT_DIR)/"

.PHONY: clean
clean:
@rm -rf $(OUT_DIR)
37 changes: 37 additions & 0 deletions examples/csharp-contracts-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# C# HTTP Client Demo

This example shows how to generate C# contracts and `HttpClient` service clients with `protoc-gen-csharp-http`.

## What It Covers

- flattened message fields with `flatten` and `flatten_prefix`
- discriminated oneofs with `oneof_config`
- nullable contract fields
- root unwrap collection contracts
- generated `I{Service}Client` / `{Service}Client` types
- request/response JSON handling for `unwrap` and `bytes_encoding`
- service route metadata
- both `newtonsoft` and `System.Text.Json` output modes

## Generate

```bash
cd examples/csharp-contracts-demo
make generate
```

Generated files:

- `gen/newtonsoft/demo/contracts/v1/Contracts.g.cs`
- `gen/system-text-json/demo/contracts/v1/Contracts.g.cs`

Each generated file includes:

- message and enum contracts
- service clients and per-call options
- `ApiException`
- `ServiceContracts` metadata

## Proto

The example proto lives at [proto/contracts.proto](./proto/contracts.proto).
62 changes: 62 additions & 0 deletions examples/csharp-contracts-demo/proto/contracts.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
syntax = "proto3";

package demo.contracts.v1;

option go_package = "github.com/SebastienMelki/sebuf/examples/csharp-contracts-demo/gen/go;contracts";

import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "sebuf/http/annotations.proto";

message Product {
string id = 1;
optional string display_name = 2 [(sebuf.http.nullable) = true];
Metadata metadata = 3 [(sebuf.http.flatten) = true, (sebuf.http.flatten_prefix) = "meta_"];
google.protobuf.Timestamp updated_at = 4 [(sebuf.http.timestamp_format) = TIMESTAMP_FORMAT_UNIX_MILLIS];

message Metadata {
string owner = 1;
}
}

message ProductEvent {
oneof payload {
option (sebuf.http.oneof_config) = {
discriminator: "kind"
flatten: true
};

Created created = 1;
Deleted deleted = 2 [(sebuf.http.oneof_value) = "removed"];
}

message Created {
string product_id = 1;
}

message Deleted {
string product_id = 1;
string reason = 2;
}
}

message ProductIds {
repeated string values = 1 [(sebuf.http.unwrap) = true];
}

message ArchiveProductRequest {
string id = 1;
}

service ProductService {
option (sebuf.http.service_config) = {
base_path: "/api/v1"
};

rpc ArchiveProduct(ArchiveProductRequest) returns (google.protobuf.Empty) {
option (sebuf.http.config) = {
method: HTTP_METHOD_POST
path: "/products/{id}:archive"
};
}
}
Loading
Loading