Initial commit: IronNotify.Client SDK

Event notification SDK for .NET applications with:
- Multi-channel notifications (push, email, SMS, webhooks)
- Fluent event builder API
- Offline queue with automatic retry
- Real-time notifications via SignalR
This commit is contained in:
David Friedel 2025-12-25 09:08:13 +00:00
commit 058d50f99b
8 changed files with 1224 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
bin/
obj/
*.user
*.suo
.vs/
*.DotSettings.user

7
Class1.cs Executable file
View File

@ -0,0 +1,7 @@
namespace IronNotify.Client
{
public class Class1
{
}
}

25
IronNotify.Client.csproj Executable file
View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- NuGet Package Properties -->
<PackageId>IronNotify.Client</PackageId>
<Version>1.0.0</Version>
<Authors>David H Friedel Jr</Authors>
<Company>MarketAlly</Company>
<Description>Client SDK for IronNotify - Event Notification Platform. Send events, alerts, and notifications from your applications.</Description>
<PackageTags>notifications;events;alerts;monitoring;webhooks</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/ironservices/ironnotify-client</RepositoryUrl>
<PackageProjectUrl>https://www.ironnotify.com</PackageProjectUrl>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
</ItemGroup>
</Project>

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 IronServices
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

335
NotifyClient.cs Executable file
View File

