Semantic topic modeling (Topic virtualization)
The problem and the solution
Section titled “The problem and the solution”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.
3. Synchronous coupling: SyMqttChannel
Section titled “3. Synchronous coupling: SyMqttChannel”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.
The advantage of logical decoupling
Section titled “The advantage of logical decoupling”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.
Coupling with SyMqttChannel
Section titled “Coupling with SyMqttChannel”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 Channelvar channel = new SyMqttChannel( mqttBox, channelId: "telemetry_gateway", senderTopic: "devices/device123/request", receiverTopic: "devices/device123/response");How data flows through a channel
Section titled “How data flows through a channel”When you execute a synchronous call through a channel (using the FetchAsync routine):
- Your request message passes into the channel’s Sender Topic.
- The central
SyMqttBoxengine attaches a unique tracking ID inside the message envelope and pushes it to the broker. - The remote entity processes the request and sends the response back via the channel’s designated receiver topic.
- 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.
Channel instantiation modes
Section titled “Channel instantiation modes”The SyMqttChannel class provides two distinct constructors to adapt to your architectural needs:
1. The direct (quick) constructor
Section titled “1. The direct (quick) constructor”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");2. The explicit topic holder constructor
Section titled “2. The explicit topic holder constructor”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 topologyvar 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 channelvar receiverHolder = myChannel.ReceiverTopicHolder;
// Attach a dedicated logging handler without disrupting the synchronous flowreceiverHolder.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.