Poller 8.3.2
dotnet add package Poller --version 8.3.2
NuGet\Install-Package Poller -Version 8.3.2
<PackageReference Include="Poller" Version="8.3.2" />
<PackageVersion Include="Poller" Version="8.3.2" />
<PackageReference Include="Poller" />
paket add Poller --version 8.3.2
#r "nuget: Poller, 8.3.2"
#:package Poller@8.3.2
#addin nuget:?package=Poller&version=8.3.2
#tool nuget:?package=Poller&version=8.3.2
Poller
This package has moved. The Dragonfire suite is now developed in a single repository:
outboxnet/Dragonfire. Visit it for the latest version and the full suite of packages.
A production-ready .NET 8 polling framework with exponential backoff, configurable retry strategies, real-time progress streaming, and pluggable metrics.
Features
- Generic polling — any request/response type pair
- Exponential backoff with configurable multiplier and cap
- Channel-based bounded queue with configurable concurrency throttle
- Real-time progress streaming via
IAsyncEnumerable - Cancellation support at both the job and application level
- Automatic data cleanup based on configurable retention period
- Pluggable metrics (
NoOpMetricsTrackerby default, Azure App Insights included) - Thread-safe design throughout (
ConcurrentDictionary,SemaphoreSlim,Channel<T>)
Installation
dotnet add package Poller
Quick start
1. Define your request and response types
public class OrderStatusRequest
{
public string OrderId { get; set; } = "";
}
public class OrderStatusResponse
{
public string Status { get; set; } = ""; // e.g. "PENDING", "SHIPPED", "DELIVERED"
public string? TrackingNumber { get; set; }
}
2. Implement the polling strategy
using Poller.Models;
using Poller.Services;
public class OrderStatusStrategy : IPollingStrategy<OrderStatusRequest, OrderStatusResponse>
{
private readonly HttpClient _http;
public OrderStatusStrategy(HttpClient http) => _http = http;
public async Task<PollingResult<OrderStatusResponse>> PollAsync(
OrderStatusRequest request,
CancellationToken cancellationToken)
{
try
{
var response = await _http.GetFromJsonAsync<OrderStatusResponse>(
$"https://orders.example.com/api/{request.OrderId}", cancellationToken);
return response is null
? PollingResult<OrderStatusResponse>.Failure("Empty response", shouldContinue: true)
: PollingResult<OrderStatusResponse>.Complete(response);
}
catch (HttpRequestException ex)
{
// Transient error — the framework will retry with backoff
return PollingResult<OrderStatusResponse>.Failure(ex.Message, shouldContinue: true);
}
}
}
3. Implement the completion condition
using Poller.Services;
public class OrderStatusCondition : IPollingCondition<OrderStatusResponse>
{
public bool IsComplete(OrderStatusResponse response)
=> response.Status is "DELIVERED" or "CANCELLED";
public bool IsFailed(OrderStatusResponse response)
=> response.Status == "FAILED";
}
4. Register with DI
// Program.cs
builder.Services.AddPollingService<OrderStatusRequest, OrderStatusResponse>(options =>
{
options.MaxConcurrentPollings = 100;
options.QueueCapacity = 5_000;
options.DataRetentionPeriod = TimeSpan.FromHours(24);
});
// Domain implementations
builder.Services.AddScoped<IPollingStrategy<OrderStatusRequest, OrderStatusResponse>, OrderStatusStrategy>();
builder.Services.AddScoped<IPollingCondition<OrderStatusResponse>, OrderStatusCondition>();
builder.Services.AddHttpClient<OrderStatusStrategy>();
5. Use in a controller or service
public class OrdersController : ControllerBase
{
private readonly IPollingOrchestrator _orchestrator;
public OrdersController(IPollingOrchestrator orchestrator)
=> _orchestrator = orchestrator;
[HttpPost("{orderId}/track")]
public async Task<IActionResult> TrackOrder(string orderId, CancellationToken ct)
{
var response = await _orchestrator.StartPollingAsync<OrderStatusRequest, OrderStatusResponse>(
pollingType: "OrderStatus",
request: new OrderStatusRequest { OrderId = orderId },
options: new PollingOptions
{
MaxAttempts = 20,
InitialDelay = TimeSpan.FromSeconds(5),
MaxDelay = TimeSpan.FromSeconds(60),
BackoffMultiplier = 1.5,
Timeout = TimeSpan.FromMinutes(10)
},
cancellationToken: ct);
return Accepted(new { pollingId = response.PollingId, statusUrl = response.StatusUrl });
}
[HttpGet("polling/{pollingId:guid}")]
public async Task<IActionResult> GetStatus(Guid pollingId, CancellationToken ct)
{
var status = await _orchestrator.GetStatusAsync(pollingId, ct);
return status is null ? NotFound() : Ok(status);
}
}
Multiple polling types
Call AddPollingService<TRequest, TResponse>() once per type pair. The shared infrastructure (orchestrator, registry, metrics) is registered only once.
builder.Services
.AddPollingService<OrderStatusRequest, OrderStatusResponse>()
.AddPollingService<PaymentRequest, PaymentResponse>()
.AddPollingService<WeatherPollingRequest, WeatherPollingResponse>();
Configuration reference
| Property | Default | Description |
|---|---|---|
MaxConcurrentPollings |
100 |
Maximum jobs processed simultaneously |
QueueCapacity |
10 000 |
Maximum queued-but-unstarted jobs |
DefaultTimeout |
5 min |
Fallback timeout when PollingOptions.Timeout is not set |
DefaultMaxAttempts |
30 |
Fallback max attempts when PollingOptions.MaxAttempts is not set |
DataRetentionPeriod |
24 h |
How long completed/failed jobs are kept in memory |
EnableDetailedMetrics |
true |
Controls App Insights aggregation timer |
PollingOptions (per-job overrides):
| Property | Default | Description |
|---|---|---|
MaxAttempts |
30 |
Maximum retry attempts |
InitialDelay |
1 s |
Delay before the first retry |
MaxDelay |
30 s |
Ceiling for exponential backoff |
BackoffMultiplier |
2.0 |
Multiplier applied after each failed attempt |
Timeout |
5 min |
Wall-clock deadline for the entire job |
NotifyOnEachAttempt |
false |
Push updates to SubscribeToUpdatesAsync subscribers |
Real-time updates (Server-Sent Events)
[HttpGet("polling/{pollingId:guid}/stream")]
public async Task Stream(Guid pollingId, CancellationToken ct)
{
Response.Headers["Content-Type"] = "text/event-stream";
await foreach (var update in _orchestrator.SubscribeToUpdatesAsync(pollingId, ct))
{
await Response.WriteAsync($"data: {JsonSerializer.Serialize(update)}\n\n", ct);
await Response.Body.FlushAsync(ct);
}
}
Metrics
The default NoOpMetricsTracker discards all telemetry. To enable Azure Application Insights:
// After AddPollingService<>()
builder.Services.AddApplicationInsightsTelemetry();
builder.Services.AddApplicationInsightsPollingMetrics(); // replaces NoOp with AppInsights tracker
To plug in your own backend, implement IPollingMetricsTracker and register it:
builder.Services.AddSingleton<IPollingMetricsTracker, MyPrometheusTracker>();
Cancellation
var cancelled = await _orchestrator.CancelPollingAsync(pollingId, cancellationToken);
Sample — Weather API
samples/Poller.Sample.WeatherApi demonstrates the full pattern against the free Open-Meteo weather API (no API key required).
cd samples/Poller.Sample.WeatherApi
dotnet run
# Open https://localhost:5001/swagger
Start a weather job:
POST /api/weather
Content-Type: application/json
{
"latitude": 52.52,
"longitude": 13.41,
"locationName": "Berlin"
}
Poll for result:
GET /api/weather/{pollingId}
Stream updates (SSE):
GET /api/weather/{pollingId}/stream
Publishing to NuGet
Automatic (recommended)
- Add
NUGET_API_KEYto your repository's Secrets (Settings → Secrets and variables → Actions). - Create a GitHub Release — the
publish.ymlworkflow fires automatically, packs with the release tag version, and pushes to NuGet.org.
Manual trigger
Actions → Publish to NuGet → Run workflow → enter version
Local pack
dotnet pack Poller/Poller.csproj -c Release -p:Version=1.0.0 -o ./nupkg
dotnet nuget push ./nupkg/Poller.1.0.0.nupkg --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Microsoft.ApplicationInsights (>= 3.1.0)
- Microsoft.AspNetCore.Http.Abstractions (>= 2.3.9)
- Microsoft.Extensions.DependencyInjection (>= 8.0.1)
- Microsoft.Extensions.Http (>= 8.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.