Pulse.Mqtt.Serialization.MessagePack 2.28.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Pulse.Mqtt.Serialization.MessagePack --version 2.28.0
                    
NuGet\Install-Package Pulse.Mqtt.Serialization.MessagePack -Version 2.28.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Pulse.Mqtt.Serialization.MessagePack" Version="2.28.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Pulse.Mqtt.Serialization.MessagePack" Version="2.28.0" />
                    
Directory.Packages.props
<PackageReference Include="Pulse.Mqtt.Serialization.MessagePack" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Pulse.Mqtt.Serialization.MessagePack --version 2.28.0
                    
#r "nuget: Pulse.Mqtt.Serialization.MessagePack, 2.28.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Pulse.Mqtt.Serialization.MessagePack@2.28.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Pulse.Mqtt.Serialization.MessagePack&version=2.28.0
                    
Install as a Cake Addin
#tool nuget:?package=Pulse.Mqtt.Serialization.MessagePack&version=2.28.0
                    
Install as a Cake Tool

<img src="https://raw.githubusercontent.com/araxis/pulse-mqtt/main/icon.png" alt="" width="32" height="32" align="top" /> Pulse.Mqtt

NuGet Downloads CI License: MIT .NET Docs

A high-performance, resilient MQTT 5.0 / 3.1.1 client for modern .NET (net8.0 + net10.0).

Most .NET MQTT clients leave the hard parts to the application: reconnecting, re-subscribing, queueing while offline, routing topics to handlers, typed payloads. Pulse makes all of that first-class — and every major behavior is swappable behind a small contract, so replacing the reconnect policy with Polly, the offline store with a durable one, or TCP with WebSocket or QUIC is one line, not a fork.

Verified Native AOT: the full stack compiles with zero trim/AOT warnings and runs as a 3.15 MB self-contained native binary.

Highlights

  • MQTT 5.0 and 3.1.1 behind one API, with guardrails that reject v5-only features on a v3.1.1 connection instead of putting invalid bytes on the wire.
  • Resilient by default — a supervisor reconnects, re-subscribes, drains a bounded offline queue, and reports health. Terminal failures (for example NotAuthorized) fault sticky rather than looping forever.
  • Topic routing with route templates (sensors/{deviceId}/temp), typed payloads, and request/response with response-topic and correlation data handled for you.
  • Swap any layer behind a small contract: reconnect policy, retry-vs-fault decision, session and offline stores, serializer, last-will generation, transport.
  • Four transports — TCP/TLS and an in-memory loopback in the core; WebSocket and QUIC as opt-in add-ons.
  • Durable when you need it — SQLite or LiteDB session and offline-queue stores survive restarts; the default is bounded in-memory.
  • Bounded everywhere — inbound, per-route, and offline queues all apply backpressure to the socket instead of buffering without limit. All timing flows through TimeProvider.
  • Test in memory — an in-process broker delivers full pub/sub, QoS acknowledgements, and cross-client routing in milliseconds, no container required.

Requirements

  • .NET 8 or .NET 10 for the core and most add-ons.
  • The QUIC transport targets .NET 10 only and needs the msquic native library (bundled with the runtime on Windows 11+; libmsquic on Linux). Check QuicTransportFactory.IsSupported and fall back to TCP or WebSocket where it is unavailable.

Packages

Package NuGet What it is
Pulse.Mqtt.Core NuGet Wire codec (all 15 control packets, v5 + v3.1.1), transports (TCP/TLS, in-memory loopback), the raw client, all swap-point contracts. Depends only on the BCL + System.IO.Pipelines.
Pulse.Mqtt.Client NuGet The resilient client: supervisor, topic routing, typed messaging, request/response.
Pulse.Mqtt.Dataflow NuGet Dataflow source blocks for client messages, routes, acknowledged routes, and state transitions.
Pulse.Mqtt.DependencyInjection NuGet AddPulseMqttClient, named clients, hosted lifecycle, health checks.
Pulse.Mqtt.Endpoints NuGet Minimal-API-style MapMqtt endpoints: constrained route templates, per-message service scopes, subscribe-on-map.
Pulse.Mqtt.Serialization.Json NuGet Source-generated System.Text.Json payload serialization (AOT-safe).
Pulse.Mqtt.Serialization.MessagePack NuGet MessagePack payload serialization for compact binary messages.
Pulse.Mqtt.Serialization.Protobuf NuGet Protocol Buffers payload serialization for generated binary messages.
Pulse.Mqtt.Resilience.Polly NuGet Reconnect strategy backed by a Polly v8 ResiliencePipeline.
Pulse.Mqtt.Storage.LiteDB NuGet Durable LiteDB session and offline-message stores.
Pulse.Mqtt.Storage.Sqlite NuGet Durable SQLite session and offline-message stores.
Pulse.Mqtt.Transport.WebSocket NuGet MQTT over WebSocket (ws/wss).
Pulse.Mqtt.Transport.Quic NuGet MQTT over QUIC (EMQX-style listeners; .NET 10).
Pulse.Mqtt.Testing NuGet PulseMqttTestBroker — an in-process broker for millisecond tests, with opt-in retained messages, persistent sessions, and scripted responses.
Pulse.Mqtt.Analyzers NuGet Optional C# diagnostics for common Pulse MQTT usage mistakes.

Quick start

With dependency injection

services.AddPulseMqttClient("telemetry", options =>
{
    options.Host = "broker.example.com";
    options.Port = 8883;
    options.UseTls = true;
    options.ClientId = "service-1";
});
// The client connects with the host, reconnects on drops, re-subscribes, and reports health.
// Prefer manual control? options.ConnectWithHost = false, then ConnectAsync/DisconnectAsync at will.

var client = provider.GetRequiredService<IPulseMqttClientFactory>().GetClient("telemetry");

Direct construction

var factory = new TcpTransportFactory(new TcpTransportOptions { Host = "broker.example.com" });
await using var client = new ResilientMqttClient(factory, new ResilientMqttClientOptions
{
    Connect = new MqttConnectPacket { ClientId = "service-1" },
});
await client.ConnectAsync(ct); // connects in the background
await client.WaitUntilConnectedAsync(TimeSpan.FromSeconds(10), ct); // when readiness matters

Route topics to handlers

var template = MqttRouteTemplate.Parse("sensors/{deviceId}/temp");
await client.SubscribeAsync([template.ToTopicFilter(MqttQualityOfService.AtLeastOnce)], ct);

using var route = client.RegisterRoute(template, (message, values, ct) =>
{
    Console.WriteLine($"{values["deviceId"]}: {Encoding.UTF8.GetString(message.Payload.Span)}");
    return ValueTask.CompletedTask;
});
// SubscribeAsync owns broker delivery; RegisterRoute owns local dispatch and captured values.

Need broker acknowledgement to wait for application work? Make that route manual:

await using var manual = await client.Route("orders/{id}")
    .AtLeastOnce()
    .ManualAcknowledgement()
    .HandleAsync(async (message, ct) =>
    {
        await PersistAsync(message.Message, ct);
        await message.AcknowledgeAsync(ct);
    }, ct);

The low-level OpenAcknowledgedRouteStream(...) remains available for pull consumers. In both forms, call AcknowledgeAsync or RejectAsync after handling the routed message. RejectAsync is available when CanReject is true, which means the delivery can carry an MQTT 5 negative acknowledgement reason code.

Map endpoints, Minimal-API style

// One call subscribes the filter and registers the handler. Write the parameters you need —
// route values (typed by their constraints), the payload, services, a token, in any order —
// and the bundled source generator binds them at compile time. No reflection, AOT-clean.
client.MapMqtt("sensors/{deviceId:int}/temp",
    (int deviceId, Reading reading, CancellationToken ct) => Save(deviceId, reading, ct));

// In a host, app.MapMqtt(...) resolves the registered client, and handler parameters like
// IDeviceStore resolve from a fresh service scope per message.
app.MapMqtt("sensors/{deviceId:int}/reading",
    (int deviceId, Reading reading, IDeviceStore store, CancellationToken ct) =>
        store.SaveAsync(deviceId, reading, ct));

// Endpoint acknowledgement is declarative too: default automatic, manual per route when needed.
client.MapMqtt("orders/{id}", async ctx =>
{
    await PersistAsync(ctx.Message, ctx.CancellationToken);
    await ctx.AcknowledgeAsync(ctx.CancellationToken);
}, new MqttEndpointOptions { Acknowledgement = MqttAcknowledgementMode.Manual });

// Request/reply: the return value IS the reply — published to the request's response topic
// with correlation data echoed. client.RequestAsync<TReq, TRes>(...) is the caller side.
app.MapMqttRequest("devices/{deviceId:int}/status",
    (int deviceId, StatusQuery query, IDeviceStore store, CancellationToken ct) =>
        store.GetStatusAsync(deviceId, query, ct));

// A call site that cannot be bound is a compile error (PMQE001–PMQE013), never a runtime surprise.

Typed messaging

// options.Serializer = new JsonMqttSerializer(AppJsonContext.Default);
await client.PublishAsync("telemetry/1", new Reading("dev-1", 21.5));            // stamps content type
var template = MqttRouteTemplate.Parse("telemetry/{id}");
await client.SubscribeAsync([template.ToTopicFilter(MqttQualityOfService.AtLeastOnce)], ct);
using var route = client.RegisterRoute<Reading>(template, (reading, msg, ct) => Handle(reading));

Request / response

var status = await client.RequestAsync<StatusQuery, StatusReply>("devices/7/status", new StatusQuery());
// Response topic + correlation data managed for you; concurrent calls never cross.

var template = MqttRouteTemplate.Parse("devices/{id}/status");
await client.SubscribeAsync([template.ToTopicFilter(MqttQualityOfService.AtLeastOnce)], ct);
using var responder = client.RegisterRequestHandler<StatusQuery, StatusReply>(template,
    (query, msg, ct) => ValueTask.FromResult(BuildStatus(msg.Values["id"])));

Or do it all fluently

await using var client = await new PulseMqttClientBuilder()
    .WithTcp("broker.example.com", 8883, useTls: true)
    .WithClientId("service-1")
    .WithSerializer(new JsonMqttSerializer(AppJsonContext.Default))
    .BuildAndConnectAsync(ct);

await client.Publish("telemetry/1").AtLeastOnce().WithRetain()
    .WithPayload(new Reading("dev-1", 21.5)).SendAsync(ct);

var route = client.Route("sensors/{deviceId}/temp");
await client.SubscribeAsync([route.ToTopicFilter(MqttQualityOfService.AtLeastOnce)], ct);
using var registration = route
    .WithConcurrency(4).Handle<Reading>((reading, msg, ct) => Handle(reading));

Test without a broker

await using var broker = new PulseMqttTestBroker();
await using var client = new ResilientMqttClient(broker, options);
// Full pub/sub, QoS acknowledgements, and routing between clients — in memory, in milliseconds.

Need retained-message, persistent-session, denied subscription, rejected connection, publish acknowledgement failure, or broker-disconnect behavior in a workflow test? Pass PulseMqttTestBrokerOptions and keep the same client setup.

Swap any major behavior

Behavior Contract Default Swap example
Reconnect loop IReconnectStrategy Exponential backoff + jitter .UseReconnectStrategy(_ => new PollyReconnectStrategy(pipeline))
Retry vs. fault IReconnectDecision Auth/identity reasons are final Treat NotAuthorized as transient for token rotation
Connection up/down IConnectionLifecycle Re-subscribe from the session store Add cache warming on reconnect
Session state ISessionStore In-memory A durable store that survives restarts
Offline queue IMessageStore Bounded in-memory, 4 overflow policies A durable queue
Last-will generation IMqttWillProvider Static/factory will Per-attempt will from client context
Payload format IMqttSerializer none (raw bytes) JSON, MessagePack, Protobuf, or your own
Transport IMqttTransportFactory TCP / TLS WebSocket, QUIC, the in-memory test broker, or your own

Terminal failures (for example a broker answering NotAuthorized) fault the client sticky — it stops instead of retrying forever, and an explicit ConnectAsync recovers after the cause is fixed.

Sample

samples/Pulse.Mqtt.Sample is a runnable console app covering hosting, typed publishes, routed subscriptions, and request/response. It needs no infrastructure — without arguments it runs against the in-process test broker:

dotnet run --project samples/Pulse.Mqtt.Sample
dotnet run --project samples/Pulse.Mqtt.Sample -- --host localhost --port 1883