@ -0,0 +1,335 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace IronNotify.Client;
public class NotifyClient : IDisposable
{
private readonly HttpClient _httpClient;
private readonly NotifyClientOptions _options;
private readonly NotifyOfflineQueue? _offlineQueue;
private bool _disposed;
/// <summary>
/// Gets the offline queue for accessing queued items.
/// </summary>
public NotifyOfflineQueue? OfflineQueue => _offlineQueue;
public NotifyClient(NotifyClientOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
if (string.IsNullOrEmpty(options.ApiKey))
throw new ArgumentException("API key is required", nameof(options));
_httpClient = new HttpClient
{
BaseAddress = new Uri(options.BaseUrl.TrimEnd('/') + "/")
};
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", options.ApiKey);
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
if (options.Timeout.HasValue)
_httpClient.Timeout = options.Timeout.Value;
// Initialize offline queue if enabled
if (options.EnableOfflineQueue)
{
_offlineQueue = new NotifyOfflineQueue(
SendQueuedEventAsync,
options.OfflineQueueDirectory,
options.MaxOfflineQueueSize,
enableAutoRetry: true);
}
}
private async Task<bool> SendQueuedEventAsync(QueuedEvent queuedEvent)
{
var result = await NotifyAsync(queuedEvent.Request, skipQueue: true);
return result.Success;
}
public NotifyClient(string apiKey, string? appSlug = null)
: this(new NotifyClientOptions { ApiKey = apiKey, DefaultAppSlug = appSlug })
{
}
/// <summary>
/// Send an event notification
/// </summary>
public Task<EventResult> NotifyAsync(NotifyEventRequest request, CancellationToken cancellationToken = default)
{
return NotifyAsync(request, skipQueue: false, cancellationToken);
}
private async Task<EventResult> NotifyAsync(NotifyEventRequest request, bool skipQueue, CancellationToken cancellationToken = default)
{
var apiRequest = new
{
appSlug = request.AppSlug ?? _options.DefaultAppSlug,
appId = request.AppId,
eventType = request.EventType,
severity = request.Severity.ToString(),
source = request.Source ?? _options.DefaultSource,
title = request.Title,
message = request.Message,
entityId = request.EntityId,
metadata = request.Metadata,
actions = request.Actions?.Select(a => new
{
actionId = a.ActionId,
label = a.Label,
webhookUrl = a.WebhookUrl
})
};
try
{
var response = await _httpClient.PostAsJsonAsync("api/v1/events", apiRequest, cancellationToken);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<EventResponse>(cancellationToken: cancellationToken);
return new EventResult
{
Success = true,
EventId = result?.Id,
Status = result?.Status
};
}
var error = await response.Content.ReadAsStringAsync(cancellationToken);
// Queue for retry if enabled and not already retrying
if (!skipQueue && _offlineQueue != null)
{
_offlineQueue.Enqueue(request);
}
return new EventResult
{
Success = false,
Error = error,
Queued = !skipQueue && _offlineQueue != null
};
}
catch (Exception ex)
{
// Queue for retry on network errors
if (!skipQueue && _offlineQueue != null)
{
_offlineQueue.Enqueue(request);
}
return new EventResult
{
Success = false,
Error = ex.Message,
Queued = !skipQueue && _offlineQueue != null
};
}
}
/// <summary>
/// Send a simple event notification
/// </summary>
public Task<EventResult> NotifyAsync(
string eventType,
string title,
Severity severity = Severity.Info,
string? message = null,
Dictionary<string, object>? metadata = null,
CancellationToken cancellationToken = default)
{
return NotifyAsync(new NotifyEventRequest
{
EventType = eventType,
Title = title,
Severity = severity,
Message = message,
Metadata = metadata
}, cancellationToken);
}
/// <summary>
/// Create a fluent event builder
/// </summary>
public EventBuilder Event(string eventType) => new(this, eventType);
public void Dispose()
{
if (!_disposed)
{
_offlineQueue?.Dispose();
_httpClient.Dispose();
_disposed = true;
}
}
}
public class NotifyClientOptions
{
public string ApiKey { get; set; } = string.Empty;
public string BaseUrl { get; set; } = "https://ironnotify.com";
public string? DefaultAppSlug { get; set; }
public string? DefaultSource { get; set; }
public TimeSpan? Timeout { get; set; }
/// <summary>
/// Enable offline queue for failed sends. Default is true.
/// </summary>
public bool EnableOfflineQueue { get; set; } = true;
/// <summary>
/// Directory to store offline queue. Defaults to LocalApplicationData/IronNotify/Queue.
/// </summary>
public string? OfflineQueueDirectory { get; set; }
/// <summary>
/// Maximum items to store in offline queue. Default: 500.
/// </summary>
public int MaxOfflineQueueSize { get; set; } = 500;
}
public class NotifyEventRequest
{
public Guid? AppId { get; set; }
public string? AppSlug { get; set; }
public string EventType { get; set; } = string.Empty;
public Severity Severity { get; set; } = Severity.Info;
public string? Source { get; set; }
public string Title { get; set; } = string.Empty;
public string? Message { get; set; }
public string? EntityId { get; set; }
public Dictionary<string, object>? Metadata { get; set; }
public List<EventAction>? Actions { get; set; }
}
public class EventAction
{
public string ActionId { get; set; } = string.Empty;
public string Label { get; set; } = string.Empty;
public string? WebhookUrl { get; set; }
}
public enum Severity
{
Info,
Warning,
High,
Critical
}
public class EventResult
{
public bool Success { get; set; }
public Guid? EventId { get; set; }
public string? Status { get; set; }
public string? Error { get; set; }
/// <summary>
/// Whether the event was queued for later retry (when offline queue is enabled).
/// </summary>
public bool Queued { get; set; }
}
internal class EventResponse
{
[JsonPropertyName("id")]
public Guid Id { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;
}
/// <summary>
/// Fluent builder for creating events
/// </summary>
public class EventBuilder
{
private readonly NotifyClient _client;
private readonly NotifyEventRequest _request;
internal EventBuilder(NotifyClient client, string eventType)
{
_client = client;
_request = new NotifyEventRequest { EventType = eventType };
}
public EventBuilder WithSeverity(Severity severity)
{
_request.Severity = severity;
return this;
}
public EventBuilder WithTitle(string title)
{
_request.Title = title;
return this;
}
public EventBuilder WithMessage(string message)
{
_request.Message = message;
return this;
}
public EventBuilder WithSource(string source)
{
_request.Source = source;
return this;
}
public EventBuilder WithEntityId(string entityId)
{
_request.EntityId = entityId;
return this;
}
public EventBuilder WithApp(string slug)
{
_request.AppSlug = slug;
return this;
}
public EventBuilder WithApp(Guid appId)
{
_request.AppId = appId;
return this;
}
public EventBuilder WithMetadata(string key, object value)
{
_request.Metadata ??= new Dictionary<string, object>();
_request.Metadata[key] = value;
return this;
}
public EventBuilder WithMetadata(Dictionary<string, object> metadata)
{
_request.Metadata = metadata;
return this;
}
public EventBuilder WithAction(string actionId, string label, string? webhookUrl = null)
{
_request.Actions ??= new List<EventAction>();
_request.Actions.Add(new EventAction
{
ActionId = actionId,
Label = label,
WebhookUrl = webhookUrl
});
return this;
}
public Task<EventResult> SendAsync(CancellationToken cancellationToken = default)
{
return _client.NotifyAsync(_request, cancellationToken);
}
}

375
NotifyRealTimeClient.cs Normal file
View File

@ -0,0 +1,375 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.SignalR.Client;
namespace IronNotify.Client;
/// <summary>
/// Real-time notification client using SignalR for receiving live notifications
/// </summary>
public class NotifyRealTimeClient : IAsyncDisposable
{
private readonly HubConnection _connection;
private readonly NotifyRealTimeOptions _options;
private bool _disposed;
/// <summary>
/// Fired when a new notification is received
/// </summary>
public event EventHandler<NotificationReceivedEventArgs>? NotificationReceived;
/// <summary>
/// Fired when the unread count changes
/// </summary>
public event EventHandler<UnreadCountChangedEventArgs>? UnreadCountChanged;
/// <summary>
/// Fired when a notification is marked as read
/// </summary>
public event EventHandler<NotificationReadEventArgs>? NotificationRead;
/// <summary>
/// Fired when an event status changes (acknowledged, resolved)
/// </summary>
public event EventHandler<EventStatusChangedEventArgs>? EventStatusChanged;
/// <summary>
/// Fired when the connection state changes
/// </summary>
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
/// <summary>
/// Current connection state
/// </summary>
public HubConnectionState State => _connection.State;
/// <summary>
/// Whether the client is currently connected
/// </summary>
public bool IsConnected => _connection.State == HubConnectionState.Connected;
public NotifyRealTimeClient(NotifyRealTimeOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
var hubUrl = options.BaseUrl.TrimEnd('/') + "/hubs/notifications";
_connection = new HubConnectionBuilder()
.WithUrl(hubUrl, httpOptions =>
{
httpOptions.Headers.Add("Authorization", $"Bearer {options.ApiKey}");
})
.WithAutomaticReconnect(new RetryPolicy(options.MaxReconnectAttempts))
.Build();
SetupEventHandlers();
}
public NotifyRealTimeClient(string apiKey, string baseUrl = "https://ironnotify.com")
: this(new NotifyRealTimeOptions { ApiKey = apiKey, BaseUrl = baseUrl })
{
}
private void SetupEventHandlers()
{
_connection.On<RealTimeNotification>("NotificationReceived", notification =>
{
NotificationReceived?.Invoke(this, new NotificationReceivedEventArgs(notification));
});
_connection.On<int>("UnreadCountChanged", count =>
{
UnreadCountChanged?.Invoke(this, new UnreadCountChangedEventArgs(count));
});
_connection.On<NotificationReadPayload>("NotificationRead", payload =>
{
NotificationRead?.Invoke(this, new NotificationReadEventArgs(payload.EventId, payload.ReadAt));
});
_connection.On<EventStatusPayload>("EventStatusChanged", payload =>
{
EventStatusChanged?.Invoke(this, new EventStatusChangedEventArgs(payload.EventId, payload.Status, payload.UpdatedAt));
});
_connection.Reconnecting += error =>
{
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(
HubConnectionState.Reconnecting, error?.Message));
return Task.CompletedTask;
};
_connection.Reconnected += connectionId =>
{
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(
HubConnectionState.Connected, null));
// Re-subscribe to user notifications after reconnect
if (_options.UserId.HasValue)
{
_ = SubscribeToUserAsync(_options.UserId.Value);
}
return Task.CompletedTask;
};
_connection.Closed += error =>
{
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(
HubConnectionState.Disconnected, error?.Message));
return Task.CompletedTask;
};
}
/// <summary>
/// Connect to the notification hub
/// </summary>
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
if (_connection.State == HubConnectionState.Disconnected)
{
await _connection.StartAsync(cancellationToken);
ConnectionStateChanged?.Invoke(this, new ConnectionStateChangedEventArgs(
HubConnectionState.Connected, null));
}
}
/// <summary>
/// Disconnect from the notification hub
/// </summary>
public async Task DisconnectAsync(CancellationToken cancellationToken = default)
{
if (_connection.State == HubConnectionState.Connected)
{
await _connection.StopAsync(cancellationToken);
}
}
/// <summary>
/// Subscribe to notifications for a specific user
/// </summary>
public async Task SubscribeToUserAsync(Guid userId, CancellationToken cancellationToken = default)
{
await EnsureConnectedAsync(cancellationToken);
await _connection.InvokeAsync("SubscribeToUser", userId, cancellationToken);
_options.UserId = userId;
}
/// <summary>
/// Unsubscribe from user notifications
/// </summary>
public async Task UnsubscribeFromUserAsync(Guid userId, CancellationToken cancellationToken = default)
{
await EnsureConnectedAsync(cancellationToken);
await _connection.InvokeAsync("UnsubscribeFromUser", userId, cancellationToken);
}
/// <summary>
/// Subscribe to notifications for a specific app
/// </summary>
public async Task SubscribeToAppAsync(Guid appId, CancellationToken cancellationToken = default)
{
await EnsureConnectedAsync(cancellationToken);
await _connection.InvokeAsync("SubscribeToApp", appId, cancellationToken);
}
/// <summary>
/// Unsubscribe from app notifications
/// </summary>
public async Task UnsubscribeFromAppAsync(Guid appId, CancellationToken cancellationToken = default)
{
await EnsureConnectedAsync(cancellationToken);
await _connection.InvokeAsync("UnsubscribeFromApp", appId, cancellationToken);
}
/// <summary>
/// Mark a notification as read (triggers server-side update and broadcasts to other clients)
/// </summary>
public async Task MarkAsReadAsync(Guid userId, Guid eventId, CancellationToken cancellationToken = default)
{
await EnsureConnectedAsync(cancellationToken);
await _connection.InvokeAsync("MarkAsRead", userId, eventId, cancellationToken);
}
private async Task EnsureConnectedAsync(CancellationToken cancellationToken)
{
if (_connection.State == HubConnectionState.Disconnected)
{
await ConnectAsync(cancellationToken);
}
}
public async ValueTask DisposeAsync()
{
if (!_disposed)
{
await _connection.DisposeAsync();
_disposed = true;
}
}
private class RetryPolicy : IRetryPolicy
{
private readonly int _maxAttempts;
public RetryPolicy(int maxAttempts) => _maxAttempts = maxAttempts;
public TimeSpan? NextRetryDelay(RetryContext retryContext)
{
if (retryContext.PreviousRetryCount >= _maxAttempts)
return null;
// Exponential backoff: 0s, 2s, 4s, 8s, 16s, max 30s
var delay = Math.Min(Math.Pow(2, retryContext.PreviousRetryCount), 30);
return TimeSpan.FromSeconds(delay);
}
}
}
public class NotifyRealTimeOptions
{
public string ApiKey { get; set; } = string.Empty;
public string BaseUrl { get; set; } = "https://ironnotify.com";
public Guid? UserId { get; set; }
public int MaxReconnectAttempts { get; set; } = 5;
}
#region Event Args
public class NotificationReceivedEventArgs : EventArgs
{
public RealTimeNotification Notification { get; }
public NotificationReceivedEventArgs(RealTimeNotification notification)
{
Notification = notification;
}
}
public class UnreadCountChangedEventArgs : EventArgs
{
public int UnreadCount { get; }
public UnreadCountChangedEventArgs(int unreadCount)
{
UnreadCount = unreadCount;
}
}
public class NotificationReadEventArgs : EventArgs
{
public Guid EventId { get; }
public DateTime ReadAt { get; }
public NotificationReadEventArgs(Guid eventId, DateTime readAt)
{
EventId = eventId;
ReadAt = readAt;
}
}
public class EventStatusChangedEventArgs : EventArgs
{
public Guid EventId { get; }
public string Status { get; }
public DateTime UpdatedAt { get; }
public EventStatusChangedEventArgs(Guid eventId, string status, DateTime updatedAt)
{
EventId = eventId;
Status = status;
UpdatedAt = updatedAt;
}
}
public class ConnectionStateChangedEventArgs : EventArgs
{
public HubConnectionState State { get; }
public string? Error { get; }
public ConnectionStateChangedEventArgs(HubConnectionState state, string? error)
{
State = state;
Error = error;
}
}
#endregion
#region DTOs
public class RealTimeNotification
{
[JsonPropertyName("eventId")]
public Guid EventId { get; set; }
[JsonPropertyName("deliveryId")]
public Guid DeliveryId { get; set; }
[JsonPropertyName("eventType")]
public string EventType { get; set; } = string.Empty;
[JsonPropertyName("severity")]
public string Severity { get; set; } = string.Empty;
[JsonPropertyName("title")]
public string Title { get; set; } = string.Empty;
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonPropertyName("appId")]
public Guid AppId { get; set; }
[JsonPropertyName("appName")]
public string? AppName { get; set; }
[JsonPropertyName("appSlug")]
public string? AppSlug { get; set; }
[JsonPropertyName("data")]
public Dictionary<string, object>? Data { get; set; }
[JsonPropertyName("actions")]
public List<RealTimeAction>? Actions { get; set; }
}
public class RealTimeAction
{
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("label")]
public string Label { get; set; } = string.Empty;
[JsonPropertyName("url")]
public string? Url { get; set; }
[JsonPropertyName("isPrimary")]
public bool IsPrimary { get; set; }
}
internal class NotificationReadPayload
{
[JsonPropertyName("eventId")]
public Guid EventId { get; set; }
[JsonPropertyName("readAt")]
public DateTime ReadAt { get; set; }
}
internal class EventStatusPayload
{
[JsonPropertyName("eventId")]
public Guid EventId { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; } = string.Empty;
[JsonPropertyName("updatedAt")]
public DateTime UpdatedAt { get; set; }
}
#endregion

