Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL 0.8.0-preview.128

This is a prerelease version of Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL --version 0.8.0-preview.128
                    
NuGet\Install-Package Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL -Version 0.8.0-preview.128
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL" Version="0.8.0-preview.128" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL" Version="0.8.0-preview.128" />
                    
Directory.Packages.props
<PackageReference Include="Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL --version 0.8.0-preview.128
                    
#r "nuget: Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL, 0.8.0-preview.128"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL@0.8.0-preview.128
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL&version=0.8.0-preview.128&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL&version=0.8.0-preview.128&prerelease
                    
Install as a Cake Tool

Lumarin.Notify

Multi-channel notification infrastructure for .NET. Reliable delivery across in-app feeds, realtime SignalR push, email, SMS, and mobile push — from a single unified API, with durable delivery tracking, templating, retry, rate limiting, and user preference enforcement built in.

var result = await hub.SendAsync(new NotificationRequest(
    ScopeType: "user",
    ScopeId:   "user-123",
    Category:  "order.shipped",
    Content: new NotificationContent("Order shipped", "Your order #456 is on the way!"),
    Recipients: new NotificationRecipients(
        Identities: ["user-123"],
        Channels:   [NotificationChannel.InApp, NotificationChannel.Email]),
    Options: new NotificationOptions(
        IdempotencyKey: "order-456-shipped",
        Priority:       NotificationPriority.High)));

Choose your packages

What you need Packages to add
Send notifications and in-app feed Lumarin.Notify
Durable delivery tracking and EF Core persistence + Lumarin.Notify.EntityFrameworkCore
HTTP endpoints for feed, preferences, devices + AspNetCore
Template rendering (SendFromTemplateAsync) + Templates
Ordered fallback routing and simulation + PolicyRouting
Quiet-hours deferral for policy routing (durable only) + PolicyRouting + Scheduling + Scheduling.EntityFrameworkCore + PolicyRouting.Scheduling
One-time scheduled notifications (preview) + Scheduling + Scheduling.EntityFrameworkCore
Realtime WebSocket push + Channels.SignalR
Email delivery + Channels.Email + one email adapter package
Push notifications (FCM / OneSignal) + Channels.Push + one push adapter package
SMS (Twilio) + Channels.Sms + one SMS adapter package

Installation

Add the package references that match your profile:

  • Core runtime: Lumarin.Notify
  • Durable persistence profile: Lumarin.Notify.EntityFrameworkCore
  • Hosted HTTP endpoints for feed, preferences, devices, and template admin: Lumarin.Notify.AspNetCore
  • Template rendering for SendFromTemplateAsync(...): Lumarin.Notify.Templates
  • Ordered fallback routing: Lumarin.Notify.PolicyRouting
  • Durable quiet-hours deferral bridge for policy routing: Lumarin.Notify.PolicyRouting.Scheduling
  • One-time scheduling preview: Lumarin.Notify.Scheduling and Lumarin.Notify.Scheduling.EntityFrameworkCore
  • SignalR channel: Lumarin.Notify.Channels.SignalR
  • Email channel: Lumarin.Notify.Channels.Email plus one provider adapter: Lumarin.Notify.Channels.Email.Smtp, Lumarin.Notify.Channels.Email.Ses, or Lumarin.Notify.Channels.Email.SendGrid
  • Push channel: Lumarin.Notify.Channels.Push plus one provider adapter: Lumarin.Notify.Channels.Push.Fcm or Lumarin.Notify.Channels.Push.OneSignal
  • SMS channel: Lumarin.Notify.Channels.Sms plus one provider adapter: Lumarin.Notify.Channels.Sms.Twilio

Provider credential requirements

Channel package Required credentials
Channels.Email + .Email.Smtp / .Email.Ses / .Email.SendGrid SMTP host/port/credentials, AWS SES credentials, or SendGrid API key
Channels.Push + .Push.Fcm / .Push.OneSignal Firebase service account (FCM) or OneSignal App ID + API key
Channels.Sms + .Sms.Twilio Twilio Account SID, Auth Token, and sender number
Channels.SignalR None — runs in-process; Redis or Azure SignalR Service for multi-node

