Skip to main content

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:

  1. Implement the INotificationChannelSender interface.
  2. Register it in your dependency injection container.
  3. Create a NotificationChannelConfig record in the database.
  4. 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);
}
MethodPurpose
GetRecipientIdMaps 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.
SendAsyncPerforms 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

PropertyColumnValues
SupportedFormatSupportedFormatLkp1 = PlainText, 2 = RichText, 3 = EnhancedText
SupportedMechanismSupportedMechanismLkp1 = Direct, 2 = BulkSend, 4 = Broadcast
MaxMessageSizeMaxMessageSizeMaximum character count (e.g. 160 for SMS, 0 for unlimited)
StatusStatusLkp1 = Enabled, 2 = Disabled, 3 = Suppressed
SupportsAttachmentSupportsAttachmenttrue 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}}");
Avoiding template collisions

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:

  1. Looks up the NotificationChannelConfig record.
  2. Resolves the INotificationChannelSender implementation by matching its simple type name against SenderTypeName.
  3. Finds a NotificationTemplate whose MessageFormat matches the channel's SupportedFormat.
  4. Renders the template placeholders against the notification data model.
  5. Calls your SendAsync method with the rendered NotificationMessage.

If SendAsync returns a failure, the framework queues a retry, up to three attempts with delays of 10, 20, and 20 seconds.