Skip to content
Document DB v7.1: Temporal Support, Telemetry Collection, & Orleans Storage Providers! Feed The Machine Here

Entity Registration

Endpoints are registered inside the AddDataSync callback — one URL per entity type, one stable key. This page lists every option on SyncEndpoint.

Anything sync’d through IDataSyncManager must implement ISyncEntity — a single property:

public interface ISyncEntity
{
string Identifier { get; }
}

The Identifier is the stable, server-recognized id. The engine uses it to build per-op URLs (PUT {url}/{id}, DELETE {url}/{id}), to deduplicate within a batch, and to dispatch tombstones.

builder.Services.AddDataSync<MyDataSyncDelegate>(opts =>
{
opts.RegisterEndpoint<TodoItem>("https://api.example.com/todos");
opts.RegisterEndpoint<Project>("https://api.example.com/projects", ep =>
{
// -- Direction --
ep.Direction = SyncDirection.Both; // or PullOnly / PushOnly
// -- Network policy --
ep.UseMeteredConnection = false; // wait for WiFi
ep.Batch = true; // coalesce ops per round-trip
ep.MaxAttempts = 8; // retry transient failures up to 8x
ep.RetryBaseDelay = TimeSpan.FromSeconds(3); // base for exponential backoff
// -- Conflicts --
ep.DefaultConflictPolicy = ConflictPolicy.ServerWins;
// -- Inbox throttle --
ep.MinPullInterval = TimeSpan.FromMinutes(5); // PullAll / SyncJob skip; PullNow bypasses
// -- Per-verb URL overrides --
ep.PullUrl = "https://api.example.com/projects/feed"; // GET a different URL on pull
ep.BatchUrl = "https://api.example.com/projects/bulk"; // POST batched ops elsewhere
ep.CursorParameter = "updatedSince"; // default "since"; set to null to omit
// -- Tombstones (separate server-side delete stream) --
ep.TombstoneUrl = "https://api.example.com/projects/deleted";
ep.TombstoneCursorParameter = "since";
// -- Soft-delete / expiry predicates (evaluated inside the inbox dispatch loop) --
ep.SoftDeletePredicate = entity => entity is Project p && p.IsArchived;
ep.ExpiryPredicate = entity => entity is Project p && p.OwnerId == null;
// -- Per-endpoint request hook (runs after global ISyncInterceptor) --
ep.OnBeforeSend = req =>
{
req.Headers.Add("X-Trace-Id", Guid.NewGuid().ToString("N"));
return Task.CompletedTask;
};
});
});
PropertyDefaultDescription
KeyCLR full name of TStable key persisted with every SyncOperation. Changing it strands queued work. Override only when two endpoints share a CLR type.
EntityTypetypeof(T)Set automatically by RegisterEndpoint<T>.
Urlfrom RegisterEndpoint<T>The base URL. Drives POST (Create), PUT /{id} (Update), DELETE /{id} (Delete), and — unless overridden — inbox pulls and batched pushes.
DirectionBehavior
Both (default)Outbox push + inbox pull both work.
PullOnlyQueue<T> throws. Use for reference / read-only data.
PushOnlyPullNow<T> throws; PullAll silently skips. Use for telemetry / audit / sync-up queues.
PropertyDefaultDescription
UseMeteredConnectiontrueWhen false, the engine waits for an unmetered (WiFi) connection before sending outbox ops.
BatchfalseWhen true, the outbox coalesces multiple queued ops for this endpoint into one POST {url}/batch. See Batching.
MaxAttempts5Number of send attempts before the op is permanently handed to IDataSyncDelegate.OnError.
RetryBaseDelay2sBase delay for exponential backoff. The actual wait is baseDelay * 2^(attempts - 1) capped at 60s. The retry timestamp is persisted on the SyncOperation, so a process restart resumes the wait window.
PropertyDefaultDescription
MinPullIntervalnull (no throttle)Minimum wall-clock between scheduled pulls. When LastPulledAt is within this window, PullAll / SyncJob skip the endpoint. PullNow<T> always bypasses.
CursorParameter"since"Query-string parameter used to pass the persisted cursor to the server. Set to null to omit.
PullUrlnull (uses Url)Optional override for the inbox-pull URL.
PropertyDefaultDescription
TombstoneUrlnullWhen set, every successful pull is followed by a GET against this URL. Each returned id is dispatched to OnReceived with Verb = Delete and Entity = null.
TombstoneCursorParameter"since"Cursor query parameter for the tombstone stream. Tracked independently from SyncCursor.

Both predicates run inside the inbox dispatch loop on the deserialized entity, before delegates fire. When either returns true for a Create / Update item, the verb is rewritten to Delete and Entity stays populated so consumers can read the final state on the way out the door.

PropertyWhen to use
SoftDeletePredicateServer signals deletes via a flag (IsDeleted = true) instead of a separate verb.
ExpiryPredicateServer-driven state change should evict the entity (e.g. AssignedTo == null).

See Removal Strategies for the full discussion.

PropertyDescription
OnBeforeSendAsync hook invoked just before each request is sent. Runs after any global ISyncInterceptor, so endpoint-specific logic wins on header conflicts. On iOS / Mac Catalyst the request has no body attached (uploads stream from disk), so signers that hash the body won’t work on Apple.
PropertyDefaultDescription
DefaultConflictPolicyAskDelegateWhat to do when the server returns 409 / 412. See Conflict Resolution.

When Batch = true, queued ops for the endpoint are coalesced into one POST {url}/batch request. Coalescing rules:

  • Trailing Delete wins — any preceding Create / Update for the same entity drop.
  • Create + Update(s) → single Create with the latest payload.
  • Update + Update(s) → single Update with the latest payload.

Override the batch URL with BatchUrl if your server bundles ops at a different route. Batching applies to every transport except the Apple NSURLSession path, which sends one upload task per op by design.

See Server API Contracts → Batched outbox for the expected request and response shapes.

opts.RegisterEndpoint<Country>("https://api.example.com/countries", ep =>
{
ep.Direction = SyncDirection.PullOnly;
ep.MinPullInterval = TimeSpan.FromHours(24);
});
opts.RegisterEndpoint<TelemetryEvent>("https://api.example.com/telemetry", ep =>
{
ep.Direction = SyncDirection.PushOnly;
ep.Batch = true;
ep.MaxAttempts = 20; // telemetry can survive heavy backoff
ep.UseMeteredConnection = false;
});

Just call sync.Queue(SyncVerb.Create, telemetryEvent)PullAll will skip it silently.