Channel registration fails fast at startup if the matching provider adapter package or required credentials are missing.

The root builder now owns the default provider-registration story:

builder.Services.AddLumarinNotify(options =>
{
    options.UsePostgreSql(builder.Configuration.GetConnectionString("LumarinNotify")!);

    options.EnableEmail(email =>
    {
        email.FromAddress = "noreply@example.com";
        email.FromName = "Lumarin.Notify";
    });
    options.UseSendGrid(builder.Configuration["LumarinNotify:Channels:Email:SendGrid:ApiKey"]!);
});

Prerequisites

  • .NET 10 SDK
  • PostgreSQL or SQL Server for production; EF Core InMemory for dev/tests

Architecture and governance

For repository rules and contribution guidance:

  • human contributors: CONTRIBUTING.md
  • AI coding agents: AGENTS.md
  • canonical governance docs: docs/architecture/00-README.md

The durable source of truth for architecture guardrails, package boundaries, composition modes, docs governance, and review checklists lives under docs/architecture/.

Adoption profiles

Profile When to use it Core registration
Simple Operational without EF Core using in-memory defaults, no hosted delivery runtime, consumer-owned orchestration and dispatch AddLumarinNotify(...) with Delivery.EnableBackgroundWorkers = false and no durable storage registration
Durable Notifications and deliveries stored in EF Core, direct-delivery runtime when background workers are enabled, no outbox by default AddLumarinNotify(...) + UsePostgreSql(...) or UseSqlServer(...) inside the builder root
Outbox Explicit durable outbox mode with mandatory startup validation and outbox runtime selection when background workers are enabled Durable profile + EnableOutbox(...) inside AddLumarinNotify(...)

Simple setup

// Program.cs
using Lumarin.Notify.DependencyInjection;

builder.Services.AddLumarinNotify(options =>
{
    options.Delivery.EnableBackgroundWorkers = false;
    options.Delivery.MaxRetries = 3;
    options.Templates.Engine = "scriban";
    options.EnableTemplates();
});

Use this profile when you want the public notification services without EF Core. The library registers in-memory defaults for the required persistence seams and does not register a hosted delivery runtime in simple mode. Notifications and queued deliveries are created in-memory, and dispatch remains consumer-owned.

Durable setup

// Program.cs
using Lumarin.Notify.DependencyInjection;

builder.Services.AddLumarinNotify(options =>
{
    options.UsePostgreSql(builder.Configuration.GetConnectionString("LumarinNotify")!); // or UseSqlServer(...)
    options.Delivery.MaxRetries = 3;
    options.EnableTemplates();
    options.EnableEmail(email =>
    {
        email.FromAddress = "noreply@example.com";
        email.FromName = "Lumarin.Notify";
    });
    options.UseSendGrid(builder.Configuration["LumarinNotify:Channels:Email:SendGrid:ApiKey"]!);
});

var app = builder.Build();
app.Run();

This profile replaces the core in-memory defaults with EF Core repositories and keeps the direct-delivery runtime unless you explicitly opt into outbox mode. Durable registrations apply the active Lumarin.Notify migration stream at startup by default. Set Database.Migrations.ApplyOnStartup = false when a separate deploy or migrator step owns schema changes. When you need controlled orchestration, keep using the explicit migration helpers such as MigrateLumarinNotifyAsync() and MigrateLumarinNotifyAdminAsync().

Inject INotificationHub and send:

