Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

twiks228/NotifyForge

Repository files navigation

NotifyForge

A universal notification library for .NET 8. One fluent API for Email, SMS, Telegram, Push, Slack, Discord and WhatsApp.

await notifier.SendAsync(n => n
 .To("alice@example.com", "Alice")
 .Phone("+79991234567")
 .TelegramChat("123456789")
 .UseTemplate("order-shipped")
 .WithData(new { Name = "Alice", OrderId = "ORD-001", TrackNumber = "TRK-999" })
 .Retry(3)
 .OnFailure(f => f.Channel(NotificationChannel.Sms))
);

Table of Contents


Why NotifyForge

Every notification channel ships its own SDK with its own authentication model, its own message format, and its own error handling. A typical project that needs email, SMS and Telegram ends up with three completely separate integrations:

// Email — one library, one pattern
var smtp = new SmtpClient("smtp.gmail.com");
var mail = new MailMessage();
mail.To.Add(user.Email);
mail.Subject = $"Order #{orderId} confirmed";
mail.Body = $"Hi {user.Name}, your order has been confirmed.";
await smtp.SendMailAsync(mail);
// SMS — a completely different library and pattern
TwilioClient.Init(accountSid, authToken);
await MessageResource.CreateAsync(
 to: new PhoneNumber(user.Phone),
 from: new PhoneNumber(twilioNumber),
 body: $"Order #{orderId} confirmed.");
// Telegram — yet another library and yet another pattern
var bot = new TelegramBotClient(botToken);
await bot.SendTextMessageAsync(
 chatId: user.TelegramChatId,
 text: $"Order #{orderId} confirmed.");

Problems this creates:

  • Each channel needs separate setup, credentials and error handling
  • No unified retry logic — if SMS fails, the message is lost
  • Notification content is hardcoded strings scattered across the codebase
  • Adding a new channel means touching many unrelated files
  • No record of what was sent, to whom, or when
  • Testing requires mocking three completely different APIs separately

NotifyForge replaces all of that with one consistent API that handles routing, rendering, retry, fallback, scheduling and persistence automatically:

await notifier.SendAsync(n => n
 .To(user.Email, user.Name)
 .Phone(user.Phone)
 .TelegramChat(user.TelegramChatId)
 .UseTemplate("order-confirmed")
 .WithData(new { user.Name, OrderId = orderId })
 .Retry(3, TimeSpan.FromSeconds(30))
);

Supported channels

Channel Package Providers
Email NotifyForge.Email SMTP · SendGrid · Mailgun · AWS SES
SMS NotifyForge.Sms Twilio · SMS.ru · SMSC
Telegram NotifyForge.Telegram Telegram Bot API
Push NotifyForge.Push Firebase FCM · Apple APNS
Slack NotifyForge.Slack Incoming Webhooks · Bot API
Discord NotifyForge.Discord Webhooks
WhatsApp NotifyForge.WhatsApp WhatsApp Business Cloud API

Install only the packages you need. Each channel package is independent — unused ones add zero dependencies.


Quick start

1. Install

# Core is always required
dotnet add package NotifyForge.Core
# Add channels you need
dotnet add package NotifyForge.Email
dotnet add package NotifyForge.Sms
dotnet add package NotifyForge.Telegram
# Install in your test project — never in the main application
dotnet add package NotifyForge.Testing

2. Register

// Program.cs
builder.Services
 .AddNotifyForge()
 .AddEmail(o =>
 {
 o.Provider = EmailProvider.Smtp;
 o.FromAddress = "noreply@myapp.com";
 o.FromName = "My App";
 o.Smtp.Host = "smtp.gmail.com";
 o.Smtp.Port = 587;
 o.Smtp.UseTls = true;
 o.Smtp.Username = "your@gmail.com";
 o.Smtp.Password = "your-app-password"; // use user-secrets in practice
 })
 .AddSms(o =>
 {
 o.Provider = SmsProvider.Twilio;
 o.Twilio.AccountSid = "ACxxxxxxxxxxxxxxxx";
 o.Twilio.AuthToken = "your-auth-token";
 o.Twilio.From = "+1234567890";
 })
 .AddTelegram(o =>
 {
 o.BotToken = "123456789:AABBccDDeeFFggHH";
 o.ParseMode = TelegramParseMode.Html;
 });

Or load from appsettings.json:

