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, 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.

Preserve the previous result

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

Creating new OnTenantResolvedResult(...) does not copy any value from that instance. In particular, no constructor accepts ClientOnlyContext. 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