var result = await hub.SendAsync(new NotificationRequest(
    ScopeType: "user",
    ScopeId:   "user-123",
    Category:  "order.shipped",
    Content: new NotificationContent("Order shipped", "Your order #456 is on the way!"),
    Recipients: new NotificationRecipients(
        Identities: ["user-123"],
        Channels:   [NotificationChannel.InApp, NotificationChannel.Email]),
    Options: new NotificationOptions(
        IdempotencyKey: "order-456-shipped",
        Priority:       NotificationPriority.High)));

Outbox setup

using Lumarin.Notify.Outbox.DependencyInjection;

builder.Services.AddLumarinNotify(options =>
{
    options.UsePostgreSql(builder.Configuration.GetConnectionString("LumarinNotify")!);
    options.EnableOutbox(outbox =>
    {
        outbox.BatchSize = 50;
        outbox.MaxRetries = 5;
    });
});

Outbox settings are configured through EnableOutbox(...) on the root builder or, for advanced direct composition, UseLumarinNotifyOutbox(...). When background workers are enabled, outbox registration selects the outbox runtime profile instead of mutating existing hosted-service registrations. When background workers are disabled, the consumer owns outbox dispatch and retention orchestration explicitly. Mandatory outbox startup validation still runs even if optional startup validation diagnostics were disabled earlier in core options. Outbox EF persistence stays opt-in for runtime and migrations. Base SQL Server/PostgreSQL provider packages no longer pull the outbox EF package by default; generate outbox-aware migrations from the dedicated provider-specific Outbox migration assemblies or from an application-owned migration assembly that references Lumarin.Notify.Outbox.EntityFrameworkCore. Outbox-capable durable hosts also bootstrap the active migration stream at startup by default. Set Database.Migrations.ApplyOnStartup = false only when an external migrator owns the schema step. If you bootstrap schema creation at runtime, use the helper that matches the active profile:

// Durable profile
await app.Services.MigrateLumarinNotifyAsync();

// Outbox profile
await app.Services.MigrateLumarinNotifyOutboxAsync();

If the host also registers AddLumarinNotifyAdminEntityFrameworkCore(), use await app.Services.MigrateLumarinNotifyAdminAsync(); so the admin control-plane provider and policy schema is applied before enabling admin mutation or policy routes on an existing database.

Scheduling preview

Lumarin.Notify.Scheduling is an optional preview package for durable one-time scheduled notifications. It is notification-native, not a general scheduler, and it is not part of the minimal Release 0 validation path.

using Lumarin.Notify.Scheduling.DependencyInjection;
using Lumarin.Notify.Scheduling.EntityFrameworkCore.DependencyInjection;
using Lumarin.Notify.Scheduling.EntityFrameworkCore.Extensions;

builder.Services.AddLumarinNotifySchedulingEntityFrameworkCore();
builder.Services.UseLumarinNotifyScheduling(options =>
{
    options.BatchSize = 100;
    options.ClaimLeaseDuration = TimeSpan.FromMinutes(1);
    options.MaxDispatchAttempts = 5;
});

var app = builder.Build();
await app.Services.MigrateLumarinNotifySchedulingAsync();

var scheduling = app.Services.GetRequiredService<ISchedulingOperations>();
await scheduling.ScheduleAsync(new ScheduleNotificationRequest(
    new NotificationRequest(
        ScopeType: "user",
        ScopeId: "user-123",
        Category: "digest.morning",
        Content: new NotificationContent("Morning digest", "3 new jobs match your search."),
        Recipients: new NotificationRecipients(["user-123"])),
    ScheduleInput.ForDelay(TimeSpan.FromMinutes(30))));

UseLumarinNotifyScheduling(...) activates the feature runtime and startup validation. AddLumarinNotifySchedulingEntityFrameworkCore() wires persistence only. If you need recurrence, cron, workflows, dashboards, or general background jobs, use Hangfire or Quartz instead.

Policy routing

Lumarin.Notify.PolicyRouting is an optional package for ordered channel routing, priority-based route profiles, bounded delivery-path escalation, tenant overrides, bounded cost-aware ordering, structured traces, simulation, and failure-time continuation when a delivery step reaches a terminal failure.

