Topology

Exchanges, queues and bindings declared in configuration and applied at startup, before any consumer starts.

{
  "acemq": {
    "topology": {
      "apply": "Create",
      "failOnDrift": false,
      "exchanges": [
        { "name": "orders", "type": "topic" }
      ],
      "queues": [
        {
          "name": "orders.new",
          "type": "Quorum",
          "deadLetter": true,
          "arguments": { "x-max-length": "100000" }
        }
      ],
      "bindings": [
        { "queue": "orders.new", "exchange": "orders", "routingKey": "order.created" }
      ]
    }
  }
}

Nothing is declared unless something is written

An empty topology section declares nothing at all. This package will not guess a queue name from a consumer registration, because a typo in a queue name should fail loudly rather than quietly create the queue it was a typo for.

apply

Create declare what is missing. The default, once anything is declared.
DryRun declare nothing; log the plan. What to run against production before running Create against it.
None declare nothing and say nothing. The declarations become documentation.

Exchanges

type is topic, direct, fanout or headers, and defaults to topic.

Queues

type is Classic, Quorum or Stream, and defaults to Quorum. That default is deliberate: a classic queue on a cluster acknowledges a message that one node has, and that node can be the one that dies.

Stream declares a stream, which is a log read rather than emptied and cannot be consumed by AddConsumer — see streams.

deadLetter: true declares {name}.dlq alongside and points this queue's dead letters at it.

arguments are passed to the broker as declared, with one convenience: a value that parses as an integer is sent as one, and true/false as a boolean. RabbitMQ rejects x-max-length as the string "100000" with an error that names the type and not the setting, and a JSON configuration file has only strings to offer.

Every consumer also declares {queue}.dlq and {queue}.parked for itself, plus the rungs of its retry ladder when it has one — that is the library's behaviour, not this package's, and it happens whether or not the queue is declared here. See retries and duplicates.

Bindings

routingKey defaults to the empty string, which is what a fanout exchange wants and what a topic exchange almost never does.

Drift

Drift is a queue or exchange that exists with settings a declaration cannot change — a classic queue where the configuration asks for quorum, different arguments, a different exchange type. Redeclaring over it is not something the broker offers, so the only question is whether to say so and carry on or to refuse to start.

By default it is logged at warning and the application starts. failOnDrift: true makes it refuse, which is the right setting for an environment where the topology is supposed to be under this application's control and a difference means somebody changed it by hand.

The plan is rendered into the log either way, so the warning says what differs rather than that something does.

Declaring more than configuration can express

The injected AceMqConnection has the library's whole topology surface on it — streams, retry ladders with a named policy, arguments that are not scalars. A hosted service registered after AddAceMq runs after the connection host has opened the connection:

builder.Services.AddAceMq(builder.Configuration.GetSection("acemq"));
builder.Services.AddHostedService<ExtraTopology>();

sealed class ExtraTopology(IAceMqConnectionProvider connections) : IHostedService
{
    public async Task StartAsync(CancellationToken ct)
    {
        var mq = await connections.GetAsync(ct);
        await mq.DeclareStreamAsync("events", TimeSpan.FromDays(7), maxLengthBytes: null);
    }

    public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}

Registered after AddAceMq but before any consumer needs the result — consumers start from AceMqConsumerHost, which AddAceMq registered second, so anything added later starts after it. If a consumer must find a queue this code declares, declare it in configuration instead.