Twin Reader
Adapters can read the current persisted twin via
AdapterContext.Twin (the ITwinReader SDK service). Use it whenever
you need the desired/reported/tags state without hand-writing repository
queries — most commonly inside a reconciling poller or webhook handler.
Contract
public interface ITwinReader
{
Task<TwinSnapshot?> ReadAsync(string deviceId, CancellationToken ct = default);
}
public sealed record TwinSnapshot
{
public required string DeviceId { get; init; }
public JsonObject? Desired { get; init; }
public int DesiredVersion { get; init; }
public JsonObject? Reported { get; init; }
public int ReportedVersion { get; init; }
public JsonObject? Tags { get; init; }
public DateTime LastActivity { get; init; }
}
BaseAdapter exposes the seam as protected ITwinReader Twin parallel
to Telemetry, Properties, and Diagnostics. Background services and
controllers can inject ITwinReader directly via DI.
Behaviour
- Returns
nullfor a device that does not exist or is soft-deleted. - Hits the repository on every call — no SDK-level cache. Adapters that poll the same deviceId at high frequency should cache locally.
DesiredVersionandReportedVersionare0for newly-created twins that have never been written; they increment on every successful PATCH.LastActivityis UTC.
Common pattern: diff-against-truth-of-record
The ReconcilingPoller<T> base class uses Twin.ReadAsync internally so
adapter code stays focused on upstream IO + projection. If you write a
custom poller, the same shape applies:
foreach (var item in upstreamItems)
{
var twin = await Twin.ReadAsync(deviceId, ct);
var newReported = ProjectToReported(item);
if (!Equals(twin?.Reported, newReported))
{
Properties.Report(deviceId, newReported);
}
}
This avoids the in-process cache that would otherwise re-emit on every process restart.
When NOT to use it
- For high-throughput telemetry forwarding, write directly via
Telemetry.Send— the pipeline owns its own deduplication. - For per-property granular reads, prefer the twin-management REST endpoints with JSON Pointer projection.
See also
- Adapter Interface for the full
AdapterContext - Reconciling Poller for the canonical use case