DHI.Services.Notifications.WebApi — Internal Guide¶
A thin REST façade for writing and querying notification entries (logs/alerts) using the Domain Services style. Ships with a JSON-file repository for quick starts and supports a PostgreSQL provider for production.
Conceptually, this is for notifications / alerts / events that matter to humans (operators, support, auditors), with richer querying and pluggable storage. For raw technical logs and CLEF/Serilog integration, see DHI.Services.Logging.WebApi.
If you want a simpler logging mechanisms, see Logging module: Logging Web API
How this differs from DHI.Services.Logging.WebApi¶
Both modules expose Add / Query / Last over HTTP, but:
Notifications.WebApi focuses on:
- Semantically meaningful events: things you might display in an “Alerts” or “Notifications” view.
- Strong data model (
NotificationEntrywithNotificationLevel,Source,Tag,MachineName,Metadata). - Richer querying on multiple fields and operators via Domain Services
QueryConditions. - Pluggable repositories:
- JSON file (
NotificationRepository) for dev / simple setups. - PostgreSQL (
DHI.Services.Provider.PostgreSQL.NotificationRepository) for production, durability and SQL.
- JSON file (
Logging.WebApi focuses on:
- High-volume, technical logs written as CLEF per Tag.
- CLEF/Serilog ecosystem integration (Seq, Serilog.Formatting.Compact.Reader, etc.).
- Very simple Web API querying:
Tag == "..."only, optimized for streaming files.
Rule of thumb
- If it’s a technical trace (job internals, noisy debug lines) → Logging.WebApi.
- If it’s a user / system facing event or alert you want to query flexibly and maybe put in a DB → Notifications.WebApi.
You can log both: write to ILogger / Serilog for traces and at the same time produce a NotificationEntry for the high-value events.
What you get¶
- Add notifications (
POST /api/notifications/{connectionId}) - Query notifications by query string (
GET /api/notifications/{connectionId}) - Advanced query with a request body (
POST /api/notifications/{connectionId}/query) - Fetch last matching entry (
POST /api/notifications/{connectionId}/last) - Role-protected write endpoint (policy:
EditorsOnly)
Data model (immutable struct):
NotificationEntry → Id (Guid), DateTime, NotificationLevel (Debug|Information|Warning|Error|Critical), Source, Text, optional Tag, MachineName, and Metadata.
Install¶
dotnet add package DHI.Services.Notifications.WebApi
(Plus DHI.Services.Provider.PostgreSQL if you want the PostgreSQL repository.)
Wire it up (Program.cs)¶
You typically register a NotificationService with a repository and refer to it by a connectionId in the URL. For simple setups, use the JSON file repository; for production, use the PostgreSQL provider.
1) Add core JSON converters (already in your BaseWebApi template)¶
builder.Services
.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.WriteIndented = true;
options.JsonSerializerOptions
.AddConverters(new JsonStringEnumConverter());
// No module-specific converter line required for Notifications
});
2a) Register a notifications service (JSON file repository)¶
using DHI.Services;
using DHI.Services.Notifications;
using DHI.Services.Notifications.WebApi; // NotificationRepository (JSON) + controller
// set App_Data as your data directory (template already does this)
AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(builder.Environment.ContentRootPath, "App_Data"));
// ensure the file exists (json repo expects an existing file)
Directory.CreateDirectory("[AppData]".Resolve());
var path = "[AppData]notifications.json".Resolve();
if (!File.Exists(path)) File.WriteAllText(path, "{}");
// create repo + service, then register under your chosen connectionId
var jsonRepo = new NotificationRepository(path); // wraps JsonNotificationRepository
var notifSvc = new NotificationService(jsonRepo); // concrete service lives in DHI.Services.Notifications
ServiceLocator.Register(notifSvc, "notifications-json"); // <-- your connectionId
2b) Register with PostgreSQL provider (production)¶
using DHI.Services;
using DHI.Services.Notifications;
using PgNotifRepo = DHI.Services.Provider.PostgreSQL.NotificationRepository;
var conn = "Host=localhost;Port=5432;Database=app;Username=app;Password=secret;" +
"Table=public.MessageLog;Utc=true";
var pgRepo = new PgNotifRepo(conn);
var notifSvc = new NotificationService(pgRepo);
ServiceLocator.Register(notifSvc, "notifications-pg");
Both JSON and PostgreSQL repositories are provider-agnostic to the Web API. It just resolves a
NotificationServiceby{connectionId}.
2c) (Optional) Using the Connections module¶
If you use the Connections infrastructure, you can also register a NotificationServiceConnection (DHI.Services.Notifications.NotificationServiceConnection or the WebApi-specific one) and let ConnectionServiceResolver create the service based on a connection object, similar to logging.
3) Swagger XML (optional but nice)¶
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "DHI.Services.WebApi.xml"));
// options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "DHI.Services.Notifications.WebApi.xml"));
// (include if available in your build output)
4) Auth¶
The controller requires [Authorize], and Add requires the policy EditorsOnly. In your setup:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("EditorsOnly", p => p.RequireClaim(ClaimTypes.GroupSid, "Editors"));
});
For quick local testing, your existing dev bypass strategy can be used (e.g., a dummy
IAuthorizationHandler), but don’t ship that to prod.
Endpoints¶
All routes are prefixed with:
/api/notifications/{connectionId}
Where {connectionId} matches what you registered in ServiceLocator (e.g., notifications-json, notifications-pg).
POST /api/notifications/{connectionId} — Add entry¶
-
Auth:
EditorsOnly -
Body (
NotificationEntryDTO):{ "notificationLevel": "Warning", "text": "Disk usage at 85%", "source": "monitor-agent", "tag": "infra", "machineName": "node-17", "metadata": { "volume": "/data", "usedPct": 85.3 } } -
Creates a new immutable
NotificationEntry(server stampsDateTime = UtcNow). -
Response:
201 Createdwith the entry.
Compared to Logging.WebApi, this is:
- Using a strong enum
NotificationLevelinstead of astring. - Persisting Metadata (in JSON repository).
- Targeted at notification entries rather than raw CLEF events.
GET /api/notifications/{connectionId} — Query by query string¶
- Query params map to fields. Examples:
?notificationLevel=Warning?source=monitor-agent&tag=infra?machineName=node-17?text=Disk usage?dateTime=2025-10-06T00:00:00Z(interpreted as equality)
- Response:
200 OKwith an array of entries.
Query-string lookups are best for equality filters. For operators like
>=on time or level, use the POST endpoints below.
POST /api/notifications/{connectionId}/query — Advanced query (body)¶
- Body: a standard Query DTO describing field/operator/value conditions. It’s the same query model used across Domain Services Web APIs (transformed into a list of
QueryConditions server-side). - Use this to express operators (
>,>=,<,<=,=,!=) forDateTimeandNotificationLevel, and equality/inequality forSource,Tag,MachineName,Text. - Response:
200 OKwith matches.
Example body (illustrative)
{
"Query": [
{ "Item": "DateTime", "QueryOperator": "GreaterThanOrEqual", "Value": "2025-10-01T00:00:00Z" },
{ "Item": "NotificationLevel","QueryOperator": "GreaterThanOrEqual", "Value": "Warning" },
{ "Item": "Source", "QueryOperator": "Equal", "Value": "monitor-agent" }
]
}
This is a key difference from Logging.WebApi, which only accepts
Tag == "...". Here you can combine multiple fields and operators.
POST /api/notifications/{connectionId}/last — Last matching¶
- Same body shape as
/query. - Returns: the most recent matching entry by
DateTime, or404if none.
Field & operator support¶
Across implementations:
- Fields:
DateTime,NotificationLevel,Source,Tag,MachineName,Text - Operators:
DateTime,NotificationLevel:>,>=,<,<=,=,!=Source,Tag,MachineName,Text:=or!=
The JSON repository supports expression queries broadly; the PostgreSQL provider enforces the operator set above.
Curl examples¶
# Add (EditorsOnly)
curl -X POST "https://localhost:5001/api/notifications/notifications-json" \
-H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
-d '{"notificationLevel":"Information","text":"API started","source":"web","tag":"startup"}'
# Simple query (GET)
curl "https://localhost:5001/api/notifications/notifications-json?source=web&tag=startup" \
-H "Authorization: Bearer <jwt>"
# Advanced query (POST)
curl -X POST "https://localhost:5001/api/notifications/notifications-json/query" \
-H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
-d '{"where":[
{"item":"DateTime","operator":"GreaterThanOrEqual","value":"2025-10-01T00:00:00Z"},
{"item":"NotificationLevel","operator":"GreaterThanOrEqual","value":"Warning"}
]}'
# Last matching
curl -X POST "https://localhost:5001/api/notifications/notifications-json/last" \
-H "Authorization: Bearer <jwt>" -H "Content-Type: application/json" \
-d '{"where":[{"item":"source","operator":"Equal","value":"web"}]}'
Under the hood: repositories¶
JSON (NotificationRepository / JsonNotificationRepository)¶
- Stores entries in a JSON file, keyed by
Guid Id. - Uses
ExpressionBuilderto translateQueryConditions to predicates. Last()orders byDateTimedescending and returns the first.
Good for:
- Dev environments.
- Simple single-node setups.
- Situations where you still want structured notifications but don’t want to maintain a DB.
PostgreSQL (DHI.Services.Provider.PostgreSQL.NotificationRepository)¶
- Implements
INotificationRepositoryon top of PostgreSQL. - Auto-creates a minimal table and index on first use.
- Reads extra keys (
Table,Utc) from the connection string (and strips them before Npgsql). - Maps
NotificationEntryfields to columns (id,datetime,notificationlevel,source,tag,machinename,text). - Translates
QueryConditions into SQL.
Good for:
- Production, multi-node setups.
- Durable, queryable notification history.
- Integrating with BI / reporting / raw SQL queries.
Compare with Logging.WebApi, which is always file-based CLEF. If you need SQL querying and indexes, Notifications + PostgreSQL is usually the better fit.
DateTime handling (PostgreSQL provider)¶
- DB column type is
timestamp without time zone. - When
Utc=true(default):- Incoming
DateTimeis converted to UTC. - Stored with
Kind=Unspecifiedbut representing a UTC instant.
- Incoming
- When
Utc=false, theDateTimeis stored “as provided,” normalized toUnspecified.
Recommendation: Keep Utc=true. Always send/consume Z timestamps at the API boundary.
When to choose JSON vs PostgreSQL (within Notifications)¶
- JSON (this WebApi’s default example):
- Dev, demos, small deployments.
- Easy to inspect: open the JSON file.
- PostgreSQL:
- Production, durability, larger volumes.
- Better for concurrent writers/readers and reporting.
In both cases, you still get the same Web API surface (
/api/notifications/{connectionId}); only the backing repository changes.
Notifications vs Logging: choosing per use case¶
Examples
- “Job X started / finished / failed” + lots of intermediate trace lines:
- Intermediate trace: Logging.WebApi (CLEF, high-volume).
- Start/finish/fail events: Notifications.WebApi (easier to build dashboards).
- “User A changed settings” events you might audit:
- Use Notifications.WebApi so you can query by user, time range, and event type.
- Pure technical diagnostics (you mostly consume in Seq or Serilog sinks):
- Use Logging.WebApi and treat Notifications as optional.