Usage Tiers
RecurPixel.Notify supports three usage patterns. Pick the one that fits your use case. All three share the same Core contracts — your code stays consistent no matter which tier you’re on.
Tier 1 — Direct Provider Usage
Best for: Single-channel use cases, auth flows, integrating one provider into an existing app, or situations where you want zero orchestration overhead.
Install only the provider package you need:
dotnet add package RecurPixel.Notify.Email.SendGrid
Register it explicitly in DI:
builder.Services.AddSendGridChannel(options =>
{
options.ApiKey = builder.Configuration["Notify:Email:SendGrid:ApiKey"];
options.FromEmail = "no-reply@yourapp.com";
options.FromName = "Your App";
});
Inject and use the channel directly:
public class AuthService(
[FromKeyedServices("email")] INotificationChannel emailChannel)
{
public async Task SendOtpAsync(string email, string otp)
{
var result = await emailChannel.SendAsync(new NotificationPayload
{
To = email,
Subject = "Your OTP Code",
Body = $"Your OTP is {otp}. Valid for 5 minutes."
});
if (!result.Success)
logger.LogWarning("OTP email failed: {Error}", result.Error);
}
}
What you get: One channel, one provider, minimal footprint. No orchestrator. No event system. No INotifyService.
What you don’t get: Multi-channel dispatch, event definitions, retry/fallback chains, conditions, or INotifyService. Add the Orchestrator package to unlock those.
Note on DI registration: Each adapter registers itself as
INotificationChannelkeyed by channel name ("email","sms","push", etc.). The implementation class (SendGridChannel) isinternal— you never reference it directly. You only useINotificationChannel.
Tier 2 — Orchestrator + Selective Providers
Best for: Applications with multiple notification channels, event-driven sends, retry/fallback requirements, or where which provider fires should be config-driven rather than code-driven.
Install Core, Orchestrator, and the provider packages you need:
dotnet add package RecurPixel.Notify
dotnet add package RecurPixel.Notify.Email.SendGrid
dotnet add package RecurPixel.Notify.Sms.Twilio
dotnet add package RecurPixel.Notify.Push.Fcm
dotnet add package RecurPixel.Notify.InApp
Register through the single entry point with two lambdas — the first binds configuration, the second defines events and hooks:
// InApp needs a separate call because its handler is code, not JSON config
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.placed", e => e
.UseChannels("email", "sms", "push")
.WithCondition("sms", ctx => ctx.User.PhoneVerified)
.WithCondition("push", ctx => ctx.User.PushEnabled)
.WithFallback("whatsapp", "sms", "email")
.WithRetry(maxAttempts: 3, delayMs: 500)
);
});
The Orchestrator reads your NotifyOptions, sees which providers are configured, and registers only those adapters. You do not call AddSendGridChannel() yourself in Tier 2 — the Orchestrator handles that.
Inject INotifyService and trigger events or send directly:
public class OrderService(INotifyService notify)
{
public async Task PlaceOrderAsync(Order order)
{
// Orchestrated — event config drives which channels fire
var result = await notify.TriggerAsync("order.placed", context);
// Direct — bypass orchestration for time-critical sends
await notify.Email.SendAsync(otpPayload);
await notify.Sms.SendAsync(otpPayload);
}
}
What you get: Everything — INotifyService, event definitions, parallel dispatch, conditions, retry with exponential backoff, cross-channel fallback chains, bulk send via BulkTriggerAsync, delivery hooks, and direct channel access via notify.Email, notify.Sms, etc.
Tier 3 — Full SDK Meta-Package
Best for: New projects that want everything configured and ready with minimal setup decisions.
dotnet add package RecurPixel.Notify.Sdk
dotnet add package RecurPixel.Notify.InApp
The SDK meta-package pulls Core + Orchestrator + all provider adapters. Registration is identical to Tier 2:
builder.Services.AddInAppChannel(opts => opts.UseHandler<IApplicationDbContext>(...));
builder.Services.AddRecurPixelNotify(
notifyOptions =>
{
builder.Configuration.GetSection("Notify").Bind(notifyOptions);
},
orchestratorOptions =>
{
orchestratorOptions.DefineEvent("order.placed", e => e
.UseChannels("email", "sms", "push", "inapp")
.WithCondition("sms", ctx => ctx.User.PhoneVerified)
.WithCondition("push", ctx => ctx.User.PushEnabled));
});
Only providers you configure in appsettings.json are registered. Unconfigured providers are not registered at all — zero DI entries, zero runtime overhead.
Comparison
| Tier 1 | Tier 2 | Tier 3 | |
|---|---|---|---|
| Manual provider registration | ✅ Required | ❌ Automatic | ❌ Automatic |
INotifyService |
❌ Not available | ✅ Yes | ✅ Yes |
| Event definitions | ❌ No | ✅ Yes | ✅ Yes |
| Retry / fallback | ❌ No | ✅ Yes | ✅ Yes |
| Conditions | ❌ No | ✅ Yes | ✅ Yes |
| Parallel dispatch | ❌ No | ✅ Yes | ✅ Yes |
| Bulk send | ✅ Via channel directly | ✅ Yes | ✅ Yes |
| Delivery hook | ❌ No | ✅ Yes | ✅ Yes |
| Package footprint | Minimal | Medium | Larger |
| Config-driven routing | ❌ No | ✅ Yes | ✅ Yes |
Can I mix tiers?
Yes. The most common pattern is using Tier 2 (Orchestrator) for most sends, while also calling individual channels directly for time-critical flows like OTP:
// Orchestrated — event config, conditions, fallback all apply
await notify.TriggerAsync("order.placed", context);
// Direct — fires immediately, no event lookup, no conditions
await notify.Sms.SendAsync(new NotificationPayload { To = phone, Body = $"OTP: {otp}" });
Direct channel access on INotifyService bypasses the event system but still goes through the configured provider. It is the correct pattern for auth flows and any send where latency matters.