Skip to content

How NSmithy works

Generated code describes the service; runtime libraries act on it. The generator emits model types, a schema for every shape, and the client and server surface. Wire formats, routing, retries, validation, and error handling live in hand-written NuGet packages that read those schemas.

PartGenerated or runtimeOwns
Model typesGeneratedThe service’s data as C# records, unions, enums, and exceptions
SchemasGeneratedEach shape’s structure and traits, and serializing its values
Client and server surfaceGeneratedTyped methods, handler interfaces, and the service’s table of operations
CodecsRuntimeOne wire format each: JSON, CBOR, XML, Protobuf
ProtocolsRuntimeEncoding, routing, and error representation of one Smithy protocol
Client runtimeRuntimeA call’s lifecycle: endpoint, auth, interceptors, retries, telemetry
Server runtime and hostRuntimeDispatching a request to its handler on ASP.NET Core

Generation is part of dotnet build; there is no separate command. It runs in three steps:

  1. MSBuild collects the model files, including those of referenced contracts projects, and writes the Smithy build configuration.
  2. The Smithy CLI assembles and validates the model and runs NSmithy’s generator.
  3. MSBuild compiles the generated .g.cs files under obj/ with the project.

Unchanged models skip generation, and dotnet watch build regenerates on every model edit. Handler implementations are application code and are never overwritten. The MSBuild reference covers configuration and the bundled toolchain.

A schema such as GetCityInputSchema describes its shape’s members and traits, and serializes values of the shape by handing each member to whatever consumes it. A JSON codec writes the member as JSON, the validator checks its constraints, and an HTTP binding puts it in a path label or header. Every consumer goes through the same generated code, so a new wire format or protocol needs no new generated code, and nothing reflects over model types.

Trait values are model data, so a schema keeps traits the generator knows nothing about: a custom @owner("catalog-team") trait reaches the running application without a generator change. Keeping a trait does not implement it. A new constraint or protocol needs a runtime component that reads it, and a trait that changes the C# representation, such as @required, needs generator support. Both can come from a package outside NSmithy.

A protocol is a runtime object, such as RestJson1Protocol. It binds an operation’s schema to HTTP requests and responses: how input is encoded, which route an operation answers on, and how a modeled error is represented and recognized. Generated code never branches on a protocol, so the generated types are the same under every protocol the service declares. A client uses one protocol at a time and can switch it in configuration; a server can serve the same handlers over several at once, as Hosting multiple protocols shows.

client.GetCityAsync(input) passes the input and the operation’s binding to the client runtime, which runs every call the same way:

resolve the endpoint and select an auth scheme
-> run interceptors
-> protocol serializes the request
-> sign and send the attempt (retried by the retry strategy)
-> protocol deserializes the output, or throws the modeled error

Auth, retries, interceptors, and telemetry are therefore client configuration, not generated code, and apply to every operation and protocol alike. Paginators and waiters call the client’s own operation methods, so each page or poll is a normal call.

MapWeather hands the service’s operations and the selected protocols to the ASP.NET Core host. Each protocol reports its routes, and the host maps them as endpoints. A request then flows:

host converts the HttpContext into a Smithy request
-> protocol deserializes the input
-> validator checks the model's constraints
-> handler runs: GetCityAsync(input, cancellationToken)
-> protocol serializes the output, or the modeled error the handler threw
host writes the response

The handler sees only typed model values. Validation rejects a request that breaks the model before the handler runs, and the MCP host serves the same operations from the same table.

Because behavior lives in the runtime, one model and one implementation serve several protocols, fakes and MCP reuse the same operations, and NSmithy can be extended with a new protocol or service generator from a package of its own; see Extending NSmithy.