---
name: sdk-conventions
description: The public-API contract SmithyDotNet-generated code must match against the shipping AWS SDK for .NET - what must match vs. what can differ. Use before writing or changing any SmithyDotNet writer.
---
# Skill: .NET SDK Conventions

## Reviewing Generated Output

When reviewing a service migration's generated output, open and diff **every single generated file** — every operation's request marshaller and response unmarshaller, every model, exception, and client file. No sampling. Reviewing one operation and generalizing "clean" to its neighbors is how regressions ship. Thousands of files is not a reason to skip any.

The public API surface can be identical while the wire behavior changes. AssemblyComparer and "the public surface is unchanged" only check the public contract; they **cannot** see marshaller/unmarshaller bodies — `ResourcePath`, query parameters, headers, serialization. A clean AssemblyComparer is necessary, not sufficient; it is not a substitute for reading the diff.

A **removed** line in generated output is a red flag — investigate it, do not wave it through. Real example: the generator dropping `request.AddSubResource("aws_iam", "t")` and folding the query literal into `request.ResourcePath = "/token?aws_iam=t"` left the public API identical while silently changing the request sent to the wire (`?` gets percent-encoded, dropping the flag).

## What Must Match (Public API Contract)

- Public class/interface names and their base types. An event structure (a non-error member of a
  `@streaming` union) implements `Amazon.Runtime.EventStreams.IEventStreamEvent`, fully qualified (no
  `using`) so it can't clash with a per-stream `{Namespace}.Model.IEventStreamEvent`. A request event
  stream's input member becomes a `Func<Task<I{Union}Event>> {Member}Publisher` property: it keeps any
  modeled `[AWSProperty]`/`[Obsolete]` but has no `IsSet` (the marshaller wires the Func unconditionally).
  Its doc content matches C2J.
- Public property names, types, and nullability
- Public method signatures (name, parameters, return type)
- `[AWSProperty]` attributes on public members (Required, Min, Max)
- XML doc comments on public types and members (content, not formatting)
- `partial` modifier on all generated types
- Namespace structure (`{Namespace}`, `{Namespace}.Model`)
- `internal bool IsSet{Property}()` per member — the public `AWSSDKUtils.IsPropertySet`
  reflection API and existing marshallers invoke these by name

## What Can Differ

- Whitespace, indentation, blank lines (the Roslyn formatter runs on every C# file whose raw output differs
  from the file on disk; emitting exact spacing lets a run skip it)
- File names — prefer `{TypeName}.g.cs` to distinguish generated files
- Backing fields and other private helpers (the generator omits them — see Property Pattern below)
- `using` directive order
- `#region` blocks (purely cosmetic)
- Code comments outside XML docs

## Pragma Warnings

The SDK builds with warnings-as-errors. Generated files must include `#pragma warning disable` for warnings that would otherwise break the build. The exact set doesn't need to match the current SDK file-for-file, but the output must compile cleanly. Common ones:
- `CS0612` / `CS0618` — obsolete/deprecated member usage (generated code may reference deprecated shapes)
- `CS1570` — malformed XML doc comments (common with complex HTML from `@documentation` traits)

## License Header

Every generated file starts with the full Apache 2.0 license block followed by the
"Do not modify this file. This file is generated from the {model-filename} service model." notice,
where `{model-filename}` is the Smithy model file name (e.g. `cloudtrail-data-2021-08-11.normal.json`).
The exact text lives in `Writers/FileHeader.cs`.

## Naming Rules

### Which Name Goes Where

A service has two derived names, equal for most services:

