Custom Transports
The default RestSyncTransport speaks plain HTTP + JSON. If you need gRPC, GraphQL, a custom envelope, or simply a different REST shape, implement ISyncTransport and register it after AddDataSync.
ISyncTransport
Section titled “ISyncTransport”public interface ISyncTransport{ Task<SyncPullResult> Pull(SyncEndpoint endpoint, string? cursor, CancellationToken ct);
Task<SyncSendResult> Send(SyncEndpoint endpoint, SyncOperation op, CancellationToken ct);
Task<IReadOnlyList<SyncSendResult>> SendBatch(SyncEndpoint endpoint, IReadOnlyList<SyncOperation> ops, CancellationToken ct);
Task<SyncTombstoneResult> FetchTombstones(SyncEndpoint endpoint, string? cursor, CancellationToken ct);}Pullreturns deltas + the next cursor + ahasMoreflag.Sendreturns the per-op outcome (success / conflict / transient / terminal).SendBatchreturns one outcome per op, keyed back to the input order.FetchTombstonesreturns deleted ids + the tombstone cursor.
Each result type carries either the success body, the conflict payload, or the exception that caused failure — the engine pattern-matches on that.
Replacing the default transport
Section titled “Replacing the default transport”public class GrpcSyncTransport : ISyncTransport{ public Task<SyncPullResult> Pull(SyncEndpoint endpoint, string? cursor, CancellationToken ct) { /* ... */ } public Task<SyncSendResult> Send(SyncEndpoint endpoint, SyncOperation op, CancellationToken ct) { /* ... */ } public Task<IReadOnlyList<SyncSendResult>> SendBatch(SyncEndpoint endpoint, IReadOnlyList<SyncOperation> ops, CancellationToken ct) { /* ... */ } public Task<SyncTombstoneResult> FetchTombstones(SyncEndpoint endpoint, string? cursor, CancellationToken ct) { /* ... */ }}
builder.Services.AddDataSync<MyDelegate>(opts =>{ opts.RegisterEndpoint<TodoItem>("todos"); // Url is your choice — gRPC method name, GraphQL operation, etc.});
// Override the default REST transportbuilder.Services.AddSingleton<ISyncTransport, GrpcSyncTransport>();The engine resolves the last registered ISyncTransport, so the override above replaces RestSyncTransport cleanly.
When to use the named HttpClient instead
Section titled “When to use the named HttpClient instead”For the common case — same protocol but different headers, auth handlers, or base addresses — don’t write a custom transport. Configure the named HttpClient:
builder.Services .AddHttpClient(RestSyncTransport.HttpClientName, c => { c.BaseAddress = new Uri("https://api.example.com"); c.Timeout = TimeSpan.FromMinutes(2); }) .AddPolicyHandler(GetRetryPolicy()) .AddHttpMessageHandler<SigningHandler>();RestSyncTransport.HttpClientName is the string "Shiny.Data.Sync". Anything you bolt onto that client (Polly, signing handlers, base address) is picked up automatically.
Hybrid: REST inbox, gRPC outbox
Section titled “Hybrid: REST inbox, gRPC outbox”You can’t currently mix transports per endpoint — there is one ISyncTransport per host. Two ways to express this:
-
Custom transport that internally dispatches. Inspect
endpoint.Key(or any property you set) and route inbound / outbound calls accordingly. Most apps that need this end up wrappingRestSyncTransportfor one half and a custom client for the other. -
Two hosts. Spin up a second DI scope with its own
IDataSyncManagerand transport. Heavy-handed, but tractable when you genuinely need both protocols.
Caveats
Section titled “Caveats”- On iOS / Mac Catalyst, the default
RestSyncTransportis not what runs the outbox or inbox — the NSURLSession-backedDataSyncManagerhandles those directly. A customISyncTransportregistered there is ignored. If you need a custom protocol on Apple, opt out of NSURLSession withAddHttpClientDataSync<TDelegate>and your transport runs cross-platform. - The transport contract is stable but not finalised — if you write one, pin to the version you tested against and review release notes when upgrading.