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:
commit
058d50f99b
|
|
@ -0,0 +1,6 @@
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
.vs/
|
||||||
|
*.DotSettings.user
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace IronNotify.Client
|
||||||
|
{
|
||||||
|
public class Class1
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
[](https://www.nuget.org/packages/IronNotify.Client)
|
||||||
|
[](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.
|
||||||
Loading…
Reference in New Issue