Configuration reference
AnyProtocol keeps contract and implementation registration code-first. Named client and server registrations can overlay their protocol selections from IConfiguration, including environment-variable providers.
{
"AnyProtocol": {
"Clients": {
"OrdersClient": { "Protocol": "Grpc" }
},
"Servers": {
"OrdersServer": { "Protocols": ["Rest", "Grpc", "Mcp"] }
}
}
}
builder.Services.AddAnyProtocol(
builder.Configuration.GetSection("AnyProtocol"),
link => link
.UseSerializer(new TextJsonMessageSerializer())
.AddRestServer(ProtocolKey.Rest)
.AddGrpcServer(ProtocolKey.Grpc)
.AddClient<IOrders>("OrdersClient")
.AddServer<IOrders, Orders>("OrdersServer"));
The configuration section overrides only protocol selections; serializers, transports, contract types, implementations, filters, timeouts, and retries stay in code. Missing entries preserve code defaults. Unknown registration keys, missing client protocol values, empty server protocol arrays, unknown protocols, and duplicate protocols fail atomically during registration.
Core builder defaults
| API | Default |
|---|---|
ClientOptionsBuilder.UseProtocol |
ProtocolKey.Default |
ServerOptionsBuilder.UseProtocols |
[ProtocolKey.Default] |
EventOptionsBuilder.UseProtocol |
ProtocolKey.Default |
| Client timeout | 30 seconds |
| Client maximum attempts | 1 |
| Event channel | anyprotocol.event.{event-type-in-kebab-case} |
| Event consumer group | null |
| Required ordering | TransportOrdering.None |
SubscriptionOptions.MaxConcurrency |
1 |
SubscriptionOptions.ConsumerGroup |
null |
| Large payload offload | Disabled |
UseSerializer and every referenced transport are required. Protocol keys are case-insensitive and duplicates fail configuration. String-based UseTransport and AddTransport overloads remain available for compatibility.
Large payload offload is enabled only with UseLargePayloadOffload. It requires a named
ILargePayloadStore, positive MaxInlinePayloadBytes and TimeToLive, and a
MaxStoredPayloadBytes greater than the inline threshold. See large payload offload.
ProtocolKey provides Default, Rest, Grpc, Mcp, Kafka, RabbitMq, ZeroMq, and InMemory. Use ProtocolKey.Create(name) for custom protocols or named instances. One contract cannot select the same native server transport type through two keys because that would create ambiguous REST or gRPC routes.
Clients select one protocol. Servers select one or more protocols in a single logical registration:
link.AddServer<IOrders, Orders>(server => server.UseProtocols(
ProtocolKey.Rest,
ProtocolKey.Grpc,
ProtocolKey.Mcp));
Validation is atomic: if any selected protocol is unknown or lacks a capability required by any contract method, configuration build fails and no partial endpoint set is exposed. If MCP is selected, host startup also requires AddAnyProtocolMcp() plus MapAnyProtocolMcp(), or AddAnyProtocolMcpStdio(); an incomplete exposure stops the host.
Transport options
InMemory
new InMemoryMessagingProtocol() uses InMemoryProtocolOptions with:
| Option | Default |
|---|---|
DeliveryDelay |
TimeSpan.Zero |
FaultInjector |
null |
These options are intended for process-local execution and deterministic fault/delay testing.
REST
RestMessagingProtocol accepts either HttpClient plus an IMessageSerializer, or IHttpClientFactory plus a serializer and clientName; both forms accept routePrefix and an optional fallback HTTP method resolver. The prefix defaults to /anyprotocol, and methods without explicit REST or ASP.NET Core metadata default to POST.
AddRestServer("rest") registers the server transport. AddAnyProtocolRest(options => ...) controls the ASP.NET Core endpoint surface: MapOperationEndpoints enables typed operation routes, DocumentationContentType controls request/response metadata, and the fallback route and HTTP method resolvers apply when a contract has no explicit metadata. MapAnyProtocol("/anyprotocol") supplies the server prefix. Operation endpoints are disabled by default; see REST / HTTP protocol for examples and precedence rules.
Channels cannot contain {, }, ?, #, empty path segments, or whitespace-only segments.
gRPC
GrpcMessagingProtocol(GrpcChannel channel, bool disposeChannel = false) or GrpcMessagingProtocol(CallInvoker) configures the client. A CallInvoker cannot report channel connectivity, so readiness is Unknown. AddGrpcServer("grpc") and MapAnyProtocolGrpc() have no transport options.
Kafka
KafkaProtocolOptions property |
Default |
|---|---|
BootstrapServers |
Required |
ClientId |
anyprotocol-{random Guid:N} |
TopicPrefix |
null |
AutoCreateTopics |
true |
TopicPartitions |
3 |
TopicReplicationFactor |
1 |
ReplyTopicRetention |
5 minutes |
DeadLetterSuffix |
.dead-letter |
EnableDeadLetter |
true |
SubscriptionStartupTimeout |
30 seconds |
ConsumerPollInterval |
100 milliseconds |
ProducerFlushTimeout |
10 seconds |
ProducerConfig / ConsumerConfig |
Empty ordinal dictionaries |
The built producer starts with idempotence enabled and Acks.All; ProducerConfig overrides are applied afterward. Consumers disable auto-commit and auto-offset-store. Topic names are limited to 249 ASCII letters/digits plus ., _, and -.
ZeroMQ
ZeroMqProtocolOptions property |
Default |
|---|---|
Role |
Required (Client or Server) |
RouterEndpoint |
Required |
PublisherEndpoint |
Required |
ClientIdentity |
null (a random Guid is used) |
HighWatermark |
1,000 |
PollInterval |
2 milliseconds |
The high watermark must be positive and poll interval non-negative.
RabbitMQ
RabbitMqProtocolOptions property |
Default |
|---|---|
ConnectionUri |
Required absolute amqp/amqps URI |
ClientProvidedName |
anyprotocol-{random Guid:N} |
ExchangeName |
anyprotocol |
RoutingKeyPrefix |
null |
PrefetchCount |
32 |
MaxDeliveryAttempts |
5 |
ShouldRetryHandlerException |
Retry except cancellation |
EnableDeadLetter |
true |
DeadLetterSuffix |
.dead-letter |
ConfirmTimeout |
10 seconds |
ReadinessTimeout |
2 seconds |
NetworkRecoveryInterval |
5 seconds |
ShutdownTimeout |
10 seconds |
var uri = builder.Configuration["ANYPROTOCOL_RABBITMQ_URI"]
?? throw new InvalidOperationException("RabbitMQ URI is required.");
link.AddRabbitMq(new RabbitMqProtocolOptions
{
ConnectionUri = new Uri(uri),
ExchangeName = builder.Configuration["ANYPROTOCOL_RABBITMQ_EXCHANGE"] ?? "orders"
});
The application owns these example environment names. Never print the URI because it may contain credentials.
Serializers
TextJsonMessageSerializer()usesJsonSerializerOptions.Default.TextJsonMessageSerializer(JsonSerializerOptions)uses the supplied options.TextJsonMessageSerializer(JsonSerializerContext)is the Native AOT-safe form.MessagePackMessageSerializerusesContractlessStandardResolver.Options.
All serialization failures are wrapped in SerializationFailedException. Producer and consumer processes must agree on serializer and message shape.
Validation, error handling, and message metadata
Request validation is opt-in. Register a validator in DI and add ValidationFilter to the
server pipeline; registering a validator alone does not activate it:
public sealed class GetOrderValidator : IRequestValidator<GetOrder>
{
public ValueTask<IValidationResult> ValidateAsync(
GetOrder request,
CancellationToken cancellationToken = default)
{
var result = new ValidationResult();
if (string.IsNullOrWhiteSpace(request.Id))
{
result.AddError(new ValidationError("Id is required.", nameof(GetOrder.Id)));
}
return ValueTask.FromResult<IValidationResult>(result);
}
}
services.AddScoped<IRequestValidator<GetOrder>, GetOrderValidator>();
services.AddAnyProtocol(link => link
// serializer, transports, and contracts
.AddServerFilter(new ValidationFilter()));
An invalid result becomes an AnyProtocolValidationException with fault code
validation_failed and structured error details. REST maps this fault to HTTP 400. The filter
resolves validators from the current handler scope, so scoped validators are supported.
Register IErrorHandler implementations in DI when the application needs audit, alerting, or
custom error reporting. AnyProtocol invokes them for handler, materialization, fault-delivery,
and dead-letter error paths; an error handler failure is logged and does not replace the
original operation fault.
The default IMessageIdGenerator emits GUID N strings and the default envelope factory owns
message ID, SentAt, correlation, and reply metadata. Override these services before
AddAnyProtocol when the application needs a different policy:
services.AddSingleton<IMessageIdGenerator, UlidMessageIdGenerator>();
services.AddSingleton<IMessageEnvelopeFactory, CustomMessageEnvelopeFactory>();
The ULID implementation is supplied by the optional AnyProtocol.MessageIdGenerator.Ulid
package. Custom envelope factories should preserve the standard metadata contract; use the
metadata generation allowlist when deciding which identifiers may be
application-generated.
Timeout, retry, and idempotency
WithTimeout(TimeSpan) sets the client request timeout and rejects non-positive values. WithRetry(int maxAttempts) sets total attempts, not retry count, and rejects values below one.
For explicit pipeline control:
link.AddClientFilter(new TimeoutFilter(TimeSpan.FromSeconds(10)));
link.AddClientFilter(new RetryFilter(new RetryOptions
{
MaxAttempts = 3,
InitialDelay = TimeSpan.FromMilliseconds(100),
MaxDelay = TimeSpan.FromSeconds(5),
JitterFactor = 0.2,
MaxTotalTime = TimeSpan.FromSeconds(20)
}));
RetryOptions defaults are one attempt, 100 ms initial delay, 5 s maximum delay, 0.2 jitter, and no total-time limit. The filter retries only methods marked [Idempotent], and only TimeoutException, IOException, or AnyProtocolFaultException whose fault has Retryable = true. The attribute does not store idempotency keys or deduplicate delivery.
Configure a durable inbox separately:
services.AddAnyProtocolRedisDeduplicationStore("inbox", options =>
{
options.ConnectionString = configuration.GetConnectionString("Redis");
options.KeyPrefix = "orders:inbox";
});
services.AddAnyProtocol(link => link
// serializer, transports, and contracts
.AddServerFilter(new InboxDeduplicationFilter(new InboxDeduplicationOptions
{
StoreName = "inbox",
LeaseDuration = TimeSpan.FromMinutes(5),
Retention = TimeSpan.FromDays(1)
})));
ISchemaRegistry is the provider-neutral contract for centralized schema versions. The built-in
InMemorySchemaRegistry and JsonSchemaCompatibilityChecker support deterministic local
backward/forward/full compatibility checks:
var registry = new InMemorySchemaRegistry();
await registry.RegisterAsync(
subject: "orders",
format: "json-schema",
definition: "{}",
compatibility: SchemaCompatibilityMode.Backward);
The registry is caller-managed and is not automatically wired into transports or serializers. Production deployments can supply a durable registry adapter without changing contract code; the built-in registry is intended for local composition and tests.
Lifecycle
AddAnyProtocol registers AnyProtocolHostedService. Host startup calls IAnyProtocolBus.StartAsync; shutdown calls StopAsync. Startup subscribes event and emulated-server routes before changing the state to Started. Failure rolls back created subscriptions and throws an AggregateException. Shutdown cancels the bus run token and disposes subscriptions; disposal additionally disposes transports.
Environment example
Read configuration explicitly and construct typed options:
var kafka = new KafkaProtocolOptions
{
BootstrapServers = builder.Configuration["ANYPROTOCOL_KAFKA_BOOTSTRAP_SERVERS"]
?? throw new InvalidOperationException("Kafka bootstrap servers are required."),
TopicPrefix = builder.Configuration["ANYPROTOCOL_KAFKA_TOPIC_PREFIX"],
AutoCreateTopics = builder.Configuration.GetValue(
"ANYPROTOCOL_KAFKA_AUTO_CREATE_TOPICS",
false)
};
builder.Services.AddAnyProtocol(link => link
.UseSerializer(new TextJsonMessageSerializer())
.AddKafka(kafka));
Example process values:
ANYPROTOCOL_KAFKA_BOOTSTRAP_SERVERS=kafka-0:9092,kafka-1:9092
ANYPROTOCOL_KAFKA_TOPIC_PREFIX=orders
ANYPROTOCOL_KAFKA_AUTO_CREATE_TOPICS=false
These names are examples owned by the application, not reserved framework keys.
Startup validation errors
Configuration fails before the service provider is returned when:
- no serializer is configured;
- a contract or event references an unknown transport;
- a server, request/reply, stream, consumer group, partition key, or required ordering is unsupported by the selected transport;
- an event has multiple partition keys, or its key is unreadable/indexed;
- per-partition ordering is required without a partition key on every operation;
- the same server route is registered more than once on one transport;
- a transport name is duplicated;
- timeout, retry, concurrency, Kafka, RabbitMQ, REST route, codec, or ZeroMQ option validation fails;
- Native AOT generated registration or serializer metadata is incomplete.
- a large-payload policy has invalid bounds/TTL or its named store is not registered.