KubeMQ
Integrations.NET AspireHow-to guides

Multiple KubeMQ Instances (Keyed DI)

Register and inject multiple keyed KubeMQ clients in a single Aspire service.

A single .NET service often needs to talk to more than one KubeMQ broker — for example, one broker for the order pipeline and a separate one for outbound notifications. The Aspire client integration supports this through keyed dependency injection: each broker gets its own IKubeMQClient registered under a string key, and you inject the one you need by that key.

Why Keyed Registration

AddKubeMQClient registers a single, non-keyed IKubeMQClient singleton. It can only be called once per service. The integration tracks this with an internal marker, and a second call throws:

Exception on duplicate AddKubeMQClient
System.InvalidOperationException: AddKubeMQClient has already been called.
For multiple KubeMQ connections, use AddKeyedKubeMQClient with distinct service keys.

This is by design. When you need two or more connections in the same container, switch to AddKeyedKubeMQClient, which registers each client under a distinct key instead of competing for the single non-keyed slot.

Keyed and non-keyed registration are mutually exclusive within the same service. Pick one model per service: either a single AddKubeMQClient call, or one AddKeyedKubeMQClient call per broker. Mixing them in the same container is not supported.

Prerequisites

You need a KubeMQ broker reachable over gRPC. When running outside Aspire orchestration (or to point at an external broker), start one with Docker:

docker run -d \  --name kubemq \  -p 50000:50000 \  -p 9090:9090 \  -e KUBEMQ_TOKEN=YOUR_LICENSE_KEY \  europe-docker.pkg.dev/kubemq/images/kubemq:next
PortPurpose
50000gRPC — the SDK / client connection
9090REST / HTTP API

Under Aspire orchestration the AppHost provisions these containers for you, so the Docker command above is only needed for standalone runs.

AppHost: Provision Two Resources

In the AppHost, add one KubeMQ resource per broker and reference both from the service that needs them. The resource name ("orders", "notifications") is what the client side will use as its key.

AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);

var orders = builder.AddKubeMQ("orders");
var notifications = builder.AddKubeMQ("notifications");

builder.AddProject<Projects.MyService>("service")
    .WithReference(orders)
    .WithReference(notifications);

builder.Build().Run();

Each WithReference call injects a connection string into the service's configuration under the resource name, so orders and notifications become resolvable connection names downstream.

Service: Register One Client Per Key

In the service, call AddKeyedKubeMQClient once per broker. The key you pass doubles as the connection name used to resolve the connection string from configuration:

MyService/Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.AddKeyedKubeMQClient("orders");
builder.AddKeyedKubeMQClient("notifications");

var app = builder.Build();
app.Run();

Internally, each call binds settings, resolves the connection string for that name, applies the settings to a fresh set of client options, and registers a keyed IKubeMQClient singleton under the key.

Inject by Key

Resolve a specific client in a constructor with the [FromKeyedServices] attribute, passing the matching key:

OrderService.cs
using KubeMQ.Sdk.Client;
using Microsoft.Extensions.DependencyInjection;

public class OrderService(
    [FromKeyedServices("orders")] IKubeMQClient client)
{
    public Task PublishAsync(string body) =>
        client.SendEventAsync(new KubeMQ.Sdk.Events.EventMessage
        {
            Channel = "events.orders",
            Body = System.Text.Encoding.UTF8.GetBytes(body),
        });
}

You can also resolve clients imperatively from the service provider with GetRequiredKeyedService:

Resolve keyed clients manually
var ordersClient = host.Services.GetRequiredKeyedService<IKubeMQClient>("orders");
var notifClient = host.Services.GetRequiredKeyedService<IKubeMQClient>("notifications");

await ordersClient.ConnectAsync();
await notifClient.ConnectAsync();

await ordersClient.SendEventAsync(new EventMessage
{
    Channel = "events.orders",
    Body = Encoding.UTF8.GetBytes("New order created"),
});

await notifClient.SendEventAsync(new EventMessage
{
    Channel = "events.notifications",
    Body = Encoding.UTF8.GetBytes("User notified"),
});

Per-Key Configuration

Keyed settings bind from a named subsection under Aspire:KubeMQ:Client, where the section name matches the key. So AddKeyedKubeMQClient("orders") reads its settings from Aspire:KubeMQ:Client:orders. Each broker gets its own fully independent settings block — AuthToken, TLS, timeouts, gRPC tuning, reconnection, and so on.

appsettings.json
{
  "Aspire": {
    "KubeMQ": {
      "Client": {
        "orders": {
          "AuthToken": "orders-broker-token",
          "ClientId": "order-service",
          "HealthCheckTimeout": "00:00:05",
          "ConnectionTimeout": "00:00:10",
          "UseTls": true,
          "TlsCaFile": "/certs/orders-ca.pem"
        },
        "notifications": {
          "AuthToken": "notifications-broker-token",
          "ClientId": "notification-service",
          "HealthCheckTimeout": "00:00:03",
          "DisableMetrics": true,
          "ReconnectEnabled": true,
          "ReconnectMaxAttempts": 0
        }
      }
    }
  }
}

The two subsections are bound separately, so settings never leak between brokers — orders can run over TLS while notifications stays on plain gRPC, each with its own auth token and timeouts. You can still adjust settings in code via the optional delegate:

Per-key settings delegate
builder.AddKeyedKubeMQClient("orders", settings =>
{
    settings.ClientId = "order-service";
    settings.DisableMetrics = false;
});

Prop

Type

How the Keyed Path Works

There is one important implementation detail to be aware of. The non-keyed AddKubeMQClient delegates to the SDK's own AddKubeMQ DI helper. That helper does not support keyed DI, so the keyed path cannot reuse it.

Instead, AddKeyedKubeMQClient constructs the KubeMQClient directly and registers it under the key, then registers a KubeMQKeyedHostedService to manage that client's lifecycle (connect on start, dispose on shutdown):

What AddKeyedKubeMQClient does internally
var client = new KubeMQClient(keyOptions);

builder.Services.AddKeyedSingleton<IKubeMQClient>(name, (_, _) => client);
builder.Services.AddSingleton<IHostedService>(
    _ => new KubeMQKeyedHostedService(client));

This is documented, intentional behavior. Because the keyed path bypasses the SDK's AddKubeMQ, any future additions the SDK makes there (extra hosted services or wrapper registrations) will not automatically apply to keyed clients. Parity between the two paths is covered by the integration's unit tests.

Health Checks Per Key

Each keyed client registers its own readiness and liveness checks, named after the key:

KeyReadiness checkLiveness check
orderskubemq-orders-readykubemq-orders-live
notificationskubemq-notifications-readykubemq-notifications-live

The readiness checks carry the ready tag and the liveness checks carry the live tag, so each broker is reported independently in the health endpoint. Set DisableHealthChecks on a key's settings section to skip registration for that broker.

Was this page helpful?

On this page