builder.Services
 .AddNotifyForge()
 .AddEmail(builder.Configuration) // reads "NotifyForge:Email"
 .AddSms(builder.Configuration) // reads "NotifyForge:Sms"
 .AddTelegram(builder.Configuration); // reads "NotifyForge:Telegram"

3. Send

Inject INotifier wherever you need it:

public class OrderService
{
 private readonly INotifier _notifier;
 public OrderService(INotifier notifier) => _notifier = notifier;
 public async Task NotifyShippedAsync(Order order)
 {
 await _notifier.SendAsync(n => n
 .To(order.CustomerEmail, order.CustomerName)
 .Phone(order.CustomerPhone)
 .TelegramChat(order.CustomerTelegramId)
 .Subject($"Order #{order.Id} has shipped!")
 .Body(
 $"Hi {order.CustomerName}! " +
 $"Your order #{order.Id} is on its way. " +
 $"Tracking: {order.TrackingNumber}"
 )
 .ForOrder(order.Id)
 .Retry(3)
 .OnFailure(f => f
 .Channel(NotificationChannel.Sms)
 .To(RecipientAddress.Sms(order.CustomerPhone))
 .WithMessage($"Order {order.Id} shipped! Track: {order.TrackUrl}")
 )
 );
 }
}

4. Test

[Fact]
public async Task NotifyShipped_ShouldDeliverToAllChannels()
{
 // Arrange — FakeNotifier captures everything, sends nothing real
 var fake = new FakeNotifier();
 var service = new OrderService(fake);
 // Act
 await service.NotifyShippedAsync(new Order
 {
 Id = "ORD-001",
 CustomerEmail = "alice@example.com",
 CustomerPhone = "+79991234567",
 CustomerTelegramId = "123456789",
 CustomerName = "Alice",
 TrackingNumber = "TRK-999"
 });
 // Assert
 fake.AssertSent(1);
 fake.AssertSentTo("alice@example.com");
 fake.AssertSentTo("+79991234567");
 fake.AssertSentTo("123456789");
 var sent = fake.Last!;
 sent.Subject.Should().Contain("ORD-001");
 sent.CorrelationId.Should().Be("ORD-001");
}

Features

Templates

Move notification content out of code into editable files. One template name — automatically rendered as HTML for email, plain text for SMS, Markdown for Telegram.

dotnet add package NotifyForge.Templates
.AddTemplates(o => o.File.RootPath = Path.Combine(AppContext.BaseDirectory, "Templates"))
await notifier.SendAsync(n => n
 .To("alice@example.com", "Alice")
 .Phone("+79991234567")
 .UseTemplate("order-shipped")
 .WithData(new
 {
 Name = "Alice",
 OrderId = "ORD-001",
 TrackNumber = "TRK-999888777",
 TrackUrl = "https://track.example.com/TRK-999888777"
 })
 .ForOrder("ORD-001")
);

Templates/email/order-shipped.html:

<h1>Hi {{ Name }}!</h1>
<p>Your order <strong>#{{ OrderId }}</strong> has shipped.</p>
<p>Tracking number: <code>{{ TrackNumber }}</code></p>
<p><a href="{{ TrackUrl }}">Track your order</a></p>

Templates/sms/order-shipped.txt:

Order #{{ OrderId }} shipped! Track: {{ TrackUrl }}

Templates/telegram/order-shipped.md:

📦 <b>Order #{{ OrderId }} shipped!</b>
Tracking: <code>{{ TrackNumber }}</code>
👉 <a href="{{ TrackUrl }}">Track your order</a>

Full template documentation


Scheduling

Send a notification at a future time. A background service fires it when the moment arrives.

dotnet add package NotifyForge.Scheduling
.AddScheduling(o =>
{
 o.PollingInterval = TimeSpan.FromSeconds(30);
 o.BatchSize = 50;
})
// Abandoned cart reminder — 24 hours from now
await notifier.SendAsync(n => n
 .To("alice@example.com")
 .Subject("You left something in your cart")
 .Body("Complete your purchase before it sells out!")
 .SendAfter(TimeSpan.FromHours(24))
 .AsLow()
);
// Subscription renewal — 7 days before expiry
await notifier.SendAsync(n => n
 .To("alice@example.com")
 .Subject("Your subscription renews in 7 days")
 .Body("Manage your plan at myapp.com/billing")
 .ScheduleAt(renewalDate.AddDays(-7))
 .AsHigh()
);
// Cancel a scheduled notification before it fires
await scheduler.CancelAsync(scheduleId);

