Lyo.Sms
1.0.2
dotnet add package Lyo.Sms --version 1.0.2
NuGet\Install-Package Lyo.Sms -Version 1.0.2
<PackageReference Include="Lyo.Sms" Version="1.0.2" />
<PackageVersion Include="Lyo.Sms" Version="1.0.2" />
<PackageReference Include="Lyo.Sms" />
paket add Lyo.Sms --version 1.0.2
#r "nuget: Lyo.Sms, 1.0.2"
#:package Lyo.Sms@1.0.2
#addin nuget:?package=Lyo.Sms&version=1.0.2
#tool nuget:?package=Lyo.Sms&version=1.0.2
Lyo.Sms
A production-ready SMS library for .NET with extensible architecture for multiple providers.
Features
- Clean API - Fluent builder pattern for constructing messages
- Phone Number Validation - Automatic validation and normalization to E.164 format
- Bulk Messaging - Efficient bulk SMS sending with rate limiting and BulkSmsBuilder
- Error handling — Failures surface as
Result<SmsRequest>(bulk:BulkResult<SmsRequest>); add retries/timeouts yourself (see provider packages, e.g. Twilio README “Resilience”). - Custom Exceptions - InvalidFormatException and ArgumentOutsideRangeException for better error messages
- Logging - Built-in logging support via Microsoft.Extensions.Logging
- Dependency Injection - Full support for .NET dependency injection
- Async/Await - Fully asynchronous API with cancellation token support
- Message Querying - Query messages by various filter criteria
- Extensible Architecture - Abstract base class (SmsServiceBase) for easy provider implementation
- Configurable Limits - Configurable bulk SMS limits, message length limits, and concurrency limits
- Events - Events for message sending, message sent, bulk sending, and bulk sent
Examples
1. Configure Options
var options = new ProviderOptions // Replace with your provider's options class
{
DefaultFromPhoneNumber = "+1234567890",
BulkSmsConcurrencyLimit = 10, // Max concurrent bulk SMS requests (default: 10)
MaxMessageBodyLength = 1600, // Max message body length in characters (default: 1600)
MaxBulkSmsLimit = 1000 // Max messages per bulk operation (default: 1000)
};
3. Use the Service
public class MyService
{
private readonly ISmsService _smsService;
public MyService(ISmsService smsService)
{
_smsService = smsService;
}
public async Task SendSmsAsync()
{
// Simple send
var result = await _smsService.SendSmsAsync(
to: "+1234567890",
body: "Hello, World!",
from: "+1987654321"
);
if (result.IsSuccess)
{
Console.WriteLine($"Message sent! ID: {result.MessageId}");
}
else
{
Console.WriteLine($"Failed: {result.ErrorMessage}");
}
}
}
Using the Builder Pattern
var builder = SmsMessageBuilder
.New()
.SetTo("+1234567890")
.SetFrom("+1987654321")
.SetBody("Hello, World!");
var result = await _smsService.SendAsync(builder);
Sending Bulk Messages
var messages = new[]
{
SmsMessageBuilder.New().SetTo("+1111111111").SetBody("Message 1"),
SmsMessageBuilder.New().SetTo("+2222222222").SetBody("Message 2"),
SmsMessageBuilder.New().SetTo("+3333333333").SetBody("Message 3")
};
var results = await _smsService.SendBulkAsync(messages);
foreach (var result in results)
{
if (result.IsSuccess)
{
Console.WriteLine($"Sent to {result.To}: {result.MessageId}");
}
}
Sending Bulk Messages (2)
var bulkBuilder = BulkSmsBuilder
.New()
.SetDefaultFrom("+1987654321") // Optional: set default sender for all messages
.SetMaxLimit(100) // Optional: limit number of messages
.Add("+1111111111", "Message 1")
.Add("+2222222222", "Message 2")
.Add("+3333333333", "Message 3", "+19998887777"); // Override sender for specific message
var bulkResult = await _smsService.SendBulkAsync(bulkBuilder);
Console.WriteLine($"Total: {bulkResult.TotalCount}");
Console.WriteLine($"Success: {bulkResult.SuccessCount}");
Console.WriteLine($"Failed: {bulkResult.FailureCount}");
Console.WriteLine($"Elapsed: {bulkResult.ElapsedTime}");
if (bulkResult.IsCompleteSuccess)
{
Console.WriteLine("All messages sent successfully!");
}
foreach (var result in bulkResult.Results)
{
if (result.IsSuccess)
{
Console.WriteLine($"Sent to {result.To}: {result.MessageId}");
}
else
{
Console.WriteLine($"Failed to send to {result.To}: {result.ErrorMessage}");
}
}
Querying Messages
var filter = new SmsMessageQueryFilter
{
From = "+1987654321",
DateSentAfter = DateTime.UtcNow.AddDays(-7),
PageSize = 50
};
var result = await _smsService.GetMessagesAsync(filter);
foreach (var message in result.Items)
{
Console.WriteLine($"{message.DateSent}: {message.Body}");
}
// Cursor-based pagination: use result.NextCursor as DateSentBefore for next page when result.HasMore
Getting a Message by ID
var message = await _smsService.GetMessageByIdAsync("SM1234567890abcdef");
if (message.IsSuccess)
{
Console.WriteLine($"Status: {message.Status}");
Console.WriteLine($"Body: {message.Body}");
}
Testing Connection
var isConnected = await _smsService.TestConnectionAsync();
if (isConnected)
{
Console.WriteLine("Connected to SMS service!");
}
Using Events
_smsService.MessageSending += (sender, args) =>
{
var request = args.SmsRequest;
Console.WriteLine($"Sending SMS to {request.To}: {request.Body}");
};
Using Events (2)
_smsService.MessageSent += (sender, args) =>
{
var result = args.SmsResult;
if (result.IsSuccess)
{
Console.WriteLine($"SMS sent successfully: {result.MessageId}");
}
else
{
Console.WriteLine($"SMS failed: {result.ErrorMessage}");
}
};
Using Events (3)
_smsService.BulkSending += (sender, args) =>
{
Console.WriteLine($"Starting bulk send for {args.BulkSmsMessage.Count} messages");
};
Using Events (4)
_smsService.BulkSent += (sender, args) =>
{
var bulkResult = args.BulkSmsResult;
Console.WriteLine($"Bulk send completed:");
Console.WriteLine($" Total: {bulkResult.TotalCount}");
Console.WriteLine($" Success: {bulkResult.SuccessCount}");
Console.WriteLine($" Failure: {bulkResult.FailureCount}");
Console.WriteLine($" Elapsed: {bulkResult.ElapsedTime}");
};
Using Events (5)
public class SmsNotificationService
{
private readonly ISmsService _smsService;
public SmsNotificationService(ISmsService smsService)
{
_smsService = smsService;
SubscribeToEvents();
}
private void SubscribeToEvents()
{
_smsService.MessageSending += OnMessageSending;
_smsService.MessageSent += OnMessageSent;
_smsService.BulkSending += OnBulkSending;
_smsService.BulkSent += OnBulkSent;
}
private void OnMessageSending(object? sender, SmsSendingEventArgs args)
{
Console.WriteLine($"Preparing to send SMS to {args.SmsRequest.To}");
}
private void OnMessageSent(object? sender, SmsSentEventArgs args)
{
if (args.SmsResult.IsSuccess)
{
Console.WriteLine($" SMS sent: {args.SmsResult.MessageId}");
}
else
{
Console.WriteLine($" SMS failed: {args.SmsResult.ErrorMessage}");
}
}
private void OnBulkSending(object? sender, SmsBulkSendingEventArgs args)
{
Console.WriteLine($"Starting bulk SMS operation: {args.BulkSmsMessage.Count} messages");
}
private void OnBulkSent(object? sender, BulkSmsSentEventArgs args)
{
var bulkResult = args.BulkSmsResult;
Console.WriteLine($"Bulk SMS completed: {bulkResult.SuccessCount}/{bulkResult.TotalCount} successful in {bulkResult.ElapsedTime.TotalSeconds:F2}s");
}
}
Exception Handling Examples
try
{
var builder = SmsMessageBuilder.New()
.SetTo("invalid-phone") // Will throw InvalidFormatException
.SetBody("Test");
}
catch (InvalidFormatException ex)
{
Console.WriteLine($"Invalid phone number: {ex.InvalidValue}");
Console.WriteLine($"Expected formats: {string.Join(", ", ex.ValidFormats)}");
}
try
{
var builder = SmsMessageBuilder.New()
.SetTo("+1234567890")
.SetBody(new string('A', 1601)); // Will throw ArgumentOutsideRangeException
}
catch (ArgumentOutsideRangeException ex)
{
Console.WriteLine($"Message too long: {ex.ActualValue} characters");
Console.WriteLine($"Maximum allowed: {ex.MaxValue} characters");
}
Testing
dotnet test
1. Configure Options
Each provider will have its own options class that inherits from SmsServiceOptions:
2. Register Services
Register the provider-specific service using the provider's extension methods. Each provider will have its own registration methods.
3. Use the Service
The contract is ISmsService<TResult> where TResult : Result<SmsRequest>. ISmsService is shorthand for ISmsService<Result<SmsRequest>>. Twilio surfaces * TwilioSmsResult* as TResult when you want provider-specific fields.
Sending Bulk Messages
Using IEnumerable of Builders
Using BulkSmsBuilder (Recommended)
Using Events
The SMS service provides events for monitoring message operations:
MessageSending Event
Fired before each message is sent (including during bulk operations):
MessageSent Event
Fired after each message is sent (success or failure):
BulkSending Event
Fired before a bulk send operation starts:
BulkSent Event
Fired after a bulk send operation completes:
Complete Event Example
Note: Events fire even when operations fail, allowing you to track all SMS operations regardless of success or failure.
Phone Number Formats
- E.164:
+1234567890 - US Format:
(555) 123-4567 - US Format:
555-123-4567 - US Format:
555.123.4567 - US Format:
5551234567(assumes US country code +1)
Message Limits
- Maximum Length: 1600 characters (10 segments of 160 characters each) - configurable via
MaxMessageBodyLength - Messages longer than 160 characters are automatically split into multiple segments
- The library validates message length before sending
- Bulk SMS Limit: Maximum number of messages per bulk operation (default: 1000) - configurable via
MaxBulkSmsLimit - BulkSmsBuilder Limit: Can set per-builder limit using
SetMaxLimit()method
Error Handling
The stack surfaces structured Result errors and validates inputs early (builders / normalization). Providers may attach error codes on specialized result types.
- No built-in retries: callers or HTTP layers should implement policy if needed
- Error codes:
SmsErrorCodes(inLyo.Sms) attaches the following constants to failed results raised bySmsServiceBase:
| Constant | Value | Raised when |
|---|---|---|
BuildFailed |
BUILD_FAILED |
A builder threw while constructing the request. |
MessageNotBuilt |
MESSAGE_NOT_BUILT |
The bulk pipeline reached the send step with no built request. |
OperationCancelled |
OPERATION_CANCELLED |
The bulk send was cancelled via CancellationToken. |
MissingFromNumber |
MISSING_FROM_NUMBER |
No From number was provided or configured. |
Providers attach their own codes on derived result types (e.g. TwilioSmsResult.TwilioErrorCode).
- Exception Details: Full exception information available in results
- Logging: All operations are logged for debugging
- Custom Exceptions:
InvalidFormatException- Thrown when phone number format is invalid (includes valid format examples)ArgumentOutsideRangeException- Thrown when values are outside allowed ranges (e.g., message length)
var result = await _smsService.SendSmsAsync("+1234567890", "Hello");
if (!result.IsSuccess)
{
Console.WriteLine($"Error: {result.ErrorMessage}");
Console.WriteLine($"Error Code: {result.ErrorCode}");
if (result.Exception != null)
{
Console.WriteLine($"Exception: {result.Exception.Message}");
// Handle specific exception types
if (result.Exception is InvalidFormatException formatEx)
{
Console.WriteLine($"Invalid Value: {formatEx.InvalidValue}");
Console.WriteLine($"Valid Formats: {string.Join(", ", formatEx.ValidFormats)}");
}
else if (result.Exception is ArgumentOutsideRangeException rangeEx)
{
Console.WriteLine($"Value: {rangeEx.ActualValue}, Range: [{rangeEx.MinValue}, {rangeEx.MaxValue}]");
}
}
}
Rate Limiting
- Concurrent Requests: Limited to 10 concurrent requests (configurable via
BulkSmsConcurrencyLimit) - Automatic Throttling: Built-in semaphore-based throttling
- Non-blocking: Uses async/await for efficient resource usage
- Bulk Limits: Maximum number of messages per bulk operation (configurable via
MaxBulkSmsLimit) - Per-Builder Limits: BulkSmsBuilder supports
SetMaxLimit()to restrict messages at the builder level
Logging
The library uses Microsoft.Extensions.Logging for all logging:
services.AddLogging(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Information);
});
Log levels:
- Information: Successful operations, message details
- Warning: Retries, long messages
- Error: Failures, exceptions
Metrics
SmsServiceBase emits its counters/timers under the keys exposed by Constants.Metrics. Providers override
CreateMetricNamesDictionary() to prefix these with their own namespace (Twilio uses sms.twilio.*).
Constant key (Lyo.Sms.Constants.Metrics) |
Metric name | Kind |
|---|---|---|
SendDuration |
sms.send.duration |
Timer |
SendSuccess |
sms.send.success |
Counter |
SendFailure |
sms.send.failure |
Counter |
BulkSendDuration |
sms.bulk.send.duration |
Timer |
BulkSendTotal |
sms.bulk.send.total |
Counter |
BulkSendSuccess |
sms.bulk.send.success |
Counter |
BulkSendFailure |
sms.bulk.send.failure |
Counter |
BulkSendLastDurationMs |
sms.bulk.send.last_duration_ms |
Gauge |
Convenience MMS overloads
ISmsService<TResult> exposes typed MMS convenience methods that wrap the builder:
// Pass URLs as strings (validated/converted to Uri internally)
await _smsService.SendMmsAsync(
to: "+1234567890",
mediaUrls: ["https://example.com/image.jpg"],
body: "Check this out");
// Or as System.Uri instances
await _smsService.SendMmsAsync(
to: "+1234567890",
mediaUrls: [new Uri("https://example.com/image.jpg")]);
Both overloads enforce to/mediaUrls (must not be null/empty), apply DefaultFromPhoneNumber when from
is omitted, and route through the same SendCoreAsync path as SendSmsAsync.
Architecture
- Lyo.Sms: Core interfaces and models (provider-agnostic)
ISmsService- Main service interfaceSmsServiceBase- Abstract base class providing common bulk SMS functionalitySmsServiceOptions- Base options class with common configuration propertiesSmsMessageBuilder- Builder for individual messagesBulkSmsBuilder- Builder for bulk SMS operationsSmsMessageQueryFilter- Generic filter for querying messages- Provider Packages: Provider-specific implementations (e.g.,
Lyo.Sms.Twilio) - Provider-specific service class - Inherits from
SmsServiceBase, implements provider-specific methods - Provider-specific options class - Inherits from
SmsServiceOptions, adds provider-specific properties
Architecture — Extending for New Providers
To add support for a new SMS provider, simply:
- Create an options class inheriting from
SmsServiceOptions:
public class MyProviderOptions : SmsServiceOptions
{
public string ApiKey { get; set; } = null!;
public string ApiSecret { get; set; } = null!;
}
- Implement
SmsServiceBase<TResult>. OverrideSendCoreAsync(the actual provider call afterSmsRequestis built),GetMessageByIdAsync,GetMessagesAsync,TestConnectionCoreAsync, andCreateFailure. Everything else onISmsService(SendSmsAsync,SendBulkAsync, events, concurrency throttling, metrics hooks) stays in the base.
public class MyProviderSmsService : SmsServiceBase<Result<SmsRequest>>
{
public MyProviderSmsService(MyProviderOptions options, ILogger<MyProviderSmsService>? logger = null, IMetrics? metrics = null)
: base(options, logger, metrics)
{
}
protected override Task<Result<SmsRequest>> SendCoreAsync(SmsRequest request, CancellationToken ct)
=> Task.FromResult(Result<SmsRequest>.Failure("Not implemented", "sms.myprovider"));
public override Task<Result<SmsRequest>> GetMessageByIdAsync(string messageId, CancellationToken ct = default)
=> Task.FromResult(Result<SmsRequest>.Failure("Not implemented", "sms.myprovider"));
public override Task<SmsMessageQueryResults<Result<SmsRequest>>> GetMessagesAsync(SmsMessageQueryFilter filter, CancellationToken ct = default)
=> Task.FromResult(new SmsMessageQueryResults<Result<SmsRequest>>([], filter.PageSize, false));
protected override Task<bool> TestConnectionCoreAsync(CancellationToken ct = default) => Task.FromResult(false);
protected override Result<SmsRequest> CreateFailure(Exception exception, string code, SmsRequest? request = null)
=> Result<SmsRequest>.Failure(exception, code);
}
Richer TResult types (Twilio TwilioSmsResult) substitute SmsServiceBase<TwilioSmsResult> — see Lyo.Sms.Twilio.
- Create extension methods for dependency injection:
public static class Extensions
{
extension(IServiceCollection services)
{
public IServiceCollection AddMyProviderSmsService(Action<MyProviderOptions> configure)
{
// Register options and service
// Register ISmsService interface
}
public IServiceCollection AddMyProviderSmsServiceViaConfiguration(string configSectionName = "MyProviderOptions")
{
// Register via configuration binding
// Register ISmsService interface
}
}
}
All bulk SMS operations, rate limiting, and common functionality are automatically provided by the base class!
Dependencies
Generated from ProjectReference / PackageReference (same model as docs/Lyo.ProjectGraph.html).
Lyo.Common— (direct, lyo)Lyo.Exceptions— (direct, lyo)Lyo.Metrics— (direct, lyo)Lyo.Result— (direct, lyo)Lyo.Sms.Models— (direct, lyo)Microsoft.Extensions.Logging.Abstractions10.0.5— (direct, microsoft)Microsoft.Extensions.DependencyInjection.Abstractions10.0.5— (transitive, microsoft)Microsoft.Extensions.Options.ConfigurationExtensions10.0.5— (transitive, microsoft)System.Memory4.6.3— (transitive, microsoft, netstandard2.0)System.Text.Json10.0.5— (transitive, microsoft, netstandard2.0)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. 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 is compatible. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Lyo.Common (>= 1.0.2)
- Lyo.Exceptions (>= 1.0.2)
- Lyo.Metrics (>= 1.0.2)
- Lyo.Result (>= 1.0.2)
- Lyo.Sms.Models (>= 1.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.5)
-
net10.0
- Lyo.Common (>= 1.0.2)
- Lyo.Exceptions (>= 1.0.2)
- Lyo.Metrics (>= 1.0.2)
- Lyo.Result (>= 1.0.2)
- Lyo.Sms.Models (>= 1.0.2)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.5)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Lyo.Sms:
| Package | Downloads |
|---|---|
|
Lyo.Sms.Postgres
PostgreSQL implementation of Lyo.Sms using Entity Framework Core. |
|
|
Lyo.Sms.Twilio
Twilio implementation of the Lyo SMS service for sending SMS messages via Twilio. |
|
|
Lyo.Sms.Web.Components
Reusable Blazor components for provider-neutral SMS and MMS composition workflows. |
GitHub repositories
This package is not used by any popular GitHub repositories.