Poller 8.3.2

dotnet add package Poller --version 8.3.2
                    
NuGet\Install-Package Poller -Version 8.3.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Poller" Version="8.3.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Poller" Version="8.3.2" />
                    
Directory.Packages.props
<PackageReference Include="Poller" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Poller --version 8.3.2
                    
#r "nuget: Poller, 8.3.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Poller@8.3.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Poller&version=8.3.2
                    
Install as a Cake Addin
#tool nuget:?package=Poller&version=8.3.2
                    
Install as a Cake Tool

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.

NuGet CI

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 (NoOpMetricsTracker by 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

  1. Add NUGET_API_KEY to your repository's Secrets (Settings → Secrets and variables → Actions).
  2. Create a GitHub Release — the publish.yml workflow 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
8.3.2 55 5/1/2026
8.0.0 69 4/24/2026