Full scheduling documentation


Retry

Automatically retry failed deliveries with exponential backoff. Jitter prevents thundering herd when many messages fail simultaneously.

// Simple — 3 retries with default 30s initial delay
.Retry(3)
// Explicit delay
.Retry(3, TimeSpan.FromSeconds(30))
// Full control
.WithRetryPolicy(new RetryPolicy
{
 MaxAttempts = 5,
 InitialDelay = TimeSpan.FromSeconds(10),
 BackoffMultiplier = 2.0,
 MaxDelay = TimeSpan.FromMinutes(5),
 UseJitter = true
})
// Pre-built policies
.WithRetryPolicy(RetryPolicy.None) // no retries
.WithRetryPolicy(RetryPolicy.Default) // 3 retries, 30s delay
.WithRetryPolicy(RetryPolicy.Aggressive) // 5 retries, 5s delay
// Disable retry for OTP codes and time-sensitive content
.NoRetry()

Delay sequence with default settings:

Initial send fails → wait ~30s
Retry 1 fails → wait ~60s
Retry 2 fails → wait ~120s
Retry 3 → success or permanent failure

Full retry documentation


Fallback

When the primary channel fails after all retries, automatically switch to a backup channel.

// Try Email → if it fails, send SMS instead
await notifier.SendAsync(n => n
 .To("alice@example.com")
 .Body("Your password reset link: https://myapp.com/reset?token=abc")
 .Retry(3)
 .OnFailure(f => f
 .Channel(NotificationChannel.Sms)
 .To(RecipientAddress.Sms("+79991234567"))
 .WithMessage("Reset your password at: https://myapp.com/reset?token=abc")
 )
);

Fallback only fires when all primary channels fail. If even one channel succeeds, the fallback does not run.

Full fallback documentation


Throttling

Control send rate for bulk notifications to avoid hitting provider limits.

// Natural language syntax
.BulkSend(perSecond: 100)
// Explicit policy
.Throttle(ThrottlePolicy.PerSecond(100))
.Throttle(ThrottlePolicy.PerMinute(500))
.Throttle(ThrottlePolicy.PerHour(10000))
// Integer extension syntax
.Throttle(100.PerSecond())
// Send a newsletter to 50,000 subscribers at 100 emails per second
await notifier.SendAsync(n => n
 .ToMany(allSubscriberEmails)
 .Subject("Monthly newsletter — June 2024")
 .Body(newsletterHtmlContent)
 .AsLow()
 .BulkSend(perSecond: 100)
 .NoRetry()
);

User preferences

Respect per-user channel opt-ins. Implement IUserPreferenceProvider to load preferences from your database. NotifyForge filters channels automatically at send time.

// Registers your implementation
services.AddSingleton<IUserPreferenceProvider, DatabaseUserPreferenceProvider>();
// Only delivers to channels the user has enabled
await notifier.SendAsync(n => n
 .To("alice@example.com")
 .Phone("+79991234567")
 .TelegramChat("123456789")
 .Body("Your order has shipped.")
 .ForUser("user-123") // triggers preference lookup
);
// Critical notifications always bypass user opt-out
await notifier.SendAsync(n => n
 .Phone("+79991234567")
 .Body("Your OTP: 847291. Valid 5 minutes.")
 .AsCritical() // delivered regardless of user preferences
);

Persistence

Store notification history in a database. Query what was sent, to whom, and when.

dotnet add package NotifyForge.Persistence
.AddPersistence(o =>
{
 o.Provider = DatabaseProvider.SqlServer;
 o.ConnectionString = "Server=db.myapp.com;Database=MyApp;...";
 o.AutoMigrate = true;
})
// Query history
var store = serviceProvider.GetRequiredService<INotificationStore>();
var recent = await store.GetRecentAsync(50);
var forOrder = await store.GetByCorrelationAsync("order-ORD-001");
var byId = await store.GetByIdAsync(notificationId);

Supported databases: SQL Server, PostgreSQL, SQLite.


Dashboard

Built-in HTTP endpoints for monitoring channel health, browsing history, and sending test notifications.