using Lumarin.Notify.Models;
using Lumarin.Notify.PolicyRouting.DependencyInjection;
using Lumarin.Notify.PolicyRouting.Models;

builder.Services.UseLumarinNotifyPolicyRouting(options =>
{
    options.Policy.DefaultChannels.AddRange([
        NotificationChannel.Push,
        NotificationChannel.Email,
    ]);

    options.Policy.LowPriorityChannels.AddRange([
        NotificationChannel.InApp,
        NotificationChannel.Email,
    ]);

    options.Policy.HighPriorityEscalationChannels.AddRange([
        NotificationChannel.Email,
        NotificationChannel.Sms,
    ]);

    options.Policy.QuietHoursStrategy = PolicyRoutingQuietHoursStrategy.DeferWhenAllRoutesQuietHoursBlocked;
    options.Policy.CriticalBypassesQuietHours = true;
});

UseLumarinNotifyPolicyRouting(...) is the feature switch. It does not add a hosted runtime, it works with Simple, Durable, and Outbox profiles, and it keeps delivery on the normal Lumarin.Notify pipeline.

Release B Track 3 supports ordered channel routing, priority-based route profiles, bounded high/critical route widening, static cost hints, tenant overrides, structured execution traces, simulation, and continuation after terminal failure. Continuation depends on internal route-plan metadata; if that metadata is missing or unreadable, the runtime falls back to the normal dead-letter path. The base package stays channel-level and mode-neutral: no provider-level routing, timed workflow escalation, percentage rollout, or workflow/state-machine orchestration is included in this slice.

Policy-routing quiet-hours deferral bridge

Lumarin.Notify.PolicyRouting.Scheduling is the only package that turns policy-routing quiet-hours suppression into durable deferral.

using Lumarin.Notify.PolicyRouting.Scheduling.DependencyInjection;
using Lumarin.Notify.Scheduling.DependencyInjection;
using Lumarin.Notify.Scheduling.EntityFrameworkCore.DependencyInjection;
using Lumarin.Notify.Scheduling.EntityFrameworkCore.Extensions;

builder.Services.AddLumarinNotifySchedulingEntityFrameworkCore();
builder.Services.UseLumarinNotifyScheduling(options =>
{
    options.BatchSize = 100;
    options.ClaimLeaseDuration = TimeSpan.FromMinutes(1);
    options.MaxDispatchAttempts = 5;
});
builder.Services.UseLumarinNotifyPolicyRoutingScheduling();

var app = builder.Build();
await app.Services.MigrateLumarinNotifySchedulingAsync();

This bridge is durable-only. It requires the durable or outbox profile plus active policy routing and scheduling registrations, and it fails fast in simple mode.

When the effective policy uses DeferWhenAllRoutesQuietHoursBlocked, SendAsync(...) may return Success = true with AcceptedMode = Deferred, a ScheduledNotificationId, a DeferredUntilUtc timestamp, and a compact DecisionTrace. If any identity still has an immediate route, Lumarin.Notify keeps the immediate path; Release B does not split one request into mixed immediate and scheduled clones.

HTTP endpoints (AspNetCore adapter)

Lumarin.Notify.AspNetCore adds ready-made minimal API endpoints for the notification feed, user preferences, device registration, template admin, and delivery tracking.

builder.Services.AddLumarinNotify(options =>
{
    options.EnableAspNetCore(hosted =>
    {
        hosted.ScopeType = "user";
        // Default identity claim order: ClaimTypes.NameIdentifier, "sub", "oid", "user_id"
        // hosted.IdentityClaimTypes = ["sub", "oid"]; // e.g. raw JWT / MapInboundClaims = false
    });
    options.EnableTemplates();
    options.EnableTracking();
});

// Optional: register a custom resolver if claim-order configuration is not enough
builder.Services.AddSingleton<ILumarinNotifyIdentityResolver, YourIdentityResolver>();

