Reconciling Poller
ReconcilingPoller<TItem> is an SDK-provided BackgroundService base
that handles the boilerplate every aggregator-backed adapter would
otherwise hand-roll: cancellation-aware delay loop, exception isolation
per item, diff against the persisted twin, projection into the
reported-properties write path.
Pattern
internal sealed class EnodePoller : ReconcilingPoller<EnodePoller.Item>
{
private readonly EnodeApiClient _api;
private readonly IServiceProvider _services;
public EnodePoller(
ITwinReader twin,
IPropertiesClient properties,
IServiceProvider services,
EnodeApiClient api,
ILogger<EnodePoller> logger)
: base(twin, properties, logger)
{
_api = api;
_services = services;
}
protected override TimeSpan Interval => TimeSpan.FromSeconds(60);
protected override async IAsyncEnumerable<Item> ListAsync(
[EnumeratorCancellation] CancellationToken ct)
{
var page = await _api.ListChargersAsync(ct);
foreach (var node in page!["data"]!.AsArray())
{
yield return new Item(
ExternalId: node!["id"]!.GetValue<string>(),
Reported: (JsonObject)node);
}
}
protected override string? DeviceIdFor(Item item)
{
// map upstream id → BT device via the new SDK seam
using var scope = _services.CreateScope();
var devices = scope.ServiceProvider.GetRequiredService<IDeviceRepository>();
var device = devices.FindByExternalIdAsync(item.ExternalId)
.GetAwaiter().GetResult();
return device?.DeviceId;
}
protected override JsonObject ReportedFor(Item item) => item.Reported;
public sealed record Item(string ExternalId, JsonObject Reported);
}
Register it:
builder.Services.AddHostedService<EnodePoller>();
What the base class does for you
- Initial
StartupDelay(default 5s) before the first cycle so SDK background services finish coming up. Interval(default 60s) between cycles.- Cancellation-aware delay loop that respects shutdown.
- Exception isolation: a cycle failure logs and continues; a single item's failure logs at warning and continues with the next item.
- Diff against the persisted twin via
ITwinReader.ReadAsync. Only emitsProperties.Reportwhen reported-state actually changed.
What you implement
| Override | Returns |
|---|---|
ListAsync(ct) | IAsyncEnumerable<TItem> of upstream items |
DeviceIdFor(item) | BT deviceId (or null to skip) |
ReportedFor(item) | Canonical reported-properties JSON |
Optional:
| Override | Default |
|---|---|
Interval | 60s |
StartupDelay | 5s |
HasChanged(item, persistedReported, upstreamReported) | Equality-by-serialized-JSON |
When NOT to use it
- For high-throughput telemetry forwarding, write directly via
Telemetry.Send— the pipeline owns its own batching + dedup. - If the upstream supports reactive subscriptions (websocket, AMQP, webhook firehose), prefer those over polling. Use webhooks for the push path and a sparse poller as a safety net.