Porting Guide

Status: DRAFT v0.1

Notes for implementing Benzene in another language. The strategy is spec-first: a port implements core-concepts.md, wire-contracts.md, and transport-bindings.md idiomatically — it does not translate the C# API. Interop with .NET Benzene services comes from the wire contracts, and is the first thing a port should prove, not the last.

Once a port is under way, every shippable increment is held to port-quality-standards.md — the Definition of Done that gates port work (DX-champion-in-the-loop workflow; conformance; layered packaging; a runnable multi-transport example per cloud provider tested through the port's own dogfooded test helpers; a CI gate; website-ready docs). This guide is how to translate; that document is when the translation is done.

1. What is concept vs C# idiom

.NET mechanism The concept underneath Idiomatic equivalent elsewhere
[Message("topic")] attribute + assembly scanning Explicit handler registration records (core-concepts §9) Explicit registration calls (Go, Rust); decorators (TS/Python); codegen
IMessageHandler<TRequest,TResponse> generic interface handle: TRequest → Result<TResponse> A function type / single-method interface; generics where available, per-registration types where not
MS DI + IBenzeneServiceContainer adapter Registration + per-invocation scope + overridable defaults (core-concepts §8) A context/registry object (Go); constructor injection frameworks where cultural (Java/TS)
IMiddleware<TContext> + Func<Task> next Ordered onion pipeline with short-circuit (core-concepts §4) The language's standard middleware shape (Go http-style wrappers, Express/Koa, Python ASGI-like)
IAsyncEnumerable<T> streaming handlers Async stream of items, one pipeline run per call (core-concepts §3) Channels (Go), async generators (TS/Python), Stream (Rust)
GrpcServerCallAccessor scoped accessor Invocation-scoped facts available to handler code without transport coupling Context values (Go context.Context), ALS (Node), contextvars (Python)
Reflection-cached protobuf Descriptor/Parser lookups proto3-JSON bridging rule (wire-contracts §6) Each language's protobuf library exposes the same JSON mapping natively
BenzeneStartUp abstract class The three-phase lifecycle + platform no-op rule (core-concepts §7) A builder or plain functions; the ordering and no-op semantics are what must survive

Rule of thumb: if removing a mechanism would change what's on the wire or what a handler observes, it's a concept and it's in the spec. Otherwise it's an idiom — do what's natural in the target language.

Client generation (the codegen row above) has its own pinned contract: the .spec.json a service derives and serves (contract-document.md), the topic-scoping and schema-closure rules a generator must implement identically, and the language-neutral contractHash algorithm — all conformance-pinned by contract-document-cases.json and contract-hash-cases.json. A port that ships a client generator implements that document; method naming and file layout stay idiom, same rule of thumb as above.

2. Suggested porting order

  1. Wire contracts first: envelope + status vocabulary + header conventions, verified against a running .NET Benzene service (send/receive the envelope, assert statuses round-trip). Cross-language interop is the product; prove it in week one.
  2. Result type, topic, registry, pipeline (with short-circuit + scope semantics).
  3. One inbound binding (HTTP is the cheapest) end-to-end, including status mapping and the correlation/trace middleware.
  4. One outbound client + decorators.
  5. Health checks (reserved topic + response format).
  6. Further bindings by demand — each is additive.

Problem details. Implement the failure payload from wire-contracts.md §1.3 exactly as specified: the member table, the replace-not-wrap rule, and the problem-details-cases.json / envelope-cases.json fixtures that pin it (see conformance/). The problem-type registry (wire-contracts.md §3.1) is data to copy, not a taxonomy to invent — eleven rows keyed by the existing status vocabulary, nothing to design.

3. Conformance testing

A language-neutral test suite that every implementation runs:

Conformance comes in two levels (cloud-service-profile.md): the fixtures above establish Benzene Core. A port that also wants its services to claim the Cloud Service Profile additionally implements the service-side mesh feeds and passes mesh-descriptor-cases.json and mesh-trace-cases.json. Plan for the profile from the start if the port's services should appear in a mesh — it is the difference between "interoperates" and "fully tool-operable".

Not required for conformance (it carries nothing over the wire, so there is nothing for the fixtures to check), but strongly recommended once the outbound client + decorators step (§2.4) is done: a MessageSender/outbound-client implementation that dispatches straight to a handler pipeline built in the same runtime, with no wire hop at all — not even loopback.

It exists for the modular monolith pattern: a service built as named, topic-addressed pipelines in one process, extracted into microservices later by swapping a routing-table entry from "in-process" to a real transport. A port without this transport still works for that pattern — a hand-rolled direct function call stands in — but it loses the point of the pattern: that a module boundary is a message, indistinguishable from a call that will one day cross a process, from the first commit.

Two things vary by port, and both are architecture, not oversight:

Match the target language's existing idiom for outbound clients (a constructed object used directly, not a new routing concept invented for this one transport) rather than translating any one port's shape literally — see the .NET, Go, TypeScript, and Python implementations for four idiomatic answers to the same two questions above.

5. Known .NET-isms that must NOT leak into the spec

Recorded so they don't get accidentally specified: