AceMQ for .NET

AMQP for C# and VB.NET: the same message envelope, the same patterns and the same metric names as the JVM libraries, with an API that reads like .NET rather than transliterated Java.

Pre-1.0. Publishing, consuming, topology, request/reply, ordering, pipelines, the outbox, replay and streams all work against a real broker, with an integration suite that runs against RabbitMQ in CI. The API is still free to change.

New here? The tutorials start with nothing and finish with a working service. The rest of this site is reference.

What it looks like

using var mq = await AceMqConnection.ConnectAsync("amqp://localhost");

using var consumer = await mq.ConsumeAsync<OrderPlaced>("orders.placed", async message =>
{
    await _orders.RecordAsync(message.Payload);
    return Ack.Accept();
});

var publisher = mq.Publisher<OrderPlaced>("orders", "order.placed");
await publisher.SendAsync(new OrderPlaced("A-1", 42.50m));

Two languages, one assembly

C# and VB.NET compile to the same IL, so both reference the same package. There is no separate VB library and there will not be one.

' VB.NET -- the same assembly, the same types
Dim publisher = mq.Publisher(Of OrderPlaced)("orders", "order.placed")
Dim result = Await publisher.SendAsync(New OrderPlaced("A-1", 42.5D))

What that costs is discipline on the public API rather than a second implementation: nothing on the surface that VB cannot call, which rules out Span<T> and ref struct in signatures, members that differ only by case, and overloads separable only by optional arguments. The VB example is compiled and run in CI for that reason — it has already caught two differences that would otherwise have reached a release. See VB.NET.

What the library does

Publishing confirms by default, back pressure, unroutable messages reported rather than dropped
Consuming explicit dispositions — accept, retry, dead-letter, release — and an attempt counter
Topology declared as one unit with dead-letter wiring, a dry run, and drift the broker actually reports
Codecs JSON, XML, text and bytes; composite for a format migration; payload encryption; a claim check for payloads too large for a broker
Request/reply one reply queue per requester, replies matched by correlation
Ordering order per key across partitions, and a partition that halts rather than reorders
Pipelines steps on their own queues, type-checked against each other
Outbox write the message with your data, publish it after the commit
Replay put dead-lettered messages back, all of them or the ones that match
Streams read from an offset; consuming removes nothing
Security TLS verified by default, private CAs, client certificates, credential providers, and development certificates the library refuses unless told
Reliability retry policies with jitter, idempotency, drain before shutdown
Metrics and tracing the same metric names as Java; a trace crosses the broker and the language
Interceptors a seam around every publish and every handled message, for what belongs to all of them
Transports RabbitMQ, and an in-process broker for tests
The envelope identity, correlation, causation, attempt, origin — pinned by conformance tests

One of those is a mechanism rather than a feature: interceptors are where a tenant header, an audit trail or a policy check on what may be published goes, instead of into every call site.

Why the envelope came first

A port is only "the same library" if something proves it. The proof here is a set of fixtures generated by publishing through the Java library and reading the message back at the transport level — not copied from documentation.

Two implementations agreeing with the same prose is not interoperability. Agreeing with the same bytes is.

That exercise immediately pinned six behaviours no document stated plainly, any of which would have been a coin flip if transcribed by hand. They are on the envelope page.

A second fixture does the same for the behaviour either side of the wire — the retry schedule, the queue names, the rung arguments and the declared topology — and all five libraries carry it byte for byte. Testing explains how that is checked, and where this library still disagrees with it.

Where it runs

netstandard2.0 and net8.0. Every package ships both.

netstandard2.0 reaches .NET Framework 4.6.2+, .NET Core and modern .NET from one assembly — and the RabbitMQ transport reaches the same, because RabbitMQ.Client still ships a netstandard2.0 build.

That is deliberate rather than conservative. The applications most likely to want a supported AMQP library are the ones that cannot easily move, and netstandard2.0 is the only target that reaches all of them at once. It is not going anywhere, and CI fails if any package stops building for it.

net8.0 was added alongside it in 0.6.0, and is what a modern consumer resolves to. Nothing in the library is conditioned on which one you get: the same source compiles twice. What it buys is the compatibility shims netstandard2.0 needs — System.Text.Json, System.Diagnostics.DiagnosticSource, System.Memory — no longer arriving as package references at an application whose runtime already has them.

Every optional package holds netstandard2.0 too, AceMq.Amqp.Crypto included — which is why payload encryption takes AES-GCM from BouncyCastle on both targets rather than from System.Security.Cryptography.AesGcm, which has never existed on one of them. See serialization.

The rest of AceMQ

The JVM libraries — the AMQP core, its Spring Boot starter, the RabbitMQ management API and the load generator — are at acemq.org.

The wire contract, the metric names and the conformance suite are shared and single-sourced there. This site is where the .NET libraries are documented, not where the specification lives.