Skip to content

Endpoint rules

A service that declares @smithy.rules#endpointRuleSet or @smithy.rules#endpointBdd gets a generated {Service}EndpointResolver. Its client resolves endpoints from the rules, so it is configured with the rule parameters instead of a URL:

using NSmithy.Aws;
using var lambda = new LambdaClient(new LambdaClientConfig
{
Region = "eu-central-1",
UseFips = true,
AuthSchemes = { new AwsSigV4AuthScheme(new DefaultAwsCredentialsProvider()) },
});

Region and UseFips are generated from the parameters Lambda’s endpoint rules declare. Another service’s configuration has the properties its own rules declare.

LambdaEndpointResolver implements IEndpointResolver. The client creates it from its configuration unless EndpointResolver is set. The rules are compiled into C# at build time. Nothing is parsed or interpreted at runtime.

The rules declare their parameters. Lambda’s include:

"parameters": {
"Region": { "type": "string", "builtIn": "AWS::Region" },
"UseFIPS": { "type": "boolean", "builtIn": "AWS::UseFIPS", "required": true, "default": false },
"Endpoint": { "type": "string", "builtIn": "SDK::Endpoint" }
}

Parameters that client configuration supplies become properties of {Service}ClientConfig:

Parameter sourceConfiguration property
Built-in, such as AWS::RegionNamed by the integration that provides it, such as Region
@clientContextParams entryThe parameter name in PascalCase
SDK::EndpointThe existing Endpoint

A property left unset takes the parameter’s default from the rules. Parameters bound by @staticContextParams, @contextParam, or @operationContextParams come from each operation and its input.

ConfigurationResolution
EndpointResolver setThe configured resolver. The generated resolver is not used.
Endpoint set and rules model SDK::EndpointThe rules, with Endpoint as the custom endpoint.
Endpoint set and rules do not model SDK::EndpointThe configured URL, bypassing rules.
NeitherThe rules, from the configured parameters.

Endpoint falls back to HttpClient.BaseAddress. Rules that model SDK::Endpoint still apply to a configured URL. S3’s, for example, apply path-style addressing and reject a custom endpoint combined with FIPS.

A resolved endpoint carries the request URL, headers added to every request sent to it, and the auth schemes it supports with their properties. The auth schemes narrow auth selection, and the selected scheme’s signer reads its properties, such as the SigV4 signing name and region.

When the rules end in an error, such as a missing region, the client throws SmithyEndpointResolutionException with the rules’ message. Rules that depend only on client configuration are resolved once, when the client is constructed. Rules that also bind operation input are resolved on each call, before the request is serialized.

Endpoint rules are not specific to AWS. This service picks its host from an Environment client context parameter and accepts an explicit URL through SDK::Endpoint:

@clientContextParams(Environment: { type: "string" })
@endpointRuleSet({
version: "1.0"
parameters: {
Environment: { type: "String", required: true, default: "test" }
Endpoint: { type: "String", builtIn: "SDK::Endpoint" }
}
rules: [
{
conditions: [{ fn: "isSet", argv: [{ ref: "Endpoint" }] }]
type: "endpoint"
endpoint: { url: { ref: "Endpoint" } }
}
{
conditions: [{ fn: "stringEquals", argv: [{ ref: "Environment" }, "prod"] }]
type: "endpoint"
endpoint: { url: "https://inventory.example.com" }
}
{
conditions: [{ fn: "stringEquals", argv: [{ ref: "Environment" }, "test"] }]
type: "endpoint"
endpoint: { url: "https://inventory.test.example.com" }
}
{ conditions: [], type: "error", error: "Environment must be prod or test" }
]
})
service Inventory { ... }
using var production = new InventoryClient(new InventoryClientConfig { Environment = "prod" });
using var local = new InventoryClient(new InventoryClientConfig
{
Endpoint = new Uri("http://localhost:5000"),
});

Rules that need functions or built-ins beyond the standard library need a CSharpIntegration that maps them to C#, as NSmithy.Aws does for AWS. See Extending endpoint rules.