app.MapLumarinNotify();

Advanced hosts can still register AddLumarinNotifyAspNetCore(...) directly and use the granular MapLumarinNotify*Api() mappers when they intentionally want lower-level composition.

Signed webhook receipts can also carry additive feedback metadata such as FeedbackKind, provider-native RawStatus / RawCode, ExternalEventId, IdempotencyKey, and a push DeviceId. When those fields are present, they are included in the webhook receipt signature. Duplicate receipts with the same normalized correlation data are ignored, and invalid push-device feedback only deactivates the specific supplied DeviceId.

Default routes (rebaseable via app.MapGroup(...)):

Endpoint Route
Feed GET /api/notify/feed
Unread count GET /api/notify/feed/unread-count
Mark read POST /api/notify/feed/{notificationId}/read
Mark all read POST /api/notify/feed/mark-all-read
Dismiss POST /api/notify/feed/{notificationId}/dismiss
Archive POST /api/notify/feed/{notificationId}/archive
Unarchive DELETE /api/notify/feed/{notificationId}/archive
Dismiss (stable delete alias) DELETE /api/notify/feed/{notificationId}
Bulk mark read POST /api/notify/feed/bulk/mark-read
Bulk archive POST /api/notify/feed/bulk/archive
Bulk unarchive POST /api/notify/feed/bulk/unarchive
Bulk dismiss POST /api/notify/feed/bulk/dismiss
Preferences GET/PUT /api/notify/preferences
Register device POST /api/notify/devices
Delete device DELETE /api/notify/devices/{deviceId}
Templates GET/PUT /api/notify/templates/{templateKey}
Deliveries GET /api/notify/deliveries/{notificationId}
Dead letters GET /api/notify/dead-letters

Feed queries accept optional isRead, category, and lifecycle=Active|Archived|Dismissed|All filters. Feed items now expose optional archivedAt and dismissedAt timestamps. Bulk lifecycle endpoints return NotificationFeedBulkMutationResponse with per-item machine-readable result codes instead of partial-failure transport semantics.

Upgrade note: inbox read, archive, and dismiss state is tracked per recipient. The explicit dismiss route and the stable delete alias both affect only the resolved recipient. A read, archive, or dismiss action for one recipient no longer changes feed or unread state for other recipients who share the same notification.

Security: The ASP.NET Core host adapter registers DevelopmentOnlyLumarinNotifyAuthorizationPolicy by default, which grants all callers access to template admin and tracking endpoints. Register a custom ILumarinNotifyAuthorizationPolicy before deploying to production.

Template sends

var result = await hub.SendFromTemplateAsync(
    scopeType:   "user",
    scopeId:     "user-123",
    templateKey: "order-shipped",
    data:        new { Name = "Alex", OrderId = "456" },
    recipients:  new NotificationRecipients(["user-123"]),
    locale:      "en");

Requires Lumarin.Notify.Templates. Template engine is "scriban" (default) or "liquid".

Channel registration

Each channel is registered separately. Examples:

// SignalR (realtime WebSocket push) via the primary root-builder story
builder.Services.AddLumarinNotify(options =>
{
    options.EnableSignalR(signalR =>
    {
        signalR.HubPath = "/hubs/notifications";
        signalR.RequireAuthenticatedUser = true;
    });
});
app.MapLumarinNotify(); // maps the configured hub with the rest of the enabled surfaces

// Email (read from configuration; fails fast if credentials missing)
builder.Services.AddLumarinNotify(options =>
{
    options.EnableEmail(email =>
    {
        email.FromAddress = builder.Configuration["LumarinNotify:Channels:Email:FromAddress"]!;
        email.FromName = builder.Configuration["LumarinNotify:Channels:Email:FromName"];
    });
    options.UseSendGrid(builder.Configuration["LumarinNotify:Channels:Email:SendGrid:ApiKey"]!);
});

InMemory database (dev / tests)

