---
title: CRUD, EF Core, and Chronicle
description: If you build apps with EF Core and think in tables, rows, and SaveChanges, here's how those ideas translate to Chronicle.
---

import { Tabs, TabItem } from '@astrojs/starlight/components';


If your instinct is to add a table, map an entity, and call `SaveChanges()`, you already have useful muscle memory. This page maps the CRUD/EF Core model onto Chronicle so the differences are explicit.

## The mental shift in one sentence

In CRUD you **store the current state and overwrite it**; in Chronicle you **store what happened and derive the current state** from those facts. The current state still exists — it's a [read model](/chronicle/read-models/) — you just build it from events instead of editing it in place.

## How the pieces map

| You know (CRUD / EF Core) | In Chronicle |
|---|---|
| A table / entity | An [event source](/chronicle/concepts/event-source/) and its stream of events |
| `INSERT` a row | Append a "created" [event](/chronicle/concepts/event/) |
| `UPDATE` a column | Append an event describing *what changed* (e.g. `AddressChanged`) |
| `DELETE` a row | Append a "removed/closed" event — history is never erased |
| `DbContext.SaveChanges()` | `EventLog.Append(...)` |
| `SELECT` / LINQ query | A query over a [read model](/chronicle/read-models/) built by a [projection](/chronicle/concepts/projection/) |
| A computed/denormalized view | A purpose-built read model — make as many as you need |
| `ALTER TABLE` / EF migration | [Event type migration](/chronicle/concepts/event-type-migrations/) + replay the projection |
| Optimistic concurrency token | Constraints and the event stream's ordering |

## What stays the same

- You still use C# records and dependency injection.
- You still query data and render it — the read side looks like querying a collection.
- You can still keep a relational/document database for the genuinely-CRUD parts of your app; Chronicle doesn't demand all-or-nothing.

## What changes (and why)

- **You model verbs, not just nouns.** Instead of one mutable `Customer` row, you record `CustomerRegistered`, `AddressChanged`, `AccountClosed`. Each is an immutable fact. This is the part that feels new — and it's where the value (audit, history, replay) comes from.
- **Reads are eventually consistent — by default.** A projection materializes the read model *after* the event is appended, so a stored read immediately after a write may lag by a moment. Usually fine; occasionally something to design around. When read-after-write matters, Chronicle can also compute a read model on demand with strong consistency — see [Read model consistency](/chronicle/read-models/consistency/).
- **You don't write update statements.** A projection *declares* how events map onto a read model; Chronicle keeps it current. No `UPDATE`, no merge logic.
- **You don't delete history.** "Delete" becomes an event. For real erasure obligations (GDPR), see [Compliance](/chronicle/compliance/).

## Side by side

CRUD: update a row, then read it back. This is illustrative EF Core pseudocode, not a compiled example:

```text
customer.Address = newAddress;
await db.SaveChangesAsync();
var current = await db.Customers.FindAsync(id);
```

Chronicle: the same change is a fact you append and a read side you declare. First, define the verbs as event types:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;

[EventType]
public record CrudComparisonCustomerRegistered(string Name, string Address);

[EventType]
public record CrudComparisonAddressChanged(string Address);
```

</TabItem>
<TabItem label="TypeScript">

```typescript
import { eventType } from '@cratis/chronicle';

@eventType()
class CrudComparisonCustomerRegistered {
    constructor(readonly name: string, readonly address: string) {}
}

@eventType()
class CrudComparisonAddressChanged {
    constructor(readonly address: string) {}
}
```

</TabItem>
</Tabs>

Then declare the read side. This is the [projection](/chronicle/concepts/projection/) — and notice it is *not* a 1:1 copy of the CRUD `Customer` entity. It's shaped for the screen that reads it, and it answers something the overwritten row never could:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;

[FromEvent<CrudComparisonCustomerRegistered>]
[FromEvent<CrudComparisonAddressChanged>]
public record CrudComparisonCustomerCard(
    [Key] Guid Id,
    string Name,
    string Address,
    [Count<CrudComparisonAddressChanged>] int TimesRelocated);
```

</TabItem>
<TabItem label="TypeScript">

```typescript
import { count, fromEvent, Guid, readModel } from '@cratis/chronicle';

@readModel()
@fromEvent(CrudComparisonCustomerRegistered)
@fromEvent(CrudComparisonAddressChanged)
class CrudComparisonCustomerCard {
    id: Guid = Guid.empty;
    name = '';
    address = '';

    @count(CrudComparisonAddressChanged)
    timesRelocated = 0;
}
```

</TabItem>
</Tabs>

`[FromEvent<T>]` (`@fromEvent` in TypeScript) maps event properties onto the record by name — the name arrives with `CustomerRegistered`, and the address is kept current by every `AddressChanged`. The counter tracks the moves. There is no `UPDATE` statement anywhere: Chronicle derives the card from the facts.

Now the write and the read-back:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;

public class CrudComparisonCustomerAddressUpdater(IEventStore eventStore)
{
    public async Task<CrudComparisonCustomerCard> ChangeAddress(EventSourceId customerId, string newAddress)
    {
        await eventStore.EventLog.Append(customerId, new CrudComparisonAddressChanged(newAddress));
        return await eventStore.ReadModels.GetInstanceById<CrudComparisonCustomerCard>(customerId);
    }
}
```

</TabItem>
<TabItem label="TypeScript">

```typescript
import { IEventStore } from '@cratis/chronicle';

class CrudComparisonCustomerAddressUpdater {
    constructor(private readonly store: IEventStore) {}

    async changeAddress(customerId: string, newAddress: string): Promise<CrudComparisonCustomerCard> {
        await this.store.eventLog.append(customerId, new CrudComparisonAddressChanged(newAddress));
        return this.store.readModels.getInstanceById(CrudComparisonCustomerCard, customerId);
    }
}
```

</TabItem>
</Tabs>

`GetInstanceById` computes the card from its events on demand, so this read already reflects the append above — `TimesRelocated` included, a fact the CRUD row lost the moment `SaveChanges` ran.

## Not sure it's worth it?

Read [When to use event sourcing](/chronicle/concepts/when-to-use-event-sourcing/) for an honest take — there are domains where plain CRUD is the right answer, and that's fine.

## Next

- [Get started](/chronicle/get-started/) — scaffold and run in minutes.
- [Tutorial](/chronicle/tutorial/) — build a small system from events up.
