SMS Notifications¶
The SMS notification channel (SmsNotificationChannel) provides reliable SMS delivery using the Curiosity.SMS infrastructure with queued processing, post-processing capabilities, and comprehensive error handling.
How it works¶
Architecture¶
The SMS notification system consists of:
- SmsNotificationChannel - Background service that processes SMS notifications from a queue
- SmsNotification - Represents an SMS to be sent with phone number, message, and optional parameters
- SmsNotificationBuilderBase - Abstract base class for building SMS notifications from metadata
- ISmsNotificationPostProcessor - Interface for post-processing after SMS sending
Processing Flow¶
sequenceDiagram
participant Client
participant Notificator
participant Builder
participant Channel
participant Provider
participant PostProcessor
Client->>Notificator: NotifyAsync(metadata)
Notificator->>Builder: BuildNotificationsAsync(metadata)
Builder->>Notificator: SmsNotification[]
Notificator->>Channel: SendNotificationAsync(notification)
Channel->>Provider: SendSmsAsync(phone, message)
Provider->>Channel: Response<SmsSentResult>
Channel->>PostProcessor: ProcessAsync(notification, result)
Channel->>Notificator: Task completion
Notificator->>Client: Result
Queue Processing¶
- Each
SmsNotificationChannelprocesses notifications sequentially - Uses
BlockingCollection<NotificationQueueItem<SmsNotification>>for thread-safe queuing - Provides
TaskCompletionSourcefor async completion tracking - Handles cancellation and shutdown gracefully
SMS Structure¶
public class SmsNotification : INotification
{
public string ChannelType => "curiosity.notifications.sms";
public string PhoneNumber { get; } // Recipient phone number
public string Message { get; } // SMS message content
public ISmsExtraParams? ExtraParams { get; } // Provider-specific parameters
}
Available providers¶
The SMS notification channel uses the ISmsSender interface from Curiosity.SMS package, which supports multiple providers:
Twilio¶
services.AddCuriosityTwilioSender(options =>
{
options.AccountSid = "your-account-sid";
options.AuthToken = "your-auth-token";
options.FromPhoneNumber = "+1234567890";
});
AWS SNS¶
services.AddCuriosityAwsSnsSender(options =>
{
options.AccessKey = "your-access-key";
options.SecretKey = "your-secret-key";
options.Region = "us-east-1";
});
Nexmo/Vonage¶
services.AddCuriosityNexmoSender(options =>
{
options.ApiKey = "your-api-key";
options.ApiSecret = "your-api-secret";
options.FromNumber = "YourBrand";
});
In-Memory (Testing)¶
How to add custom provider?¶
Step 1: Implement ISmsSender¶
public class CustomSmsSender : ISmsSender
{
public async Task<Response<SmsSentResult>> SendSmsAsync(
string phoneNumber,
string message,
CancellationToken cancellationToken = default)
{
try
{
// Your custom SMS sending logic here
var messageId = await SendSmsViaCustomProvider(phoneNumber, message);
var result = new SmsSentResult
{
MessageId = messageId,
Status = SmsStatus.Sent,
SentAt = DateTime.UtcNow
};
return Response<SmsSentResult>.Successful(result);
}
catch (AuthenticationException ex)
{
return Response<SmsSentResult>.Failed(new Error(1, ex.Message));
}
catch (RateLimitException ex)
{
return Response<SmsSentResult>.Failed(new Error(4, ex.Message));
}
catch (InsufficientFundsException ex)
{
return Response<SmsSentResult>.Failed(new Error(5, ex.Message));
}
catch (DeliveryException ex)
{
return Response<SmsSentResult>.Failed(new Error(6, ex.Message));
}
catch (Exception ex)
{
return Response<SmsSentResult>.Failed(new Error(0, ex.Message));
}
}
public async Task<Response<SmsSentResult>> SendSmsAsync(
string phoneNumber,
string message,
ISmsExtraParams extraParams,
CancellationToken cancellationToken = default)
{
// Handle extra parameters specific to your provider
var customParams = extraParams as CustomSmsExtraParams;
// Implementation with extra parameters
return await SendSmsAsync(phoneNumber, message, cancellationToken);
}
}
Step 2: Create Custom Extra Parameters (Optional)¶
public class CustomSmsExtraParams : ISmsExtraParams
{
public string Priority { get; set; } = "normal";
public string Category { get; set; }
public DateTime? ScheduledTime { get; set; }
public Dictionary<string, string> Tags { get; set; } = new();
}
Step 3: Register in IoC¶
public static class IoCExtensions
{
public static IServiceCollection AddCustomSmsSender(
this IServiceCollection services,
Action<CustomSmsOptions> configure)
{
services.Configure(configure);
services.AddSingleton<ISmsSender, CustomSmsSender>();
return services;
}
}
Step 4: Configure and Use¶
// In Startup.cs or Program.cs
services.AddCustomSmsSender(options =>
{
options.ApiEndpoint = "https://api.customprovider.com/sms";
options.ApiKey = "your-api-key";
});
services.AddCuriositySmsChannel();
Step 5: Create Custom Builder (Optional)¶
public class CustomSmsNotificationBuilder : SmsNotificationBuilderBase<YourNotificationMetadata>
{
protected override async Task<IReadOnlyList<SmsNotification>> BuildNotificationsAsync(
YourNotificationMetadata metadata,
CancellationToken cancellationToken = default)
{
var extraParams = new CustomSmsExtraParams
{
Priority = "high",
Category = "user-notifications",
Tags = new Dictionary<string, string> { { "user-id", metadata.UserId } }
};
var notification = new SmsNotification(
metadata.PhoneNumber,
await GenerateMessage(metadata),
extraParams
);
return new[] { notification };
}
}
Error Handling¶
The SMS channel maps provider errors to standardized notification error codes:
var notificationCode = error.Code switch
{
1 => NotificationErrorCode.Auth, // Authentication error
2 => NotificationErrorCode.Communication, // Communication error
4 => NotificationErrorCode.RateLimit, // Rate limit exceeded
5 => NotificationErrorCode.NoMoney, // Insufficient funds
6 => NotificationErrorCode.DeliveryError, // Delivery failure
_ => NotificationErrorCode.Unknown, // Unknown error
};
Post-Processing¶
Implement ISmsNotificationPostProcessor for actions after SMS sending:
public class SmsAnalyticsProcessor : ISmsNotificationPostProcessor
{
public async Task ProcessAsync(
SmsNotification notification,
Response<SmsSentResult> result,
CancellationToken cancellationToken = default)
{
// Log analytics, update database, send webhooks, etc.
await analyticsService.TrackSmsSent(new SmsAnalytics
{
PhoneNumber = notification.PhoneNumber,
Message = notification.Message,
Success = result.IsSuccess,
MessageId = result.Data?.MessageId,
ErrorCode = result.Errors?.FirstOrDefault()?.Code,
SentAt = DateTime.UtcNow
});
}
}
// Register in IoC
services.AddSmsNotificationPostProcessor<SmsAnalyticsProcessor>();
Common Usage Patterns¶
Two-Factor Authentication¶
public class TwoFactorSmsBuilder : SmsNotificationBuilderBase<TwoFactorMetadata>
{
protected override async Task<IReadOnlyList<SmsNotification>> BuildNotificationsAsync(
TwoFactorMetadata metadata,
CancellationToken cancellationToken = default)
{
var message = $"Your verification code is: {metadata.Code}. Do not share this code.";
var notification = new SmsNotification(
metadata.PhoneNumber,
message
);
return new[] { notification };
}
}
Order Status Updates¶
public class OrderStatusSmsBuilder : SmsNotificationBuilderBase<OrderStatusMetadata>
{
protected override async Task<IReadOnlyList<SmsNotification>> BuildNotificationsAsync(
OrderStatusMetadata metadata,
CancellationToken cancellationToken = default)
{
var message = metadata.Status switch
{
OrderStatus.Confirmed => $"Order #{metadata.OrderNumber} confirmed. Estimated delivery: {metadata.EstimatedDelivery:d}",
OrderStatus.Shipped => $"Order #{metadata.OrderNumber} shipped. Tracking: {metadata.TrackingNumber}",
OrderStatus.Delivered => $"Order #{metadata.OrderNumber} delivered. Thank you for your purchase!",
_ => $"Order #{metadata.OrderNumber} status updated to {metadata.Status}"
};
var notification = new SmsNotification(
metadata.CustomerPhone,
message
);
return new[] { notification };
}
}
International SMS Considerations¶
public class InternationalSmsBuilder : SmsNotificationBuilderBase<InternationalNotificationMetadata>
{
protected override async Task<IReadOnlyList<SmsNotification>> BuildNotificationsAsync(
InternationalNotificationMetadata metadata,
CancellationToken cancellationToken = default)
{
// Format phone number with country code
var formattedPhone = FormatPhoneNumber(metadata.PhoneNumber, metadata.CountryCode);
// Localize message based on country
var message = await GetLocalizedMessage(metadata.MessageKey, metadata.CountryCode);
var extraParams = new CustomSmsExtraParams
{
Priority = "normal",
Category = "international",
Tags = new Dictionary<string, string>
{
{ "country", metadata.CountryCode },
{ "language", metadata.Language }
}
};
var notification = new SmsNotification(
formattedPhone,
message,
extraParams
);
return new[] { notification };
}
}