Gesgocom.GesSms 1.0.1
GesSms
Librería .NET 10.0 para envío de SMS a través de la plataforma LabsMobile. Incluye resiliencia HTTP integrada (reintentos, circuit breaker, timeout).
Tabla de Contenidos
- Instalación
- Configuración
- Uso Básico
- Envío de SMS
- Consultas de Cuenta
- Mensajes Programados
- Utilidades SMS
- Webhooks
- Referencia de API
- Manejo de Errores
- Resiliencia HTTP
- Códigos de Error
- Ejemplos Completos
Instalación
Desde NuGet privado (Gesgocom)
# Registrar el origen NuGet (una vez por máquina)
dotnet nuget add source https://nuget.gesgocom.es/v3/index.json -n Gesgocom
# Instalar el paquete
dotnet add package Gesgocom.GesSms
Configuración
Opción 1: Configuración desde appsettings.json (Recomendado)
{
"LabsMobile": {
"Username": "tu_usuario",
"ApiToken": "tu_token_api",
"Sender": "MiApp"
}
}
// Program.cs
builder.Services.AddLabsMobileSms(builder.Configuration.GetSection("LabsMobile"));
Opción 2: Configuración en código
builder.Services.AddLabsMobileSms(options =>
{
options.Username = "tu_usuario";
options.ApiToken = "tu_token_api";
options.Sender = "MiApp"; // o DefaultSenderId
options.TestMode = false; // Opcional
options.TimeoutSeconds = 30; // Opcional
});
Opciones de Configuración Disponibles
| Propiedad | Tipo | Descripción | Requerido |
|---|---|---|---|
Username |
string | Usuario de la cuenta LabsMobile | Sí |
ApiToken |
string | Token API generado en el panel | Sí |
Sender |
string | Identificador del remitente por defecto | No |
BaseUrl |
string | URL de la API (por defecto: https://api.labsmobile.com/json/) | No |
TimeoutSeconds |
int | Timeout de peticiones HTTP (por defecto: 30) | No |
TestMode |
bool | Modo prueba sin descontar créditos (por defecto: false) | No |
Uso Básico
Inyección del Servicio
public class MiServicio
{
private readonly ILabsMobileSmsService _smsService;
private readonly ISmsUtils _smsUtils;
public MiServicio(ILabsMobileSmsService smsService, ISmsUtils smsUtils)
{
_smsService = smsService;
_smsUtils = smsUtils;
}
}
Envío Simple
var resultado = await _smsService.SendAsync("34609123456", "Hola mundo");
if (resultado.IsSuccess)
{
Console.WriteLine($"SMS enviado. ID: {resultado.SubId}");
}
else
{
Console.WriteLine($"Error {resultado.Code}: {resultado.Message}");
}
Envío de SMS
Envío a un Destinatario
// Con remitente por defecto (configurado en options)
var resultado = await _smsService.SendAsync(
"34609123456",
"Tu código de verificación es: 123456"
);
// Con remitente específico
var resultado = await _smsService.SendAsync(
"34609123456",
"Tu código es: 123456",
"MiApp"
);
Envío Masivo
var destinatarios = new[] { "34609123456", "34666789012", "34612345678" };
var resultado = await _smsService.SendBulkAsync(
destinatarios,
"Mensaje para todos los destinatarios",
"MiApp"
);
// Máximo 10,000 destinatarios por petición
Envío Programado
var fechaEnvio = DateTime.UtcNow.AddHours(2);
var resultado = await _smsService.ScheduleAsync(
"34609123456",
"Este mensaje se enviará en 2 horas",
fechaEnvio,
"MiApp"
);
// Guarda resultado.SubId para poder cancelarlo después
Envío de Prueba
// No descuenta créditos, útil para desarrollo
var resultado = await _smsService.SendTestAsync(
"34609123456",
"Mensaje de prueba"
);
Envío con Opciones Avanzadas
var request = new SendSmsRequest
{
Recipients = new List<Recipient>
{
new("34609123456"),
new("34666789012")
},
Message = "Mensaje con emojis 😀 y enlaces",
SenderId = "MiApp",
// Opciones avanzadas
Ucs2 = "1", // Habilitar Unicode/emojis
Long = "1", // Permitir SMS concatenados
SubId = "pedido-12345", // ID personalizado (máx 20 chars)
Label = "campaña-navidad", // Metadatos (máx 255 chars)
// Webhooks
AckUrl = "https://mi-servidor.com/webhook/delivery",
ClickUrl = "https://mi-servidor.com/webhook/click"
};
var resultado = await _smsService.SendAsync(request);
Consultas de Cuenta
Consultar Saldo
var balance = await _smsService.GetBalanceAsync();
if (balance.IsSuccess)
{
Console.WriteLine($"Créditos disponibles: {balance.CreditsDecimal}");
// Verificar si hay suficientes créditos
if (balance.CreditsDecimal < 100)
{
// Alertar que quedan pocos créditos
}
}
Consultar Precios
// Precios de varios países
var precios = await _smsService.GetPricesAsync(new[] { "ES", "FR", "US", "MX" });
foreach (var (codigo, precio) in precios.Prices)
{
Console.WriteLine($"{precio.Name} ({precio.Prefix}): {precio.Credits} créditos");
}
// Precio de un solo país
var precioEspana = await _smsService.GetPriceAsync("ES");
if (precioEspana != null)
{
Console.WriteLine($"España: {precioEspana.Credits} créditos por SMS");
}
Mensajes Programados
Cancelar Mensaje
// Cancelar un mensaje específico
var resultado = await _smsService.CancelScheduledAsync("65f33a88ceb3d");
if (resultado.IsSuccess)
{
Console.WriteLine("Mensaje cancelado");
}
Cancelar Todos
var resultado = await _smsService.CancelAllScheduledAsync();
Enviar Inmediatamente
// Enviar ahora un mensaje que estaba programado
var resultado = await _smsService.SendScheduledNowAsync("65f33a88ceb3d");
// Enviar todos los programados inmediatamente
var resultado = await _smsService.SendAllScheduledNowAsync();
Utilidades SMS
La librería incluye utilidades para validar y calcular información de mensajes SMS sin necesidad de enviarlos.
Usar Solo Utilidades (Sin Credenciales)
// Si solo necesitas las utilidades sin el servicio de envío
builder.Services.AddSmsUtils();
Analizar Mensaje Completo
var info = _smsUtils.AnalyzeMessage("Hola, tu código es 123456");
Console.WriteLine($"Caracteres: {info.CharacterCount}");
Console.WriteLine($"Segmentos: {info.SegmentCount}");
Console.WriteLine($"Codificación: {info.Encoding}");
Console.WriteLine($"Caracteres restantes: {info.RemainingChars}");
Console.WriteLine($"Es concatenado: {info.IsConcatenated}");
Console.WriteLine($"Excede límite (4 segmentos): {info.ExceedsRecommendedLimit}");
Console.WriteLine($"Multiplicador créditos: {info.CreditMultiplier}x");
Calcular Segmentos
// GSM-7 (caracteres estándar)
var segmentos1 = _smsUtils.CalculateSegments("Hola mundo"); // 1
var segmentos2 = _smsUtils.CalculateSegments(new string('A', 200)); // 2
var segmentos3 = _smsUtils.CalculateSegments(new string('A', 500)); // 4
// Unicode (emojis, caracteres especiales)
var segmentos4 = _smsUtils.CalculateSegments("Hola 😀"); // 1 (pero Unicode)
var segmentos5 = _smsUtils.CalculateSegments("😀" + new string('A', 100)); // 2
Detectar Codificación
var encoding1 = _smsUtils.DetectEncoding("Texto normal con ñ"); // Gsm7
var encoding2 = _smsUtils.DetectEncoding("Con emoji 🎉"); // Unicode
var encoding3 = _smsUtils.DetectEncoding("Текст на русском"); // Unicode
Validar Compatibilidad GSM-7
var mensaje = "Tu pedido está listo 📦";
if (_smsUtils.IsGsmCompatible(mensaje))
{
Console.WriteLine("Usa GSM-7: 160 chars/segmento");
}
else
{
var invalidChars = _smsUtils.GetInvalidGsmCharacters(mensaje);
Console.WriteLine($"Requiere Unicode (70 chars/segmento)");
Console.WriteLine($"Caracteres problemáticos: {string.Join(", ", invalidChars)}");
}
Contar Caracteres GSM
// Algunos caracteres GSM extendidos ocupan 2 espacios: ^ { } \ [ ~ ] | €
var count1 = _smsUtils.CountGsmCharacters("Hola"); // 4
var count2 = _smsUtils.CountGsmCharacters("Precio: 100€"); // 13 (€ cuenta como 2)
var count3 = _smsUtils.CountGsmCharacters("Array[0]"); // 10 ([ y ] cuentan como 2)
Validar Números de Teléfono
// Validación completa
var result = _smsUtils.ValidatePhoneNumber("+34 609 123 456");
if (result.IsValid)
{
Console.WriteLine($"Normalizado: {result.NormalizedNumber}"); // 34609123456
Console.WriteLine($"País: {result.CountryCode}"); // 34
}
else
{
Console.WriteLine($"Error: {result.ErrorMessage}");
}
// Con código de país por defecto
var result2 = _smsUtils.ValidatePhoneNumber("609123456", "34");
// Resultado: 34609123456
// Normalización directa
var normalized = _smsUtils.NormalizePhoneNumber("609-123-456", "34"); // 34609123456
// Validación rápida
if (_smsUtils.IsValidPhoneNumber("34609123456"))
{
// Número válido
}
Estimar Costes
var mensaje = "Tu pedido #12345 está en camino";
var destinatarios = 100;
var precioPorCredito = 1.0m; // Precio del país (ej: España)
var estimacion = _smsUtils.EstimateCost(mensaje, destinatarios, precioPorCredito);
Console.WriteLine($"Segmentos por mensaje: {estimacion.SegmentsPerMessage}");
Console.WriteLine($"Total segmentos: {estimacion.TotalSegments}");
Console.WriteLine($"Créditos por destinatario: {estimacion.CreditsPerRecipient}");
Console.WriteLine($"Créditos totales: {estimacion.TotalCredits}");
Truncar Mensajes
// Truncar a máximo 1 segmento
var truncado = _smsUtils.TruncateMessage(mensajeLargo, maxSegments: 1);
// Truncar a máximo 2 segmentos
var truncado2 = _smsUtils.TruncateMessage(mensajeLargo, maxSegments: 2);
Dividir Mensajes Largos
// Dividir en partes de 1 segmento cada una
var partes = _smsUtils.SplitMessage(mensajeMuyLargo, maxSegmentsPerPart: 1);
foreach (var (parte, index) in partes.Select((p, i) => (p, i)))
{
Console.WriteLine($"Parte {index + 1}: {parte}");
}
Convertir Unicode a GSM
// Reemplaza caracteres Unicode por equivalentes GSM cuando es posible
var original = "Código: «ABC» — información…";
var converted = _smsUtils.ReplaceUnicodeWithGsm(original);
// Resultado: "Codigo: \"ABC\" - informacion..."
// Útil para reducir costes cuando el contenido lo permite
Caracteres Restantes
var mensaje = "Tu código es: ";
var restantes = _smsUtils.GetRemainingCharacters(mensaje);
Console.WriteLine($"Puedes añadir {restantes} caracteres más en este segmento");
Webhooks
Configurar URLs de Webhook
var request = new SendSmsRequest
{
Recipients = [new("34609123456")],
Message = "Mensaje con tracking",
AckUrl = "https://mi-servidor.com/api/sms/delivery",
ClickUrl = "https://mi-servidor.com/api/sms/click"
};
Recibir Confirmación de Entrega
LabsMobile envía una petición GET a tu ackurl:
[ApiController]
[Route("api/sms")]
public class SmsWebhookController : ControllerBase
{
[HttpGet("delivery")]
public IActionResult DeliveryCallback(
[FromQuery] string acklevel,
[FromQuery] string desc,
[FromQuery] string status,
[FromQuery] string msisdn,
[FromQuery] string subid,
[FromQuery] string timestamp)
{
var callback = new DeliveryStatusCallback
{
AckLevel = acklevel, // operator, handset, error
Description = desc, // DELIVRD, REJECTD, EXPIRED, etc.
Status = status, // ok, ko
Msisdn = msisdn,
SubId = subid,
Timestamp = timestamp
};
if (callback.IsDelivered)
{
// SMS entregado exitosamente
_logger.LogInformation("SMS {SubId} entregado a {Msisdn}", subid, msisdn);
}
else
{
// Error en la entrega
_logger.LogWarning("SMS {SubId} falló: {Desc}", subid, desc);
}
return Ok();
}
}
Estados de entrega posibles:
DELIVRD- Mensaje entregadoREJECTD- Mensaje rechazadoEXPIRED- Mensaje expiradoUNDELIV- No entregableBLOCKED- BloqueadoUNKNOWN- Estado desconocido
Recibir Tracking de Clics
LabsMobile envía una petición POST con JSON a tu clickurl:
[HttpPost("click")]
public IActionResult ClickCallback([FromBody] ClickTrackingCallback callback)
{
_logger.LogInformation(
"Clic en mensaje {SubId} desde {Msisdn}, IP: {Ip}",
callback.SubId,
callback.Msisdn,
callback.Ip
);
// Registrar el clic para analytics
_analyticsService.TrackClick(callback.SubId, callback.Msisdn);
return Ok();
}
Recibir Mensajes Entrantes
Si tienes un número virtual configurado:
[HttpPost("inbound")]
public IActionResult InboundCallback([FromBody] InboundMessageCallback callback)
{
_logger.LogInformation(
"Mensaje entrante de {Msisdn}: {Message}",
callback.Msisdn,
callback.Message
);
// Procesar respuesta del usuario
await _messageProcessor.ProcessInboundAsync(
callback.Msisdn,
callback.Message
);
return Ok();
}
Referencia de API
ILabsMobileSmsService
| Método | Descripción |
|---|---|
SendAsync(request) |
Envío completo con todas las opciones |
SendAsync(phone, message, sender?) |
Envío simple a un destinatario |
SendBulkAsync(phones, message, sender?) |
Envío a múltiples destinatarios |
ScheduleAsync(phone, message, datetime, sender?) |
Programar envío futuro |
SendTestAsync(phone, message, sender?) |
Envío de prueba (sin créditos) |
GetBalanceAsync() |
Consultar saldo de créditos |
GetPricesAsync(countryCodes) |
Consultar precios por país |
GetPriceAsync(countryCode) |
Consultar precio de un país |
CancelScheduledAsync(subId) |
Cancelar mensaje programado |
CancelAllScheduledAsync() |
Cancelar todos los programados |
SendScheduledNowAsync(subId) |
Enviar programado inmediatamente |
SendAllScheduledNowAsync() |
Enviar todos los programados ahora |
ISmsUtils
| Método | Descripción |
|---|---|
AnalyzeMessage(message) |
Análisis completo del mensaje |
CalculateSegments(message) |
Calcular número de segmentos |
CountGsmCharacters(message) |
Contar caracteres GSM |
DetectEncoding(message) |
Detectar codificación necesaria |
IsGsmCompatible(message) |
Verificar compatibilidad GSM-7 |
GetInvalidGsmCharacters(message) |
Obtener caracteres no GSM |
ValidatePhoneNumber(phone, countryCode?) |
Validar y normalizar teléfono |
NormalizePhoneNumber(phone, countryCode?) |
Normalizar teléfono |
IsValidPhoneNumber(phone) |
Validación rápida de teléfono |
EstimateCost(message, recipients, price?) |
Estimar coste de envío |
TruncateMessage(message, maxSegments) |
Truncar mensaje |
SplitMessage(message, maxSegmentsPerPart) |
Dividir mensaje largo |
ReplaceUnicodeWithGsm(message) |
Convertir Unicode a GSM |
GetRemainingCharacters(message) |
Caracteres restantes |
Manejo de Errores
La librería lanza excepciones tipadas LabsMobileApiException que encapsulan los errores de la API y de red. Esto permite un manejo granular de los distintos tipos de fallo.
Tipos de Errores
try
{
var resultado = await _smsService.SendAsync("34609123456", "Hola");
}
catch (LabsMobileApiException ex) when (ex.StatusCode != null)
{
// Error HTTP de la API (401, 403, 500, etc.)
_logger.LogError("Error HTTP {StatusCode}: {Message}", ex.StatusCode, ex.Message);
}
catch (LabsMobileApiException ex) when (ex.InnerException is HttpRequestException)
{
// Error de conexión (red caída, DNS, etc.)
_logger.LogError("Error de red: {Message}", ex.Message);
}
catch (LabsMobileApiException ex) when (ex.InnerException is TaskCanceledException)
{
// Timeout en la petición
_logger.LogWarning("La petición a LabsMobile superó el timeout");
}
catch (InvalidOperationException)
{
// Servicio no configurado (faltan Username o ApiToken)
_logger.LogError("El servicio SMS no está configurado");
}
catch (ArgumentException ex)
{
// Validación de parámetros (teléfono vacío, mensaje vacío, etc.)
_logger.LogWarning("Parámetro inválido: {Message}", ex.Message);
}
Propiedades de LabsMobileApiException
| Propiedad | Tipo | Descripción |
|---|---|---|
StatusCode |
HttpStatusCode? |
Código HTTP cuando el error es de la API |
ErrorCode |
string? |
Código de error de LabsMobile |
InnerException |
Exception? |
Excepción original (HttpRequestException, TaskCanceledException, JsonException) |
Resiliencia HTTP
La librería incluye resiliencia HTTP integrada mediante Microsoft.Extensions.Http.Resilience (basado en Polly). Al registrar el servicio con AddLabsMobileSms(), se configura automáticamente:
- Reintentos con backoff exponencial: Para errores transitorios (408, 429, 500, 502, 503, 504)
- Circuit breaker: Evita saturar la API cuando está caída, cortando las peticiones temporalmente
- Timeout por intento: Cada intento individual tiene un timeout configurado
- Timeout total: Límite máximo para toda la operación incluyendo reintentos
Esto significa que las llamadas HTTP se reintentan automáticamente ante fallos transitorios sin necesidad de código adicional.
Códigos de Error
| Código | Descripción | Solución |
|---|---|---|
| 0 | Éxito | - |
| 23 | No hay destinatarios | Añadir al menos un número |
| 24 | Demasiados destinatarios | Máximo 10,000 por petición |
| 35 | Créditos insuficientes | Recargar saldo |
| 39 | Formato de fecha inválido | Usar formato YYYY-MM-DD HH:MM:SS |
| 52 | Mensajes programados no encontrados | Verificar SubId |
Límites de Caracteres
GSM-7 (Texto Estándar)
| Segmentos | Caracteres | Créditos |
|---|---|---|
| 1 | 1-160 | 1x |
| 2 | 161-306 | 2x |
| 3 | 307-459 | 3x |
| 4 | 460-612 | 4x |
Unicode (Emojis/Especiales)
| Segmentos | Caracteres | Créditos |
|---|---|---|
| 1 | 1-70 | 1x |
| 2 | 71-134 | 2x |
| 3 | 135-201 | 3x |
| 4 | 202-268 | 4x |
Nota: No se recomienda exceder 4 segmentos por mensaje.
Caracteres GSM-7 Extendidos
Estos caracteres ocupan 2 espacios: ^ { } \ [ ~ ] | €
Ejemplos Completos
Ejemplo 1: Servicio de Verificación por SMS
public class VerificationService
{
private readonly ILabsMobileSmsService _smsService;
private readonly ISmsUtils _smsUtils;
public VerificationService(ILabsMobileSmsService smsService, ISmsUtils smsUtils)
{
_smsService = smsService;
_smsUtils = smsUtils;
}
public async Task<VerificationResult> SendVerificationCodeAsync(string phoneNumber)
{
// Validar número
var validation = _smsUtils.ValidatePhoneNumber(phoneNumber, "34");
if (!validation.IsValid)
{
return new VerificationResult
{
Success = false,
Error = validation.ErrorMessage
};
}
// Generar código
var code = Random.Shared.Next(100000, 999999).ToString();
// Enviar SMS
var mensaje = $"Tu código de verificación es: {code}";
var resultado = await _smsService.SendAsync(
validation.NormalizedNumber,
mensaje
);
return new VerificationResult
{
Success = resultado.IsSuccess,
Code = resultado.IsSuccess ? code : null,
MessageId = resultado.SubId,
Error = resultado.IsSuccess ? null : resultado.Message
};
}
}
Ejemplo 2: Notificaciones de Pedidos
public class OrderNotificationService
{
private readonly ILabsMobileSmsService _smsService;
private readonly ISmsUtils _smsUtils;
public async Task NotifyOrderShippedAsync(Order order)
{
var mensaje = $"¡Tu pedido #{order.Id} ha sido enviado! " +
$"Tracking: {order.TrackingNumber}. " +
$"Entrega estimada: {order.EstimatedDelivery:dd/MM}";
// Verificar que el mensaje no exceda límites
var info = _smsUtils.AnalyzeMessage(mensaje);
if (info.ExceedsRecommendedLimit)
{
mensaje = _smsUtils.TruncateMessage(mensaje, 4);
}
await _smsService.SendAsync(order.CustomerPhone, mensaje);
}
public async Task SendBulkPromotionAsync(List<Customer> customers, string promotion)
{
// Estimar coste antes de enviar
var estimacion = _smsUtils.EstimateCost(promotion, customers.Count);
// Verificar saldo
var balance = await _smsService.GetBalanceAsync();
if (balance.CreditsDecimal < estimacion.TotalBaseCredits)
{
throw new InsufficientCreditsException(
$"Se necesitan {estimacion.TotalBaseCredits} créditos, " +
$"disponibles: {balance.CreditsDecimal}"
);
}
// Enviar en lotes de 10,000
var batches = customers
.Select(c => c.Phone)
.Chunk(10000);
foreach (var batch in batches)
{
await _smsService.SendBulkAsync(batch, promotion);
}
}
}
Ejemplo 3: Recordatorios Programados
public class AppointmentReminderService
{
private readonly ILabsMobileSmsService _smsService;
public async Task ScheduleReminderAsync(Appointment appointment)
{
var mensaje = $"Recordatorio: Tienes cita el {appointment.Date:dd/MM} " +
$"a las {appointment.Date:HH:mm} en {appointment.Location}";
// Programar 24 horas antes
var fechaEnvio = appointment.Date.AddHours(-24);
var resultado = await _smsService.ScheduleAsync(
appointment.PatientPhone,
mensaje,
fechaEnvio
);
// Guardar SubId para poder cancelar si se reprograma la cita
appointment.ReminderMessageId = resultado.SubId;
await _appointmentRepository.UpdateAsync(appointment);
}
public async Task CancelReminderAsync(string messageId)
{
if (!string.IsNullOrEmpty(messageId))
{
await _smsService.CancelScheduledAsync(messageId);
}
}
}
Ejemplo 4: Validación y Optimización de Mensajes
public class MessageComposer
{
private readonly ISmsUtils _smsUtils;
public MessageComposer(ISmsUtils smsUtils)
{
_smsUtils = smsUtils;
}
public ComposedMessage Compose(string content)
{
var info = _smsUtils.AnalyzeMessage(content);
var result = new ComposedMessage
{
Original = content,
Optimized = content,
OriginalSegments = info.SegmentCount,
OriginalEncoding = info.Encoding
};
// Intentar optimizar si usa Unicode innecesariamente
if (info.Encoding == SmsEncoding.Unicode)
{
var optimized = _smsUtils.ReplaceUnicodeWithGsm(content);
var optimizedInfo = _smsUtils.AnalyzeMessage(optimized);
if (optimizedInfo.Encoding == SmsEncoding.Gsm7)
{
result.Optimized = optimized;
result.OptimizedSegments = optimizedInfo.SegmentCount;
result.OptimizedEncoding = SmsEncoding.Gsm7;
result.CreditsSaved = info.SegmentCount - optimizedInfo.SegmentCount;
}
}
return result;
}
}
Mejores Prácticas
- Validar números antes de enviar: Usa
ValidatePhoneNumber()para normalizar y verificar - Verificar codificación: Usa
DetectEncoding()para saber el coste real - Estimar costes: Usa
EstimateCost()antes de envíos masivos - Verificar saldo: Consulta
GetBalanceAsync()antes de campañas grandes - No exceder 4 segmentos: Mensajes muy largos tienen mala entrega
- Usar modo test: Desarrolla con
TestMode = truepara no gastar créditos - Guardar SubId: Necesario para cancelar mensajes programados
- Implementar webhooks: Para confirmar entregas y tracking
Requisitos
- .NET 10.0 o superior
- Cuenta en LabsMobile con credenciales API
Licencia
MIT License - ver LICENSE para más detalles.
Autor
Jorge Jerez Sabater - Gesgocom
No packages depend on Gesgocom.GesSms.
.NET 10.0
- Microsoft.Extensions.Http (>= 10.0.2)
- Microsoft.Extensions.Http.Resilience (>= 10.2.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.2)
- Microsoft.Extensions.Options (>= 10.0.2)
| Version | Downloads | Last updated |
|---|---|---|
| 1.0.1 | 1 | 02/09/2026 |