Skip to content

MessageInterception

blehnen edited this page Nov 10, 2026 · 9 revisions

Message Interception

You can intercept messages after serialization, but before the transport stores them. Use this to compress or encrypt messages as needed. The queue keeps track of which message interceptors a message used, and it re-creates them as best it can. For that to work:

  • The queue must be able to create the interceptor class
  • You must create any configuration classes and inject them into the queue container

There are two built-in message interceptors. A third one, 3DES, was removed in 0.16.0:

  • Gzip (GZipMessageInterceptor)

This interceptor uses the built-in gzip compression class to compress messages after serialization and to decompress them on dequeue.

  • AES-256-GCM encryption (AesMessageInterceptor): recommended

This interceptor uses AES-256-GCM authenticated encryption (confidentiality and tamper detection). It encrypts the message after serialization and decrypts it on dequeue. It needs a 32-byte key, which you inject through AesMessageInterceptorConfiguration. The interceptor generates a fresh random nonce for each message and stores it with the ciphertext, so there is no IV to manage. Consumers must use the same key. The queue never saves your key. You must provide it on both the producer and the consumer.

  • 3DES encryption (TripleDesMessageInterceptor): removed in 0.16.0

Removed in 0.16.0: TripleDesMessageInterceptor no longer exists. NIST SP 800-131A retired Triple-DES, and Triple-DES is vulnerable to Sweet32. AesMessageInterceptor replaced it in 0.9.43, and 0.16.0 removed the deprecated type. 0.15.0 is the last release that can decrypt a 3DES message. See Migrating from 3DES to AES. You must complete that migration before you upgrade past 0.15.0.

On the releases that had it, this interceptor encrypted the message after serialization and decrypted it on dequeue. It required a key and an initialization vector through TripleDesMessageInterceptorConfiguration. Consumers had to use the same key and vector.

The queue runs message interceptors in the order you provide. If you use both compression and encryption, make sure that you specify the compression module first.

Registering the interceptors - Producer
var queueContainer = new QueueContainer<TTransportInit>(serviceRegister => serviceRegister.RegisterCollection<IMessageInterceptor>(new[]
{
 typeof (GZipMessageInterceptor), //gzip compression
 typeof (AesMessageInterceptor)   //encryption (AES-256-GCM)
}).Register(() => new AesMessageInterceptorConfiguration(
    RandomNumberGenerator.GetBytes(32) /* load your stable 32-byte key from a secret store */),
    LifeStyles.Singleton));

Security warning: RandomNumberGenerator.GetBytes(32) above is a placeholder. It generates a new key every run. Real deployments must load a stable 32-byte key from a secrets manager or an environment variable (the producer and every consumer need the same key). Never hardcode the key in source. AES-256-GCM needs no IV. The interceptor generates a fresh nonce for each message automatically.

In the example above, we:

  • register the gzip compression first
  • register the encryption second
  • register the encryption configuration so that we have a key to use (AES-256-GCM needs only a key, but the older 3DES interceptor also needs an initialization vector)
Registering the interceptors - Consumer
var queueContainer = new QueueContainer<TTransportInit>(
serviceRegister => serviceRegister.Register(() => new AesMessageInterceptorConfiguration(
    yourStable32ByteKey), LifeStyles.Singleton));

The consumers only need the configuration. They re-create the interceptors that the producers used, as needed. You may register the interceptors the same way as the producer, but it is not required.

Note: The same key management rules apply on the consumer side. See the security warning in the producer section above.

Migrating from 3DES to AES

⚠️ Do this on 0.15.0 or earlier. Both interceptors ship there, and neither this page nor any setting brings 3DES back afterwards. Nothing in 0.16.0 can read a 3DES message.

On 0.15.0, AesMessageInterceptor and TripleDesMessageInterceptor coexist. The per-message interceptor graph records which one encrypted each message, so a consumer can decrypt both during the migration.

  • If you can drain the queue: stop producing, let consumers finish the existing 3DES messages, then replace TripleDesMessageInterceptor with AesMessageInterceptor everywhere.
  • If you cannot drain (long-lived messages):
    • Producers: register only AesMessageInterceptor, so new messages are encrypted with AES (no double-encryption).
    • Consumers: register both configurations: AesMessageInterceptorConfiguration and TripleDesMessageInterceptorConfiguration. For built-in interceptors, consumers only need the configuration. The queue re-creates the interceptors as needed, and the graph selects AES for new messages and 3DES for old ones (a consumer never re-encrypts, so 3DES only ever decrypts).
    • Once the 3DES backlog is gone, drop TripleDesMessageInterceptorConfiguration from the consumers. Only then upgrade to 0.16.0.

Do not forget error queues and scheduled jobs that have not fired. A 3DES message sitting in either is still a 3DES message, and upgrading strands it.

Custom interceptor

To create your own message interceptor, implement IMessageInterceptor. You use it the same way as the built-in interceptors. But on the consumer, you may need to register your custom interceptor explicitly so the queue can resolve it.

public interface IMessageInterceptor
{
    // Transforms bytes before storage (serialization path).
    // Return MessageInterceptorResult(output, addToGraph, interceptorType).
    // Set addToGraph = false to opt out (e.g., when compression is not beneficial).
    MessageInterceptorResult MessageToBytes(byte[] input, IReadOnlyDictionary<string, object> headers);

    // Transforms bytes on retrieval (deserialization path).
    byte[] BytesToMessage(byte[] input, IReadOnlyDictionary<string, object> headers);

    // Human-readable name used in logging and diagnostics.
    string DisplayName { get; }

    // Base type used by the queue to re-create the interceptor on the consumer side.
    Type BaseType { get; }
}

MessageInterceptorResult carries three values:

Parameter Type Purpose
output byte[] The transformed message bytes
addToGraph bool When true, the interceptor records itself so consumers can reverse the transformation. Set false to skip (e.g., the GZip interceptor opts out when the compressed result would be larger than the input)
interceptorType Type The interceptor type used for re-creation on the consumer side

Clone this wiki locally