Skip to content

Extending NSmithy

NSmithy is extensible without changes to NSmithy itself. A package of your own can add a wire protocol, replace the generated client and server for a service that is not request/response, such as one that sends messages over a broker, or add functions and built-ins to endpoint rules. NSmithy’s built-in protocols use the same API, and its AWS support is an integration of its own.

Such an extension is a C# integration: a JAR on the codegen classpath that NSmithy’s generator discovers, usually paired with a NuGet package holding the runtime types the generated code calls. Projects opt in by adding both as dependencies.

Smaller extensions need no code generation. Generated clients take custom auth schemes, interceptors, and transports at runtime.

CSharpIntegration extends Smithy’s SmithyIntegration, so an integration gets Smithy’s hooks (preprocessModel, decorateSymbolProvider, interceptors, customize) and NSmithy’s own. Three of them shape a service’s generated surface:

HookUseNSmithy still generates
protocols()A request/response protocol over HTTPClient, server, DI registration, endpoint resolution, shapes
serviceGenerator(context, service)A service that is not request/response, such as messagingShapes, schemas, and operations
additionalShapes(model, settings, service)Shapes the service does not reach through its operations, such as events it publishesEverything, plus these shapes and the shapes they reference

Four more extend generated endpoint resolvers; see Extending endpoint rules. Through Smithy’s interceptors hook, an integration can also append to code sections of the generated code, such as ClientConfigDefaultsSection for a client configuration’s defaults.

Integrations run in the order of their name(), runBefore(), and runAfter().

Build against the codegen artifact, which the Smithy CLI provides at generation time:

build.gradle.kts
dependencies {
compileOnly("io.github.thomaslaich.nsmithy:smithy-csharp-codegen:0.14.0")
}

Register the integration for the Java ServiceLoader:

src/main/resources/META-INF/services/io.github.thomaslaich.nsmithy.csharp.codegen.integrations.CSharpIntegration
com.acme.nsmithy.MsgPackIntegration

A protocol has three parts: a protocol trait, a runtime type, and an integration that connects them.

Define the trait in a Smithy model inside the integration’s JAR, so services that reference the JAR can apply it:

src/main/resources/META-INF/smithy/msgpack.smithy
$version: "2"
namespace acme.protocols
@trait(selector: "service")
@protocolDefinition
structure msgpack {}

List the file in META-INF/smithy/manifest. Traits the protocol needs on members, and constraints it places on the model, belong in the same JAR as Smithy validators registered under META-INF/services/software.amazon.smithy.model.validation.Validator.

The runtime type implements IProtocol from NSmithy.Http and has a public parameterless constructor, which generated code calls:

TypeResponsibility
IProtocolBinds to a service through ForService(ServiceSchema). Optionally sets HttpVersionPreference.
IServiceProtocolHands out a per-operation protocol for the client and the server side. Reports server routes through GetServerRouting.
IClientOperationProtocol<TInput, TOutput>Serializes requests, deserializes responses, and recognizes and deserializes errors.
IServerOperationProtocol<TInput, TOutput>Deserializes requests, validates input, and serializes responses and modeled errors.

Protocols serialize from schemas, so they need no generated serializers. The codecs in NSmithy.Codecs.* cover JSON, CBOR, XML, and Protobuf.

GetServerRouting returns one of:

  • ServerRouting.ByRoute: one route per operation, matched by method and path, as in REST protocols.
  • ServerRouting.ByDispatch: one route for the service, and a function that names the operation from the request, as in RPC protocols.
  • ServerRouting.None: the protocol is client-only. This is the default.

Ship the runtime type in a NuGet package.

Return a CSharpProtocol from protocols():

MsgPackIntegration.java
public final class MsgPackIntegration implements CSharpIntegration {
@Override
public String name() {
return "msgpack";
}
@Override
public List<CSharpProtocol> protocols() {
return List.of(
new CSharpProtocol(
ShapeId.from("acme.protocols#msgpack"),
"MsgPack",
CSharpProtocol.runtimeType("Acme.Protocols.MsgPackProtocol"),
false, // eventStreams
true)); // server
}
}
FieldMeaning
traitThe protocol trait. A service that carries it gets the protocol.
nameA C# identifier used in generated names, such as the member of the server’s protocols enum.
runtimeTypeThe full name of the IProtocol implementation.
eventStreamsWhether the runtime type binds event-stream operations.
serverWhether the runtime type serves requests.