dotnet add package NotifyForge.AspNetCore
app.UseNotifyForge();
app.MapNotifyForgeEndpoints();
Endpoint Description
GET /notifyforge/health Configuration status of all channels
POST /notifyforge/send Send a notification via HTTP
GET /notifyforge/history Recent notification records
GET /notifyforge/status/{id} Status of a specific notification
DELETE /notifyforge/cancel/{id} Cancel a scheduled notification

All endpoints appear in Swagger UI under the NotifyForge tag.

Full dashboard documentation


Testing

FakeNotifier captures every notification in memory. Nothing is sent to real providers. Includes assertion helpers and a fluent assertion chain.

# Install in your test project only
dotnet add package NotifyForge.Testing
var fake = new FakeNotifier();
var service = new OrderService(fake);
await service.ConfirmOrderAsync("ORD-001");
// Simple assertions
fake.AssertSent(1);
fake.AssertSentTo("alice@example.com");
fake.AssertSentVia(NotificationChannel.Email);
fake.AssertTemplateUsed("order-confirmed");
// Inspect the captured notification
var sent = fake.Last!;
sent.Subject.Should().Contain("ORD-001");
sent.Priority.Should().Be(NotificationPriority.High);
sent.CorrelationId.Should().Be("ORD-001");
sent.Recipients.Should().HaveCount(2);
sent.WasScheduled.Should().BeFalse();
// Fluent chain
fake.Should()
 .HaveSent(1)
 .ToAddress("alice@example.com")
 .ViaChannel(NotificationChannel.Email)
 .WithTemplate("order-confirmed")
 .WithPriority(NotificationPriority.High);

Full testing documentation


All builder methods

await notifier.SendAsync(n => n
 // ── Recipients ──────────────────────────────────────────────
 .To("email@example.com") // email
 .To("email@example.com", "Display Name") // email with display name
 .Phone("+79991234567") // SMS (E.164 format)
 .TelegramChat("123456789") // Telegram chat ID
 .TelegramChat("@channelname") // Telegram public channel
 .PushToken("device-registration-token") // FCM or APNS device token
 .SlackTarget("#general") // Slack channel name
 .SlackTarget("U012AB3CD") // Slack user ID
 .DiscordWebhook("https://discord.com/api/...") // Discord webhook URL
 .WhatsAppPhone("+79991234567") // WhatsApp phone number
 .ToMany(listOfRecipientAddresses) // bulk — add many at once
 // ── Content ─────────────────────────────────────────────────
 .UseTemplate("template-name") // template key
 .WithData(new { Name = "Alice", OrderId = 1 }) // data for template rendering
 .Subject("Notification subject") // subject line (email header)
 .Body("Plain text or HTML body") // inline body — no template
 // ── Notification type ────────────────────────────────────────
 .Type("order-shipped") // type key for filtering and analytics
 // ── Priority ─────────────────────────────────────────────────
 .Priority(NotificationPriority.Normal) // explicit priority
 .AsLow() // Low — throttled, respects opt-out
 .AsHigh() // High — skips throttle queue
 .AsCritical() // Critical — bypasses everything
 // ── Scheduling ───────────────────────────────────────────────
 .ScheduleAt(DateTimeOffset.UtcNow.AddHours(24)) // specific time (UTC)
 .SendAfter(TimeSpan.FromHours(24)) // delay from now
 .SendTomorrow() // next midnight UTC
 // ── Retry ────────────────────────────────────────────────────
 .Retry(3) // 3 retries, default 30s delay
 .Retry(3, TimeSpan.FromSeconds(30)) // explicit initial delay
 .WithRetryPolicy(RetryPolicy.None) // no retries
 .WithRetryPolicy(RetryPolicy.Default) // 3 retries, 30s delay
 .WithRetryPolicy(RetryPolicy.Aggressive) // 5 retries, 5s delay
 .WithRetryPolicy(new RetryPolicy { ... }) // full control
 .NoRetry() // same as RetryPolicy.None
 // ── Throttle ─────────────────────────────────────────────────
 .Throttle(ThrottlePolicy.PerSecond(100)) // 100 per second
 .Throttle(ThrottlePolicy.PerMinute(500)) // 500 per minute
 .BulkSend(perSecond: 100) // shorthand for PerSecond
 // ── Fallback ─────────────────────────────────────────────────
 .OnFailure(f => f
 .Channel(NotificationChannel.Sms) // fallback channel
 .To(RecipientAddress.Sms("+79991234567")) // fallback recipient
 .WithMessage("Short SMS fallback text") // optional custom body
 )
 // ── Metadata ─────────────────────────────────────────────────
 .CorrelationId("order-ORD-001") // business entity ID
 .ForUser("user-123") // sets UserId metadata
 .ForOrder("ORD-001") // sets OrderId + CorrelationId
 .WithMetadata("key", "value") // arbitrary key-value metadata
);