Set EndpointResolver to replace generated resolution. This works whether or not the service models endpoint rules, and the client then needs no Endpoint. A resolver implements:

public interface IEndpointResolver
{
ValueTask<SmithyEndpoint> ResolveEndpointAsync(
SmithyEndpointParameters parameters,
CancellationToken cancellationToken = default);
SmithyEndpoint? StaticEndpoint => null;
}

SmithyEndpointParameters describes one call:

MemberContent
ServiceId, OperationIdThe Smithy shape ids of the service and operation
ConfiguredEndpointThe client’s Endpoint, if set
InputThe operation input. Cast it to the generated input type to route on its members.
OperationThe operation’s schema, with its traits and input and output schemas

Anything else the resolver needs, such as a service registry, comes in through its constructor.

The client resolves the endpoint once per call, before serializing the request and before interceptors run. Retries reuse it. One resolver serves concurrent calls, so it must be thread-safe. A resolver whose result never changes returns it from StaticEndpoint, and the client then skips per-call resolution.

This resolver looks services up in a registry:

public sealed class RegistryResolver(ServiceRegistry registry) : IEndpointResolver
{
public async ValueTask<SmithyEndpoint> ResolveEndpointAsync(
SmithyEndpointParameters parameters,
CancellationToken cancellationToken = default) =>
new SmithyEndpoint(await registry.LookupAsync(parameters.ServiceId, cancellationToken));
}

To take the resolver from dependency injection, use the registration overload that passes the service provider:

services.AddSingleton<RegistryResolver>();
services.AddInventoryClient((provider, config) =>
{
config.EndpointResolver = provider.GetRequiredService<RegistryResolver>();
});

A generated resolver is an ordinary IEndpointResolver, so a decorator can call it and adjust the result:

public sealed class TracingHeaderResolver(IEndpointResolver inner) : IEndpointResolver
{
public async ValueTask<SmithyEndpoint> ResolveEndpointAsync(
SmithyEndpointParameters parameters,
CancellationToken cancellationToken = default)
{
var endpoint = await inner.ResolveEndpointAsync(parameters, cancellationToken);
return endpoint with
{
Headers = new Dictionary<string, IReadOnlyList<string>>(
endpoint.Headers ?? new Dictionary<string, IReadOnlyList<string>>())
{
["x-trace-origin"] = ["inventory"],
},
};
}
}
var config = new InventoryClientConfig { Environment = "prod" };
config.EndpointResolver = new TracingHeaderResolver(new InventoryEndpointResolver(config));

The record copy keeps the endpoint’s auth schemes and their signing properties. A decorator that changes the host should check that they still apply.

AWS rules use the aws.partition, aws.parseArn, and aws.isVirtualHostableS3Bucket functions and AWS::* built-ins. Reference NSmithy.Aws, which implements the functions and carries the partition data of the Smithy version NSmithy builds with.

The AWS built-ins map to these properties. A client has only those its service’s rules declare.

Built-inProperty
AWS::RegionRegion
AWS::UseFIPSUseFips
AWS::UseDualStackUseDualStack
AWS::Auth::AccountIdAccountId
AWS::Auth::AccountIdEndpointModeAccountIdEndpointMode
AWS::Auth::CredentialScopeCredentialScope
AWS::S3::AccelerateAccelerate
AWS::S3::DisableMultiRegionAccessPointsDisableMultiRegionAccessPoints
AWS::S3::ForcePathStyleForcePathStyle
AWS::S3::UseArnRegion, AWS::S3Control::UseArnRegionUseArnRegion
AWS::S3::UseGlobalEndpoint, AWS::STS::UseGlobalEndpointUseGlobalEndpoint

S3’s rules put the bucket in the host, or in the endpoint’s path for path-style addressing, so generated S3 operations have no /{Bucket} path segment. A custom S3 resolver adds the bucket to the endpoint itself.

Every resolved AWS endpoint names its SigV4 signing scope: either the one the rules set, or the service’s @sigv4 signing name and the configured Region. That is why AwsSigV4AuthScheme needs only credentials here.