Getting Started
This guide walks you from a fresh .NET project to sending your first notification.
Prerequisites
- .NET 8 or later
- An ASP.NET Core application (or any .NET host with
IServiceCollection) - API credentials for at least one provider (e.g. a SendGrid API key, Twilio account, etc.)
Step 1 — Install packages
Choose the packages you need. You do not need to install everything.
# Option A: Full SDK — pulls all channels and providers
dotnet add package RecurPixel.Notify.Sdk
# Option B: RecurPixel.Notify + only the providers you use
dotnet add package RecurPixel.Notify
dotnet add package RecurPixel.Notify.Email.SendGrid
dotnet add package RecurPixel.Notify.Sms.Twilio
# Option C: Single channel, no orchestrator
dotnet add package RecurPixel.Notify.Email.SendGrid
See Usage Tiers to understand which option fits your use case.
Step 2 — Add configuration
Add a Notify section to appsettings.json:
{
"Notify": {
"Email": {
"Provider": "sendgrid",
"SendGrid": {
"ApiKey": "SG.xxxxxxxxxxxxxxxxxx",
"FromEmail": "no-reply@yourapp.com",
"FromName": "Your App"
}
},
"Sms": {
"Provider": "twilio",
"Twilio": {
"AccountSid": "ACxxxxxxxxxxxxxxxxxx",
"AuthToken": "xxxxxxxxxxxxxxxxxx",
"FromNumber": "+15550001234"
}
},
"Retry": {
"MaxAttempts": 3,
"DelayMs": 500,
"ExponentialBackoff": true
}
}
}
You only need to configure channels you actually use. Unconfigured channels are ignored.
Step 3 — Register in DI
In Program.cs, call AddRecurPixelNotify with two lambdas: the first binds your configuration, the second defines events and hooks.
builder.Services.AddRecurPixelNotify(
notifyOptions =>
{
builder.Configuration.GetSection("Notify").Bind(notifyOptions);
},
orchestratorOptions =>
{
// Define events and delivery hooks here — see Steps 5 and 6
});
The library reads your configuration, validates it at startup, and registers only the providers whose credentials are present. If a required credential is missing, startup throws — never silently at send time.
Step 4 — Inject and send
using RecurPixel.Notify; // INotifyService, TriggerResult, NotifyContext
using RecurPixel.Notify.Channels; // INotificationChannel
using RecurPixel.Notify.Configuration; // EmailOptions, NotifyOptions
public class OrderService(INotifyService notify, ILogger<OrderService> logger)
{
public async Task ConfirmOrderAsync(Order order)
{
var result = await notify.TriggerAsync("order.confirmed", new NotifyContext
{
User = new NotifyUser
{
UserId = order.UserId.ToString(),
Email = order.CustomerEmail,
Phone = order.CustomerPhone,
PhoneVerified = order.Customer.PhoneVerified,
},
Channels = new Dictionary<string, NotificationPayload>
{
["email"] = new() { Subject = "Order Confirmed", Body = emailHtml },
["sms"] = new() { Body = $"Your order #{order.Id} is confirmed." }
}
});
// Check the result
if (result.AllSucceeded)
{
logger.LogInformation("Order notification sent successfully via all channels");
}
else if (result.AnySucceeded)
{
// Some channels worked, some didn't
foreach (var failure in result.Failures)
logger.LogWarning("Channel {Channel} failed: {Error}", failure.Channel, failure.Error);
}
else
{
logger.LogError("All notification channels failed");
}
}
}
About TriggerResult: The return value is a TriggerResult object with the following properties:
ChannelResults—IReadOnlyList<NotifyResult>containing one result per channelAllSucceeded—bool, true if all channels succeededAnySucceeded—bool, true if at least one channel succeededFailures—IReadOnlyList<NotifyResult>, all channels that failedEventName— the event name you passed inUserId— the user ID from the context (for tracing)
Each NotifyResult contains:
| Property | Type | Description |
|---|---|---|
Channel |
string |
Logical channel name: "email", "sms", etc. |
Provider |
string? |
Provider that sent the message: "sendgrid", "twilio", etc. |
Success |
bool |
Whether the send succeeded |
Recipient |
string? |
Email address, phone number, device token, etc. |
ProviderId |
string? |
Provider’s own message ID for delivery tracking |
Error |
string? |
Error message (null when Success is true) |
UsedFallback |
bool |
True if this channel fired as a fallback |
SentAt |
DateTime |
UTC timestamp of the send attempt |
EventName |
string? |
Event name from TriggerAsync (null for direct sends) |
BulkBatchId |
string? |
Shared ID across all results in a BulkTriggerAsync call |
Subject |
string? |
Notification subject, populated from the payload |
Step 5 — (Optional) Define events at startup
Events let you configure which channels fire, conditions, retry, and fallback chains — all in one place. Add them inside the second lambda in Program.cs:
builder.Services.AddRecurPixelNotify(
notifyOptions =>
{
builder.Configuration.GetSection("Notify").Bind(notifyOptions);
},
orchestratorOptions =>
{
orchestratorOptions.DefineEvent("order.confirmed", e => e
.UseChannels("email", "sms")
.WithCondition("sms", ctx => ctx.User.PhoneVerified)
.WithRetry(maxAttempts: 3, delayMs: 500)
);
orchestratorOptions.DefineEvent("auth.otp", e => e
.UseChannels("sms")
.WithRetry(maxAttempts: 3, delayMs: 300)
);
});
If you call TriggerAsync with an event name that has no definition, all channels in NotifyContext.Channels are sent with global retry/fallback settings applied.
Step 6 — (Optional) Hook into delivery results
Plug in your own delivery log writer inside the same second lambda:
orchestratorOptions.OnDelivery<IApplicationDbContext>(async (result, db) =>
{
await db.NotificationLogs.AddAsync(new NotificationLog
{
Channel = result.Channel,
Provider = result.Provider,
Recipient = result.Recipient,
Status = result.Success ? "sent" : "failed",
ProviderId = result.ProviderId,
Error = result.Error,
SentAt = result.SentAt
});
await db.SaveChangesAsync();
});
OnDelivery is called for every individual send result — including each result within a bulk operation. The typed overload OnDelivery<TService> resolves TService from a fresh DI scope per call, so scoped services like DbContext are safe to use directly. You own the log table. We just call the hook.
Step 7 — (Optional) In-App channel
The InApp channel works differently from provider-backed channels: it has no config section and requires you to wire a handler that IS the delivery (e.g. writing to your database or pushing via SignalR). Register it as a separate call before AddRecurPixelNotify:
builder.Services.AddInAppChannel(opts =>
opts.UseHandler<IApplicationDbContext>(async (notification, db) =>
{
await db.Notifications.AddAsync(new Notification
{
UserId = notification.UserId,
Title = notification.Subject ?? string.Empty,
Message = notification.Body,
IsRead = false
});
await db.SaveChangesAsync();
return new NotifyResult { Success = true };
}));
builder.Services.AddRecurPixelNotify(
notifyOptions => builder.Configuration.GetSection("Notify").Bind(notifyOptions),
orchestratorOptions =>
{
orchestratorOptions.DefineEvent("order.confirmed", e => e
.UseChannels("email", "sms", "inapp")
.WithCondition("sms", ctx => ctx.User.PhoneVerified));
});
UseHandlerprovides the delivery implementation — it IS the send operation.OnDeliveryis an audit callback called after every channel send. They are distinct.
Step 8 — (Optional) Dashboard
The Dashboard package captures every send result automatically and serves a delivery log UI + REST API from inside your app. Install the two packages, register the store, and add the middleware:
dotnet add package RecurPixel.Notify.Dashboard
dotnet add package RecurPixel.Notify.Dashboard.EfCore
Option A — standalone context (own connection string, own migration):
builder.Services.AddNotifyDashboard(opts => opts.RequireRole = "Admin");
builder.Services.AddNotifyDashboardEfCore(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("NotifyDashboard")));
dotnet ef migrations add AddNotifyDashboard --context NotifyDashboardDbContext
dotnet ef database update --context NotifyDashboardDbContext
Option B — plug into your existing DbContext (recommended — one DB, one migration):
Add modelBuilder.AddNotifyDashboard() to your existing context’s OnModelCreating, then:
builder.Services.AddNotifyDashboard(opts => opts.RequireRole = "Admin");
builder.Services.AddDbContext<AppDbContext>(...); // your existing registration
builder.Services.AddNotifyDashboardEfCore<AppDbContext>();
dotnet ef migrations add AddNotifyDashboard --context AppDbContext
dotnet ef database update --context AppDbContext
Wire the middleware:
app.UseAuthentication();
app.UseAuthorization();
app.UseNotifyDashboard(); // serves at /notify-dashboard by default
The dashboard is now at /notify-dashboard. See Dashboard for the full reference including the REST API, custom stores, and auth options.
Next steps
- Quick Start — minimal code examples for each channel
- Usage Tiers — understand the three ways to use the library
- Examples — complete Tier 1, 2, and 3 setups with real code
- Adapter Reference — all providers, their config fields, and native bulk support
- Features — retry, fallback chains, conditions, bulk send, delivery hooks
- Dashboard — delivery log UI, REST API, and observability
- Migration Guide — upgrading from an older version