In a previous project, I used MQTTnet for MQTT communication. Initially, I relied on the standard MqttClient, which required me to manually implement reconnection logic—a process that was both cumbersome and verbose. Later, I discovered that MQTTnet offers an extension called ManagedMqttClient that handles disconnection reconnection and offline message queuing automatically, resulting in a much smoother development experience.

Additionally, I had always struggled to clearly distinguish between QoS, CleanSession, and Retain in MQTT. This is a good opportunity to document and clarify these concepts.


Installation

dotnet add package MQTTnet
dotnet add package MQTTnet.Extensions.ManagedClient

ManagedMqttClient is a standalone extension package and is not included in the MQTTnet main package.

Basic Usage

var options = new ManagedMqttClientOptionsBuilder()
    .WithAutoReconnectDelay(TimeSpan.FromSeconds(5))
    .WithClientOptions(new MqttClientOptionsBuilder()
        .WithClientId("my-client-001")
        .WithTcpServer("broker.hivemq.com", 1883)
        .WithCredentials("username", "password")
        .WithCleanSession(false)
        .Build())
    .Build();

var mqttClient = new MqttFactory().CreateManagedMqttClient();

// 收到消息的回调
mqttClient.ApplicationMessageReceivedAsync += e =>
{
    var topic = e.ApplicationMessage.Topic;
    var payload = Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment);
    Console.WriteLine($"收到消息 [{topic}]: {payload}");
    return Task.CompletedTask;
};

// 连接状态变化
mqttClient.ConnectedAsync += e =>
{
    Console.WriteLine("已连接到 Broker");
    return Task.CompletedTask;
};

mqttClient.DisconnectedAsync += e =>
{
    Console.WriteLine("连接断开,等待重连...");
    return Task.CompletedTask;
};

// 订阅主题
await mqttClient.SubscribeAsync(
    new MqttTopicFilterBuilder()
        .WithTopic("device/sensor/temperature")
        .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
        .Build());

// 启动
await mqttClient.StartAsync(options);

StartAsync returns immediately after being called, as it internally spawns a thread to maintain the connection. If the connection is lost, it will automatically reconnect at the interval specified by WithAutoReconnectDelay. Once reconnection is successful, previous subscriptions are automatically restored, so you don't need to manually Subscribe again.

Release Message

await mqttClient.EnqueueAsync(
    new MqttApplicationMessageBuilder()
        .WithTopic("device/sensor/temperature")
        .WithPayload("26.5")
        .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
        .WithRetainFlag(false)
        .Build());

Note that EnqueueAsync is used here, not PublishAsync. Messages first enter an internal queue and are sent automatically when the connection is active. If the connection is currently disconnected, messages wait in the queue and are sent after a successful reconnection. This is one of the advantages of ManagedMqttClient over the standard MqttClient.

QoS (Quality of Service)

MQTT has three QoS levels that control message delivery guarantees:

QoS 0 — At Most Once

Fire and forget. No acknowledgment, no retry. It is the fastest option, but messages may be lost. This is suitable for scenarios where losing a message is acceptable, such as a sensor reporting temperature once per second, where missing one or two readings has no impact.

QoS 1 — At Least Once

The broker sends an acknowledgment (PUBACK) after receiving a message. If the sender does not receive the acknowledgment, it will resend the message. This guarantees that messages are not lost, but duplicates may be received. This level is sufficient for most use cases.

QoS 2 — Exactly Once

A four-way handshake (PUBLISH → PUBREC → PUBREL → PUBCOMP) ensures that messages are neither lost nor duplicated. This is the most reliable level, but also the slowest and most resource-intensive. It should only be used in scenarios where duplicates are strictly unacceptable, such as payment notifications or command dispatch.

In real-world projects, QoS 1 is used in most cases. Avoid using QoS 2 whenever possible, as the performance difference is quite significant.

CleanSession (Clear Session)

When connecting to the Broker, there is a CleanSession switch:

CleanSession = true

Each connection is completely fresh, and the Broker does not retain any state associated with you. Any messages sent to you while you are disconnected are discarded. Upon reconnection, you need to resubscribe.

CleanSession = false

The Broker remembers your subscriptions and any QoS 1/2 messages received while you were offline. Once you reconnect, the Broker will push these queued messages to you. This requires your ClientId to remain fixed; it must not be randomly generated for each connection.

If your scenario involves a device that occasionally loses network connectivity and needs to retrieve messages from the offline period upon reconnection, set CleanSession to false. If you do not care about offline messages, setting it to true is sufficient and will also reduce the load on the Broker.

Retain (Retain Messages)

You can set a Retain flag when publishing a message:

Retain = true

The Broker retains the last message with the Retain flag on this Topic. When a new subscriber subscribes to this Topic, it immediately receives this retained message without waiting for the next publication.

Retain = false

The message is delivered and then discarded. New subscribers must wait for the next publication to receive a message.

A typical scenario: After a device comes online, it publishes an online status message with Retain set to true. This way, whenever a new client subscribes to this device's status Topic, it can immediately know that the device is online, without waiting for the device's next report.

To clear the retained message on a specific Topic, simply publish a message with an empty payload and Retain set to true.

How Do These Components Work Together

For example: a temperature sensor reports data every 10 seconds, and the monitoring dashboard subscribes to this data.

Sensor side publishing: QoS 1 + Retain = true. QoS 1 ensures no message loss, while Retain ensures that after the monitoring dashboard refreshes the page, it can immediately see the latest temperature value without waiting 10 seconds.

Monitoring dashboard subscription: QoS 1 + CleanSession = true. The dashboard does not need to backfill historical messages; seeing the current value each time it opens is sufficient. The Retained message solves the "see it immediately upon opening" problem.

If this were an alarm system, the approach would be different: QoS 1 or 2 + CleanSession = false. No alarm messages during disconnection can be lost; all of them must be delivered upon reconnection.