Scribe.Notifications 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Scribe.Notifications --version 1.0.0
                    
NuGet\Install-Package Scribe.Notifications -Version 1.0.0
                    
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="Scribe.Notifications" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Scribe.Notifications" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Scribe.Notifications" />
                    
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 Scribe.Notifications --version 1.0.0
                    
#r "nuget: Scribe.Notifications, 1.0.0"
                    
#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 Scribe.Notifications@1.0.0
                    
#: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=Scribe.Notifications&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Scribe.Notifications&version=1.0.0
                    
Install as a Cake Tool

Scribe.Notifications

NuGet NuGet Downloads License: MIT .NET

A high-performance Notification Pattern library for .NET. Simple, focused, and built for demanding workloads.


What is Scribe.Notifications?

Scribe.Notifications is a library that implements the Notification Pattern. Instead of throwing exceptions for expected validation failures, you collect notifications and inspect them at the end of an operation. This results in cleaner code, better performance, and richer feedback to the caller.

The library is built around three core principles:

  • Performance first: lazy evaluation, Flyweight caching, string interning, and Span<T> support minimize allocations and maximize throughput
  • Focused: does one thing well, with no external dependencies
  • Thread-safe: safe for concurrent workloads out of the box

Architecture

┌─────────────────────────────────────────────┐
│              INotificationStore              │  ← contract
└─────────────────────┬───────────────────────┘
                      │ implements
                      ▼
┌─────────────────────────────────────────────┐
│           NotificationCollection            │  ← thread-safe, lazy
│                                             │
│  Add / AddRange / AddAsync                  │
│  Remove / RemoveByType / Clear              │
│  GetErrors()      → IEnumerable  (lazy)     │
│  GetErrorsAsList() → IReadOnlyList          │
│  TryGetAsSpan()   → ReadOnlySpan (zero-copy)│
└─────────────────────┬───────────────────────┘
                      │ contains
             ┌────────┴────────┐
             ▼                 ▼
┌────────────────────┐ ┌──────────────────────┐
│ NotificationMessage│ │   NotificationType   │
│  (readonly struct) │ │     (singleton)      │
│                    │ │                      │
│  Id (optional)     │ │  Error   (100, fail) │
│  Type              │ │  Warning  (60)       │
│  Message           │ │  Info     (20)       │
│  CreatedAt         │ │  Success   (0)       │
│  Metadata          │ │  + custom types      │
└────────────────────┘ └──────────────────────┘

Installation

dotnet add package Scribe.Notifications

Quick Start

using Scribe.Notifications.Core.Notifications;

var notifications = new NotificationCollection();

// Quick add, ID is generated automatically
notifications.Add(NotificationType.Error, "Email is invalid.");
notifications.Add(NotificationType.Warning, "Name is too short.");

// With explicit ID, recommended when observability matters
notifications.Add(new NotificationMessage("USER_EMAIL_INVALID", NotificationType.Error, "Email is invalid."));

// Check and react
if (notifications.HasErrors())
{
    foreach (var error in notifications.GetErrors())
        Console.WriteLine(error); // [USER_EMAIL_INVALID] Error: Email is invalid.
}

Core Concepts

NotificationMessage

An immutable readonly struct that represents a single notification. Because it is a struct, instances are stack-allocated with no heap pressure for the common case.

// Without explicit ID: a unique identifier is generated automatically
var notification = new NotificationMessage(NotificationType.Error, "Order amount must be greater than zero.");

// With explicit ID: recommended for tracing and observability
var notification = new NotificationMessage(
    id: "ORDER_AMOUNT_INVALID",
    type: NotificationType.Error,
    message: "Order amount must be greater than zero."
);

// Attach contextual metadata without breaking immutability
var withContext = notification.WithMetadata(
    new Dictionary<string, object?> { { "field", "amount" }, { "value", -5 } }
);

Key characteristics:

  • Immutable: all properties are set at construction
  • Optional ID: omit it for quick usage; a unique identifier is generated automatically
  • With* methods return new instances, never mutate
  • IDs are automatically interned: 10,000 notifications with the same ID use one string in memory
  • Implements IEquatable<T> with value semantics

NotificationType

A singleton type that represents a category of notification. Predefined types are stored in a FrozenDictionary for allocation-free, thread-safe lookups. Custom types are created on demand and cached. The same configuration always returns the same instance.

Predefined types:

Type Severity IsFailure
Error 100 true
Warning 60 false
Info 20 false
Success 0 false
// Predefined singletons
var error = NotificationType.Error;
var warning = NotificationType.Warning;

// Custom types, cached by composite key (name + displayName + severity + isFailure)
var critical = NotificationType.GetOrCreate(
    name: "critical",
    displayName: "Critical Error",
    severityLevel: 95,
    isFailure: true
);

