# Multiple KubeMQ Instances (Keyed DI) (/integrations/aspire/how-to/keyed-multi-instance)



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 [#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:

```text title="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.

<Callout type="warn">
  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.
</Callout>

## Prerequisites [#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:

<RunKubeMQ ports="[50000, 9090]" />

| 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 [#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.

```csharp title="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 [#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:

```csharp title="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 [#inject-by-key]

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

```csharp title="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`:

```csharp title="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 [#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.

```json title="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:

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

<TypeTable
  type="{
  AuthToken: { type: &#x22;string | null&#x22;, description: &#x22;Per-broker auth token applied to that key's connection.&#x22; },
  ClientId: { type: &#x22;string | null&#x22;, description: &#x22;Client identifier reported to this broker.&#x22; },
  HealthCheckTimeout: { type: &#x22;duration&#x22;, default: &#x22;00:00:05&#x22;, description: &#x22;Readiness check timeout for this key.&#x22; },
  UseTls: { type: &#x22;boolean&#x22;, default: &#x22;false&#x22;, description: &#x22;Enable TLS for this broker's gRPC connection.&#x22; },
  DisableMetrics: { type: &#x22;boolean&#x22;, default: &#x22;false&#x22;, description: &#x22;Disable OpenTelemetry metrics for this key.&#x22; },
  ReconnectMaxAttempts: { type: &#x22;integer | null&#x22;, description: &#x22;Max reconnect attempts (0 = unlimited).&#x22; },
}"
/>

## How the Keyed Path Works [#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):

```csharp title="What AddKeyedKubeMQClient does internally"
var client = new KubeMQClient(keyOptions);

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

<Callout type="info">
  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.
</Callout>

## Health Checks Per Key [#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 [#related]

* [.NET Aspire integration overview](/integrations/aspire)
* [Getting started with Aspire](/integrations/aspire/tutorials/getting-started)
* [Health checks and observability](/integrations/aspire/how-to/health-checks-observability)