samples/Pulse.Mqtt.AspNetCoreSample is a Minimal API host covering named/keyed dependency injection, health checks, diagnostics snapshots, typed publishing, routed consumption, and safe broker-capability checks for MQTT 5-only behavior:

dotnet run --project samples/Pulse.Mqtt.AspNetCoreSample
dotnet run --project samples/Pulse.Mqtt.AspNetCoreSample -- --Mqtt:Host localhost --Mqtt:Port 1883

samples/Pulse.Mqtt.WorkerSample is a worker pipeline using bounded Dataflow source blocks, explicit subscriptions, graceful shutdown, and capability checks:

dotnet run --project samples/Pulse.Mqtt.WorkerSample
dotnet run --project samples/Pulse.Mqtt.WorkerSample -- --Mqtt:Host localhost --Mqtt:Port 1883

samples/Pulse.Mqtt.BlazorWasmSample runs the client in the browser: MQTT over the browser's WebSocket from Blazor WebAssembly, with live connection state and pub/sub against any broker with a WebSocket listener (EMQX's is ws://host:8083/mqtt):

dotnet run --project samples/Pulse.Mqtt.BlazorWasmSample

Performance

Measured with BenchmarkDotNet (MemoryDiagnoser) on .NET 10:

Operation Mean Allocated
Publish encode (v5, no properties) ~60 ns 0 B
Frame + decode the same packet ~96 ns 144 B (the decoded objects themselves)
Topic filter match ~32 ns 0 B
Route template match (2 captures) ~56 ns 104 B (the captured values)
Variable-length integer round-trip ~26 ns 0 B

Head-to-head against MQTTnet over a real broker — lower allocations in every measured scenario, comparable throughput, and protocol-compliance differences: the full comparison.

Everything is bounded: inbound queues, per-route queues, the offline queue. Backpressure flows to the socket instead of buffering without limits. All timing goes through TimeProvider, so the whole stack is testable with a fake clock.

Broker compatibility

A single conformance suite (BrokerScenarios) runs the same scenarios — handshake, all three QoS round trips, retained messages, shared subscriptions, large payloads, persistent-session resume, receive-maximum flow control, and topic aliases — against every supported broker, so a failure names the broker and the capability, not a one-off test.

Broker How it runs
Eclipse Mosquitto 2.x Every push and merge (the fast lane)
EMQX 5.8 Broker matrix, over both TCP and its QUIC listener
HiveMQ CE 2024.3 Broker matrix

Brokers run as Testcontainers images, so the suite needs only a Docker daemon — no broker installs. See the broker compatibility reference.

Verification

  • 600+ tests: codec round-trips and fuzzing, QoS state machines, reconnect scenarios, routing isolation, and RPC as fast unit tests, plus the multi-broker conformance matrix above.
  • Chaos and soak coverage over both TCP and QUIC: a persistent-session publisher takes random connection kills under sustained QoS 1 load and must lose nothing — verified in a captured multi-thousand-message soak with flat heap and zero loss.
  • The decoder never throws anything but MqttProtocolException, fuzz-proven on tens of thousands of malformed inputs.
  • Native AOT: zero warnings; the published native smoke binary runs the full stack, enforced in CI.

Documentation

The full documentation is a VitePress site under docs/ — run it locally with cd docs && npm install && npm run docs:dev, or read the pages directly:

Guides

Reference

Project

License

MIT — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.29.0 117 7/9/2026
2.29.0-preview.219 64 7/9/2026
2.29.0-preview.218 65 7/9/2026
2.29.0-preview.217 55 7/6/2026
2.28.0 109 7/6/2026
2.28.0-preview.215 54 7/6/2026
2.28.0-preview.214 59 7/6/2026
2.28.0-preview.213 62 7/6/2026
2.28.0-preview.212 72 7/6/2026
2.28.0-preview.211 54 7/4/2026
2.28.0-preview.210 60 7/4/2026
2.28.0-preview.209 57 7/4/2026
2.28.0-preview.208 57 7/4/2026
2.27.0 113 7/4/2026
2.27.0-preview.206 56 7/4/2026
2.27.0-preview.205 60 7/4/2026
2.27.0-preview.204 75 7/3/2026
2.27.0-preview.203 55 7/3/2026
2.27.0-preview.202 55 7/3/2026
2.27.0-preview.201 55 7/3/2026
Loading failed