Files
register-referentie/services/projection-api/Projection.ReadModel/ProjectionDbContext.cs
T
not 142ed454aa test(event-subscriber): the projection is sourced from register records (refs #153)
Ports, schema and failing tests for the subscriber half of S-19b-2, ahead of the
implementation.

The subscriber now listens on the `objecten` kanaal instead of `zaken`. An Objecten
notification carries no record data — only the object URL — so the record is read back
through the ACL (§8.1), and the zaak-shaped surface goes away: IsZaakCreated /
IsZaakStatusSet / ZaakUrl / ZaakId and ToEntry's `Resource == "status"` mapping are replaced
by IsRegisterRecordWritten + ObjectUrl.

The notification log now holds the projected row itself (register id, status, reference),
so a rebuild is a replay with no mapping rules and no upstream reads. The migration drops
the old columns rather than renaming them — EF scaffolded renames that would have carried
ZGW values into columns meaning something else — and empties both tables, since a
pre-slice row is neither reprojectable nor re-derivable from the new source.

Red: HandleAsync recognises a register write but does not yet read or project it, so the
seven projection assertions fail on an empty store.
2026-08-28 12:32:15 +02:00

76 lines
3.4 KiB
C#

using Microsoft.EntityFrameworkCore;
namespace Projection.ReadModel;
/// <summary>
/// The read projection's persistence. This single store is the system's rebuildable read model
/// (PRD §8.4): the Event Subscriber writes to it (projector) and the projection-api reads it
/// (query). Both processes are the one "Read Projection" bounded context and share this schema —
/// not a cross-domain DB share (ADR-0008 reconciles this with CLAUDE.md §8.5).
/// </summary>
public sealed class ProjectionDbContext(DbContextOptions<ProjectionDbContext> options) : DbContext(options)
{
/// <summary>The public-facing register rows (public-safe filtering is tightened in S-09).</summary>
public DbSet<RegisterEntryRow> RegisterEntries => Set<RegisterEntryRow>();
/// <summary>The Event Subscriber's durable log of accepted notifications — idempotency guard and rebuild source.</summary>
public DbSet<ProcessedNotificationRow> ProcessedNotifications => Set<ProcessedNotificationRow>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<RegisterEntryRow>(e =>
{
e.ToTable("register_projection");
e.HasKey(r => r.Id);
e.Property(r => r.Id).HasColumnName("id");
e.Property(r => r.Status).HasColumnName("status").IsRequired();
e.Property(r => r.Reference).HasColumnName("reference");
e.Property(r => r.Bsn).HasColumnName("bsn");
e.Property(r => r.NaamPlaceholder).HasColumnName("naam_placeholder");
});
modelBuilder.Entity<ProcessedNotificationRow>(e =>
{
e.ToTable("processed_notifications");
e.HasKey(r => r.Key);
e.Property(r => r.Key).HasColumnName("key");
e.Property(r => r.RegisterId).HasColumnName("register_id").IsRequired();
e.Property(r => r.Status).HasColumnName("status").IsRequired();
e.Property(r => r.Reference).HasColumnName("reference");
e.Property(r => r.ReceivedAt).HasColumnName("received_at");
});
}
}
/// <summary>A register projection row. <c>Bsn</c>/<c>NaamPlaceholder</c> are deferred (ADR-0008).</summary>
public sealed class RegisterEntryRow
{
public required string Id { get; set; }
public required string Status { get; set; }
/// <summary>The citizen-facing reference (the zaak's identificatie) — matches the submit confirmation (#78).</summary>
public string? Reference { get; set; }
public string? Bsn { get; set; }
public string? NaamPlaceholder { get; set; }
}
/// <summary>An accepted notification, retained so the projection can be rebuilt without reading
/// Objecten or ZGW (§8.1, §8.4). Since S-19b-2 it holds the projected row itself — the register
/// record's id, status and reference — so a rebuild is a replay with no mapping rules (ADR-0030).</summary>
public sealed class ProcessedNotificationRow
{
public required string Key { get; set; }
/// <summary>The registration this record is for (the zaak id) — the projection row's key.</summary>
public required string RegisterId { get; set; }
/// <summary>The register status the record carried (INGEDIEND / INGESCHREVEN).</summary>
public required string Status { get; set; }
/// <summary>The citizen-facing reference the record carried — matches the submit confirmation (#78).</summary>
public string? Reference { get; set; }
public DateTimeOffset ReceivedAt { get; set; }
}