PrimusSaaS.Notifications
2.0.1
dotnet add package PrimusSaaS.Notifications --version 2.0.1
NuGet\Install-Package PrimusSaaS.Notifications -Version 2.0.1
<PackageReference Include="PrimusSaaS.Notifications" Version="2.0.1" />
<PackageVersion Include="PrimusSaaS.Notifications" Version="2.0.1" />
<PackageReference Include="PrimusSaaS.Notifications" />
paket add PrimusSaaS.Notifications --version 2.0.1
#r "nuget: PrimusSaaS.Notifications, 2.0.1"
#:package PrimusSaaS.Notifications@2.0.1
#addin nuget:?package=PrimusSaaS.Notifications&version=2.0.1
#tool nuget:?package=PrimusSaaS.Notifications&version=2.0.1
PrimusSaaS.Notifications
Notification building blocks for Primus SaaS applications. The library provides SMTP email delivery, SMS via Twilio/AWS SNS/Azure Communication Services, file-based templating with Fluid, an in-memory queue with a background worker, and instrumentation hooks for observability. For production, use a durable queue if you need persistence across restarts.
Features
- SMTP email channel with retry/backoff and structured logging
- Multi-provider SMS support: Twilio, AWS SNS, Azure Communication Services
- File-based Liquid templates with caching and subject/body conventions
- In-memory bounded queue plus background worker for async delivery
- Lightweight diagnostics via
NotificationMetricsandNotificationRuntimeStats - Opt-in logger channel for non-email scenarios or local development
NotificationResultsurface that reports per-channel outcomes and can throw on failure to prevent silent drops- Channel health service (
NotificationHealthService) to report configuration status (email/SMS/logger) - Optional startup template validation to catch Liquid syntax issues before runtime
Installation
dotnet add package PrimusSaaS.Notifications --version 2.0.0
Minimal Complete Example (Copy-Paste Ready)
This is a complete, working Program.cs for a minimal ASP.NET Core app:
using PrimusSaaS.Notifications;
using PrimusSaaS.Notifications.Abstractions;
var builder = WebApplication.CreateBuilder(args);
// Configure notifications (logger mode for development)
builder.Services.AddPrimusNotifications(notifications =>
{
notifications
.UseSmtp(opts =>
{
opts.Host = "smtp.example.com"; // Replace with your SMTP host
opts.Port = 587;
opts.Username = "your-username"; // Or use builder.Configuration["Smtp:Username"]
opts.Password = "your-password"; // Or use builder.Configuration["Smtp:Password"]
opts.FromAddress = "no-reply@yourdomain.com";
opts.FromName = "My App";
opts.EnableSsl = true;
})
.UseLogger(); // Also logs notifications (useful for development)
});
var app = builder.Build();
// Send an email
app.MapPost("/send-email", async (INotificationService notifications) =>
{
var result = await notifications.SendEmailAsync(
"recipient@example.com",
"Hello from PrimusSaaS!",
"<h1>Welcome!</h1><p>Your account has been created.</p>"
);
return result.Success
? Results.Ok("Email sent!")
: Results.Problem($"Failed: {result.FailureReason}");
});
// Send an SMS (logs by default, configure Twilio for real SMS)
app.MapPost("/send-sms", async (INotificationService notifications) =>
{
var result = await notifications.SendSmsAsync(
"+15551234567",
"Your verification code is 123456"
);
return result.Success
? Results.Ok("SMS sent!")
: Results.Problem($"Failed: {result.FailureReason}");
});
app.Run();
Using Dependency Injection (Recommended)
// In your controller or service, inject INotificationService:
public class MyController : ControllerBase
{
private readonly INotificationService _notifications;
public MyController(INotificationService notifications)
{
_notifications = notifications;
}
[HttpPost("welcome")]
public async Task<IActionResult> SendWelcome(string email, string name)
{
var result = await _notifications.SendEmailAsync(
email,
$"Welcome, {name}!",
$"<p>Hello {name}, thanks for joining!</p>"
);
return result.Success ? Ok() : StatusCode(500, result.FailureReason);
}
}
Quick start (ASP.NET Core)
builder.Services.AddPrimusNotifications(notifications =>
{
notifications
.UseSmtp(opts =>
{
opts.Host = builder.Configuration["Smtp:Host"];
opts.Port = 587;
opts.Username = builder.Configuration["Smtp:Username"];
opts.Password = builder.Configuration["Smtp:Password"];
opts.FromAddress = "no-reply@primus.local";
opts.FromName = "Primus Notifications";
opts.EnableSsl = true;
})
.UseFileTemplates(Path.Combine(builder.Environment.ContentRootPath, "NotificationTemplates"), validateOnStartup: true)
.UseInMemoryQueue(options =>
{
options.BoundedCapacity = 500;
options.MaxParallelHandlers = 2;
})
.UseSms() // logs SMS payloads by default; swap ISmsSender for your provider
.UseLogger()
.ConfigureDispatch(opts =>
{
opts.ThrowOnFailure = true;
opts.FallbackToLogger = false;
opts.QueueOnFailure = true;
});
});
SMTP tuning (actual property names)
TimeoutSeconds(default30)MaxRetryCount(default2)RetryBaseDelayMs(default200, exponential backoff)
Older names you may see online (
Timeout,RetryCount,RetryDelayMs) do not exist. Use the properties above.
Create a notification type:
public record WelcomeNotification(string Email, string Name) : INotification
{
public string Type => "Welcome";
public object Data => new { Name };
public IEnumerable<string> Channels => new[] { "Email", "Logger" };
public Recipient Recipient => new() { Email = Email, Name = Name };
}
Render templates from NotificationTemplates/Welcome/EmailSubject.liquid and NotificationTemplates/Welcome/EmailBody.liquid, then enqueue or send directly:
var notification = new WelcomeNotification("ada@example.com", "Ada");
// Async queue (recommended)
await queue.EnqueueAsync(notification, cancellationToken);
// Or immediate dispatch
var result = await notificationService.SendAsync(notification, cancellationToken);
if (!result.Success)
{
// Log/return a 500 so the failure is visible
logger.LogError("Notification failed: {Reason}", result.FailureReason);
}
Send one-off notifications without creating a custom INotification type:
await notificationService.SendEmailAsync("user@example.com", "Welcome", "<p>Thanks for signing up!</p>");
await notificationService.SendSmsAsync("+15551234567", "Your code is 123456");
Both helpers return NotificationResult and will throw NotificationFailedException when NotificationOptions.ThrowOnFailure is enabled (default) and nothing was delivered.
Plug in your SMS provider by implementing ISmsSender and registering it:
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseSms<MySmsSender>(); // replaces the default logging sender
});
Twilio SMS (Built-in)
The library includes a production-ready Twilio SMS sender. Configure it with your Twilio credentials:
Option 1: Inline configuration
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseTwilio(opts =>
{
opts.AccountSid = builder.Configuration["Twilio:AccountSid"];
opts.AuthToken = builder.Configuration["Twilio:AuthToken"];
opts.FromNumber = builder.Configuration["Twilio:FromNumber"];
});
});
Option 2: From appsettings.json
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseTwilio(builder.Configuration);
});
{
"Twilio": {
"AccountSid": "your-account-sid",
"AuthToken": "your-auth-token",
"FromNumber": "+1234567890"
}
}
Send SMS
await notificationService.SendSmsAsync("+15551234567", "Your verification code is 123456");
If Twilio credentials are missing,
NotificationResult.ServiceUnavailablewill be true. Map to HTTP 503 in your API if desired.
Environment variables (examples):
Twilio__AccountSid=ACxxxxTwilio__AuthToken=...Twilio__FromNumber=+16205538468
Notes: Twilio trial accounts require verifying each recipient number before sending. Keep AuthToken in secrets/Key Vault/env vars-never commit secrets. Twilio options are validated when configured (can be disabled with
ValidateOnStartup = false). Email-only deployments without Twilio settings skip startup validation; the first SMS send will throw a helpful error if Twilio is still unconfigured. Missing credentials surface as service-unavailable.
Inspecting results
NotificationResult.Channelsis the per-channel breakdown (Channel,Status,Detail/Exception). (ChannelResultsdoes not exist.)ChannelUsedis set only when at least one channel succeeds.- For HTTP APIs, map
ServiceUnavailableto 503 if your SMS provider is offline.
AWS SNS SMS (Built-in)
AWS Simple Notification Service (SNS) is an alternative to Twilio with global coverage and pay-per-use pricing.
Option 1: Inline configuration
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseAwsSns(opts =>
{
opts.AccessKeyId = builder.Configuration["AwsSns:AccessKeyId"];
opts.SecretAccessKey = builder.Configuration["AwsSns:SecretAccessKey"];
opts.Region = "us-east-1";
opts.SmsType = "Transactional"; // or "Promotional"
});
});
Option 2: From appsettings.json
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseAwsSns(builder.Configuration);
});
{
"AwsSns": {
"AccessKeyId": "AKIAIOSFODNN7EXAMPLE",
"SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"Region": "us-east-1",
"SmsType": "Transactional",
"SenderId": "MyApp"
}
}
Notes:
SmsTypecan beTransactional(higher priority, for OTP/alerts) orPromotional(marketing messages)SenderIdis optional and only supported in some countries (not US)- Ensure your IAM user has
sns:Publishpermission- Request SMS spending limits increase in AWS Support Center if needed
Azure Communication Services SMS (Built-in)
Azure Communication Services provides SMS capabilities integrated with the Azure ecosystem.
Option 1: Inline configuration
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseAzureCommunicationServices(opts =>
{
opts.ConnectionString = builder.Configuration["AzureCommunicationServices:ConnectionString"];
opts.FromNumber = "+18001234567"; // Your provisioned Azure phone number
opts.EnableDeliveryReport = true;
});
});
Option 2: From appsettings.json
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseAzureCommunicationServices(builder.Configuration);
});
{
"AzureCommunicationServices": {
"ConnectionString": "endpoint=https://your-resource.communication.azure.com/;accesskey=...",
"FromNumber": "+18001234567",
"EnableDeliveryReport": true,
"Tag": "my-app"
}
}
Notes:
- Get a phone number in Azure Portal Communication Services Phone numbers
- Connection string is in Azure Portal Communication Services Keys
Tagis optional and helps with analytics in Azure Portal- Azure SMS supports toll-free and local numbers in 180+ countries
Choosing an SMS Provider
| Provider | Best For | Key Advantages |
|---|---|---|
| Twilio | Most use cases | Widest carrier support, rich features, excellent docs |
| AWS SNS | AWS-heavy stack | Pay-per-use, no monthly fees, great for transactional |
| Azure | Azure-heavy stack | Native Azure integration, combined billing |
SMTP configuration
SmtpOptions supports host, port, credentials, SSL, sender info, timeout, retry count, and exponential backoff base delay. Validation runs during DI configuration to catch missing host/port/from settings early.
Templates
- File layout:
{BasePath}/{NotificationType}/EmailSubject.liquidandEmailBody.liquid - SMS layout:
{BasePath}/{NotificationType}/SmsBody.liquid(optional) - Fluid syntax with anonymous/POCO models
- Templates are cached after first parse for performance
- See
TEMPLATE_GUIDE.mdfor conventions and examples
Direct helpers (SendEmailAsync(to, subject, body) / SendSmsAsync(phone, message)) skip template lookup entirely to avoid confusing failures when you just want to send raw content.
Set validateOnStartup: true in UseFileTemplates to fail fast on template syntax errors during app boot.
Health endpoint
Add a minimal endpoint to surface channel configuration status:
app.MapGet("/health/notifications", async (NotificationHealthService health) =>
{
var snapshot = await health.GetChannelHealthAsync();
return Results.Json(snapshot);
});
Background processing
Calling .UseInMemoryQueue() registers InMemoryNotificationQueue and NotificationBackgroundService to drain the queue using scoped NotificationService instances. Configure capacity, max parallel handlers, and retry/backoff per NotificationQueueOptions.
Persistent queues (Redis or Azure Service Bus)
Use a durable queue instead of the in-memory queue:
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseRedisQueue(opts =>
builder.Configuration.GetSection("Notifications:RedisQueue").Bind(opts));
});
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseAzureServiceBusQueue(opts =>
builder.Configuration.GetSection("Notifications:ServiceBusQueue").Bind(opts));
});
Delivery store (Redis)
Persist delivery outcomes to Redis:
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseRedisDeliveryStore(opts =>
builder.Configuration.GetSection("Notifications:RedisDeliveryStore").Bind(opts));
});
Distributed rate limiting (Redis)
Enable Redis-backed rate limiting for multi-instance deployments. This uses a fixed window per key.
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseRedisRateLimiting(opts =>
builder.Configuration.GetSection("Notifications:RedisRateLimit").Bind(opts));
});
builder.Services.Configure<NotificationOptions>(o =>
{
o.RateLimit.Enabled = true;
o.RateLimit.MaxPerWindow = 100;
o.RateLimit.WindowSeconds = 60;
});
Diagnostics
Expose metrics via NotificationMetrics (System.Diagnostics.Metrics instruments) and quick in-process stats via NotificationRuntimeStats.GetSnapshot().
Building and packing
dotnet test sdk/dotnet/Primus.Notifications.Tests/Primus.Notifications.Tests.csproj
dotnet pack sdk/dotnet/Primus.Notifications/Primus.Notifications.csproj -c Release
Packages are emitted to nupkg/ with symbols and README included.
Troubleshooting
Common Issues
"The type or namespace name 'Notifications' does not exist in the namespace 'Primus'"
Make sure you're using the correct namespace:
// Correct
using PrimusSaaS.Notifications;
// Wrong (old namespace)
using Primus.Notifications;
"Cannot resolve INotificationService from DI container"
Ensure you've registered the notifications services in Program.cs:
builder.Services.AddPrimusNotifications(notifications =>
{
notifications.UseSmtp(opts => { /* ... */ });
});
Email not being sent
- Check your SMTP credentials are correct
- Verify port 587 (TLS) or 465 (SSL) is open
- Check
result.Successandresult.FailureReason:
var result = await notifications.SendEmailAsync(...);
if (!result.Success)
{
Console.WriteLine($"Failed: {result.FailureReason}");
}
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- AWSSDK.SimpleEmail (>= 3.7.401)
- AWSSDK.SimpleNotificationService (>= 3.7.400.43)
- Azure.Communication.Sms (>= 1.0.1)
- Azure.Identity (>= 1.11.4)
- Azure.Messaging.ServiceBus (>= 7.17.5)
- Fluid.Core (>= 2.5.0)
- MailKit (>= 4.3.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 7.0.0)
- Microsoft.Extensions.Hosting (>= 7.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 7.0.0)
- Microsoft.Extensions.Http (>= 7.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 7.0.0)
- Microsoft.Extensions.Options (>= 7.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 7.0.0)
- SendGrid (>= 9.29.3)
- StackExchange.Redis (>= 2.7.33)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
v2.0.0:
- Standardized Framework Release.
- Renamed all packages to PrimusSaaS.* namespace.
- Synchronized versions across the entire suite.
- Enhanced metadata and fixed consistency issues.