// Same parameters always return the same instance
var same = NotificationType.GetOrCreate("critical", "Critical Error", 95, true);
Assert.Same(critical, same); // true

// Look up a predefined type by name
if (NotificationType.TryGetPredefinedType("error", out var type))
    Console.WriteLine(type.SeverityLevel); // 100

NotificationCollection

A thread-safe collection that implements INotificationStore. Designed for both high-concurrency and high-throughput scenarios.

Adding notifications:

var notifications = new NotificationCollection();

// Quick add. ID generated automatically
notifications.Add(NotificationType.Error, "Something went wrong.");

// Explicit ID. recommended for tracing and observability
notifications.Add(new NotificationMessage("ID_001", NotificationType.Error, "Something went wrong."));

// Batch add
ReadOnlySpan<NotificationMessage> batch = stackalloc NotificationMessage[]
{
    new("ID_002", NotificationType.Warning, "Low disk space."),
    new("ID_003", NotificationType.Info, "Cache refreshed.")
};
notifications.AddRange(batch);

// Async add
await notifications.AddAsync(NotificationType.Warning, "Low disk space.");
await notifications.AddAsync(new NotificationMessage("ID_004", NotificationType.Error, "Timeout."));

Querying with lazy evaluation:

Lazy methods use yield return and do not allocate a new list. Prefer them when iterating once.

// Check presence
bool hasErrors = notifications.HasErrors();
bool hasWarnings = notifications.HasWarnings();
int total = notifications.Count();

// Lazy enumeration (no intermediate list allocation)
foreach (var error in notifications.GetErrors())
    Console.WriteLine(error.Message);

foreach (var item in notifications.GetByType(NotificationType.Warning))
    Process(item);

Querying materialized lists:

Use these when you need to pass the result to another method, serialize it, or access it multiple times.

IReadOnlyList<NotificationMessage> errors = notifications.GetErrorsAsList();
IReadOnlyList<NotificationMessage> all = notifications.GetAllAsList();

High-performance zero-copy access:

For performance-critical paths where you need direct access to the underlying data without any allocation:

if (notifications.TryGetAsSpan(out ReadOnlySpan<NotificationMessage> span))
{
    // Direct access to the backing array, zero allocation.
    for (int i = 0; i < span.Length; i++)
        Process(span[i]);
}

⚠️ The span is valid only as long as the collection is not modified. In concurrent scenarios, use GetAllAsList() instead.

Removing notifications:

// Remove by ID
bool removed = notifications.Remove("ID_001");

// Remove all of a type
int count = notifications.RemoveByType(NotificationType.Warning);

// Clear everything
notifications.Clear();

Async Support

All mutating and querying operations have async counterparts returning ValueTask, zero allocation in the synchronous fast path.

// Quick async add
await notifications.AddAsync(NotificationType.Error, "Something went wrong.");

// Explicit ID async add
await notifications.AddAsync(new NotificationMessage("ID_001", NotificationType.Error, "Timeout."));
await notifications.AddRangeAsync(batch);

bool hasErrors = await notifications.HasErrorsAsync();
int count = await notifications.CountAsync();

bool removed = await notifications.RemoveAsync("ID_001");
await notifications.ClearAsync();

Custom Notification Types

// Register a domain-specific type
var blocked = NotificationType.GetOrCreate(
    name: "blocked",
    displayName: "Blocked",
    severityLevel: 80,
    isFailure: true
);

notifications.Add(new NotificationMessage("ACC_BLOCKED", blocked, "Account is blocked."));

// Query by custom type
foreach (var n in notifications.GetByType(blocked))
    Console.WriteLine(n);

Implementing INotificationStore

You can build your own store by implementing INotificationStore, useful for decorating with persistence, telemetry, or custom routing.

public class LoggingNotificationStore : INotificationStore
{
    private readonly INotificationStore _inner;
    private readonly ILogger _logger;

    public LoggingNotificationStore(INotificationStore inner, ILogger logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public void Add(in NotificationMessage notification)
    {
        _logger.LogInformation("Notification added: {Id}", notification.Id);
        _inner.Add(notification);
    }

    // ... delegate remaining members to _inner
}

Roadmap

  • v1.1 NotificationRules: reusable, registrable validation rules with a fluent builder
  • v1.2 Grouping & Aggregation: GroupBy, OrderBySeverity, and filtering extensions
  • v1.3 Localization: multi-language message support with .resx integration
  • v2.0 Observability: built-in OpenTelemetry integration with automatic metrics per operation

Contributing

Contributions are welcome. Please open an issue first to discuss what you'd like to change.


License

MIT. See LICENSE for details.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Scribe.Notifications:

Package Downloads
Scribe.Notifications.Checks

Reusable, observable domain checks for Scribe.Notifications. Encapsulate business validations as first-class notification producers.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.1 127 6/25/2026
1.1.0 127 6/24/2026
1.0.0 107 6/12/2026