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 INotificationChannel keyed by channel name ("email", "sms", "push", etc.). The implementation class (SendGridChannel) is internal — you never reference it directly. You only use INotificationChannel.


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.