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.