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:
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| Port | Purpose |
|---|---|
| 50000 | gRPC — the SDK / client connection |
| 9090 | REST / 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.
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:
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:
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:
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.
{
"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:
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):
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:
| Key | Readiness check | Liveness check |
|---|---|---|
orders | kubemq-orders-ready | kubemq-orders-live |
notifications | kubemq-notifications-ready | kubemq-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.
Related
Was this page helpful?