Architecture

Your application code
 │
 │ INotifier.SendAsync(n => n.To(...).Body(...))
 ▼
┌───────────────────────────────────────────────────────────────┐
│ NotificationPipeline │
│ │
│ Step 1 ValidationMiddleware validates input │
│ Step 2 UserPreferenceMiddleware filters by user opt-ins │
│ Step 3 TemplateMiddleware renders template │
│ Step 4 ThrottleMiddleware enforces rate limit │
│ Step 5 RetryMiddleware handles transient failures │
│ Step 6 FallbackMiddleware switches channel on fail │
│ Step 7 PersistenceMiddleware saves history to database │
│ Step 8 DispatchMiddleware sends to providers │
└───────────────────────────────────────────────────────────────┘
 │
 ├──▶ EmailChannel SMTP · SendGrid · Mailgun · AWS SES
 ├──▶ SmsChannel Twilio · SMS.ru · SMSC
 ├──▶ TelegramChannel Telegram Bot API
 ├──▶ PushChannel Firebase FCM · Apple APNS
 ├──▶ SlackChannel Incoming Webhooks · Bot API
 ├──▶ DiscordChannel Webhooks
 └──▶ WhatsAppChannel WhatsApp Business API

The pipeline is built from IPipelineMiddleware components registered in DI — the same pattern as ASP.NET Core middleware. Each step calls await next() to pass control to the next one.

You can add your own middleware to extend the pipeline without modifying library code:

public sealed class AuditMiddleware : IPipelineMiddleware
{
 private readonly IAuditLog _audit;
 public AuditMiddleware(IAuditLog audit) => _audit = audit;
 public async Task InvokeAsync(
 PipelineContext context,
 Func<Task> next,
 CancellationToken cancellationToken = default)
 {
 await next();
 await _audit.RecordAsync(new AuditEntry
 {
 NotificationId = context.NotificationId,
 SuccessCount = context.DeliveryResults.Count(r => r.IsSuccess),
 FailureCount = context.DeliveryResults.Count(r => !r.IsSuccess),
 Timestamp = DateTimeOffset.UtcNow
 }, cancellationToken);
 }
}
// Register after AddNotifyForge()
services.AddSingleton<IPipelineMiddleware, AuditMiddleware>();

Project structure

NotifyForge/
├── src/
│ ├── NotifyForge.Core pipeline, models, interfaces, builder
│ ├── NotifyForge.Email SMTP, SendGrid, Mailgun, AWS SES
│ ├── NotifyForge.Sms Twilio, SMS.ru, SMSC
│ ├── NotifyForge.Telegram Telegram Bot API
│ ├── NotifyForge.Push Firebase FCM, Apple APNS
│ ├── NotifyForge.Slack Incoming Webhooks, Bot API
│ ├── NotifyForge.Discord Discord Webhooks
│ ├── NotifyForge.WhatsApp WhatsApp Business Cloud API
│ ├── NotifyForge.Templates Liquid and Razor template engines
│ ├── NotifyForge.Scheduling delayed and scheduled delivery
│ ├── NotifyForge.Persistence EF Core notification history
│ ├── NotifyForge.AspNetCore DI extensions, middleware, HTTP endpoints
│ └── NotifyForge.Testing FakeNotifier and assertion helpers
├── tests/
│ ├── NotifyForge.Core.Tests unit tests for pipeline and builder
│ ├── NotifyForge.Email.Tests unit tests for email channel
│ ├── NotifyForge.Templates.Tests unit tests for template rendering
│ └── NotifyForge.Integration.Tests end-to-end tests with FakeNotifier
├── samples/
│ ├── NotifyForge.Sample.WebApi ASP.NET Core Web API with Swagger
│ └── NotifyForge.Sample.Console Console app demonstrating the API
└── docs/
 ├── getting-started.md
 ├── channels.md
 ├── templates.md
 ├── scheduling.md
 ├── retry-fallback.md
 ├── testing.md
 ├── configuration.md
 └── dashboard.md

