Table of Contents

Tenant resolved interceptor

A tenant resolved interceptor runs after a user has been authenticated and their tenant has been resolved. This happens when the application is not multi-tenant, when the user has one tenant, or after the user selects a tenant.

Use an interceptor to configure the application context, theme, tenant visual identity, application culture, or input cultures for the authenticated session.

There can be several interceptors in a cluster. They run in module dependency order; the order is not guaranteed for independent modules. Each interceptor receives the result returned by the preceding interceptor.

Important

Treat previousTenantResolvedResult as the current session result. Update and return this same instance unless you intentionally need to replace every part of the result.

Implement an interceptor

Create a class that implements ITenantResolvedInterceptor.

The following interceptor adds a server-readable value, a client-only value, and a theme while preserving all values added by earlier interceptors:

public class TenantResolvedInterceptor : ITenantResolvedInterceptor
{
    public Task<OnTenantResolvedResult> OnTenantResolvedAsync(NeosTenantInfo? tenant, OnTenantResolvedResult previousTenantResolvedResult)
    {
        return Task.FromResult(previousTenantResolvedResult
            .WithContextValue("CompanyId", 1)
            .WithClientOnlyContextValue("CompanyName", "Acme")
            .WithTheme("AcmeTheme"));
    }
}

In this example, CompanyId is added to the application context, CompanyName is added to the client-only application context, and AcmeTheme is selected.

WithContextValue makes a value available in the client application context and propagates it to the API context for subsequent requests. Use it only when server-side APIs must read the value.

WithClientOnlyContextValue makes a value available in the client application context only. It is returned by POST $neos/ui/on-authenticated, but is not propagated in subsequent API requests.

Tip

Prefer WithClientOnlyContextValue by default. Values added with WithContextValue are sent in every subsequent API request, so use it only when the server must read them. See Application context for details about consuming these values.

Services, such as repositories, can be injected into the interceptor.

Choose the tenant visual identity

In a multitenant cluster, VisualIdentity holds the name of the tenant visual identity chosen by the rules of Tenant Management, or null when the tenant keeps the standard look. The framework sets it before the first interceptor, whatever the order of the interceptors, and applies the value left by the last one. WithVisualIdentity replaces it with another style of Tenant Management, chosen by its exact name, or with the standard look when the name is null:

public Task<OnTenantResolvedResult> OnTenantResolvedAsync(NeosTenantInfo? tenant, OnTenantResolvedResult previousTenantResolvedResult)
{
    // The administrators of the tenant see a style that tells them apart
    if (_currentUser.IsAdministrator)
    {
        return Task.FromResult(previousTenantResolvedResult.WithVisualIdentity("Administration"));
    }

    return Task.FromResult(previousTenantResolvedResult);
}

In this example, _currentUser stands for a service of the cluster injected into the interceptor. A name that Tenant Management does not know is logged as a warning, and the style chosen by the rules is kept. See Choose the style from the cluster code for the rules that apply to the chosen style.

Preserve the previous result

previousTenantResolvedResult contains the complete result built so far. It includes its authorization state, application context, client-only context, theme, tenant visual identity, cultures, and input cultures.

Creating new OnTenantResolvedResult(...) does not copy any value from that instance. In particular, no constructor accepts ClientOnlyContext. Only the tenant visual identity is kept: a new result keeps the VisualIdentity of the previous one, unless the interceptor calls WithVisualIdentity on it. Replacing the instance can therefore silently remove client-only values supplied by framework or cluster interceptors, even when the replacement copies Context, the theme, and cultures.

This can make UI components or permissions unavailable after sign-in. For example, an interceptor that replaces the previous result loses client-only values such as UserCustomViewMode or NotificationCenterPermission:

// Do not do this: ClientOnlyContext and any omitted values are lost.
return new OnTenantResolvedResult(
    previousTenantResolvedResult.Context,
    "AcmeTheme",
    previousTenantResolvedResult.ApplicationCulture,
    previousTenantResolvedResult.InputCultures);

Update the existing instance instead:

return previousTenantResolvedResult
    .WithTheme("AcmeTheme")
    .WithApplicationCulture("en-US")
    .WithInputCultures(["en-US"]);

This approach is also correct for an early return. Do not replace a result merely because optional data is absent:

if (userAccount == null)
{
    return previousTenantResolvedResult.WithTheme("AcmeTheme");
}
Warning

Do not use Context == null to determine whether the user is authenticated. A valid result can contain no server-propagated context while still containing client-only context, a theme, or other session values.

Deny access intentionally

new OnTenantResolvedResult(false) is not a neutral fallback. It explicitly changes the current result to unauthorized, even when authentication succeeded before the interceptor ran.

When an interceptor returns an unauthorized result, the interceptor pipeline stops immediately. Later interceptors are not executed, and the on-authenticated response represents an unauthorized session. Return this result only when the interceptor deliberately denies access, for example because the authenticated user is not allowed to use the resolved tenant:

if (!await _tenantAccessService.CanAccessAsync(tenant, _currentUser.Id))
{
    return new OnTenantResolvedResult(false);
}

return previousTenantResolvedResult;

If the interceptor cannot apply an optional customization, preserve the existing result instead:

return previousTenantResolvedResult;

Register the interceptor

Create or update the Startup class at the root of the project and register the interceptor in ConfigureServices with AddTenantResolvedInterceptor:

public static class Startup
{
    public static void ConfigureServices(IServiceCollection services)
    {
        services.AddTenantResolvedInterceptor<TenantResolvedInterceptor>();
    }
}

Regenerate your application for the services configuration to take effect.

See also