Implicit Event Store Subscriptions
Chronicle supports implicit subscriptions — automatic routing of events from a source event store’s outbox to the correct inbox sequence, driven entirely by [EventStore] attributes on event types.
Implicit subscriptions are ideal when:
- Event types are published as a NuGet package by the source service
- You want zero configuration — just reference the package and observe the events
- The consuming service doesn’t need fine-grained control over which event types to forward
How Implicit Subscriptions Work
Section titled “How Implicit Subscriptions Work”When every event type handled by an observer shares the same [EventStore] attribute, Chronicle infers the source event store name and automatically routes the observer to the corresponding inbox sequence. A kernel-side subscription is created and persisted automatically the first time an observer targets those events.
The [EventStore] Attribute
Section titled “The [EventStore] Attribute”Apply [EventStore] to any event type that originates from a foreign event store:
[EventType][EventStore("fulfillment-service")]public record ShipmentDispatched(Guid OrderId, string TrackingNumber);
[EventType][EventStore("fulfillment-service")]public record ShipmentFailed(Guid OrderId, string Reason);The string argument is the name of the source event store — the same name you would use if setting up an explicit subscription.
Publishing Event Types in a NuGet Package
Section titled “Publishing Event Types in a NuGet Package”A common pattern is to publish your public event contracts as a NuGet package. Consumers add a reference to your package; the [EventStore] attribute provides all the routing information needed without additional configuration.
A typical public events package looks like this:
using Cratis.Chronicle.Events;
[EventType][EventStore("fulfillment-service")]public record ShipmentDispatched(Guid OrderId, string TrackingNumber);
[EventType][EventStore("fulfillment-service")]public record ShipmentFailed(Guid OrderId, string Reason);Assembly-Level Configuration with [EventStore]
Section titled “Assembly-Level Configuration with [EventStore]”To avoid repeating [EventStore] on every event type, apply the attribute once at the assembly level in your GlobalUsings.cs or AssemblyInfo.cs file. This is the recommended approach for creating reusable event contracts.
The .Contracts Project Pattern
Section titled “The .Contracts Project Pattern”Create a separate, thin project with the naming convention {ServiceName}.Events.Contracts to publish your event types:
FulfillmentService.Events.Contracts/├── AssemblyInfo.cs├── ShipmentDispatched.cs├── ShipmentFailed.cs└── FulfillmentService.Events.Contracts.csprojThis .Contracts project contains only your event type definitions and minimal dependencies. It’s lightweight, fast to build, and safe to share as a NuGet package.
Project File Configuration
Section titled “Project File Configuration”Configure your .Contracts .csproj with appropriate metadata for sharing:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <TargetFramework>net9.0</TargetFramework> <AssemblyName>FulfillmentService.Events.Contracts</AssemblyName> <PackageId>FulfillmentService.Events.Contracts</PackageId> <PackageVersion>1.0.0</PackageVersion> <Title>Fulfillment Service Event Contracts</Title> <Description>Shared event types published by FulfillmentService</Description> <Authors>Your Team</Authors> <RepositoryUrl>https://github.com/yourorg/FulfillmentService</RepositoryUrl> <RepositoryType>git</RepositoryType> </PropertyGroup>
<ItemGroup> <PackageReference Include="Cratis.Chronicle" Version="1.0.0" /> </ItemGroup>
</Project>Assembly-Level Attribute Definition
Section titled “Assembly-Level Attribute Definition”In your .Contracts project, create an AssemblyInfo.cs file or use GlobalUsings.cs to apply [EventStore] once:
Option 1: Using AssemblyInfo.cs
Section titled “Option 1: Using AssemblyInfo.cs”using Cratis.Chronicle.Events;
[assembly: EventStore("fulfillment-service")]Option 2: Using GlobalUsings.cs (Recommended)
Section titled “Option 2: Using GlobalUsings.cs (Recommended)”global using System;global using System.Collections.Generic;global using Cratis.Chronicle.Events;
[assembly: EventStore("fulfillment-service")]Option 3: Using <AssemblyAttribute> in the .csproj (No C# File Needed)
Section titled “Option 3: Using <AssemblyAttribute> in the .csproj (No C# File Needed)”You can apply the assembly-level [EventStore] attribute directly from your .csproj, without creating any C# file. Add an <AssemblyAttribute> item to your project:
<ItemGroup> <AssemblyAttribute Include="Cratis.Chronicle.Events.EventStoreAttribute"> <_Parameter1>fulfillment-service</_Parameter1> </AssemblyAttribute></ItemGroup>MSBuild will emit [assembly: Cratis.Chronicle.Events.EventStoreAttribute("fulfillment-service")] in the auto-generated AssemblyInfo.cs file — no manual C# file required.
Note: This approach requires
<GenerateAssemblyInfo>to betrue(the default). If your project sets<GenerateAssemblyInfo>false</GenerateAssemblyInfo>, the<AssemblyAttribute>item will have no effect since it relies on the auto-generated file.
Event Type Definitions
Section titled “Event Type Definitions”Now your event types only need [EventType]:
namespace FulfillmentService.Events;
[EventType]public record ShipmentDispatched(Guid OrderId, string TrackingNumber);
[EventType]public record ShipmentFailed(Guid OrderId, string Reason);All events in this assembly are automatically associated with the fulfillment-service event store.
Publishing and Consuming
Section titled “Publishing and Consuming”In your FulfillmentService (source):
- Reference your own
.Contractsproject - Event types are published to
fulfillment-serviceoutbox
In consuming services:
- Add a NuGet reference to
FulfillmentService.Events.Contracts - Create observers (reactors, projections, reducers) that handle the event types
- Events are automatically routed to the
inbox-fulfillment-servicesequence - No explicit subscription or configuration needed
Example consumer setup:
var builder = WebApplication.CreateBuilder(args);
builder.AddCratisChronicle(options => options.EventStore = "order-service");
// Just reference the events from the NuGet packageusing var app = builder.Build();// Reactors/projections that observe FulfillmentService.Events types// are automatically routed to the inboxAutomatic Inbox Routing for Observers
Section titled “Automatic Inbox Routing for Observers”Automatic inbox routing is documented per observer type:
- Reactors: Subscribe Reactors to External Event Stores
- Reducers: Subscribe Reducers to External Event Stores
Projections
Section titled “Projections”public class FulfillmentProjection : IProjectionFor<FulfillmentReadModel>{ public void Define(IProjectionBuilderFor<FulfillmentReadModel> builder) => builder .From<ShipmentDispatched>(_ => _ .Set(m => m.TrackingNumber).To(e => e.TrackingNumber) .Set(m => m.Status).To(e => "Dispatched")) .From<ShipmentFailed>(_ => _ .Set(m => m.Status).To(e => "Failed"));}Subscription Lifecycle
Section titled “Subscription Lifecycle”The kernel automatically manages the implicit subscription:
- Creation — When the first observer targeting the event type is registered, the kernel creates a persistent subscription to the source outbox.
- Persistence — The subscription is stored and persists across kernel restarts.
- Forwarding — Events appended to the source outbox are continuously forwarded to the inbox.
- Cleanup — If no observers are targeting events from a source, the subscription remains active but idle. Subscriptions are not automatically removed.
Mixing Event Stores Is Not Allowed
Section titled “Mixing Event Stores Is Not Allowed”An observer may only handle event types from a single event store. Mixing types annotated with different [EventStore] values on the same observer throws MultipleEventStoresDefined at startup:
// ❌ This will throw MultipleEventStoresDefinedpublic class InvalidReactor : IReactor{ // ShipmentDispatched has [EventStore("fulfillment-service")] public Task Handle(ShipmentDispatched @event) => Task.CompletedTask;
// OrderPlaced has [EventStore("ordering-service")] public Task Handle(OrderPlaced @event) => Task.CompletedTask;}To observe events from multiple sources, create a separate observer for each source event store:
public class FulfillmentReactor : IReactor{ public Task ShipmentDispatched(ShipmentDispatched @event, EventContext context) => HandleFulfillmentAsync(@event);
Task HandleFulfillmentAsync(ShipmentDispatched @event) => Task.CompletedTask;}
public class OrderingReactor : IReactor{ public Task OrderPlaced(OrderPlaced @event, EventContext context) => HandleOrderAsync(@event);
Task HandleOrderAsync(OrderPlaced @event) => Task.CompletedTask;}Schema Registration and Metadata
Section titled “Schema Registration and Metadata”When event types are registered with the kernel, the source event store name is included in the registration metadata. This allows the kernel to associate inbox events with their originating store and display that information in the Workbench.
When to Use Implicit Subscriptions
Section titled “When to Use Implicit Subscriptions”Use implicit subscriptions when:
- Event types are published in a shared NuGet package
- All events from a source should be forwarded (or at least all events you observe on)
- You want zero configuration overhead
- The package owner controls the event types and their
[EventStore]attributes
Use explicit subscriptions when:
- You need fine-grained control over which event types are forwarded
- You want to manually manage subscription lifecycle and cleanup
- Events are not in a shared NuGet package, or you need dynamic subscription configuration
- You need to subscribe to only a subset of events from a source
See Also
Section titled “See Also”- Explicit Event Store Subscriptions — manual subscription using the Subscribe API
- Outbox and Inbox — conceptual explanation of how events flow between stores
- Reactors — processing inbox events with reactors
- Projections — building read models from inbox events