If you do not need durable persistence at all, prefer the simple profile (AddLumarinNotify(...) with Delivery.EnableBackgroundWorkers = false) instead of this override.

builder.Services.AddDbContext<LumarinNotifyDbContext>(opts =>
    opts.UseInMemoryDatabase("LumarinNotify_Dev"));

builder.Services.AddScoped<INotificationRepository, NotificationRepository>();
builder.Services.AddScoped<ITemplateRepository, TemplateRepository>();
builder.Services.AddScoped<IDeliveryRepository, DeliveryRepository>();
builder.Services.AddScoped<IDeviceRepository, DeviceRepository>();
builder.Services.AddScoped<IDeadLetterRepository, DeadLetterRepository>();
builder.Services.AddScoped<IPreferenceRepository, PreferenceRepository>();
builder.Services.AddScoped<ITemplateAuditRepository, TemplateAuditRepository>();

No migrations needed. All data resets on restart.

Docker Compose

# API-only
docker compose --profile api up --build

# API + frontend
docker compose --profile frontend up --build

Copy .env.example as the starting point for runtime parameters.

Runnable hosts

Lumarin.Notify ships two runnable hosts with different roles:

Project Role Use when
src/Lumarin.Notify.ReferenceHost Production-shaped operator/admin reference host and Docker Compose API target You want the clearest admin-capable, PostgreSQL-backed adoption path
samples/Lumarin.Notify.Sample.EmbeddedHost Minimal embedded-host sample You want the smallest one-host example of mounting the hosted API/admin surface inside your own app

If you are unsure where to start, start with src/Lumarin.Notify.ReferenceHost. Use the embedded-host sample only when you specifically need the app-owned composition example.

See docs/hosts-and-samples.md for the full host-selection guide and project-level caveats.

Shared showcase tooling is separate from those runnable hosts and lives under samples/: samples/Lumarin.Notify.Showcase, samples/Lumarin.Notify.Showcase.AspNetCore, and samples/Lumarin.Notify.Showcase.Console. Treat that slice as repo-owned sample/testing infrastructure, not as product packages to publish. The console supports direct local execution with --profile minimal, --profile postgres, --profile postgres-outbox, and --profile postgres-outbox-scheduling; the PostgreSQL-backed profiles require --connection-string or LUMARIN_NOTIFY_POSTGRES_CONNECTION. Use remote execution against a running showcase host such as src/Lumarin.Notify.ReferenceHost at /api/showcase or samples/Lumarin.Notify.Sample.EmbeddedHost at /ops/showcase when you need the host-owned HTTP, auth, and routing surface.

Next Steps

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Lumarin.Notify.Admin.EntityFrameworkCore.PostgreSQL:

Package Downloads
Lumarin.Notify.Hosting.Embedded.PostgreSQL

Optional embedded-hosting convenience profile for Lumarin.Notify on PostgreSQL

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.8.0-preview.199 48 5/14/2026
0.8.0-preview.162 53 4/26/2026
0.8.0-preview.161 62 4/26/2026
0.8.0-preview.160 50 4/26/2026
0.8.0-preview.159 60 4/26/2026
0.8.0-preview.158 56 4/26/2026
0.8.0-preview.157 53 4/25/2026
0.8.0-preview.156 51 4/25/2026
0.8.0-preview.155 57 4/25/2026
0.8.0-preview.154 51 4/25/2026
0.8.0-preview.153 49 4/25/2026
0.8.0-preview.150 50 4/25/2026
0.8.0-preview.133 60 4/23/2026
0.8.0-preview.132 56 4/23/2026
0.8.0-preview.130 48 4/23/2026
0.8.0-preview.128 59 4/23/2026
0.8.0-preview.120 127 4/21/2026
0.8.0-preview.115 56 4/18/2026
0.8.0-preview.114 57 4/18/2026
0.8.0-preview.113 56 4/17/2026
Loading failed