Kontent.Ai.Sync 2.0.0-rc.1
Pre-releaseTargets .NET 10, moving from net8.0 to net10.0, and Refit's transport is upgraded across four major
versions. The SDK also becomes a single package: Kontent.Ai.Sync.Abstractions is folded in.
The API changes too. Paging through the sync feed becomes a stream you enumerate, terminating on the
signal the API actually sends rather than on an inferred page size; the result contract splits so that
initialization stops pretending to return content; and disposal moves off the client interface onto the
client that owns resources. Most consumers touch one loop and nothing else.
See the 1.0 → 2.0 upgrade guide for the migration, change by change.
Breaking changes
-
Kontent.Ai.Sync.Abstractionsis gone; everything it held now ships inKontent.Ai.Sync, in theKontent.Ai.Syncnamespace. The split existed so contracts could be referenced without the client, and nothing ever did that — the package's only consumer wasKontent.Ai.Syncitself. Meanwhile a third of it was not abstract at all (SyncOptions,ApiMode,SyncOptionsExtensions), and keeping the contracts in a separate assembly is what forced every response model to exist three times: a public interface, an internal record, and an explicit reimplementation. The Management SDK reached the same conclusion and never split.Drop the package reference and change one
using:// Before using Kontent.Ai.Sync; using Kontent.Ai.Sync.Abstractions; // After using Kontent.Ai.Sync;
Every type keeps its name and its members; only the namespace and the assembly change. There is no compatibility shim: a stale
Kontent.Ai.Sync.Abstractionsreference fails to compile rather than resolving to a package that will never be updated again. That package is delisted at 2.0. -
The four delta entry types collapse into one generic
SyncChange<TData>, andDatais now typed.ISyncItem,ISyncType,ISyncLanguageandISyncTaxonomydeclared exactly the same two members and were backed by four identical records — while the thing that genuinely differs between them, the payload, was hidden behindobject?. The split was in the wrong place: every delta the API returns shares one envelope, and none of them share a payload.// Before — same shape four times, payload opaque foreach (ISyncItem item in page.Value.Items) { var data = item.Data; // object?, a JsonElement at runtime } // After foreach (SyncChange<SyncItemData> item in page.Value.Items) { var codename = item.Data?.System.Codename; var when = item.Timestamp; }
SyncItemData,SyncTypeData,SyncLanguageDataandSyncTaxonomyDatamodel eachsystemobject as the API documents it. They are deliberately not one type: a content item carriescollection,language,typeand workflow state, while a language carries three properties and nolast_modifiedat all.SyncTypeDataandSyncTaxonomyDatahappen to match today and are still separate, because the API may extend either alone.The deprecated
sitemap_locationsarray is not modelled. It is scheduled for removal, and leaving it out means that removal changes nothing here. -
net8.0→net10.0. There is no multi-targeting, so a project on .NET 8 cannot install this release at all — restore fails withNU1202: Package Kontent.Ai.Sync is not compatible with net8.0. Move to .NET 10 first. -
InitializeSyncAsyncreturnsISyncResultinstead ofISyncResult<ISyncInitResponse>, andISyncInitResponseis removed. Initialization establishes a starting point rather than returning content — the useful output has always been the token, onSyncToken.ISyncInitResponsewas an interface with no members, soValuewas an object you could hold but never read.ISyncResultis new and non-generic, andISyncResult<T>now derives from it, adding onlyValue; this mirrorsIManagementResult/IManagementResult<T>in the Management SDK. Every other member is unchanged and still reachable on both.// Before ISyncResult<ISyncInitResponse> init = await syncClient.InitializeSyncAsync(); // After — or simply var, as every example in the README uses ISyncResult init = await syncClient.InitializeSyncAsync(); await SaveTokenAsync(init.SyncToken); // unchanged
GetDeltaAsyncandEnumerateDeltaAsyncare untouched, includingpage.Value.Items. Initialization also no longer deserializes a response body: success now depends on the status code and the continuation token alone, so an unreadable body on an endpoint whose body is irrelevant can no longer fail the call. -
The client interfaces no longer carry
IDisposable/IAsyncDisposable; the concrete clients do. Disposal exists for one situation - a client built outside a container, which owns its own transport and must release it. Putting it on the interface meant every consumer holdingISyncClientwas offered aDispose()that, on the container path, released nothing and must not be called: the container owns that lifetime.SyncClientBuilder.Build()now returns the concreteSyncClient, which isIDisposableandIAsyncDisposable, so disposal stays available exactly where it means something.await using var client = SyncClientBuilder…Build();is unchanged, and so is every DI usage. The only code that breaks widened the builder result to the interface and then disposed it:// Before - no longer compiles, because the interface has no Dispose ISyncClient client = SyncClientBuilder…Build(); client.Dispose(); // After - keep the concrete type, or just use var var client = SyncClientBuilder…Build(); client.Dispose();
Container-resolved clients are still disposed by the container, which checks the runtime type rather than the registered service type - so nothing changes there.
-
SyncClientBuilder.ConfigureServicesis removed, replaced byWithResilience. Building a client outside dependency injection no longer stands up a private service container: the client constructs the handler chain directly and owns the resultingHttpClient, matching howManagementClientBuilderalready worked.ConfigureServicesexisted only to reach into that container and has nothing left to configure. Replacing the resilience pipeline was the one thing it was realistically used for, so that is now a first-classWithResilience(...), mirroring the DI overloads and the Management builder. Everything else is unchanged.This also removes a layer that existed only to make disposal work. Previously
Build()returned a wrapper whose sole job was to delegate every method and dispose the container behind it; now the client itself owns itsHttpClient, so disposing it releases exactly what it created. Clients resolved from a container are unaffected: the container owns their transport, and disposing one releases nothing. -
GetAllDeltaAsyncis replaced byEnumerateDeltaAsync, which returnsIAsyncEnumerable<ISyncResult<ISyncDeltaResponse>>. The old helper decided it had caught up when no collection in a response had reached 100 entries, a threshold published asSyncConstants.MaxItemsPerEntityType. The Sync API defines completion differently — as an empty response — so the walk ended one request before the API had confirmed the feed was drained, on a condition inferred from a page size rather than read from the signal the API sends. The replacement stops on the empty response, and streams pages instead of buffering every one in memory. Bounding the walk moves to the caller, whereTakeor abreakreplacesmaxPages.// Before var all = await syncClient.GetAllDeltaAsync(syncToken, maxPages: 10); foreach (var page in all.Responses) { /* ... */ } // After await foreach (var page in syncClient.EnumerateDeltaAsync(syncToken).Take(10)) { if (!page.IsSuccess) break; /* page.Value */ }
-
ISyncResult<T>.SyncTokenis no longer nullable, and a successful response without anX-Continuationheader now throws. The API issues a fresh token with every initialization and every delta, and it is the only way to make the next request — so a successful response without one leaves a caller holding data it can never continue from. That is now refused where the response is mapped, which coversInitializeSyncAsync,GetDeltaAsyncandEnumerateDeltaAsyncalike, with anInvalidOperationExceptionnaming the request. In exchangeSyncTokenis declaredstringrather thanstring?, matchingValue: both are meaningful only whenIsSuccessis true. Code that wroteresult.SyncToken ?? previouscan drop the fallback — it was dead once the guarantee existed. Nothing changes for callers that only read the token after checkingIsSuccess. -
ISyncResult<T>.HasMoreChanges,SyncConstantsandISyncAllDeltaResultare removed. All three existed only to support the threshold above. With completion defined by the API's empty response, the sequence simply ends: there is no "are there more" flag to read, and no client-side page-size constant to keep in step with the server. CarrySyncTokenfrom the last yielded result; when nothing is yielded, the token you passed in is still current. -
The
configureRefitparameter is gone from all threeAddSyncClientoverloads. The hook exposed the transport library's settings object, but everything reachable through it was load-bearing rather than configurable — the parameter-key formatter matches the API's casing, and the serializer options carry the converters the wire format requires. Overriding them broke requests silently. Delete the argument; the SDK's own tests only ever used it to assert the callback fired.
Changed
- Registering from
IConfigurationcan now customize the HTTP client and the resilience pipeline. The configuration-basedAddSyncClientoverloads took noconfigureHttpClient/configureResilience, so binding options from configuration and replacing the retry pipeline were mutually exclusive. The workaround — binding by hand inside anAction<SyncOptions>— compiles and looks equivalent, but registers no change token, soIOptionsMonitorsilently stops reloading. Both hooks are now available on every configuration overload, alongside the ones that already had them. AddSyncClientgained the overloads its sibling SDKs already had, so the three register a client the same way: options configured with access to theIServiceProvider, in both named and unnamed form. Nothing was removed, and existing calls are unaffected — this closes gaps rather than reshaping the surface.SyncOptions.DefaultConfigurationSectionNameexposes the section name the configuration overloads bind by default, matchingManagementOptions. Design-time tools that resolve the SDK's configuration from the same sources can probe it instead of hard-coding the string.- Cancellation now throws; other transport failures are results. Refit's upgrade changed the
contract: exceptions raised in the HTTP pipeline are captured into the response rather than thrown.
A network failure, DNS failure or resilience-pipeline rejection is therefore an unsuccessful result
carrying the exception, consistent with how every other failure in this SDK is reported. Cancellation
is the exception to that: when the caller's token fires, theOperationCanceledExceptionis rethrown,
soTask.IsCanceled,Task.WhenAlland cancellation handlers behave as they do everywhere else in
.NET. Previously all of these threw. An expiredHttpClient.Timeoutis not cancellation, even
though .NET surfaces it as aTaskCanceledException: the request was sent, so it is reported as a
failed result like any other transport failure. - Transport failures report status
0.ISyncResultnow carries(HttpStatusCode)0for that case rather than an invented code. Responses that did arrive are unaffected.
Fixed
- Every delta now carries its
timestamp. The API marks it required on all four delta objects and the SDK dropped it entirely, so there was no way to tell when a change happened — only that it had. It is exposed asSyncChange<TData>.Timestamp, aDateTimein UTC — matching how every other Kontent.ai SDK types a server-sent timestamp. - The configured retry pipeline can now run to completion.
HttpClient's 100-second default bounds the whole call, retries and backoff included, and nothing raised it — so a pipeline allowed four 30-second attempts plus exponential backoff was silently cut off partway through the last one. The SDK's resilience pipeline already bounds each attempt, so it now owns timing outright and the transport-level ceiling is removed. Requests still stop when yourCancellationTokenfires. - Long-running applications pick up DNS changes instead of pinning the address resolved at startup. The registered client is a singleton and takes its
HttpClientfromIHttpClientFactoryonce, so the handler chain it holds was never rotated — the factory only hands a fresh chain to a newCreateClientcall. Connections now recycle every two minutes, matching the factory's own default handler lifetime. A sync walk is exactly the kind of process that stays up long enough for this to matter. Configuring your own primary handler viaconfigureHttpClientstill overrides this, as before. - Responses are now disposed once mapped, instead of every request leaking one until finalization. Each call held its
HttpResponseMessageopen for the garbage collector to reclaim, which on a sync walk means one per page. Nothing the result carries is affected — Refit buffers the content, and header collections outlive disposal. - A failure resolving the
X-KC-SOURCEheader no longer breaks every later request. The value is cached in aLazy<string?>, and the resolution walks the call stack to attribute the calling package. An exception thrown during that walk was cached alongside the value and rethrown on every subsequent request for the lifetime of the process. Resolution failures are now contained and the header simply omitted, which was the intent. - An assembly with no informational version reports
0.0.0in tracking headers rather than an empty version. The build-metadata stripping ran after the fallback rather than before it, so a blank version survived as an empty string and travelled into the header. Only reachable for an assembly built without version attributes, which is not the case for anything this SDK ships.
Dependencies
Shipped floors on Kontent.Ai.Sync moved up, all .NET 10 aligned:
Microsoft.Extensions.*(Configuration,.Binder,Logging.Abstractions,Options,.ConfigurationExtensions,.DataAnnotations) 9.0.15 → 10.0.10.Microsoft.Extensions.Http.Resilience9.6.0 → 10.8.0.RefitandRefit.HttpClientFactory10.2.0 → 14.0.1.
Internal
Refit 14 builds request logic at compile time rather than by reflection. ISyncApi generates completely and gained that with no changes.
Installation
dotnet add package Kontent.Ai.Sync --prereleaseFull changelog: src/sync/CHANGELOG.md