Custom Notification Channels
This guide explains how to extend Shesha's notification framework with a custom delivery channel, such as Slack, Microsoft Teams, or WhatsApp. If you only need to send notifications through the built-in Email and SMS channels, see the main Notifications page instead.
Adding a custom channel involves four steps:
- Implement the
INotificationChannelSenderinterface. - Register it in your dependency injection container.
- Create a
NotificationChannelConfigrecord in the database. - Add notification templates for the new channel.
Step 1: Implement INotificationChannelSender
The interface defines two methods:
public interface INotificationChannelSender
{
/// <summary>
/// Returns the channel-specific recipient identifier for a Person
/// (e.g. email address, phone number, Slack user ID).
/// </summary>
string? GetRecipientId(Person person);
/// <summary>
/// Sends the notification message through this channel.
/// </summary>
Task<SendStatus> SendAsync(
IMessageSender? sender,
IMessageReceiver receiver,
NotificationMessage message,
List<EmailAttachment>? attachments = null);
}
| Method | Purpose |
|---|---|
GetRecipientId | Maps a Person entity to the address this channel uses. The built-in EmailChannelSender returns Person.EmailAddress1, and SmsChannelSender returns Person.MobileNumber1. Your implementation should return whatever identifier your channel needs, such as a Slack user ID or a WhatsApp number. |
SendAsync | Performs the actual delivery. Return SendStatus.Success() on success or SendStatus.Failed("reason") on failure. When a failure is returned, the framework's retry mechanism re-attempts delivery automatically, up to three times. The NotificationMessage parameter contains the already-rendered Subject and Message, so all template placeholders are resolved before your sender is called. |
Example - A Slack channel sender:
using Shesha.Domain;
using Shesha.Email.Dtos;
using Shesha.Notifications;
using Shesha.Notifications.Dto;
using Shesha.Notifications.MessageParticipants;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace MyProject.Notifications
{
/// <summary>
/// Sends notifications via Slack using incoming webhooks.
/// </summary>
public class SlackChannelSender : INotificationChannelSender
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly string _webhookUrl;
public SlackChannelSender(
IHttpClientFactory httpClientFactory,
IConfiguration configuration)
{
_httpClientFactory = httpClientFactory;
_webhookUrl = configuration["Notifications:Slack:WebhookUrl"];
}
public string? GetRecipientId(Person person)
{
// Return the channel-specific identifier for this person.
// This could be a Slack user ID stored on a custom property,
// or fall back to email for Slack's email-based lookup.
return person.EmailAddress1;
}
public async Task<SendStatus> SendAsync(
IMessageSender? sender,
IMessageReceiver receiver,
NotificationMessage message,
List<EmailAttachment>? attachments = null)
{
var recipientAddress = receiver.GetAddress(this);
if (string.IsNullOrWhiteSpace(recipientAddress))
return SendStatus.Failed("No recipient address available");
try
{
var payload = new
{
text = $"*{message.Subject}*\n{message.Message}"
};
var client = _httpClientFactory.CreateClient();
var content = new StringContent(
JsonSerializer.Serialize(payload),
Encoding.UTF8,
"application/json");
var response = await client.PostAsync(_webhookUrl, content);
return response.IsSuccessStatusCode
? SendStatus.Success(null)
: SendStatus.Failed($"Slack returned {response.StatusCode}");
}
catch (Exception ex)
{
return SendStatus.Failed(ex.Message);
}
}
}
}
Step 2: Register the channel sender
Add the channel sender to your dependency injection container in Startup.cs:
services.AddTransient<INotificationChannelSender, SlackChannelSender>();
Step 3: Create the channel configuration record
The framework needs a NotificationChannelConfig record in the database to know about your channel. You can create this through the admin UI or with a database migration.
The most important property is SenderTypeName. This must be the class name of your INotificationChannelSender implementation, matched on the simple type name (for example SlackChannelSender, not the fully qualified name). The framework uses this name to resolve which sender to invoke at runtime.
[Migration(20250227100000)]
public class M20250227100000 : Migration
{
public override void Up()
{
Insert.IntoTable("Core_NotificationChannelConfigs")
.Row(new
{
Id = "B1C2D3E4-F5A6-7890-1234-567890ABCDEF",
Name = "Slack",
Description = "Slack incoming webhook notifications",
SupportedFormatLkp = 1, // 1 = PlainText
SupportedMechanismLkp = 1, // 1 = Direct
SenderTypeName = "SlackChannelSender",
StatusLkp = 1, // 1 = Enabled
SupportsAttachment = false,
CreationTime = DateTime.UtcNow
});
}
public override void Down()
{
Delete.FromTable("Core_NotificationChannelConfigs")
.Row(new { Id = "B1C2D3E4-F5A6-7890-1234-567890ABCDEF" });
}
}
Channel configuration properties
| Property | Column | Values |
|---|---|---|
| SupportedFormat | SupportedFormatLkp | 1 = PlainText, 2 = RichText, 3 = EnhancedText |
| SupportedMechanism | SupportedMechanismLkp | 1 = Direct, 2 = BulkSend, 4 = Broadcast |
| MaxMessageSize | MaxMessageSize | Maximum character count (e.g. 160 for SMS, 0 for unlimited) |
| Status | StatusLkp | 1 = Enabled, 2 = Disabled, 3 = Suppressed |
| SupportsAttachment | SupportsAttachment | true or false |
Step 4: Create templates for the new channel
Add notification templates whose MessageFormat matches your channel's SupportedFormat. For a PlainText channel like Slack, use SMS-style templates:
this.Shesha().NotificationUpdate("MyModule", "OrderConfirmed")
.AddSmsTemplate(
"C3D4E5F6-A7B8-9012-CDEF-123456789012".ToGuid(),
"Order Confirmed Slack",
"Order {{OrderNumber}} confirmed for {{CustomerName}}");
If your custom channel uses the PlainText format, it shares templates with the SMS channel, because both match on PlainText. If you need channel-specific templates, consider using EnhancedText as the supported format for your custom channel to keep its templates separate from SMS.
How the framework routes to your channel
When a notification is sent, the framework decides which channel or channels to use (see Channel selection logic on the main notifications page). For each selected channel, it:
- Looks up the
NotificationChannelConfigrecord. - Resolves the
INotificationChannelSenderimplementation by matching its simple type name againstSenderTypeName. - Finds a
NotificationTemplatewhoseMessageFormatmatches the channel'sSupportedFormat. - Renders the template placeholders against the notification data model.
- Calls your
SendAsyncmethod with the renderedNotificationMessage.
If SendAsync returns a failure, the framework queues a retry, up to three attempts with delays of 10, 20, and 20 seconds.