Skip to content

Seeding with C Sharp

This page shows how to seed events using the Chronicle .NET client. Choose the scope before defining your seeds: global seeds reach every namespace in the event store, including tenants created later. Use ForNamespace(...) for development or sample data that belongs only in a named namespace.

Use record types for event definitions in examples:

using Cratis.Chronicle.Events;
[EventType]
public record EvtSeedingUserRegistered(string Email, string DisplayName);
[EventType]
public record EvtSeedingEmailVerified(string Email);
[EventType]
public record EvtSeedingProfileUpdated(string DisplayName);
[EventType]
public record EvtSeedingOrderPlaced(string UserId, decimal Amount);

View C# snippet source on GitHub

Implement ICanSeedEvents and use IEventSeedingBuilder to define events to append:

using Cratis.Chronicle.Seeding;
public sealed class EvtSeedingUserSeeding : ICanSeedEvents
{
public void Seed(IEventSeedingBuilder builder)
{
builder
.For<EvtSeedingUserRegistered>("user-123", [
new("john@example.com", "John")
])
.ForEventSource("user-456", [
new EvtSeedingUserRegistered("jane@example.com", "Jane"),
new EvtSeedingEmailVerified("jane@example.com")
]);
}
}

View C# snippet source on GitHub

using Cratis.Chronicle.Seeding;
public sealed class EvtSeedingMultipleSameTypeSeeding : ICanSeedEvents
{
public void Seed(IEventSeedingBuilder builder)
{
builder.For<EvtSeedingUserRegistered>("user-123", [
new("john@example.com", "John"),
new("jane@example.com", "Jane")
]);
}
}

View C# snippet source on GitHub

using Cratis.Chronicle.Seeding;
public sealed class EvtSeedingMixedTypesSeeding : ICanSeedEvents
{
public void Seed(IEventSeedingBuilder builder)
{
builder.ForEventSource("user-123", [
new EvtSeedingUserRegistered("john@example.com", "John"),
new EvtSeedingEmailVerified("john@example.com"),
new EvtSeedingProfileUpdated("John Doe")
]);
}
}

View C# snippet source on GitHub

Pass an event stream type and identifier to For or ForEventSource to route seeds onto a specific stream. The optional event source type defaults to Default. These overloads also work on the builder returned by ForNamespace.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Seeding;
public sealed class EvtStreamRoutedSeeder : ICanSeedEvents
{
public void Seed(IEventSeedingBuilder builder)
{
builder.For("company-123", "Offices", "office-1", [
new EvtStreamRoutedOfficeOpened("Oslo")
], "Company");
builder.ForNamespace("tenant-a").ForEventSource(
"company-123", "Offices", "office-2", [
new EvtStreamRoutedOfficeOpened("Bergen")
], "Company");
}
}
[EventType]
public record EvtStreamRoutedOfficeOpened(string City);

View C# snippet source on GitHub

Use ForEvents for per-event routing. Its EventForEventSourceId entries can specify an EventSource definition type and an EventStream name; Chronicle resolves their routing from the discovered definition and rejects contradictory explicit types. Entry tags are combined with static event-type tags. Set EventSource whenever you use a typed EventStream: without EventSource, the stream name is ignored and only the explicit EventStreamType and EventStreamId determine stream routing. ForEvents uses the wrapper’s routing and tags, not its append-specific occurred time, subject, causation or structured named tags.

Routing is part of seed identity: identical payloads on different streams are separate seed entries. Omitting routing preserves the defaults used by previously stored seed definitions, so adopting this API does not append legacy seeds again.

Non-default routing requires a kernel that confirms seeding routing support. Against an older kernel, Register() throws EventSeedingRoutingNotSupported before sending any seed entries, including unrouted entries in the same buffer. Upgrade the kernel before retrying that buffer. A buffer containing only default routing still registers against older kernels.

If seed data should only run in development, use conditional compilation or runtime configuration:

using Cratis.Chronicle;
using Cratis.Chronicle.Seeding;
#if DEBUG
public sealed class EvtSeedingDevelopmentSeeding : ICanSeedEvents
{
public void Seed(IEventSeedingBuilder builder)
{
builder.ForNamespace(EventStoreNamespaceName.Default).For<EvtSeedingUserRegistered>("dev-user-1", [
new("dev@example.com", "Dev User")
]);
}
}
#endif

View C# snippet source on GitHub

Chronicle does not distinguish between development and production seed data. Decide when to seed based on build configuration or runtime settings, and scope development or sample data with ForNamespace(EventStoreNamespaceName.Default) rather than the global builder. A build flag prevents declaring those seeds in that build. With an upgraded .NET client and kernel, startup replaces the program’s stored declaration, even when its global set is empty.

For larger solutions, split seeders by domain or feature:

using Cratis.Chronicle.Seeding;
public sealed class EvtSeedingUserFeatureSeeding : ICanSeedEvents
{
public void Seed(IEventSeedingBuilder builder)
{
builder.For<EvtSeedingUserRegistered>("test-user-1", [
new("test1@example.com", "Test User 1")
]);
}
}
public sealed class EvtSeedingOrderFeatureSeeding : ICanSeedEvents
{
public void Seed(IEventSeedingBuilder builder)
{
builder.For<EvtSeedingOrderPlaced>("test-order-1", [
new("test-user-1", 100.00m)
]);
}
}

View C# snippet source on GitHub

For and ForEventSource on the top-level builder define global seeds. The server stores them per event store, applies them to every existing namespace, and applies them once to each namespace created later. Opening a store for a new namespace creates that namespace. The kernel then applies stored global seeds asynchronously, usually before your application’s first write, but there is no ordering guarantee. Do not rely on that timing; put development or sample data in ForNamespace(...) rather than the global builder.