249
OfflineQueue.cs Normal file
View File

@ -0,0 +1,249 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace IronNotify.Client;
/// <summary>
/// File-based offline queue for notification events.
/// Persists failed sends to disk and retries with exponential backoff.
/// </summary>
public class NotifyOfflineQueue : IDisposable
{
private readonly string _queueFilePath;
private readonly int _maxQueueSize;
private readonly object _fileLock = new();
private readonly SemaphoreSlim _retrySemaphore = new(1, 1);
private readonly Timer? _retryTimer;
private readonly Func<QueuedEvent, Task<bool>> _sendFunc;
private int _retryAttempt;
private bool _disposed;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = false
};
/// <summary>
/// Creates a new offline queue.
/// </summary>
/// <param name="sendFunc">Function to send an event to server. Returns true on success.</param>
/// <param name="queueDirectory">Directory to store queue file. Defaults to app data.</param>
/// <param name="maxQueueSize">Maximum items to store. Oldest items dropped when exceeded.</param>
/// <param name="enableAutoRetry">Whether to automatically retry sending queued items.</param>
public NotifyOfflineQueue(
Func<QueuedEvent, Task<bool>> sendFunc,
string? queueDirectory = null,
int maxQueueSize = 500,
bool enableAutoRetry = true)
{
_sendFunc = sendFunc;
_maxQueueSize = maxQueueSize;
var directory = queueDirectory ?? GetDefaultQueueDirectory();
Directory.CreateDirectory(directory);
_queueFilePath = Path.Combine(directory, "notify_queue.json");
if (enableAutoRetry)
{
// Start retry timer - initial delay 30 seconds, then every 60 seconds
_retryTimer = new Timer(
_ => _ = RetryQueuedItemsAsync(),
null,
TimeSpan.FromSeconds(30),
TimeSpan.FromSeconds(60));
}
}
/// <summary>
/// Number of items currently in the queue.
/// </summary>
public int Count
{
get
{
lock (_fileLock)
{
var items = LoadQueue();
return items.Count;
}
}
}
/// <summary>
/// Enqueue an event that failed to send.
/// </summary>
public void Enqueue(NotifyEventRequest request)
{
lock (_fileLock)
{
var queue = LoadQueue();
queue.Add(new QueuedEvent
{
QueuedAt = DateTime.UtcNow,
Request = request
});
// Trim to max size (remove oldest)
while (queue.Count > _maxQueueSize)
{
queue.RemoveAt(0);
}
SaveQueue(queue);
}
}
/// <summary>
/// Try to send all queued items.
/// </summary>
public async Task<int> RetryQueuedItemsAsync()
{
if (!await _retrySemaphore.WaitAsync(0))
{
// Already retrying
return 0;
}
int sentCount = 0;
try
{
List<QueuedEvent> items;
lock (_fileLock)
{
items = LoadQueue();
}
if (items.Count == 0)
{
_retryAttempt = 0;
return 0;
}
var remaining = new List<QueuedEvent>();
foreach (var item in items)
{
var success = await _sendFunc(item);
if (success)
{
sentCount++;
}
else
{
remaining.Add(item);
}
}
// Save remaining items
lock (_fileLock)
{
SaveQueue(remaining);
}
if (remaining.Count == 0)
{
_retryAttempt = 0;
}
else
{
_retryAttempt++;
}
return sentCount;
}
catch
{
_retryAttempt++;
return 0;
}
finally
{
_retrySemaphore.Release();
}
}
/// <summary>
/// Clear all queued items without sending.
/// </summary>
public void Clear()
{
lock (_fileLock)
{
SaveQueue(new List<QueuedEvent>());
}
}
/// <summary>
/// Get all queued items (for display/export).
/// </summary>
public List<QueuedEvent> GetQueuedItems()
{
lock (_fileLock)
{
return LoadQueue();
}
}
private List<QueuedEvent> LoadQueue()
{
try
{
if (!File.Exists(_queueFilePath))
{
return new List<QueuedEvent>();
}
var json = File.ReadAllText(_queueFilePath);
return JsonSerializer.Deserialize<List<QueuedEvent>>(json, JsonOptions)
?? new List<QueuedEvent>();
}
catch
{
return new List<QueuedEvent>();
}
}
private void SaveQueue(List<QueuedEvent> items)
{
try
{
var json = JsonSerializer.Serialize(items, JsonOptions);
File.WriteAllText(_queueFilePath, json);
}
catch
{
// Ignore write errors
}
}
private static string GetDefaultQueueDirectory()
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"IronNotify",
"Queue");
}
public void Dispose()
{
if (!_disposed)
{
_retryTimer?.Dispose();
_retrySemaphore.Dispose();
_disposed = true;
}
}
}
/// <summary>
/// A queued notification event with metadata.
/// </summary>
public class QueuedEvent
{
public DateTime QueuedAt { get; set; }
public int RetryCount { get; set; }
public NotifyEventRequest Request { get; set; } = new();
}

