Dynamic Message Deserialization

Overview

Brighter supports dynamic type resolution, allowing you to route multiple message types through a single channel. Instead of determining the message type at compile-time through generic parameters, you can use content-based routing where the message type is determined at runtime from metadata.

This enables more flexible messaging patterns while maintaining type safety once the message type is resolved.

DataType Channel Pattern (Default)

By default, Brighter uses the DataType Channel pattern from Enterprise Integration Patterns. In this pattern, each channel carries messages of a single, known type.

DataType Channel Example

// DataType Channel - One type per subscription
var subscription = new KafkaSubscription<TaskCreated>(
    new SubscriptionName("paramore.example.tasks"),
    channelName: new ChannelName("task.created"),
    routingKey: new RoutingKey("task.created"),
    groupId: "task-processor",
    timeOut: TimeSpan.FromMilliseconds(100)
);

Characteristics:

  • Simple and straightforward

  • Type-safe at compile-time

  • One handler per channel

  • Recommended for most scenarios

  • Requires separate channel per message type

  • Cannot handle message type evolution on same channel

When to use DataType Channel:

  • You have distinct topics/queues for each message type

  • Message types are stable and don't evolve frequently

  • You want compile-time type safety

  • Simple producer-consumer patterns

This is Brighter's default and recommended approach for most scenarios.

Dynamic Message Deserialization

Dynamic message deserialization allows multiple message types on the same channel by resolving the type at runtime based on message metadata.

When to Use Dynamic Deserialization

Dynamic deserialization is useful when:

  • Multiple related message types share a single topic/queue

  • Message type evolution - new message types added to existing channels

  • CloudEvents-based routing - using the CloudEvents type attribute

  • Content-based routing - routing decisions based on message content

  • Shared infrastructure - multiple teams publishing to common topics

How It Works

Instead of specifying the type via a generic parameter, you provide a getRequestType callback in your Subscription that examines the message and returns the appropriate type:

Using CloudEvents Type for Routing

The most common approach for dynamic deserialization is using the CloudEvents type attribute. This provides a standard, interoperable way to identify message types.

CloudEvents Type Routing Example

How it works:

  1. Message arrives on task.update channel

  2. Brighter populates message.Header.Type (CloudEvents type attribute)

  3. Callback matches CloudEvents type to Request type

  4. Brighter deserializes message to correct Request type

  5. Routes to appropriate handler based on type

Setting CloudEvents Type on Publication

On the producer side, set the CloudEvents type in your Publication:

All three message types go to the same task.update topic, distinguished by their CloudEvents type.

Custom Routing Strategies

While CloudEvents type is recommended, you can implement any routing strategy by examining message properties.

Routing by Custom Header

Routing by Message Body Content

Note: Parsing the body for routing is less efficient than using headers, but can be useful when integrating with systems that don't support custom headers.

Handler Routing

Once the request type is resolved, Brighter routes the message to the appropriate handler using its standard handler resolution:

Standard 1-to-1 Handler Mapping

With dynamic deserialization:

  1. Message arrives on task.update channel

  2. getRequestType callback returns typeof(TaskCreated)

  3. Brighter deserializes to TaskCreated

  4. Routes to TaskCreatedHandler

Integration with Agreement Dispatcher

Dynamic message deserialization can be combined with Agreement Dispatcher for even more flexible routing:

This provides two levels of routing:

  1. Dynamic deserialization: CloudEvents type → OrderCreated

  2. Agreement dispatcher: Order content → Country-specific handler

Performance Considerations

Dynamic message deserialization has a small performance overhead compared to DataType Channel:

Runtime Type Resolution

DataType Channel (Compile-Time):

  • Type known at compile-time via generic parameter

  • Message mapper pipeline pre-built

  • Minimal runtime overhead

Dynamic Deserialization (Runtime):

  • Type determined by executing callback function

  • Message mapper pipeline built on first use per type

  • Pipeline cached for subsequent messages of same type

Configuration Examples

Kafka with CloudEvents Routing

RabbitMQ with CloudEvents Routing

AWS SQS with CloudEvents Routing

Best Practices

1. Use CloudEvents Type for Routing

CloudEvents provides a standard, interoperable way to identify message types:

2. Provide Comprehensive Type Mappings

Handle all expected message types and provide a clear error for unmapped types:

3. Use Meaningful CloudEvents Types

Follow reverse-DNS naming for CloudEvents types:

See CloudEvents Support for more on CloudEvents type naming.

4. Consider DataType Channel First

Start with DataType Channel (one type per topic) unless you have a specific need for dynamic deserialization:

Only use dynamic when needed:

  • Multiple related types on same channel

  • Message evolution scenarios

  • CloudEvents-based integration

5. Cache Performance-Critical Paths

If performance is critical, pre-warm the pipeline cache:

6. Document Type Mappings

Document which CloudEvents types map to which Request types:

Comparison: DataType Channel vs Dynamic Deserialization

Aspect
DataType Channel
Dynamic Deserialization

Type Resolution

Compile-time (generic)

Runtime (callback)

Performance

Fastest

Fast

Flexibility

One type per channel

Multiple types per channel

Type Safety

Compile-time

Runtime (after resolution)

Setup Complexity

Simple

Moderate

Message Evolution

Requires new channels

Same channel

CloudEvents Integration

Not needed

Natural fit

When to Use

Default, most scenarios

Multiple types, evolution

Error Handling

Handle unmapped message types gracefully:

Failed messages will go to the dead letter queue based on your failure handling configuration.

Further Reading

Sample Code

Full working examples can be found in the Brighter samples:

  • Dynamic Deserialization: Brighter/samples/TaskQueue/ - Examples using CloudEvents type routing

  • Multi-type Channels: Brighter/samples/MultiBus/ - Multiple message types on shared infrastructure

Last updated

Was this helpful?