- `BaseName` (C2J `ClassName`, metadata.json's `base-name`) → the generated type names: client,
  config, exception/request bases, endpoint types. Model classes go in `{Namespace}.Model`.
- `ServiceName` (C2J `ServiceFolderName`, the namespace minus `Amazon.`) → everything else:
  `AWSSDK.{X}` package names, `sdk/src|test/Services/{X}` trees, `_sdk-versions.json` keys,
  `{X}.slnx`, the endpoint tests `[TestCategory]`, **and the paginator factory types**
  (`I{X}PaginatorFactory` — C2J's templates use `ServiceNameRoot` there).

They diverge when metadata.json overrides the namespace: sesv2 has class
`AmazonSimpleEmailServiceV2Client` but package/folder/paginators `SimpleEmailV2`. When adding a
name to a writer, check the shipping SDK for which of the two it follows.

### Class and Member Names

- **Shape names** → PascalCase class names (Smithy shape names are already PascalCase). The service
  `rename` map wins when it has an entry for the shape; error codes still use the shape name
- **Member names** → PascalCase property names. Smithy uses camelCase (`eventData`), .NET uses PascalCase (`EventData`)
- The conversion: capitalize the first letter of the Smithy member name, or of its `emitPropertyName` (see Customizations)
- **Acronyms** are preserved as-is from the Smithy model. Example: `eventID` → `EventID` (not `EventId`)
- A response member named `ContentLength` is **omitted** from the response class —
  `AmazonWebServiceResponse` already declares it — but the response unmarshaller still assigns the
  inherited property. Response-only, matching C2J (the MediaStoreData case).

### Client Names

- Interface: `IAmazon{BaseName}` (e.g. `IAmazonCloudTrailData`)
- Class: `Amazon{BaseName}Client` (e.g. `AmazonCloudTrailDataClient`)
- Config: `Amazon{BaseName}Config`
- Service exception base: `Amazon{BaseName}Exception`
- Service request base: `Amazon{BaseName}Request`

## File Layout

Generated files go under `Generated/`. Prefer `.g.cs` suffix:

```
Generated/
  IAmazon{BaseName}.g.cs
  Amazon{BaseName}Client.g.cs
  Amazon{BaseName}Config.cs            # plain .cs so CI's Amazon*Config.cs glob stages it
  Amazon{BaseName}Exception.g.cs
  Model/
    Amazon{BaseName}Request.g.cs       # empty service request base
    {OperationName}Request.g.cs
    {OperationName}Response.g.cs
    {ShapeName}.g.cs
    {ExceptionName}.g.cs
```

A structure that doubles as an operation input/output normally gets only its
`{Op}Request`/`{Op}Response` wrappers — no `{ShapeName}.g.cs`. Exception: when other generated
code references the shape through a member (directly or as a list/map element), the standalone
class is emitted too, because member properties are typed with the plain class name (C2J parity:
drs `SourceServer` has one, kinesis `EnhancedMonitoringOutput` does not).

## Doc Samples

`DocSamplesWriter` and `DocSampleMetadataWriter` emit
`docgenerator/AWSSDKDocSamples/{ServiceName}/{ServiceName}.GeneratedSamples.cs` and
`{ServiceName}.GeneratedSamples.extra.xml` from `smithy.api#examples`, rendering values as C2J's
`Example.cs` does but with the SDK's types, so a copied sample compiles once placeholders like `<data>` are
filled in. Both are written unformatted
(`BatchGenerator` passes `format: false`): the samples file is never compiled and can hold placeholders like
`<binary data>` that the Roslyn formatter would mangle. A service with no examples, or S3 (its samples are
hand-written), gets no files, so an existing sample file stays as it is; examples the model lacks but C2J
had are a model gap. Example keys are matched to member property names ignoring case, so, as in C2J, a
member renamed by `emitPropertyName` (beyond case) drops out of the sample. The samples are built before
anything is written, so a bad example fails the service before anything is deleted, and written only once
its code has generated.

Differences from C2J:

- Region ids are `{Operation}-{n}` (the trait has no example id). They only link an `.extra.xml` entry to
  its code. Each is unique, unlike C2J's repeated `example-1`, which made the docs build (it takes the
  first matching `#region`) show the first sample's code for every later one.
- Keys are written ordinal-sorted; examples.json's order is sorted for about 92% of objects.
- Which examples exist, and their order within an operation, follow the trait.
- examples.json `comments` (C2J's `// comment` after an assignment) are lost: the trait has no such field.
- Response locals are typed as the response properties are (`int?`, `Stream`, the enum's class); C2J used
  non-nullable scalars, `MemoryStream` and `string`. Locals that are C# keywords are escaped (`@event`).
- A timestamp prints its 24-hour time and milliseconds, and a number is read as epoch seconds (or
  `DateTime.UtcNow` if it's out of range); C2J printed a 12-hour hour, dropped milliseconds and wrote
  `DateTime.UtcNow` for any number. A fractional `float` gets `f`.
- A request event stream is omitted: its `Func` publisher property can't be written from example data.
- A document value renders as `global::Amazon.Runtime.Documents.Document` with its content, using its
  collection initializer; C2J rendered an empty initializer of a class that doesn't exist.
- String literals are fully C# escaped, and titles and documentation escape `&`, `<` and `>` (titles also
  `"`) and drop characters XML 1.0 can't carry; C2J escaped only quotes, so a backslash, newline or `&`
  produced broken code or XML.
- The writers' own layout follows `CodeWriter` (platform newlines, 2-space XML indent); documentation text
  is written as it is. The docs build parses the XML and left-justifies each region, so the rendered docs
  don't change.

## Event Streams

Protocol-independent; only the per-event payload (un)marshalling and the response unmarshaller's body
differ (see `marshalling`). Each `@streaming` union is emitted once, however many operations share it.

**Request** (union sent as an operation input):
- Gets the marker interface `I{Union}Event` plus a `{Event} : I{Union}Event` partial per event. The name
  comes from the **union**, never the operation (shipped: Lex V2 `IStartConversationRequestEventStreamEvent`),
  and is emitted once per union even when several operations send it (the protocol test client shares one
  union across four).
- `@error` members get no partial: a client never sends an error event.

**Response** (union returned as an operation output):
- `{Union}` is emitted as the `EnumerableEventOutputStream<RuntimeEvent, {BaseName}EventStreamException>`
  subclass (C2J parity; `RuntimeEvent` is the alias described under **Both**). Each union member is a mapping entry keyed on the member name verbatim (the wire
  `:event-type`; the dict is `OrdinalIgnoreCase`): `@error` members feed `ExceptionMapping`, the rest
  `EventMapping` plus a PascalCase `{Name}Received` handler. The union gets no plain model class and no
  structure unmarshaller (the response unmarshaller does `new {Union}(context.Stream)`); its events keep theirs.
- The `{Op}Response` implements `IDisposable` and its dispose pattern releases the event stream member.
  Only when the operation also *sends* an event stream (bidi, or input-only) does it implement
  `Amazon.Runtime.EventStreams.IEventInputStreamContextOwner` (explicit `SetEventInputStreamContext` under a
  CA1033 suppression) and dispose the context first (C2J parity: Bedrock `ConverseStreamResponse` is
  `AmazonWebServiceResponse, IDisposable`, Lex V2 `StartConversationResponse` adds the owner interface).
- Any response event stream gates the per-service `{BaseName}EventStreamException`.

**Both:**
- Names are never adjusted for collisions: the protocol test client's union is named `EventStream`, so its
  marker is `IEventStreamEvent`, same simple name as the runtime's (the only runtime type an `I{Union}Event`
  can shadow). Anywhere under the `.Model` namespace (model classes, `MarshallTransformations`) the service's
  marker silently wins over the import; files outside it that import both namespaces (the client) hit CS0104.
  **Every writer that needs the runtime's emits `using RuntimeEvent = Amazon.Runtime.EventStreams.IEventStreamEvent;`
  and uses `RuntimeEvent`.** An alias named `IEventStreamEvent` would not help (a type in an enclosing
  namespace beats it too); one no shape can be named after does.

## Base Types

| Generated class | Inherits from |
|---|---|
| Client interface | `IAmazonService, IDisposable` |
| Client class | `AmazonServiceClient, IAmazon{BaseName}` |
| Service exception base | `AmazonServiceException` |
| Service request base | `AmazonWebServiceRequest` |
| Request classes | `Amazon{BaseName}Request` (the service request base) |
| Response classes | `AmazonWebServiceResponse`, plus `, IDisposable` when an output member is `@streaming` (emits a `#region Dispose Pattern` that disposes each streaming member's stream) |
| Structure classes | No base type (plain class) |
| Exception classes | `Amazon{BaseName}Exception` (the service exception base) |
| Config class (`Amazon{BaseName}Config`) | `ClientConfig` |

## All Types Are `partial`

Every generated class and interface uses the `partial` modifier.

## Property Pattern

The public surface must match. Internal implementation can vary.

**Required public surface:**
```csharp
/// <summary>
/// Gets and sets the property EventData. 
/// <para>
/// The content of an audit event...
/// </para>
/// </summary>
[AWSProperty(Required=true)]
public string EventData { get; set; }

/// <summary>
/// Checks to see if the EventData property is set.
/// </summary>
internal bool IsSetEventData() => this.EventData != null;
```

The generator emits auto-properties (`{ get; set; }`) plus an internal `IsSet{Property}()`
method per member. The current SDK uses explicit backing fields, but the public surface (and
the reflection API) only needs the property and the IsSet method — a backing field is not
required.

**Exception — `emitIsSetProperties` customization.** A listed member (shape name → modeled member
names) also gets a public `bool Is{Property}Set { get; set; }` whose accessors call
`InternalSDKUtils.GetIsSet`/`SetIsSet(value, ref field)`. A property can't be passed by `ref`, so the
member gets a private `_{Property}` backing field (collections keep the `InitializeCollections`
initializer) instead of an auto-property. `IsSet{Property}()` returns `Is{Property}Set`. Only nullable
value types and collections have `SetIsSet` overloads; any other listed member throws. Doc text
matches C2J's `StructureGenerator.tt`.

**`[AWSProperty]` attribute rules:**
- `Required=true` when member has `@required` trait, unless it also carries `@idempotencyToken` (the SDK fills it)
- `Min=N` when member has `@length` trait with min, or `@range` trait with min
- `Max=N` when member has `@length` trait with max, or `@range` trait with max
- Omit the attribute entirely if none of these traits are present

### Collection Properties

Collections use the `AWSConfigs.InitializeCollections` initializer to support both V4 (null
default) and V3-compat (empty list default) modes. The matching `IsSet` encodes the V3/V4
"empty counts as set?" rule so callers see consistent behavior in both modes:

```csharp
[AWSProperty(Required=true, Min=1, Max=100)]
public List<AuditEvent> AuditEvents { get; set; } = AWSConfigs.InitializeCollections ? new List<AuditEvent>() : null;

internal bool IsSetAuditEvents() => this.AuditEvents != null && (this.AuditEvents.Count > 0 || !AWSConfigs.InitializeCollections);
```

## Customizations (`*.customizations.json`)

The .NET-owned override layer (not part of the shared, upstream Smithy model). A hook with a Smithy
trait equivalent (`deprecatedMessage`, `renameShape`), or a structural edit (a member-keyed `renameShape`), is folded into the model in-memory by
`CustomizationTransform.Apply` before the `ServiceIndex` is built; every other hook is checked by
`CustomizationTransform` and read from `GenerationContext.Customizations` by shape and member
name where the member is resolved (`TypeMapper.ResolveMembers`), never stored on a shape. An unknown
hook key fails deserialization (fail-closed) rather than silently diverging from C2J.

`shapeSubstitutions.renameShape` is applied first, as an entry in the service's `rename` map (the shape ID
is unchanged, so targets still resolve). Every hook keys a renamed shape by its modeled name. C2J keys a renamed
structure by its new name, so S3's entries for its renamed structures must be rekeyed on migration. Wire error codes
keep the modeled name. A model that already renames the shape fails.
A Smithy-only `"Structure$member"` key instead copies that member's target under the new name (QApps: one
`Action` enum is C2J's `PermissionInputActionEnum` and `PermissionOutputActionEnum`); the original is dropped once
nothing targets it.

`emitPropertyName` renames only the C# property, so the member keeps its modeled name and with it its wire
name on every protocol. `CustomizationsModel.PropertyName` is used wherever a member's C# name is derived.
As in C2J, the name's first letter is upper-cased (iot's `marker` is `Marker`) and
`emitIsSetProperties`/`dataTypeSwap` list a renamed member under its new name.
`paginators.{Operation}` supplies what `@paginated` lacks but the shipped paginator has: `pageSize` (PowerShell reads
`LimitKey`); `inputToken`/`outputToken` arrays, which make a trait-less operation paginated — map tokens loop while entries
remain (DynamoDB `BatchGetItem`), several tokens page together (Route 53 `ListResourceRecordSets`); `items` (dotted paths)
added to the modeled one. A field the trait already has fails generation.
`operationModifiers.{Operation}.stopPaginationOnSameToken` stops on a repeated token (CloudWatch Logs `GetLogEvents`).
Unlike the C2J-inherited hooks above, `paginators` names members as the model does (like `@paginated`), not by their
emitted name.

- What Smithy supports today is whatever `Generation/Customizations/CustomizationsModel.cs` parses;
  each hook's per-level behavior lives on that record, its `Apply`/`Validate` step, and its lookup.
- What each hook means (and every hook C2J has) is documented in `generator/customization-hooks.md`.

## Reference: Existing Generator

When implementing transformation logic (HTML sanitization, naming rules, type mapping, etc.), consult the existing C2J generator at `generator/ServiceClientGeneratorLib/` to understand the correct behavior. Key files:
- `GeneratorHelpers.cs` / `Utils.cs` — HTML processing, naming transforms
- `Member.cs` — property naming, type resolution
- `Shape.cs` / `ExceptionShape.cs` — shape naming conventions
- `Generators/SourceFiles/StructureGenerator.tt`, `Generators/SourceFiles/Exceptions/ExceptionSerialization.t4` — model and exception class output
- `Generators/Marshallers/*.tt` — T4 templates showing exact output patterns

The new generator is a clean reimplementation, not a port — but the existing generator defines what "correct" looks like.

## XML Documentation Comments

### HTML Sanitization

The `@documentation` trait contains HTML. `DocumentationFormatter.Cleanup` ports the existing
generator's `CleanupDocumentation` (`ServiceClientGeneratorLib/Generators/BaseGenerator.cs`).
The transform, in order:
- Collapse runs of whitespace (the source doc's newlines + indentation) to single spaces. The
  meaningful `<para>` line breaks are inserted afterward.
- `<code>...</code>` → `<c>...</c>`
- `<p>...</p>` → `<para>...</para>` (including `<p>` tags carrying attributes)
- Strip `<br>`, `<fullname>`, `<function>`, `<p/>` (bare and attribute-carrying forms)
- `<i>...</i>` → keep as-is
- Remove `<examples>...</examples>` and `<!-- ... -->` snippets
- Drop the leading `<para>...</para>` wrapper (the summary's first paragraph is unwrapped)
- Soft-wrap at ~80 columns (break at the next space after a line exceeds 80 chars)

Note: HTML entities are NOT decoded (`&amp;` stays `&amp;`) — the existing generator does not
decode them, so neither do we.

### Type-Specific Summaries

- **Service interface/class**: `<para>Interface for accessing {BaseName}</para>`, a blank `///` line, then the service `@documentation`
- **Request class**: `Container for the parameters to the {OperationName} operation.` then the operation `@documentation`
- **Response class**: `This is the response object from the {OperationName} operation.`
- **Structure class**: the shape's `@documentation`

### Operation Method Docs

Each operation method includes an `<exception cref="{full exception type}">` (body = the error shape's
`@documentation`) per error, plus
`<seealso href="http://docs.aws.amazon.com/goto/WebAPI/{serviceId}-{apiVersion}/{OperationName}">REST API Reference for {OperationName} Operation</seealso>`.

## Exception Classes

Operation exceptions inherit from `Amazon{BaseName}Exception` (not directly from `AmazonServiceException`).

Must expose these public constructors:
1. Default (no args)
2. `(string message)`
3. `(string message, Exception innerException)`
4. `(Exception innerException)`
5. `(string message, Exception innerException, ErrorType, string errorCode, string requestId, HttpStatusCode)`
6. `(string message, ErrorType, string errorCode, string requestId, HttpStatusCode)`

Operation exceptions also include a `#if !NETSTANDARD` block containing:
- `[Serializable]` attribute on the class
- `protected` serialization constructor `(SerializationInfo, StreamingContext)` — deserializes each serialized exception member via `info.GetValue`, then calls `base(info, context)`
- `public override void GetObjectData(SerializationInfo, StreamingContext)` carrying all three attributes as a unit: `[System.Security.SecurityCritical]` plus the CA2123 and CA2134 `SuppressMessage` attributes; body is `base.GetObjectData(info, context)` then `info.AddValue(...)` per additional member.
  The serialization constructor and `GetObjectData` are symmetric: both loop over the same member set, every modeled member except `message` (C2J parity), so base-owned `RequestId`/`ErrorCode` are serialized here even though they get no property (see "Exception Member Property Names"). Both are keyed on the .NET property name. For exceptions whose only member is `message` (e.g. all CloudTrail Data exceptions), both bodies contain only the `base` call.

The service-level exception base (`Amazon{BaseName}Exception`) inherits from `AmazonServiceException`, exposes the same six public constructors as operation exceptions, and includes `[Serializable]` plus the protected serialization constructor, but does not need its own `GetObjectData` override unless it adds serialized fields.

### Exception Member Property Names

Canonical treatment lives in type-mapping's "Error Shape Members". Summary: `errorType` → property
`RequestErrorType` (wire name unchanged); `Retryable` emitted with `new`; `Equals` gets `new` on any
structure (not exception-specific); `RequestId`/`ErrorCode` get no property but stay in serialization
and unmarshalling; every other member is emitted as-is even when it shadows an inherited property.

### Retryable Errors

An error shape carrying the `@retryable` trait emits a public override that marks it retryable:
```csharp
public override RetryableDetails Retryable { get; } = new RetryableDetails(<throttling>);
```
`<throttling>` is the trait's `throttling` value (`true`/`false`); an empty `@retryable` (`{}`) is retryable but not throttling (`false`). The base `AmazonServiceException.Retryable` returns `null`, so emitting a non-null `RetryableDetails` is what marks the exception retryable — errors without the trait emit no override. `RetryableDetails` resolves via `Amazon.Runtime` (already in the model file's usings).

## Client Interface

Must expose:
- **Sync method** (.NET Framework): `{Op}Response {Op}({Op}Request request)` per operation
- **Async method** (all targets): `Task<{Op}Response> {Op}Async({Op}Request request, CancellationToken cancellationToken = default)`
- **HTTP/2 operations** are the exception: the whole operation (sync + async, client + interface) is wrapped in `#if NET8_0_OR_GREATER`, since C2J omits h2 operations on .NET Framework and pre-net8 netstandard.
- `Endpoint DetermineServiceOperationEndpoint(AmazonWebServiceRequest request)`
- **Static factory methods** (`#if NET8_0_OR_GREATER`): `CreateDefaultClientConfig()` and `CreateDefaultServiceClient(AWSCredentials, ClientConfig)`

Use `#if` directives to include sync methods only for .NET Framework targets (`#if NETFRAMEWORK`).

## Client Class

Must expose:
- All constructors matching the current SDK pattern (default, region, config, credentials variants — 10 constructors total)
- **Sync method** (.NET Framework): `public virtual {Op}Response {Op}({Op}Request request)` per operation
- **Async method** (all targets): `public virtual Task<{Op}Response> {Op}Async(...)` per operation
- **HTTP/2 operations** are the exception: the whole operation is wrapped in `#if NET8_0_OR_GREATER`, since C2J omits h2 operations on .NET Framework and pre-net8 netstandard.
- `DetermineServiceOperationEndpoint` implementation
- `CustomizeRuntimePipeline` override: first the `runtimePipelineOverride` customization's handlers, in file order (each
  under its `condition`, if any), then the endpoint-resolver swap and auth scheme handler. The handler classes are hand-written under the service's `Custom/`.
- `ServiceMetadata` property override

Use `#if NETFRAMEWORK` directives to include sync methods only for .NET Framework targets. Both sync and async methods are `public virtual` on the client class.

## Endpoint Discovery

`aws.api#clientEndpointDiscovery` is legacy: only dynamodb, timestream-query and timestream-write use it, and new
services use endpoint rule sets (Endpoints 2.0) instead. Matching C2J, each operation with
`aws.api#clientDiscoveredEndpoint` (except the discovery operation itself) gets an
`{Op}EndpointDiscoveryMarshaller.g.cs` and sets `options.EndpointDiscoveryMarshaller` and `options.EndpointOperation`;
the client overrides `EndpointOperation` to call the discovery operation.

Anything those three services don't model fails generation (discovery ids, discovery input, swapped
endpoint members, ...); see `EndpointDiscoveryResolver` and `ClientClassWriter.WriteEndpointOperation`.

C2J's `{BaseName}EndpointDiscoveryMarshallingTests.cs` is not generated: with no discovery ids it only checks the
`required` flag against the C2J model, which the Smithy trait replaces.