206
README.md Normal file
View File

@ -0,0 +1,206 @@
# IronNotify.Client
Event notification SDK for .NET applications. Send alerts via push, email, SMS, webhooks, and in-app notifications with offline queue support.
[![NuGet](https://img.shields.io/nuget/v/IronNotify.Client.svg)](https://www.nuget.org/packages/IronNotify.Client)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## Installation
```bash
dotnet add package IronNotify.Client
```
## Quick Start
### Simple Usage
```csharp
using IronNotify.Client;
// Create client with API key
var client = new NotifyClient("your-api-key", appSlug: "my-app");
// Send an event
var result = await client.NotifyAsync(
eventType: "order.completed",
title: "New Order Received",
severity: Severity.Info,
message: "Order #12345 has been placed"
);
if (result.Success)
{
Console.WriteLine($"Event sent! ID: {result.EventId}");
}
```
### Fluent Builder
```csharp
var result = await client
.Event("payment.failed")
.WithSeverity(Severity.High)
.WithTitle("Payment Failed")
.WithMessage("Customer payment was declined")
.WithEntityId("order-12345")
.WithMetadata("amount", 99.99)
.WithMetadata("currency", "USD")
.WithAction("retry", "Retry Payment", webhookUrl: "https://api.example.com/retry")
.SendAsync();
```
## Configuration Options
```csharp
var client = new NotifyClient(new NotifyClientOptions
{
// Required
ApiKey = "your-api-key",
// Optional
DefaultAppSlug = "my-app",
DefaultSource = "backend-service",
BaseUrl = "https://ironnotify.com",
Timeout = TimeSpan.FromSeconds(30),
// Offline queue (enabled by default)
EnableOfflineQueue = true,
MaxOfflineQueueSize = 500,
OfflineQueueDirectory = null // Uses LocalApplicationData by default
});
```
## Severity Levels
```csharp
Severity.Info // Informational events
Severity.Warning // Warnings that may need attention
Severity.High // Important events requiring action
Severity.Critical // Critical events requiring immediate attention
```
## Metadata
Add custom key-value pairs to events:
```csharp
await client.NotifyAsync(new NotifyEventRequest
{
EventType = "user.signup",
Title = "New User Registration",
Severity = Severity.Info,
Metadata = new Dictionary<string, object>
{
["userId"] = "user-123",
["plan"] = "pro",
["referrer"] = "google"
}
});
```
## Actions
Add clickable actions to notifications:
```csharp
await client
.Event("server.down")
.WithSeverity(Severity.Critical)
.WithTitle("Server Unreachable")
.WithAction("restart", "Restart Server", webhookUrl: "https://api.example.com/restart")
.WithAction("acknowledge", "Acknowledge")
.SendAsync();
```
## Offline Queue
Events are automatically queued when the network is unavailable:
```csharp
var result = await client.NotifyAsync("event.type", "Title");
if (result.Queued)
{
Console.WriteLine("Event queued for retry when online");
}
// Check queue status
if (client.OfflineQueue != null)
{
Console.WriteLine($"Queued items: {client.OfflineQueue.Count}");
}
```
The offline queue:
- Persists events to disk
- Automatically retries when connectivity is restored
- Respects `MaxOfflineQueueSize` limit
- Works across app restarts
## Real-Time Notifications
For receiving real-time notifications, use `NotifyRealTimeClient`:
```csharp
using IronNotify.Client;
var realtime = new NotifyRealTimeClient(
hubUrl: "https://ironnotify.com/hubs/events",
apiKey: "your-api-key"
);
// Subscribe to events
realtime.OnEventReceived += (sender, evt) =>
{
Console.WriteLine($"Received: {evt.Title} ({evt.Severity})");
};
// Connect
await realtime.ConnectAsync();
// Join app channel
await realtime.JoinAppAsync("my-app");
// Disconnect when done
await realtime.DisconnectAsync();
```
## Dependency Injection
```csharp
// Register in DI container
services.AddSingleton<NotifyClient>(sp =>
new NotifyClient(new NotifyClientOptions
{
ApiKey = configuration["IronNotify:ApiKey"],
DefaultAppSlug = configuration["IronNotify:AppSlug"]
}));
```
## Common Event Types
```csharp
// User events
"user.signup", "user.login", "user.password_reset"
// Order events
"order.created", "order.completed", "order.cancelled"
// Payment events
"payment.succeeded", "payment.failed", "payment.refunded"
// System events
"server.error", "deployment.completed", "backup.finished"
```
## Links
- [Documentation](https://www.ironnotify.com/docs)
- [Dashboard](https://www.ironnotify.com)
- [API Reference](https://www.ironnotify.com/docs/api)
- [Support](https://www.ironnotify.com/app/tickets)
## License
MIT License - see [LICENSE](LICENSE) for details.