Skip to content

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 (NotificationEntry with NotificationLevel, 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.

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 DBNotifications.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): NotificationEntryId (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 NotificationService by {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 stamps DateTime = UtcNow).

  • Response: 201 Created with the entry.

Compared to Logging.WebApi, this is:

  • Using a strong enum NotificationLevel instead of a string.
  • 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 OK with 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 (>, >=, <, <=, =, !=) for DateTime and NotificationLevel, and equality/inequality for Source, Tag, MachineName, Text.
  • Response: 200 OK with 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, or 404 if 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 ExpressionBuilder to translate QueryConditions to predicates.
  • Last() orders by DateTime descending 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 INotificationRepository on 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 NotificationEntry fields 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 DateTime is converted to UTC.
    • Stored with Kind=Unspecified but representing a UTC instant.
  • When Utc=false, the DateTime is stored “as provided,” normalized to Unspecified.

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.