| name | cross-cutting-properties |
| description | Attach audit/correlation metadata to every appended Cratis event without polluting event types — via ICanProvideAdditionalEventInformation, plus event tags and the built-in EventContext fields. Use for correlation IDs, tenant/actor context, and other cross-cutting concerns that should travel with events but are not domain payload. |
Cross-Cutting Event Properties
Some information must travel with every event — correlation/causation ids, the authenticated actor, tenant context — but adding it as a property to every [EventType] would pollute the schemas. Chronicle solves this with ICanProvideAdditionalEventInformation (metadata-envelope providers) and event tags.
First, check the built-in EventContext
Before implementing a provider, see whether what you need is already there (available in reactors/reducers):
| Property | Description |
|---|
EventSourceId | the event source appended to |
SequenceNumber | ordinal within the sequence |
Occurred | wall-clock time at append |
CorrelationId | propagated from the request (or generated) |
Causation | upstream event references |
CausedBy | actor identity (from the configured identity provider) |
You don't need a custom provider for actor identity alone — CausedBy already captures it. Reach for a provider only for additional fields.
ICanProvideAdditionalEventInformation
using System.Text.Json.Nodes;
public class TenantMetadataProvider(IHttpContextAccessor http) : ICanProvideAdditionalEventInformation
{
public Task ProvideFor(JsonObject @event)
{
@event["tenantId"] = http.HttpContext?.Request.Headers["x-tenant-id"].FirstOrDefault() ?? "Default";
return Task.CompletedTask;
}
}
Chronicle discovers providers from DI — register as scoped/singleton. Multiple providers merge; key collisions = last-registered wins. Place the class at a cross-cutting infrastructure location, not inside a slice. The properties land in the event's metadata envelope, not the event record — they are not surfaced in EventContext on reactive handlers. If a value must influence a projection/reducer, it belongs on the event type (or a dedicated audit event), not in cross-cutting metadata.
Tags vs filtering — easy to confuse
| Attribute | Where | What it does |
|---|
[Tag("analytics", "user-action")] | on an [EventType] | merges static tags into every occurrence at append time; available in EventContext.Tags. Does not filter. |
[FilterEventsByTag("tag")] | on a reactor/reducer class | restricts which events reach the handler (multiple = OR; combined with [EventSourceType]/[EventStreamType] = AND). |
[Tag] / [Tags] | on a reactor/reducer class | admin-UI label only — no effect on delivery. |
Tags are also used for concurrency scoping. To filter by tag you need [FilterEventsByTag], not [Tag].
Common pitfalls
| Pitfall | Why |
|---|
Adding correlation/tenant id to every [EventType] | pollutes schemas — use a provider |
| Injecting scoped services into a singleton provider | register the provider scoped, or use IServiceScopeFactory |
Expecting envelope properties to appear in EventContext on handlers | they don't — they're metadata only |
| Using a provider for data a projection needs | if the projection needs it, it belongs on the event type |
Quality gate
See also
vertical-slices.md — event types, EventContext in reactors/reducers.
multi-tenancy — namespace-per-tenant isolation (a different mechanism from a tenant tag).