Built-in protocols register the same way and come first. Two registrations of one trait fail code generation. When a service has several protocols, the client defaults to the first, or to the defaultProtocol setting.

A protocol on another transport, or with exchanges that are not one request and one response, replaces the generated client and server with its own:

@Override
public Optional<CSharpServiceGenerator> serviceGenerator(
GenerationContext context, ServiceShape service) {
if (!service.hasTrait(KafkaTrait.ID)) {
return Optional.empty();
}
return Optional.of(new KafkaServiceGenerator());
}

CSharpServiceGenerator.generate(context, service) writes files through context.writerDelegator(). Return a generator only for services your integration owns: two integrations that both return one for a service fail code generation.

A service generator often needs types the service’s operations never reference, such as the events it publishes. Return them from additionalShapes:

@Override
public Set<ShapeId> additionalShapes(
Model model, CSharpSettings settings, ServiceShape service) {
return service.getTrait(PublishesTrait.class)
.map(PublishesTrait::getChannels)
.orElse(Set.of());
}

NSmithy generates each shape with the shapes it references, alongside the service and any configured closure. The hook receives the model as assembled, before codegen transforms. A shape the model does not contain fails code generation.

Generated endpoint resolvers implement the rules engine’s standard library. Rules that use other functions or built-ins need an integration that maps them to C#:

HookSupplies
endpointFunctions()A rules function id and the static C# method generated resolvers call for it
endpointBuiltIns()A built-in id and the {Service}ClientConfig property that supplies it
endpointAuthSchemeIds()Auth scheme names used in endpoint authSchemes, mapped to the auth trait shape ids client auth schemes are keyed by
endpointDefaultAuthSchemes(service)Auth scheme properties for endpoints whose rules set no authSchemes, such as a SigV4 signing name

NSmithy’s AWS support, smithy-csharp-aws-codegen, registers aws.partition, AWS::Region, and the other AWS functions and built-ins through these hooks, along with the AWS protocols.

AcmeEndpointsIntegration.java
@Override
public List<EndpointFunction> endpointFunctions() {
Symbol functions =
Symbol.builder().name("AcmeEndpointFunctions").namespace("Acme.Endpoints", ".").build();
return List.of(
new EndpointFunction(
"acme.cellHost", functions, "CellHost", EndpointValueType.STRING, true)); // nullable
}
@Override
public List<EndpointBuiltIn> endpointBuiltIns() {
return List.of(new EndpointBuiltIn("Acme::Cell", "Cell", "The cell requests are routed to."));
}
AcmeEndpointFunctions.cs
namespace Acme.Endpoints;
public static class AcmeEndpointFunctions
{
public static string? CellHost(string cell) => cell.Length == 0 ? null : $"{cell}.acme.example";
}

The method takes the function’s arguments in order. A nullable function returns null where the rules expect an empty value. Smithy validates rule sets before code generation, so the same JAR also declares the functions and built-ins to the rules engine through Smithy’s EndpointRuleSetExtension, as smithy-aws-endpoints does for AWS.

Add the integration’s JAR to maven.dependencies and its runtime package to the project:

smithy-build.json
{
"version": "1.0",
"maven": {
"dependencies": ["com.acme:nsmithy-msgpack:1.0.0"],
"repositories": [{ "url": "https://maven.pkg.github.com/acme/nsmithy-msgpack" }]
}
}
MyService.Client.csproj
<ItemGroup>
<PackageReference Include="Acme.Protocols.MsgPack" Version="1.0.0" />
</ItemGroup>

NSmithy merges maven.dependencies into the effective build file; see Maven dependencies for how they resolve. A NuGet package can add the dependency for its users through a SmithyContractsBuildFile fragment.

The codegen design doc describes how the generator orders protocols and lists the built-in ones.