This document outlines the currently available features and capabilities of the CLOOPS NATS SDK. The SDK is designed to provide type-safe, reliable messaging patterns for distributed microservices.
Quick navigation to key features:
The SDK provides a framework for building reliable, type-safe NATS messaging applications with a clean separation of concerns:
- Client Layer: High-level API for connecting to NATS and performing operations (publish, subscribe, request-reply)
- Consumer Framework: Attribute-driven message consumption with automatic routing and type-safe handling
- Transport Abstraction: Seamless support for both NATS Core and JetStream protocols
- Infrastructure Services: Built-in support for metrics collection, distributed locking, and token minting
- Serialization: JSON serialization with customizable options for message encoding/decoding
Note: Message schemas, subject builders, and subject definitions are defined externally (typically in a separate schema package). The SDK works with any strongly-typed message types and subject strings.
- Attribute-driven: Use
[NatsConsumer]to mark methods as message handlers - Automatic routing: SDK automatically routes messages to appropriate handlers
- Type safety: Strongly-typed message handling with compile-time validation
- Queue groups: Built-in load balancing across multiple consumer instances
- Dual protocol support: Seamless handling of both NATS Core and JetStream messages
- Automatic message validation: If a message type has a
Validate()method, the SDK automatically validates messages before processing. Invalid messages are discarded (JetStream messages are terminated, Core NATS messages are skipped). - Explicit ACK/NAK contract: All handlers must return
Task<NatsAck>. JetStream uses the result toAckonSuccessorNakonFail; Core ignores it. - Handler signature enforcement: The SDK validates at startup that handlers return
Task<NatsAck>, preventing misconfiguration. - Automatic ACK/NAK (JetStream): After handler execution, the processor calls
AckAsyncorNakAsyncbased on the handlerβs result, guaranteeing at-least-once delivery. - Exception safety: If a handler throws and no exception handler claims it, the processor logs the error and rethrows with the original stack trace intact. The JetStream message is left unacked, so redelivery follows the consumer's
AckWait/MaxDeliverconfiguration. - Consumer exception handlers: Register
INatsConsumerExceptionHandlerimplementations viaAddNatsConsumerExceptionHandler<T>(). When a JetStream or Core handler throws, handlers run in registration order; the first non-nullNatsAckis applied (ack / nak / terminate / Core reply) instead of faulting the work item.OperationCanceledExceptionduring shutdown is never converted to an ack. - Mapped exceptions stay observable: Every mapped exception is logged at
Warningwith the original exception, the failing handler, and the exception handler that claimed it. The resultingNatsAckcarriesIsExceptionMapped,MappedException, andMappedByHandlerType, and the processor records the invocation asfailin metrics even when the exception handler chose to ACK. Mapping an exception changes the delivery outcome, never the error rate. - Exception handler isolation: If an exception handler itself throws, the failure is logged at
Errorand the next registered handler runs. A broken exception handler can never replace or hide the original handler exception. - Testable contexts:
NatsConsumerInterceptorContextandNatsConsumerExceptionContextexpose public constructors, so applications can unit test their own interceptors and exception handlers without a NATS server. The supplied message must be aNatsMsg<T>matching the declared payload type. - Delivery context: Both contexts expose
IsJetStreamand a read-onlyJetStreamview (NumDelivered,IsRedelivery,NumPending, stream/consumer sequence, timestamp, stream, consumer, domain), which isnullfor Core NATS. This is observability only β retry budgets and dead-letter routing remain owned by stream/consumer configuration in the control plane, andMaxDeliveris not carried on the message. - Unmatched subjects: If a delivered subject matches no registered handler, the SDK logs an error and leaves the JetStream message unacked so the stream's own
MaxDeliver/ DLQ policy applies. It no longer faults the listener loop.
Example:
[NatsConsumer("events.process")]
public async Task<NatsAck> HandleEvent(NatsMsg<Event> msg, CancellationToken ct)
{
await ProcessEvent(msg.Data);
return NatsAck.Success;
}Purpose: Enterprise-grade message consumption with persistence and at-least-once delivery.
Key Features (Updated):
-
Control-plane ownership: Streams and durable consumers are provisioned via GitOps. At runtime the SDK does not create/update them. It:
- Resolves the stream by subject (
ListStreamNamesAsync(subject)), then - Attaches to the existing consumer with
GetConsumerAsync(streamName, consumerId).
- Resolves the stream by subject (
-
Durable persistence: Messages remain in the stream until the consumer explicitly ACKs them (consumer AckPolicy must be Explicit).
-
At-least-once delivery: With durable consumers and explicit acks, JetStream redelivers until ACKed (subject to consumer/stream config).
-
Explicit ACK/NAK contract: All handlers must return
Task<NatsAck>.NatsAck.Successβ SDK callsAckAsyncNatsAck.Failβ SDK callsNakAsyncorAckTerminateAsyncbased onShouldRetryDelivery- Unhandled exceptions β SDK leaves the message unacked (JetStream retry behavior applies); registered exception handlers may map the exception to a
NatsAckinstead
-
Retry behavior: Redelivery timing and limits are controlled by JetStream consumer/stream config (e.g., MaxDeliver, Backoff, DLQ policies).
Example:
[NatsConsumer("CP.*.EffectTriggered.Durable", _consumerId: "effect-durable")]
public async Task<NatsAck> ProcessDurableEffect(NatsMsg<EffectTriggered> msg, CancellationToken ct)
{
try
{
await ProcessCriticalEffect(msg.Data);
return NatsAck.Success; // SDK will Ack
}
catch (Exception ex)
{
return NatsAck.Fail; // SDK will Nak (JetStream retries per config)
}
}Purpose: Optimized message processing with configurable parallelism and backpressure control.
Key Features:
- Configurable parallelism: Control concurrent message processing via environment variables
- Backpressure management: Bounded queues prevent memory overflow
- Resource management: Environment-based configuration for production flexibility
Configuration via Environment Variables:
The SDK supports high-performance processing through environment variables (see Environment Variables Documentation):
NATS_CONSUMER_MAX_DOP: Maximum degree of parallelism (default: 128)- Controls how many messages can be processed concurrently
- Higher values increase throughput but require more CPU/memory
NATS_SUBSCRIPTION_QUEUE_SIZE: Maximum queue capacity per subscription (default: 20,000)- Controls backpressure when processing is slower than message arrival
- When full, the SDK applies backpressure to prevent memory overflow
Example Consumer:
[NatsConsumer("analytics.events")]
public async Task<NatsAck> ProcessAnalytics(NatsMsg<AnalyticsEvent> msg, CancellationToken ct = default)
{
// Processing happens with parallelism and backpressure controlled by environment variables
await analyticsService.ProcessEvent(msg.Data);
return NatsAck.Success;
}Performance Tuning:
- For high-throughput scenarios, increase
NATS_CONSUMER_MAX_DOP(e.g., 200-500) - Monitor queue depth; if it consistently reaches capacity, either:
- Increase processing speed (optimize handlers)
- Increase
NATS_SUBSCRIPTION_QUEUE_SIZE(with more memory) - Scale horizontally (more consumer instances)
Purpose: Provide distributed locking capabilities using NATS Key-Value stores for coordination across multiple service instances.
Key Features:
- Distributed coordination: Lock resources across multiple service instances
- Automatic cleanup: Locks are automatically released when the handle is disposed
- Timeout support: Configurable timeout for lock acquisition attempts
- Owner identification: Track which instance holds the lock
- Resource isolation: Different keys provide independent locks
Quick Example:
var cnc = new CloopsNatsClient();
await cnc.SetupKVStoresAsync();
var lockHandle = await cnc.AcquireDistributedLockAsync("my-resource");
if (lockHandle != null)
{
await using (lockHandle)
{
// Critical section - only this instance can access
await ProcessCriticalOperation();
}
}Setup Requirements:
Before using distributed locks, you must initialize the KV stores:
var cnc = new CloopsNatsClient();
await cnc.SetupKVStoresAsync(); // Required before using locksBasic Lock Usage:
// Acquire a lock with default timeout (1.5 seconds)
var lockHandle = await cnc.AcquireDistributedLockAsync("my-resource-key");
if (lockHandle != null)
{
await using (lockHandle) // Automatic cleanup when disposed
{
// Critical section - only this instance can access the resource
await ProcessCriticalOperation();
}
// Lock is automatically released here
}
else
{
// Failed to acquire lock - another instance holds it
Console.WriteLine("Resource is currently locked by another instance");
}Advanced Lock Configuration:
// Custom timeout and owner identification
var lockHandle = await cnc.AcquireDistributedLockAsync(
key: "database-migration-lock",
timeout: TimeSpan.FromSeconds(30), // Try for 30 seconds
ownerId: "service-instance-001" // Identify this instance
);
if (lockHandle != null)
{
await using (lockHandle)
{
// Perform database migration or other critical operation
await PerformDatabaseMigration();
}
}Lock Contention Handling:
// Handle lock contention gracefully
var lockHandle = await cnc.AcquireDistributedLockAsync("shared-resource");
if (lockHandle == null)
{
// Resource is busy - implement retry logic or alternative path
await Task.Delay(1000); // Wait before retry
return; // Or implement exponential backoff
}
await using (lockHandle)
{
// Exclusive access to shared resource
await UpdateSharedResource();
}Multiple Resource Locks:
// Lock different resources independently
var lock1 = await cnc.AcquireDistributedLockAsync("resource-a");
var lock2 = await cnc.AcquireDistributedLockAsync("resource-b");
if (lock1 != null && lock2 != null)
{
await using (lock1)
await using (lock2)
{
// Both resources are locked
await ProcessWithMultipleResources();
}
}
else
{
// Release any acquired locks
lock1?.Dispose();
lock2?.Dispose();
}Purpose: Load balancing and horizontal scaling for message consumers.
Implementation Details:
- Automatic load balancing: Messages distributed across queue group members
- Fault tolerance: Surviving members continue processing if instances fail
- No configuration required: Just specify queue group name in attribute
- Multiple queue groups: Different groups can process same subjects independently
Examples:
// No queue group - all instances receive all messages (broadcast)
[NatsConsumer("notifications.all")]
public async Task<NatsAck> HandleAllNotifications(NatsMsg<Notification> msg, CancellationToken ct)
{
return NatsAck.Success;
}
// Queue group - load balanced across group members
[NatsConsumer("orders.process", _QueueGroupName: "order-workers")]
public async Task<NatsAck> ProcessOrder(NatsMsg<Order> msg, CancellationToken ct)
{
return NatsAck.Success;
}Purpose: Synchronous communication pattern for immediate response requirements.
Characteristics:
- Bidirectional: Send request and await typed response
- Timeout support: Configurable timeouts for reliability
- Type safety: Both request and response are strongly typed
Use Cases:
- Data queries
- Service-to-service API calls
- Validation requests
Example:
Responder using annotation-based handler:
// Responder handler using [NatsConsumer] attribute
[NatsConsumer("service.query")]
public async Task<NatsAck> HandleQuery(NatsMsg<string> msg, CancellationToken ct)
{
// Process the request
var result = $"Processed: {msg.Data}";
// Return NatsAck with reply data - SDK automatically sends reply to requester
return new NatsAck(true, result);
}Sending request from NATS CLI:
# Send request and await response
nats req service.query "request-data"Or using SDK programmatically:
var cnc = new CloopsNatsClient();
var response = await cnc.RequestAsync<string, string>("service.query", "request-data");
Console.WriteLine($"Response: {response.Data}");Purpose: Built-in metrics collection for monitoring message processing performance.
Key Features:
- Automatic metrics: SDK records message processing duration and status
- System.Diagnostics.Metrics integration: Uses standard .NET metrics infrastructure
- Function-level tracking: Metrics tagged with handler function name
- Status tracking: Success/failure and retryability information
Available Metrics:
nats_sub_msg_process_milliseconds: Histogram tracking message processing time- Labels:
fn(function name),status(success/fail),retryable(true/false) - Automatically generates count, sum, and bucket metrics for quantiles
- Labels:
Quick Example:
// Register metrics service
services.AddSingleton<INatsMetricsService, NatsMetricsService>();
// Metrics automatically tracked for all handlers
[NatsConsumer("events.process")]
public async Task<NatsAck> ProcessEvent(NatsMsg<Event> msg, CancellationToken ct)
{
await ProcessMessage(msg.Data);
return NatsAck.Success; // Duration and status automatically recorded
}Metrics Integration:
The NatsMetricsService uses System.Diagnostics.Metrics, which integrates with:
- OpenTelemetry exporters
- Application Insights
- Prometheus
- Custom metrics collectors
Only applicable if you are using NATS with decentralized auth.
This is typically used to issue short lived JWT to UI.
Purpose: Programmatically mint NATS user credentials (JWT tokens) for dynamic user provisioning.
Key Features:
- Dynamic credential generation: Create NATS credentials on-demand
- Fine-grained permissions: Specify allow/deny lists for publish and subscribe
- Expiration control: Set credential expiration times
- Environment-based configuration: Secure credential storage via environment variables
Security Note: This service requires account signing credentials and should only be used in trusted, secure services.
Quick Example:
// Register service (uses env vars: NATS_ACCOUNT_SIGNING_SEED, NATS_ACCOUNT_PUBLIC_KEY)
services.AddSingleton<INatsTokenMintingService, NatsTokenMintingService>();
// Mint credentials
var creds = mintingService.MintNatsUserCreds(new NatsCredsRequest
{
userName = "user-001",
allowPubs = new List<string> { "events.>" },
allowSubs = new List<string> { "events.>" },
expMs = 3600_000 // 1 hour
});Environment Variables (see Environment Variables):
NATS_ACCOUNT_SIGNING_SEED: Account signing key seed (highly confidential)NATS_ACCOUNT_PUBLIC_KEY: Main account public key
Purpose: Type-safe subject construction (available via external schema packages, e.g. for connection loops, cloops.nats.schema). You can build your own package that provides schema.
Note: Subject builders are defined in external packages and are optional. You can use plain subject strings directly with the SDK.
Using Subject Builders (if available from external package):
var sb = new CPSubjectBuilder(cnc); // From external schema package
var subject = sb.EventSubjects("cloudpathology_test").P_EffectTriggered;
await subject.Publish(new EffectTriggered { Id = "123" });Using Plain Subject Strings (SDK-native approach):
var cnc = new CloopsNatsClient();
await cnc.PublishAsync("CP.test.EffectTriggered", new EffectTriggered { Id = "123" });using a schema package will allow you to -
- Strong typing for all messages, subjects and their associations. This is provided through
cloops.nats.schema. - Compile-time safety for event publishing
- Well-defined patterns for message governance
These schemas are specific to organization functions and therefore are not included in this SDK. You'd typically want to only allow one schema per subject for reliability. Make sure to version your schemas to be backward compatible.
Purpose: Lightweight, fire-and-forget message publishing for high-throughput scenarios.
Characteristics:
- At-most-once delivery: Messages may be lost if no subscribers are available
- Low latency: Minimal overhead for real-time communication
- Stateless: No message persistence or durability guarantees
Use Cases:
- Real-time notifications
- Telemetry data
- Non-critical updates
Example:
var cnc = new CloopsNatsClient();
var sb = new CPSubjectBuilder(cnc);
var subject = sb.EventSubjects("cloudpathology_test").P_EffectTriggered;
await subject.Publish(new EffectTriggered { Id = "123" });Purpose: Durable, reliable message publishing with persistence and delivery guarantees.
Characteristics:
- At-least-once delivery: Messages are persisted and guaranteed delivery
- Stream persistence: Messages stored in streams for replay and durability
- Acknowledgment support: Confirm successful message processing
- ACK/NAK contract: Implemented. Handlers return
Task<NatsAck>; processor ACKs or NAKs JetStream messages accordingly.
Use Cases:
- Critical business events
- Audit trails
- Data synchronization between services
Example:
var cnc = new CloopsNatsClient();
var sb = new CPSubjectBuilder(cnc);
var subject = sb.EventSubjects("cloudpathology_test").P_EffectTriggered;
await subject.StreamPublish(new EffectTriggered { Id = "123" }, dedupeId: "unique-123");Please note: subject builders, subjects and their associated message type bindings are external to this SDK. Please use your own implementations for these.
Purpose: Demonstrate SDK usage as a separate consumer project.
Project Structure:
- Separate executable project:
examples/cloops.nats.examples.csproj - SDK consumption: References SDK as DLL through project reference
- Command-line interface: Complete CLI for running all examples
- Production patterns: Shows real-world usage scenarios
Available Examples:
dotnet run pub- Basic NATS Core publishingdotnet run req- Request-reply communicationdotnet run locking- distributed lock demodotnet run minting- jwt minting demo
Example Files:
ConsumerExample.cs: Comprehensive consumer examplesNatsConsumerHost.cs: Host and DI setup for consumersTokenMintingExample.cs: How to mint a JWT programmatically. Note: only works when you are using NATS decentralized auth.
Client & Connectivity:
Client.cs: Main SDK client for NATS operations and connection managementCloopsNatsClient: Primary interface for all NATS operations
Consumer Framework:
NatsSubscriptionProcessor.cs: Handles consumer registration and message routing (both NATS Core and JetStream)NatsSubscriptionQueue.cs: Queue management for consumer groups and batch processingNatsConsumerAttribute.cs: Attribute for marking consumer methods with configuration options
Serialization & Utilities:
Util.cs/BaseNatsUtil: Type conversion utilities for NatsMsg processing and JSON serializationCloopsSerializer.cs: Custom serializer implementation with camelCase naming and flexible number handling- Method overloading: Handles both
NatsMsg<byte[]>andNatsJSMsg<byte[]>message types
Infrastructure Services:
NatsMetricsService.cs: Metrics collection using System.Diagnostics.MetricsNatsTokenMintingService.cs: Programmatic NATS credential generationKvDistributedLock.cs: Distributed locking using NATS Key-Value storesSubjectMatcher.cs: Efficient subject pattern matching for wildcard subscriptions
Note: Subject builders, message schemas, and event definitions are provided by external packages (e.g., cloops.nats.schema). The SDK works with any strongly-typed message types.
There is an quick example of schema in examples folder.