Porting Guide

Guidance (informative).

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 One definition, several transports, unhonourable mounts do nothing, registration validated before the first message (core-concepts §7) A builder or plain functions; those three properties 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 and registrationHash 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 one rule, not a taxonomy to invent — type is the registry prefix plus the status, 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 passes the contract-document producer groups; one whose services should be meshed (mesh.md §7.1) additionally implements the service-side mesh feeds and trace propagation and passes mesh-trace-cases.json. Plan for both from the start — together they are the difference between "interoperates" and "fully tool-operable and visible in the fleet".

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.