Scribe.Notifications
1.1.1
dotnet add package Scribe.Notifications --version 1.1.1
NuGet\Install-Package Scribe.Notifications -Version 1.1.1
<PackageReference Include="Scribe.Notifications" Version="1.1.1" />
<PackageVersion Include="Scribe.Notifications" Version="1.1.1" />
<PackageReference Include="Scribe.Notifications" />
paket add Scribe.Notifications --version 1.1.1
#r "nuget: Scribe.Notifications, 1.1.1"
#:package Scribe.Notifications@1.1.1
#addin nuget:?package=Scribe.Notifications&version=1.1.1
#tool nuget:?package=Scribe.Notifications&version=1.1.1
<p align="center"> <img src="docs/images/scribe-logo-v1.png" width="220" alt="Scribe.Notifications" /> </p> <p align="center"> High-performance Notification Pattern for .NET. </p> <p align="center"> <strong>Record, don't throw.</strong> </p> <p align="center"> Collect validation failures, warnings and domain notifications without relying on exceptions. </p> <p align="center"> <a href="https://www.nuget.org/packages/Scribe.Notifications"> <img src="https://img.shields.io/nuget/v/Scribe.Notifications.svg" /> </a> <a href="https://www.nuget.org/packages/Scribe.Notifications"> <img src="https://img.shields.io/nuget/dt/Scribe.Notifications.svg" /> </a> <img src="https://img.shields.io/badge/.NET-8%20%7C%209%20%7C%2010-purple.svg" /> <img src="https://img.shields.io/badge/License-MIT-yellow.svg" /> </p>
What is Scribe?
Scribe.Notifications is a high-performance implementation of the Notification Pattern for .NET.
Instead of throwing exceptions for expected validation failures, Scribe collects notifications and allows callers to inspect them at the end of an operation.
This approach produces:
- Cleaner domain code
- Better performance
- Richer validation feedback
- Improved observability
- Better separation between validation and control flow
Why Scribe?
Using exceptions for expected validation scenarios has drawbacks:
- Exceptions are expensive
- Only the first failure is returned
- Aggregating failures becomes difficult
- Validation logic leaks into control flow
Instead of this:
throw new ValidationException("Email is invalid.");
Use this:
notifications.Add(
new NotificationMessage(
"USER_EMAIL_INVALID",
NotificationType.Error,
"Email is invalid."
)
);
Scribe enables you to:
- Collect all validation failures
- Return rich feedback
- Attach metadata
- Improve diagnostics
- Avoid exception-driven validation flows
Features
- High-performance Notification Pattern implementation
- Thread-safe notification storage
- Immutable readonly struct notifications
- Lazy evaluation APIs
- Span<T> support
- Async APIs powered by ValueTask
- Custom notification types
- Rich metadata support
- String interning optimizations
- FrozenDictionary-backed type registry
- Zero external dependencies
Packages
| Package | Version | Description |
|---|---|---|
Scribe.Notifications |
Core Notification Pattern implementation | |
Scribe.Notifications.Checks |
Reusable, observable domain checks |
Installation
# Core
dotnet add package Scribe.Notifications
# Checks (optional)
dotnet add package Scribe.Notifications.Checks
Quick Start
using Scribe.Notifications.Core.Notifications;
var notifications = new NotificationCollection();
notifications.Add(
NotificationType.Error,
"Email is invalid."
);
notifications.Add(
NotificationType.Warning,
"Name is too short."
);
if (notifications.HasErrors())
{
foreach (var error in notifications.GetErrors())
{
Console.WriteLine(error);
}
}
Real World Example
public NotificationCollection ValidateUser(
string email,
int age)
{
var notifications = new NotificationCollection();
if (string.IsNullOrWhiteSpace(email))
{
notifications.Add(
new NotificationMessage(
"USER_EMAIL_REQUIRED",
NotificationType.Error,
"Email is required."
)
);
}
if (age < 18)
{
notifications.Add(
new NotificationMessage(
"USER_AGE_INVALID",
NotificationType.Error,
"User must be at least 18 years old."
)
);
}
return notifications;
}
Performance
Scribe was designed for high-throughput applications.
Performance optimizations include:
- readonly struct notifications
- String interning
- Flyweight caching
- FrozenDictionary lookups
- Lazy evaluation
- Span<T> support
- ValueTask APIs
Benchmarks
| Scenario | Mean | Allocated |
|---|---|---|
| Add with explicit ID | 91.06 ns | 264 B |
| Add without ID (auto-generated) | 1,278.79 ns | 368 B |
| HasErrors 2,000 items | 12.96 ns | — |
| HasErrors empty collection | 12.88 ns | — |
| GetErrors lazy 1,000 errors | 48,103.83 ns | 78.4 KB |
| GetErrorsAsList materialized | 19,797.52 ns | 78.3 KB |
| TryGetAsSpan zero-copy | 14.83 ns | — |
| AddRange 100 items | 129,835.13 ns | 25.9 KB |
Environment:
- BenchmarkDotNet v0.14.0
- .NET 10.0.2 Arm64
- Apple M1 Pro, 8 cores
Core Concepts
NotificationMessage
Immutable readonly struct representing a single notification.
var notification = new NotificationMessage(
"ORDER_AMOUNT_INVALID",
NotificationType.Error,
"Order amount must be greater than zero."
);
NotificationType
Represents notification categories.
Built-in types:
| Type | Severity | IsFailure |
|---|---|---|
| Error | 100 | true |
| Warning | 60 | false |
| Info | 20 | false |
| Success | 0 | false |
Custom types:
var critical = NotificationType.GetOrCreate(
"critical",
"Critical Error",
95,
true
);
NotificationCollection
Thread-safe notification store.
var notifications = new NotificationCollection();
notifications.Add(
NotificationType.Error,
"Something went wrong."
);
bool hasErrors = notifications.HasErrors();
ASP.NET Minimal API Example
app.MapPost("/users", (CreateUserRequest request) =>
{
var notifications = Validate(request);
if (notifications.HasErrors())
{
return Results.BadRequest(
notifications.GetErrorsAsList()
);
}
return Results.Ok();
});
Custom Notification Types
var blocked = NotificationType.GetOrCreate(
name: "blocked",
displayName: "Blocked",
severityLevel: 80,
isFailure: true
);
notifications.Add(
new NotificationMessage(
"ACCOUNT_BLOCKED",
blocked,
"Account is blocked."
)
);
Checks
Scribe.Notifications.Checks brings reusable, observable domain validations built on top of the Notification Pattern.
Instead of duplicating business rules across services, you encapsulate them once and apply them anywhere:
using Scribe.Notifications.Checks;
using Scribe.Notifications.Checks.Extensions;
notifications.Apply(UserChecks.EmailMustBeValid(email));
notifications.Apply(UserChecks.MustBeAdult(age));
await notifications.ApplyAsync(UserChecks.EmailNotAlreadyTakenAsync(email, ct));
A Check is the observable result of a domain validation, it carries zero or more notifications produced during evaluation.
public static class UserChecks
{
public static Check EmailMustBeValid(string email) =>
email.Contains('@')
? Check.Pass()
: Check.Fail("USER_EMAIL_INVALID", NotificationType.Error, "Email is invalid.");
public static async ValueTask<Check> EmailNotAlreadyTakenAsync(string email, CancellationToken ct)
{
var exists = await _repository.ExistsAsync(email, ct);
return exists
? Check.Fail("USER_EMAIL_TAKEN", NotificationType.Error, "Email is already taken.")
: Check.Pass();
}
}
Apply multiple checks at once, all evaluated regardless of previous failures:
notifications.ApplyAll([
UserChecks.EmailMustBeValid(request.Email),
UserChecks.MustBeAdult(request.Age),
UserChecks.NameMustNotBeEmpty(request.Name)
]);
Implementing INotificationStore
You can build your own store by implementing INotificationStore, useful for decorating with logging, 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);
}
// Implement remaining INotificationStore members by delegating to _inner
}
Roadmap
v1.1 ✅
Scribe.Notifications.Checksreusable, observable domain checks
v1.2
- Grouping and aggregation APIs
- Severity ordering
- Filtering extensions
v1.3
- Localization
- .resx integration
v2.0
- OpenTelemetry integration
- Metrics and diagnostics
Contributing
Contributions are welcome.
If you have suggestions, bug reports, or feature requests, please open an issue first so we can discuss the proposal.
License
MIT License.
See LICENSE for details.
| Product | Versions 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. |
-
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.