Skip to content

Semantic topic modeling (Topic virtualization)

Standard MQTT infrastructure forces developers to interact directly with raw, error-prone string paths scattered across the application layer. SyMqtt solves this by introducing a semantic virtualization model.

Instead of treating topics as simple network addresses, SyMqtt abstracts the underlying broker paths into strongly-typed, functional objects designed around their specific operational roles:

1. Outbound messaging: SyMqttSenderTopicHolder

Section titled “1. Outbound messaging: SyMqttSenderTopicHolder”

Virtualizes a physical MQTT topic into a dedicated outbound execution pipe. It encapsulates the publishing logic, ensuring that your application writes data cleanly to a specific functional target without exposing low-level client routines.

2. Inbound messaging: SyMqttReceiverTopicHolder

Section titled “2. Inbound messaging: SyMqttReceiverTopicHolder”

Virtualizes subscription paths (including single-level + and multi-level # wildcards) into an isolated, inbound messaging pipe. It allows you to wire up custom asynchronous handlers directly to the object, ensuring incoming messages are cleanly consumed and processed as they arrive.

The highest level of topic virtualization. It couples exactly one SyMqttSenderTopicHolder (acting as the request line) and one SyMqttReceiverTopicHolder (acting as the response line) into a single logical entity.
The channel manages the internal TaskCompletionSource lifecycle and correlates unique message IDs automatically under the hood to achieve synchronous Request-Response execution.

Every Topic Holder has a logical ID independent of the actual network topic string:

// The application logic refers to this object strictly by its ID: "pump_commands"
var holder = new SyMqttSenderTopicHolder("pump_commands", "factory/floor1/sectionB/pumps/cmd");

If your broker architecture or device layout changes tomorrow, you only update the topic string in one single initialization section. The rest of your enterprise business logic remains untouched because it continues to interact with the immutable Topic Holder ID ("pump_commands").


Dual-role topography (same topic, different roles)

Section titled “Dual-role topography (same topic, different roles)”

A common question when architectural constraints arise is: Can I publish and subscribe to the exact same MQTT topic using SyMqtt?

Yes, absolutely. Because SyMqtt segregates the infrastructure cleanly by functional design, a single physical MQTT network topic can be simultaneously encapsulated into both a SyMqttSenderTopicHolder and a SyMqttReceiverTopicHolder instance if your system design requires it.
(In fact, a single physical MQTT network topic can be encapsulated in as many SyMqttSenderTopicHolder and SyMqttReceiverTopicHolder instances as needed.)

This dual-role topography is highly valuable in specific patterns:

  • Loopback & echo verification: When a controller broadcasts a state change on a topic, and your app needs to monitor that exact same channel for telemetry.
  • State syncing / distributed Systems: Where devices listen for configuration state changes on a topic but can also overwrite/publish updates back to that same exact path.

By splitting them into separate sender and receiver objects, your writing logic and your reading/event-handling logic remain isolated, preventing monolithic event loops.


While individual Topic Holders excel at one-way asynchronous telemetry (Fire-and-Forget Pub/Sub), high-level distributed systems frequently require synchronous request-response execution. You send a specific command, and you expect a deterministic response.

This is where the SyMqttChannel comes into play.

A SyMqttChannel is a high-level logical construct that permanently couples exactly one SyMqttSenderTopicHolder and one SyMqttReceiverTopicHolder together.

// Explicitly binding an outbound pipe and an inbound pipe into a single synchronous Channel
var channel = new SyMqttChannel(
mqttBox,
channelId: "telemetry_gateway",
senderTopic: "devices/device123/request",
receiverTopic: "devices/device123/response"
);

When you execute a synchronous call through a channel (using the FetchAsync routine):

  1. Your request message passes into the channel’s Sender Topic.
  2. The central SyMqttBox engine attaches a unique tracking ID inside the message envelope and pushes it to the broker.
  3. The remote entity processes the request and sends the response back via the channel’s designated receiver topic.
  4. The Channel captures the incoming message, matches the correlation ID instantly, and safely releases the specific .NET execution thread that was suspended awaiting the result.

By wrapping this complex async-await lifecycle inside a unified SyMqttChannel object, your application achieves clean, readable code that reads like a local RPC method call, hiding all the low-level asynchronous network mechanics entirely.


The SyMqttChannel class provides two distinct constructors to adapt to your architectural needs:

Ideal when a channel owns its topics exclusively. You pass the topic strings directly, and SyMqtt automatically instantiates the underlying topic holders in the background, automatically naming their logical IDs based on the channel’s identity (e.g., {ChannelId}_sender).

// Underlying topic holders are created automatically with IDs: "conveyor_sender" and "conveyor_receiver"
var channel = new SyMqttChannel(mqttBox, "conveyor", "conveyor/cmd", "conveyor/res");

Ideal when you want to reuse pre-existing topic holders or need custom configurations on the receiver (such as custom trailing private levels or standalone event handlers).

var mySender = new SyMqttSenderTopicHolder("shared_out", "factory/global/cmd");
var myReceiver = new SyMqttReceiverTopicHolder("shared_in", "factory/global/res");
// Both objects are injected explicitly into the channel topology
var channel = new SyMqttChannel(mqttBox, "my_channel", mySender, myReceiver);

Channel: direct receiver topic holder inspection

Section titled “Channel: direct receiver topic holder inspection”

A SyMqttChannel is designed as an open infrastructure component rather than a closed black box. While its primary job is to orchestrate the internal synchronous Request-Response lifecycle, it exposes its underlying SenderTopicHolder and ReceiverTopicHolder instances as public properties.

This access is useful for implementing cross-cutting concerns like logging, auditing, performance tracking, or real-time UI monitoring. For example, if you need to inspect or log every inbound message arriving on the channel’s response path without interfering with the active synchronous execution, you can attach an independent event handler directly to the receiver:

// Access the underlying receiver topic holder from the channel
var receiverHolder = myChannel.ReceiverTopicHolder;
// Attach a dedicated logging handler without disrupting the synchronous flow
receiverHolder.AddReceivedMessageHandler((payload, messageInfo) =>
{
Console.WriteLine($"[Audit Log] Received payload on topic {messageInfo.PublishTopic}: {payload}");
return Task.CompletedTask;
});

Because SyMqttReceiverTopicHolder supports multiple independent subscribers, your telemetry or logging logic runs cleanly in parallel, while the SyMqttChannel continues to execute its core functionality related to synchronous communication management.