To target a specific namespace, use ForNamespace to get a scoped builder:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Seeding;
[EventType]
public record EvtSeedingProductCreated(string Name, decimal Price);
[EventType]
public record EvtSeedingOrganizationCreated(string Name);
[EventType]
public record EvtSeedingBillingSetUp(string BillingEmail);
public sealed class EvtSeedingTenantSeeding : ICanSeedEvents
{
public void Seed(IEventSeedingBuilder builder)
{
// Global seed data — applied to every namespace
builder.For<EvtSeedingProductCreated>("product-1", [
new("Laptop", 1299.00m)
]);
// Namespace-scoped seed data — applied only to the "acme" namespace
builder.ForNamespace("acme")
.For<EvtSeedingUserRegistered>("user-1", [
new("admin@acme.com", "Acme Admin")
]);
// A second namespace with different seed data
builder.ForNamespace("contoso")
.For<EvtSeedingUserRegistered>("user-1", [
new("admin@contoso.com", "Contoso Admin")
])
.ForEventSource("org-1", [
new EvtSeedingOrganizationCreated("Contoso"),
new EvtSeedingBillingSetUp("contoso@billing.com")
]);
}
}

View C# snippet source on GitHub

The scoped builder supports the same For<TEvent> and ForEventSource methods as the global builder. Each namespace receives only its own scoped events in addition to any global events. ForNamespace(EventStoreNamespaceName.Default) never sends those entries to another namespace.

The .NET client declares the complete discovered global set for each program. A declaration replaces that program’s previous declaration, including when the new set is empty. Removing an entry from ICanSeedEvents stops it reaching namespaces created afterwards; events already appended to existing namespaces are never retracted. Explicitly queued globals (eventStore.Seeding.For(...) or an independent CreateEventSeeding() buffer) remain append-only and are not removed by a later declaration, even if an identical seed was already declared.

If two programs declare the same seed, a namespace receives it once, not once per program. For repeated identical seeds, Chronicle uses the largest occurrence count in any program’s declaration or explicit additions, not the sum across programs. Seed identity includes the event source, event type, content, tags, and routing. Clearing one program’s declaration does not remove a seed another program still declares or has added explicitly. Existing namespaces receive only missing occurrences; no events are retracted.

If any discovered seeder cannot activate or its callback fails, the client logs the failing seeder types and sends no discovered global declaration during that registration. The previous declaration stays in place; the partial discovered set is not stored as explicit additions either. Namespace-scoped entries and manually queued entries still register. The next registration retries discovery, and a complete successful discovery declares the global set as usual.

Ownership uses ChronicleOptions.ProgramIdentifier, falling back to the entry assembly name when the option is still "[N/A]". Set a distinct, stable ProgramIdentifier for each program when several programs share a store, so one program does not replace another’s declaration. The first authoritative registration after upgrading also clears legacy stored global seeds whose owner was not recorded. Other programs’ owned declarations and explicit additions remain stored.

This requires an upgraded client and kernel. Old clients keep append-only behavior. A new client still works against an older kernel, but that kernel ignores declarations and retains its stored globals.

  1. Upgrade both the .NET client and the kernel.
  2. Remove the development seeder, or scope its entries with ForNamespace(EventStoreNamespaceName.Default).
  3. Start the application once so it declares its remaining global set, even if empty.

New namespaces no longer receive the removed declarations or legacy global entries. Existing namespaces keep all events already appended. This does not remove explicitly queued globals.

Register corrected seed definitions without reconnecting

Section titled “Register corrected seed definitions without reconnecting”

When registration fails, Chronicle retains the attempted entries in the event store’s default Seeding buffer. That makes retrying an unchanged idempotent batch safe after a transient failure. If the definitions themselves were wrong, create an independent empty buffer so the corrected set is not combined with the retained set:

using Cratis.Chronicle;
using Cratis.Chronicle.Seeding;
public static class EvtSeedingCorrection
{
public static async Task Register(IEventStore eventStore)
{
var correctedSeeding = eventStore.CreateEventSeeding();
correctedSeeding.For<EvtSeedingUserRegistered>("user-123", [
new("john@example.com", "John Doe")
]);
await correctedSeeding.Register();
}
}

View C# snippet source on GitHub

The new buffer uses the event store’s existing connection, event types, serializer, and client configuration. A failure from its Register() call still propagates normally.

  • Seeders are automatically discovered at application startup.
  • Each client sends its discovered global seed set once per event store name per connection lifetime, not once per namespace store it opens. After a disconnect and reconnect, it sends the global set again.
  • Each namespace store still sends its ForNamespace(...) entries during registration.
  • Every entry is applied once per namespace, and seeding is idempotent across restarts: entries that have already been appended are skipped on the next run.
  • An entry that your seeder yields twice is two events, not one. Two events of the same type, on the same event source, carrying the same payload are two facts, and both are appended.
  • Events are appended in batches of 100. A batch is validated as a whole before anything is written, so a single entry that violates a constraint means none of its batch is appended. The server reports the rejection at Error with the violations, leaves those entries unseeded, and fails registration. The remote client surfaces that failure through its normal transport error. Retry the unchanged buffer for a transient failure; after correcting the definitions, restart the process or use CreateEventSeeding() to offer a fresh set.
  • Keep seed data minimal and deterministic.
  • Use clear event source IDs to make debugging easier.
  • Group seeders by scenario; dropping a discovered global seed stops future application, not events already appended.
  • Use build flags or runtime settings to prevent seeding in production.
  • Use ForNamespace when seed data is tenant-specific or environment-specific to avoid polluting other namespaces.