Skip to content

Read-model interception

An interceptor lets you apply a cross-cutting transformation without repeating it in each query. Typical uses include localization, enrichment, and controlled field transformations. It is not a replacement for query authorization or safe data selection.

PathInterception
Ordinary model-bound query dataApplied after rendering
Ordinary Arc-wrapped MVC GET dataApplied after rendering
Supported direct WebSocket/SSE streams and multiplexed hub emissionsApplied per emission
Observable HTTP snapshot, including waitForFirstResult=trueNot currently applied by ObservableQueryHttp
MVC [AspNetResult]Opts out of the Arc result-processing path

Query result

Ordinary data

Interceptors

Stream wrapper

Streaming emission

HTTP snapshot: current exception

Client

This type example reuses the shared AccountId concept; the host discovers IInterceptReadModel<T> implementations and resolves constructor dependencies from the supplied service provider. This example enriches public display data, rather than using masking as access control.

using System.Globalization;
using System.Threading.Tasks;
using Cratis.Arc.Queries;
namespace Banking.Accounts;
public record AccountSummary(AccountId Id, decimal Balance, string FormattedBalance);
public class FormatAccountBalance : IInterceptReadModel<AccountSummary>
{
public Task<AccountSummary> Intercept(AccountSummary readModel) =>
Task.FromResult(readModel with
{
FormattedBalance = readModel.Balance.ToString("C", CultureInfo.GetCultureInfo("en-US"))
});
}

The returned instance is served to the caller. Prefer a with copy over mutating a record that another subscriber may share. An interceptor is bound to its exact model type; a DTO with the same fields is not automatically intercepted.

Interceptors run in discovery order. Avoid making security depend on an undocumented ordering between implementations. Collections are processed item by item; streaming paths process each emitted model before delivery.

The pipeline skips stream wrappers because a wrapper is not a model instance. The supported streaming transports intercept emissions instead; the HTTP snapshot path currently has no corresponding interception step. Test ordinary GET, observable snapshot GET, direct SSE/WebSocket, and hub delivery separately for any transformation the application depends on.

Continue with observable queries for lifetime rules and emission guards for per-emission access decisions.