Export as NuGet packages

Build a single DLL

dotnet build src/NotifyForge.Core -c Release
# Output: src/NotifyForge.Core/bin/Release/net8.0/NotifyForge.Core.dll

Pack all projects as NuGet packages

dotnet pack src/NotifyForge.Core -c Release -o ./packages
dotnet pack src/NotifyForge.Email -c Release -o ./packages
dotnet pack src/NotifyForge.Sms -c Release -o ./packages
dotnet pack src/NotifyForge.Telegram -c Release -o ./packages
dotnet pack src/NotifyForge.Push -c Release -o ./packages
dotnet pack src/NotifyForge.Slack -c Release -o ./packages
dotnet pack src/NotifyForge.Discord -c Release -o ./packages
dotnet pack src/NotifyForge.WhatsApp -c Release -o ./packages
dotnet pack src/NotifyForge.Templates -c Release -o ./packages
dotnet pack src/NotifyForge.Scheduling -c Release -o ./packages
dotnet pack src/NotifyForge.Persistence -c Release -o ./packages
dotnet pack src/NotifyForge.AspNetCore -c Release -o ./packages
dotnet pack src/NotifyForge.Testing -c Release -o ./packages

Use locally in another project

# Register the local folder as a NuGet source
dotnet nuget add source ./packages --name NotifyForge-Local
# Add packages to your project
dotnet add package NotifyForge.Core
dotnet add package NotifyForge.Email
dotnet add package NotifyForge.Sms

Use via project reference (same repository)

<!-- In YourProject.csproj -->
<ItemGroup>
 <ProjectReference
 Include="../../NotifyForge/src/NotifyForge.Core/NotifyForge.Core.csproj" />
 <ProjectReference
 Include="../../NotifyForge/src/NotifyForge.Email/NotifyForge.Email.csproj" />
</ItemGroup>

Publish to NuGet.org

dotnet nuget push ./packages/NotifyForge.Core.1.0.0-alpha.nupkg \
 --api-key YOUR_NUGET_API_KEY \
 --source https://api.nuget.org/v3/index.json

Documentation

Document Contents
Getting Started Step-by-step setup from zero to first notification
Channels Configuring every provider in detail
Templates Liquid and Razor templates with file and database loaders
Scheduling Delayed and future-dated notification delivery
Retry and Fallback Handling failures automatically
Testing FakeNotifier, assertion helpers and coverage reports
Configuration Complete reference for every option
Dashboard Built-in HTTP endpoints for monitoring

Running tests

# Run all tests
dotnet test --verbosity normal
# Run a specific project
dotnet test tests/NotifyForge.Core.Tests/
dotnet test tests/NotifyForge.Email.Tests/
dotnet test tests/NotifyForge.Integration.Tests/
# Run with coverage
dotnet test --collect:"XPlat Code Coverage" --results-directory ./TestResults
# Generate HTML coverage report (requires reportgenerator tool)
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator \
 -reports:"TestResults/**/coverage.cobertura.xml" \
 -targetdir:"TestResults/coverage-html" \
 -reporttypes:"Html;HtmlSummary"
# Open the report
start TestResults/coverage-html/index.html # Windows
open TestResults/coverage-html/index.html # macOS

Current test results:

NotifyForge.Core.Tests 42 passed 0 failed
NotifyForge.Email.Tests 6 passed 0 failed
NotifyForge.Integration.Tests 15 passed 0 failed
──────────────────────────────────────────────────
Total 63 passed 0 failed

Contributing

  1. Fork the repository on GitHub
  2. Create a feature branch: git checkout -b feature/your-feature-name
  3. Write tests for your changes — all new code should have test coverage
  4. Make sure the full suite passes: dotnet test
  5. Open a pull request with a clear description of what changed and why

For bug reports and feature requests, open an issue on GitHub.


License

MIT — see LICENSE for details.

About

Universal notification library for .NET 8 — Email, SMS, Telegram, Push, Slack, Discord and WhatsApp behind one fluent API with retry, fallback, scheduling and templates.

Topics

Resources

Stars

Watchers

Forks

Packages

Contributors

AltStyle によって変換されたページ (->オリジナル) /