Appearance
Channels
GrydNotifications supports three notification channels out of the box: Email, Push, and In-App. Each channel has its own provider abstraction, allowing you to swap implementations without changing application code.
Channel Architecture
SendNotificationCommand
│
┌───────┴───────┐
│ Channel │
│ Resolver │
└───┬───┬───┬───┘
│ │ │
┌─────────┘ │ └─────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Email │ │ Push │ │ In-App │
│ Provider │ │ Provider │ │ Sender │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
┌────┴────┐ ┌───┴────┐ ┌────┴────┐
│ MailKit │ │ FCM │ │ SignalR │
│ SendGrid│ │ │ │ + DB │
└─────────┘ └────────┘ └──────────┘Email Channel
The email channel supports SMTP via MailKit (default) and SendGrid as an alternative provider.
MailKit (SMTP) — Default
MailKit is the default email provider, recommended by Microsoft over System.Net.Mail. It supports OAuth2, connection pooling, DKIM, and S/MIME.
csharp
options.Email.SmtpHost = "smtp.myapp.com";
options.Email.SmtpPort = 587;
options.Email.SmtpUseSsl = true;
options.Email.SmtpUsername = "user";
options.Email.SmtpPassword = "password";
options.Email.DefaultFrom = "noreply@myapp.com";
options.Email.DefaultFromName = "My App";Sending Emails
csharp
// Via unified pipeline
await _mediator.Send(new SendNotificationCommand
{
Channel = NotificationChannel.Email,
Recipients = [
new RecipientDto("john@example.com", "John Doe"),
new RecipientDto("jane@example.com", "Jane Doe")
],
Subject = "Weekly Report",
HtmlBody = "<h1>Your weekly report is ready</h1>",
TextBody = "Your weekly report is ready"
}, ct);
// Via direct email shortcut
await _mediator.Send(new SendEmailCommand
{
To = ["john@example.com"],
Cc = ["manager@example.com"],
Bcc = ["audit@myapp.com"],
Subject = "Quarterly Summary",
HtmlBody = "<h1>Q4 2025 Summary</h1>",
FromAddress = "reports@myapp.com",
FromName = "Reports System"
}, ct);Email with Attachments
Attachments are files kept by GrydFiles, never bytes in the request or in the notifications database. Upload each file first through POST /files under the gryd.notification-attachment profile, wait for the antivirus to release it (status: Available), then send its id:
csharp
await _mediator.Send(new SendEmailCommand
{
To = ["user@example.com"],
Subject = "Invoice Attached",
HtmlBody = "<p>Please find your invoice attached.</p>",
AttachmentFileIds = [invoiceFileId]
}, ct);SendNotificationCommand accepts the same AttachmentFileIds for the Email channel. The module checks each file when the command arrives:
| Refusal | Code |
|---|---|
| The host did not register GrydFiles | 422 NOTIFICATION_ATTACHMENT_DISABLED_UNPROCESSABLE |
| Still being scanned | 409 FILE_NOT_AVAILABLE_CONFLICT — send again once it is released |
| Infected (quarantined) | 409 FILE_INFECTED_CONFLICT — never attached |
| Purged | 409 FILE_PURGED_CONFLICT |
| Another tenant's file, or none | 404 FILE_NOT_FOUND |
| Uploaded under another profile | 422 NOTIFICATION_ATTACHMENT_PROFILE_UNPROCESSABLE |
Above MaxAttachmentSizeMb | 422 NOTIFICATION_ATTACHMENT_TOO_LARGE_UNPROCESSABLE |
More than MaxAttachmentsPerEmail, or the same file twice | 422 NOTIFICATION_ATTACHMENT_LIMIT_UNPROCESSABLE |
| A channel other than e-mail | 422 NOTIFICATION_ATTACHMENT_CHANNEL_UNPROCESSABLE |
Each attachment of a persisted notification holds a reference on its file (owner scope gryd.notification-attachment) while the e-mail can still be delivered — including retries and a scheduled send — and lets go once the notification is Sent, Delivered, DeadLettered or Cancelled. From then on the file follows its profile's retention. The SMTP and SendGrid providers read each file's bytes from GrydFiles at the moment of sending; an attachment that is no longer available fails the send with the file module's code. The notification detail lists each attachment's storedFileId; its download URL comes from GET /files/{storedFileId}/download-url.
GrydFiles is optional, as SMTP is. Attachments are the only part of the module that needs it. A host that registers AddGrydFiles — before or after AddGrydNotifications, the order does not matter — gets attachments, and refuses to start unless the gryd.notification-attachment profile is declared with ScanRequired: true. A host that does not register it starts without any GrydFiles, object storage or ClamAV configuration: in-app, push and e-mail without attachments work as usual, and a send with AttachmentFileIds is refused with NOTIFICATION_ATTACHMENT_DISABLED_UNPROCESSABLE.
Email Limits
| Limit | Default | Configurable |
|---|---|---|
| Max attachment size | 10 MB | ✅ MaxAttachmentSizeMb |
| Max attachments/email | 5 | ✅ MaxAttachmentsPerEmail |
| Max recipients (To+Cc+Bcc) | 50 | ✅ MaxRecipientsPerEmail |
| Bulk batch size | 100 | ✅ BulkBatchSize |
SendGrid Provider
For high-volume sending, use SendGrid's HTTP API:
csharp
// SendGrid is available as an additional provider
// Configure SMTP credentials for SendGrid
options.Email.SmtpHost = "smtp.sendgrid.net";
options.Email.SmtpUsername = "apikey";
options.Email.SmtpPassword = "SG.your_api_key_here";Push Channel (FCM)
The push channel sends notifications via Firebase Cloud Messaging to mobile devices and web browsers.
Setup
- Create a Firebase project at console.firebase.google.com
- Download the service account JSON
- Set the environment variable:
bash
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/firebase-adminsdk.json"- Enable push in options:
csharp
options.Push.Enabled = true;
options.Push.DefaultTimeToLive = TimeSpan.FromHours(24);
options.Push.AutoRemoveInvalidTokens = true;Sending Push Notifications
csharp
// Direct push to device tokens
await _mediator.Send(new SendPushNotificationCommand
{
Title = "New Message",
Body = "You have a new message from John",
DeviceTokens = ["device_token_1", "device_token_2"],
Data = new Dictionary<string, string>
{
["messageId"] = "abc123",
["type"] = "new_message"
}
}, ct);
// Via unified pipeline
await _mediator.Send(new SendNotificationCommand
{
Channel = NotificationChannel.Push,
Recipients = [new RecipientDto { Address = "device_token_abc" }],
Subject = "New Message",
TextBody = "You have a new message from John"
}, ct);Device Token Management
GrydNotifications tracks device tokens per user with automatic lifecycle management:
| Feature | Description |
|---|---|
| Registration | Store tokens when users sign in on devices |
| Deactivation | Auto-deactivate tokens reported invalid by FCM |
| Stale cleanup | Remove tokens unused for StaleTokenThreshold (default 90 days) |
| Multicast limit | Auto-splits sends into batches of MaxDeviceTokensPerMulticast (500) |
FCM Payload Customization
csharp
await _mediator.Send(new SendPushNotificationCommand
{
Title = "Flash Sale!",
Body = "50% off all items — ends in 2 hours",
DeviceTokens = tokens,
ImageUrl = "https://cdn.myapp.com/sale-banner.jpg",
Data = new Dictionary<string, string>
{
["screen"] = "promotions",
["promo_id"] = "FLASH50"
}
}, ct);In-App Channel
The in-app channel creates notifications for the user's bell/drawer in your application. Notifications are persisted in the database and delivered in real-time via SignalR.
For a detailed guide on the In-App channel, see In-App & SignalR.
Quick Overview
csharp
// Send an in-app notification
await _mediator.Send(new SendInAppNotificationCommand
{
UserId = userId,
Title = "Task Assigned",
Body = "You've been assigned to task #123",
Icon = "📋",
Category = "tasks",
ActionUrl = "/tasks/123",
Priority = NotificationPriority.High
}, ct);Features
| Feature | Description |
|---|---|
| Read/Unread tracking | Mark individual or all as read |
| Categories | Filter by category (alerts, tasks, comments, etc.) |
| Expiration | Auto-expire with ExpiresAfter |
| Collapse keys | Group related notifications |
| Real-time (SignalR) | Instant delivery via WebSocket |
| Badge count | Unread count endpoint for UI badge |
Multi-Channel Sending
Send the same notification to multiple channels:
csharp
// Email + In-App
var notification = new SendNotificationCommand
{
Channel = NotificationChannel.Email,
Recipients = [new RecipientDto("user@example.com")],
TemplateSlug = "order-confirmation",
TemplateData = templateData
};
await _mediator.Send(notification, ct);
// Also create an in-app notification
await _mediator.Send(new SendInAppNotificationCommand
{
UserId = userId,
Title = "Order Confirmed",
Body = $"Your order #{orderId} is confirmed",
Category = "orders",
ActionUrl = $"/orders/{orderId}"
}, ct);Provider Abstraction
Each channel is defined by an abstraction in GrydNotifications.Core:
| Abstraction | Description |
|---|---|
IEmailSender | Email delivery (MailKit, SendGrid) |
IPushNotificationSender | Push delivery (FCM) |
IInAppNotificationSender | In-app create + SignalR broadcast |
INotificationTemplateRenderer | Template compilation + rendering (Scriban) |
INotificationQueue | Async processing queue |
To swap a provider, register your implementation:
csharp
// Replace the default email sender
services.AddSingleton<IEmailSender, MyCustomEmailSender>();