> For the complete documentation index, see [llms.txt](https://docs.ecotone.tech/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ecotone.tech/messaging/multi-tenancy-support/different-scenarios/deriving-tenant-from-inbound-messages.md).

# Deriving Tenant from Inbound Messages

{% hint style="info" %}
**Enterprise feature.** `#[WithTenantResolver]` requires an Ecotone Enterprise licence.
{% endhint %}

## The Problem

When messages enter our application from outside — a Kafka topic, an AMQP queue, a scheduled poller pulling from a third-party API — they often **don't carry the tenant header** that the rest of the system relies on for connection routing. The information is there (the originating topic, a payload field, a queue name), it just hasn't been translated into Ecotone's tenant header yet.

Trying to handle this with a normal `#[Before]` interceptor or `#[AddHeader]` is brittle: by the time those run, the connection-switching interceptors have already fired and thrown *"Lack of context about tenant in Message Headers".*

## The Solution

Place `#[WithTenantResolver(expression: ...)]` on the **inbound channel adapter method** — the method that consumes externally-arriving messages. The expression is evaluated against the inbound message *before* any tenant-aware interceptor sees it, and its result is written into the tenant header.

```php
final class OrderEventConsumer
{
    #[KafkaConsumer('orders', topics: ['orders_eu', 'orders_us'])]
    #[WithTenantResolver(expression: "headers['kafka_topic']")]
    public function handle(string $payload, #[Headers] array $headers): void
    {
        // headers['tenant'] is now populated from the originating Kafka topic
    }
}
```

The expression has access to `payload` and `headers` of the inbound message. Whatever it returns is injected as the tenant header (whose name comes from your `MultiTenantConfiguration`). Downstream tenant-aware interceptors then see the correct tenant and route the connection accordingly — your handler code remains unchanged.

## Where It Can Be Placed

`#[WithTenantResolver]` is only valid on **inbound channel adapter methods**, where messages may arrive from outside the application without a tenant header:

* `#[KafkaConsumer]`
* `#[AmqpConsumer]`
* `#[Scheduled]`
* Any other inbound channel adapter

Placing it on a synchronous or asynchronous `#[CommandHandler]`, `#[EventHandler]`, or `#[QueryHandler]` is rejected at boot with a `ConfigurationException`. Internal Message Channels — including those used by async handlers — already carry the tenant context propagated from the originating bus call, so a resolver there has nothing to derive from.

If your async handler is processing externally-arrived messages, attach `#[WithTenantResolver]` to the inbound adapter that produces them, not to the handler itself.

## Scheduled Pollers

The same attribute works for `#[Scheduled]` methods that pull events from external sources:

```php
final class ExternalEventPoller
{
    public function __construct(private readonly PendingEventSource $source) {}

    #[Scheduled(requestChannelName: 'externalEventArrived', endpointId: 'externalPoller')]
    #[WithTenantResolver(expression: "payload['tenantId']")]
    public function poll(): ?Message
    {
        $event = $this->source->next();
        return $event === null
            ? null
            : MessageBuilder::withPayload($event['data'])
                ->setHeader('tenantId', $event['tenantId'])
                ->build();
    }
}
```

## Looking Up Tenant Through External Mapping

The expression can reference any service registered in the container. This is useful when the originating identifier (a topic, a queue, a code) does not literally equal the tenant name and needs to be translated through a mapping service:

```php
#[KafkaConsumer('orders', topics: ['orders_topic_1', 'orders_topic_2'])]
#[WithTenantResolver(expression: "reference('topicTenantMap').lookup(headers['kafka_topic'])")]
public function handle(string $payload, #[Headers] array $headers): void
{
    // ...
}
```

`topicTenantMap` is just a regular service in your container. You can hold the mapping in configuration, in a database table, or anywhere else.

## Explicit Tenant Header Wins

If the inbound message *already* carries the tenant header (for example, an upstream Kafka producer sets it explicitly), the resolver does **not** override it. Explicit headers always take precedence. This makes `#[WithTenantResolver]` safe to add even when only *some* of your incoming messages need translation.

## Null Result

If the expression evaluates to `null`, no tenant header is added — and downstream tenant-aware code will fail loudly with the standard *"Lack of context about tenant"* exception. The resolver does not invent a default; misconfiguration surfaces as a clear failure rather than silent routing to an arbitrary tenant.

## Non-Scalar Result

If the expression evaluates to something other than `string`, `int`, or `null` (for example, an object or array), the resolver throws an `InvalidArgumentException` naming the expression and the actual type. Tenant identifiers are always scalars; this catches misconfigured expressions early.

{% hint style="success" %}
`#[WithTenantResolver]` lets you keep multi-tenancy concerns at the system boundary — exactly where external messages enter your application. Downstream handler code stays the same as in non-multi-tenant systems, just like the rest of Ecotone's multi-tenancy story.
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.ecotone.tech/messaging/multi-tenancy-support/different-scenarios/deriving-tenant-from-inbound-messages.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
