PgNotify.Listener
1.1.0
dotnet add package PgNotify.Listener --version 1.1.0
NuGet\Install-Package PgNotify.Listener -Version 1.1.0
<PackageReference Include="PgNotify.Listener" Version="1.1.0" />
<PackageVersion Include="PgNotify.Listener" Version="1.1.0" />
<PackageReference Include="PgNotify.Listener" />
paket add PgNotify.Listener --version 1.1.0
#r "nuget: PgNotify.Listener, 1.1.0"
#:package PgNotify.Listener@1.1.0
#addin nuget:?package=PgNotify.Listener&version=1.1.0
#tool nuget:?package=PgNotify.Listener&version=1.1.0
PgNotify
PostgreSQL LISTEN/NOTIFY, integrated into EF Core as a first-class feature: configure it like
any other EF Core concern, get trigger DDL generated by migrations automatically, and consume
notifications at runtime through handlers keyed on your entities — no hand-written SQL, no manual
LISTEN management, and no channel name written twice.
[NotifyChanges]
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
public class UserProjection : IDatabaseUpdatedHandler<User>
{
public Task HandleAsync(NotificationEnvelope envelope, CancellationToken cancellationToken)
{
// envelope.Keys["id"] — the row that changed; envelope.Changed — which columns did
return Task.CompletedTask;
}
}
builder.Services.AddDbContext<AppDbContext>(o => o
.UseNpgsql(connectionString)
.UseNpgsqlNotifications());
builder.Services.AddPostgresNotifications(o =>
{
o.AddHandlersFromAssembly(typeof(Program).Assembly);
// Channels and connection string both come from AppDbContext, because it opted in above.
o.AddNotificationMappingFromDbContexts();
});
Handlers are keyed on the entity type, not on an event type: IDatabaseInsertedHandler<T>,
IDatabaseUpdatedHandler<T>, IDatabaseDeletedHandler<T> for one operation,
IDatabaseNotificationHandler<T> for all three, and the non-generic
IDatabaseNotificationHandler for every entity — which is what a listener with no CLR entity types
uses, alongside MapChannel("some_channel").
A listener with no DbContext states its channels itself (o.MapChannel<User>("User"),
o.ConnectionString = ...); one that has a DbContext derives both from it and states nothing.
dotnet ef migrations add picks up the [NotifyChanges] configuration automatically and
generates the trigger/trigger-function DDL alongside the usual CREATE TABLE — see
docs/migrations.md for exactly what gets generated.
Why
Most PostgreSQL notification setups mean hand-writing trigger functions, remembering to update
them when columns change, and hand-rolling a LISTEN connection with reconnect logic. This
library treats all of that as EF Core's job: the trigger SQL is derived from the same model you
already declare for your tables, regenerated automatically whenever that configuration changes,
and the runtime side gives you the same DX EF Core migrations/DbSets already give you for
everything else.
Features
- Attribute or fluent configuration —
[NotifyChanges]or.HasDatabaseNotifications(...). Every option exists in both forms and they write identical model state, including the payload default, so moving an entity between them changes nothing about what is generated. - Property filtering —
OnUpdate(x => new { x.Name, x.Email })only fires when those columns actually change (IS DISTINCT FROM, computed in SQL). - Three channel strategies — one channel per entity (default), one shared channel, or
topic-style (
user.created/user.updated/user.deleted) — seeINotificationChannelNamingStrategy. - Two built-in payload shapes —
WithPayload(NotificationPayloadKind.Minimal | Extended), minimal by default (smallest payload, least row data leaving the database, and the key at the top level where a typed event binds it) — plus a projection,WithPayload(x => new { x.Status, x.Total }), which puts exactly those columns in the payload (the row's key always comes along) so arecord OrderUpdated(int Id, string Status, decimal Total)binds without anyone having to know which payload shape the configuration picked. A fully customINotificationPayloadBuilderis still available for anything else. - A payload that never breaks your writes —
pg_notifyrejects payloads over 7999 bytes by raising inside the trigger, which aborts the transaction that wrote the row. The generated trigger checks the size and sends a reduced payload (entity,operation, the key, and"truncated": true) instead, so a large row still writes and the consumer knows to re-read it. Opt out withWithPayloadOverflow(NotificationPayloadOverflow.Fail)where a reduced payload is worse than no write. - Deterministic, idempotent migrations —
CREATE OR REPLACE FUNCTION+DROP TRIGGER IF EXISTS+CREATE TRIGGER, verified to produce an empty diff when nothing changed (seedocs/migrations.md). - Database-first support — no
dotnet ef migrationsrequired:context.Database.EnsureNotificationTriggersAsync()applies the same idempotent trigger DDL directly against existing tables, andGenerateNotificationTriggersScript()renders it as SQL to check into Flyway/DbUp/your own tooling instead (seedocs/migrations.md#database-first-without-dotnet-ef-migrations). - Configurable object naming —
WithNamePrefix(...)/[NotifyChanges(NamePrefix = ...)]/modelBuilder.HasNotificationNamePrefix(...)prefix every generated trigger/function name, so they never collide with functions/triggers you didn't generate (seedocs/migrations.md#avoiding-name-collisions-with-a-custom-prefix). - A real runtime listener — dedicated connection, automatic reconnect with jittered exponential backoff, graceful shutdown, health check.
- Channels derived from the model —
options.AddNotificationMappingFromDbContexts()reads the channel names out of the same entity configuration that generates the triggers, so the two sides cannot drift; it also inherits the context's connection string, adjusted for aLISTENconnection (MultiplexingandPoolingoff — both measured to matter). - Entity-keyed handlers —
IDatabaseInsertedHandler<T>/IDatabaseUpdatedHandler<T>/IDatabaseDeletedHandler<T>/IDatabaseNotificationHandler<T>/IDatabaseNotificationHandler(multiple handlers, DI-scoped), orawait foreach (var e in notifications.Events<T>())for strongly-typed streams. - A middleware pipeline —
UseLogging()/UseRetry()/UseMetrics()/ customINotificationMiddleware. IChangeTokenchange tracking —AddChangeTracking()gives every entity anIEntityChangeTracker<T>forIMemoryCacheexpiration tokens andETag/Last-Modifiedgeneration, withLastModifiedtaken from the trigger's clock so ETags stay identical across instances (seedocs/architecture.md#change-tracking-ichangetoken).- Roslyn analyzers —
PGN001–PGN004catch common misconfiguration at compile time.
See docs/architecture.md for how these fit together, including a
sequence diagram of the full trigger-to-handler lifecycle.
Getting started
- Reference
PgNotify.EFCoreandPgNotify.Migrationsfrom your data-access project, andPgNotify.Runtime(plusPgNotify.Runtime.EFCore, to derive the channels from aDbContext) from whatever process hosts the listener. Or, to avoid picking individual packages by hand, reference the two meta-packages instead:PgNotify.Writer(EFCore + Migrations + Analyzers) from the project that owns theDbContext, andPgNotify.Listener(Runtime + Runtime.EFCore) from whatever project listens — a single-process app just references both. A listener with no EF Core at all referencesPgNotify.Runtimealone. - Configure notifications on your entities (attribute or fluent API — see above).
- Chain
.UseNpgsqlNotifications()after.UseNpgsql(...)— both where the app builds itsDbContextOptionsand in yourIDesignTimeDbContextFactory(fordotnet eftooling). dotnet ef migrations add .../dotnet ef database update(orDatabase.Migrate()at startup) as usual.builder.Services.AddPostgresNotifications(o => { o.AddHandlersFromAssembly(...); o.AddNotificationMappingFromDbContexts(); });(or, with noDbContextin the process,o.ConnectionString = ...ando.MapChannel<Entity>("channel")).- Consume via an
IDatabase…Handler<TEntity>implementation orIPostgresNotificationService.Events<T>().
The full working example: samples/CacheInvalidation.WebApi
(run it with docker compose up -d && dotnet run — see its own README). For the same problem
solved with IChangeToken/ETag instead of a hand-written handler — and with no dotnet ef
required, against either the bundled container or any PostgreSQL you already have — see
samples/HttpCaching.WebApi. For a two-process setup —
a web app with a UI that writes the data, and a completely separate console app with no EF Core
dependency at all that listens for changes — see
samples/TaskBoard.WebApi + samples/TaskBoard.Watcher.
For a setup built on the PgNotify.Writer/PgNotify.Listener meta-packages instead of the
individual library projects — a write-side API and a separate CQRS-style read-model projector,
each with a single package reference — see
samples/Orders.WebApi + samples/Orders.Projector.
Project layout
src/ PgNotify.Core / .EFCore / .Migrations / .Runtime / .Runtime.EFCore / .Analyzers
PgNotify.Writer / .Listener - empty meta-packages bundling the above by role
tests/ One test project per src/ project, plus PgNotify.IntegrationTests (Testcontainers)
samples/ CacheInvalidation.WebApi, HttpCaching.WebApi, TaskBoard.*, Orders.* (full) +
design sketches for two other scenarios
docs/ architecture.md, migrations.md, troubleshooting.md, performance.md, versioning.md
See docs/architecture.md for why each project exists.
Testing
- Unit tests per project (
Core,EFCore,Runtime), no external dependencies. PgNotify.Migrations.Tests— SQL-generation snapshot-style tests: builds a real Npgsql relational pipeline and asserts on the exact generated DDL, including idempotency (an unchanged model produces zero migration operations).PgNotify.IntegrationTests— real PostgreSQL via Testcontainers: applies a migration, inserts/updates/deletes through EF Core, and asserts the runtime listener receives and correctly deserializes the resulting notifications, including a reconnect scenario (pg_terminate_backendmid-test) and watched-column filtering.PgNotify.Analyzers.Tests— drives the analyzer directly against realCSharpCompilations.PgNotify.Runtime.EFCore.Tests— derives channel mappings from real EF Core models, against an unroutable host: reading a model needs no connection.
dotnet test # everything except IntegrationTests network/docker cost
dotnet test tests/PgNotify.IntegrationTests # requires Docker
Documentation
docs/architecture.md— design, lifecycle diagram, extensibility points, limitsdocs/migrations.md— exact generated SQL, idempotency, composite keys, schemasdocs/troubleshooting.md— common misconfigurations and how to diagnose themdocs/performance.md— trigger overhead, dispatch cost, metricsdocs/versioning.md— semver policy, annotation/generated-code compatibility
Supported scenarios
Schemas, composite keys, identity columns, owned-type-mapped scalar columns, JSON/XML columns
(as watched/payload columns, not as a source of structural notification config; both are cast
before comparison, since neither has a PostgreSQL IS DISTINCT FROM operator), and enums are
all supported wherever they map to ordinary scalar columns. Unsupported/limited: navigation and
collection properties can't be watched for changes (flagged by analyzer rule PGN002); table
splitting and entities mapped only to a view are refused at model-build time with a descriptive
error rather than supported (map the entity that needs notifications to its own table instead).
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- Microsoft.EntityFrameworkCore (>= 10.0.10)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection (>= 10.0.10)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.10)
- Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Logging (>= 10.0.10)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.10)
- Npgsql (>= 10.0.3)
- PgNotify.Runtime (>= 1.1.0)
- PgNotify.Runtime.EFCore (>= 1.1.0)
-
net11.0
- Microsoft.EntityFrameworkCore (>= 10.0.10)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection (>= 10.0.10)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.10)
- Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions (>= 10.0.10)
- Microsoft.Extensions.Logging (>= 10.0.10)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.10)
- Npgsql (>= 10.0.3)
- PgNotify.Runtime (>= 1.1.0)
- PgNotify.Runtime.EFCore (>= 1.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.0 | 115 | 8/11/2026 |
| 1.1.0-beta.4 | 64 | 8/11/2026 |
| 1.1.0-beta.3 | 65 | 8/11/2026 |
| 1.1.0-beta.2 | 66 | 8/10/2026 |
| 1.1.0-beta.1 | 62 | 8/10/2026 |
| 1.0.1 | 103 | 8/8/2026 |
| 1.0.0 | 95 | 8/7/2026 |