Use PostgreSQL with jsonb event data

If you store the EF Core event-data column as jsonb (recommended on Postgres), the default event-property filter — a substring match on the text representation — silently stops matching. Postgres normalises jsonb to a canonical form with a space after every colon, so LIKE '%"Name":"Alice"%' never finds the row that contains {"Name": "Alice"}.

The Memoria.EventSourcing.Store.EntityFrameworkCore.Npgsql package replaces the default filter with one that uses the Postgres @> JSON-containment operator. It works regardless of whitespace formatting and is GIN-indexable.

For the registration call, see Configuration: Entity Framework Core.

Why this matters

Stored as Serialised form returned by Postgres
text {"Name":"Alice"} (as written)
jsonb {"Name": "Alice"} (canonical)

Without the Npgsql filter, eventPropertyFilter = { "Name": "Alice" } queries silently miss every row stored as jsonb. There is no error — just an empty result set. This is the failure mode this package exists to fix.

Map the column as jsonb

The new filter requires the events.Data column to be a JSON column. Configure this in your DbContext’s OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<EventEntity>()
        .Property(e => e.Data)
        .HasColumnType("jsonb");
}

Index for fast property filters

Add a GIN index when streams grow large and property filtering becomes a hot path:

modelBuilder.Entity<EventEntity>()
    .HasIndex(e => e.Data)
    .HasMethod("gin");

How filters translate

For eventPropertyFilter = { "OrderId": "abc-123" }, the filter generates SQL roughly equivalent to:

WHERE "Data" @> '{"OrderId":"abc-123"}'::jsonb

Property names and values are JSON-encoded before being passed to Postgres, so quotes and backslashes are safe.

Plugging in a custom filter

IEventDataFilter is a public abstraction in Memoria.EventSourcing.Store.EntityFrameworkCore.Filtering. Provide your own implementation and register it before calling AddMemoriaEntityFrameworkCore<TDbContext>():

services.AddSingleton<IEventDataFilter, MyFilter>();
services.AddMemoriaEntityFrameworkCore<ApplicationDbContext>();

The default registration uses TryAdd, so a pre-registered filter is honoured.

The dynamic consistency boundary store

There is no Memoria.EventSourcing.Dcb.Store.EntityFrameworkCore.Npgsql package, and there will not be one. Everything above is about the event-property filter, and the DCB store does not have one: tags select events directly, which is the job property filtering was doing.

jsonb is still worth having there, for the same reason it is worth having here — your own queries and GIN indexes over the payload. It needs no package, only the same override in your own context:

public class BoxOfficeDbContext(
    DbContextOptions<DcbDbContext> options,
    TimeProvider timeProvider,
    IHttpContextAccessor httpContextAccessor)
    : DcbDbContext(options, timeProvider, httpContextAccessor)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<DcbEventEntity>().Property(@event => @event.Data).HasColumnType("jsonb");
        modelBuilder.Entity<DcbSnapshotEntity>().Property(snapshot => snapshot.Data).HasColumnType("jsonb");
    }
}

A container test runs exactly this context against PostgreSQL and appends, folds and snapshots through it, so the override is verified rather than merely suggested.

Note that jsonb normalises what it stores: key order is not preserved and whitespace is rewritten. Events still deserialise, because JSON objects are unordered, but the stored bytes are no longer the bytes the serializer produced. If you need an event payload preserved verbatim, leave the column as text.