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.
Supported paths and exceptions
Section titled “Supported paths and exceptions”| Path | Interception |
|---|---|
| Ordinary model-bound query data | Applied after rendering |
| Ordinary Arc-wrapped MVC GET data | Applied after rendering |
| Supported direct WebSocket/SSE streams and multiplexed hub emissions | Applied per emission |
Observable HTTP snapshot, including waitForFirstResult=true | Not currently applied by ObservableQueryHttp |
MVC [AspNetResult] | Opts out of the Arc result-processing path |
Implement an interceptor
Section titled “Implement an interceptor”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.
Multiple interceptors and streams
Section titled “Multiple interceptors and streams”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.