Skip to content

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:

RefusalCode
The host did not register GrydFiles422 NOTIFICATION_ATTACHMENT_DISABLED_UNPROCESSABLE
Still being scanned409 FILE_NOT_AVAILABLE_CONFLICT — send again once it is released
Infected (quarantined)409 FILE_INFECTED_CONFLICT — never attached
Purged409 FILE_PURGED_CONFLICT
Another tenant's file, or none404 FILE_NOT_FOUND
Uploaded under another profile422 NOTIFICATION_ATTACHMENT_PROFILE_UNPROCESSABLE
Above MaxAttachmentSizeMb422 NOTIFICATION_ATTACHMENT_TOO_LARGE_UNPROCESSABLE
More than MaxAttachmentsPerEmail, or the same file twice422 NOTIFICATION_ATTACHMENT_LIMIT_UNPROCESSABLE
A channel other than e-mail422 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 ​

LimitDefaultConfigurable
Max attachment size10 MB✅ MaxAttachmentSizeMb
Max attachments/email5✅ MaxAttachmentsPerEmail
Max recipients (To+Cc+Bcc)50✅ MaxRecipientsPerEmail
Bulk batch size100✅ 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 ​

  1. Create a Firebase project at console.firebase.google.com
  2. Download the service account JSON
  3. Set the environment variable:
bash
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/firebase-adminsdk.json"
  1. 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:

FeatureDescription
RegistrationStore tokens when users sign in on devices
DeactivationAuto-deactivate tokens reported invalid by FCM
Stale cleanupRemove tokens unused for StaleTokenThreshold (default 90 days)
Multicast limitAuto-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 ​

FeatureDescription
Read/Unread trackingMark individual or all as read
CategoriesFilter by category (alerts, tasks, comments, etc.)
ExpirationAuto-expire with ExpiresAfter
Collapse keysGroup related notifications
Real-time (SignalR)Instant delivery via WebSocket
Badge countUnread 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:

AbstractionDescription
IEmailSenderEmail delivery (MailKit, SendGrid)
IPushNotificationSenderPush delivery (FCM)
IInAppNotificationSenderIn-app create + SignalR broadcast
INotificationTemplateRendererTemplate compilation + rendering (Scriban)
INotificationQueueAsync processing queue

To swap a provider, register your implementation:

csharp
// Replace the default email sender
services.AddSingleton<IEmailSender, MyCustomEmailSender>();

Released under the MIT License.