Releases: kontent-ai/dotnet
Release list
Kontent.Ai.Sync 2.0.0-rc.2
Fixed
-
The named-options accessor requires the client name it reads. It accepted a null name and fell back to the unnamed registration, which only exists when a default client was registered — so on a named-only setup that path would have resolved a
SyncOptionsnobody configured and built requests against a blank environment rather than failing. No caller passed null; the parameter is now non-nullable, matching the Delivery SDK's equivalent. -
The pass-through
CreateRefitSettingswrapper is gone, along with its summary describing a customization hook that had been removed. -
The Refit settings no longer configure a query string this API does not have. A collection format and URL key formatter were carried over from the Delivery SDK, but the sync endpoints send the environment in the path and the continuation token in a header — there is no query parameter for either setting to apply to.
-
SyncClientBuilder's remark matches its signature, which returns the concreteSyncClient— that is what makes the client it hands back disposable. -
ChangeTypeserializes the value the API sends. Its own converter took precedence over the SDK's and carried no naming policy, so writing a delta produced"Changed"where the wire uses"changed"— reading was unaffected, being case-insensitive, so this only surfaced for a consumer re-sending what they had read. Each member now states its wire name. -
The client factory no longer relabels an exception that came from your own registration.
Get(name)caughtInvalidOperationExceptionand reported it as a missing client, so aconfigureHttpClientthat rejected its input came back as "No sync client registered with name '…'". A genuinely missing registration still says so. -
The 1.0 → 2.0 upgrade guide's first paragraph no longer links to a guide that was retired.
-
SyncOptionsBuilder.Buildcopies by reflection rather than property by property. It listed the properties it carried, which keeps compiling when an option is added and silently stops carrying it — a value the caller set that the client never sees. -
The
X-KC-SOURCEheader keeps naming the integration that made the call. Attribution matched the SDK assembly by full name, which carries the version — and nothing pinsAssemblyVersion, so the reference an integration recorded when it was built stopped matching on the first SDK release after that. The header then went silently missing for every consumer who had not rebuilt. Matching is now by simple name. -
A request is bounded again when the SDK's own resilience pipeline is not the one installed.
HttpClient.Timeoutwas set toTimeout.InfiniteTimeSpanunconditionally, on the premise that the resilience pipeline owns timing - but the 30-second per-attempt timeout that premise rests on exists only whileEnableResilienceis left on and noconfigureResiliencehook replaces the default pipeline. SettingEnableResilience = false, or supplying a pipeline that adds no timeout of its own, therefore left a call with no attempt timeout, no overall timeout and no ceiling of any kind, so a connection that stopped responding hung the caller indefinitely. This affected both the container-registered client and the container-free one, which builds its own transport. The ceiling is now lifted only for the default pipeline; otherwiseHttpClient's 100-second default applies, as it did in 1.0. A custom pipeline that legitimately needs longer can raise it throughconfigureHttpClient. -
An attempt the resilience pipeline timed out is now retried instead of failing the whole call. The default pipeline wraps retry around a 30-second per-attempt timeout, so a hung attempt reaches the retry as Polly's
TimeoutRejectedException- a type the SDK's transient classifier did not recognise. The single situation that per-attempt timeout exists for, a connection that stops responding and that a fresh attempt would recover from, therefore failed the whole call after 30 seconds with no retries at all.
Installation
dotnet add package Kontent.Ai.Sync --prereleaseFull changelog: src/sync/CHANGELOG.md
Kontent.Ai.ModelGenerator 11.0.0-rc.2
Fixed
-
The tool no longer ships the Visual Basic compiler.
Microsoft.CodeAnalysisis the meta-package; only the C# syntax and workspace formatting APIs are used, so it now referencesMicrosoft.CodeAnalysis.CSharp.Workspacesdirectly. -
A blank comment argument reports
ArgumentExceptionrather thanArgumentNullException. The value is present, just empty — the two are different mistakes, and the tests had frozen the wrong one. -
Generated members are ordered ordinally rather than by the current culture, so the same content model produces the same file on every machine.
-
appSettings.jsonnames the option the tool actually reads. It still listedBaseClass, which was renamed toBaseRecord. -
The startup banner no longer reports success before anything is generated. A failed run's first line was "Models were generated for …"; it now says what it is about to do.
-
IClassCodeGeneratorFactorycovers both emitters. It offered only the Delivery generator while the Management path constructed its own directly — a seam that looked like the way in and was not. It now has a method per emitter, and the Management path goes through it. -
The config-file documentation matches where the tool actually looks. The README described
appSettings.jsonas living beside the executable; the tool reads it from the working directory, and as adotnet toolit has no executable directory to speak of. The file is also not installed with the tool, so the README now points at it as a template to copy. -
Management mode no longer skips elements over identifiers it never emits. Every content type reserved the names the Delivery emitter uses for its codename constants —
{Property}Codenamefor each element, plus the type's ownContentTypeCodename— regardless of mode. The Management emitter writes none of those, so the reservation only rejected valid input there: a type carrying bothtitleandtitle_codenamehad the second skipped with a collision warning, and an element codenamedcontent_type_codenamewas renamed for no reason. Constant registration is now the Delivery emitter's, so Management mode has the whole identifier space its own output uses. -
--baseRecordis rejected at startup when it is not a valid C# record name.-b "My-Base"wrotepublic partial record My-Baseand an extender deriving every generated model from it, so the whole output failed to compile over one argument. The name is checked before any API call and reported like any other configuration problem. -
Forgetting
--managementnow fails instead of generating Delivery models. Validation accepted the union of both modes' parameters while binding only ever applied the active mode's, so-k(or--apiKey) without-mwas accepted, dropped, and the run continued as a full Delivery generation - writing Delivery models over whatever was in the output directory and exiting0. The reverse dropped the Delivery-only-p/--projectidin Management mode and then failed with a message about an emptyEnvironmentId, which read as a configuration problem rather than a wrong flag. Each argument is now checked against the mode that is actually running, and one that belongs to the other mode names the mode switch:-k configures the Management API. Add --management (or -m) to generate from it.The same check now covers the section-qualified form (
--ManagementOptions:ApiKeywithout--management, and--DeliveryOptions:*with it), which bound straight into configuration without needing a switch mapping and so slipped past the mode entirely. Only command-line arguments are checked - anappSettings.jsoncarrying both sections is unaffected, and the section belonging to the mode you run is the one that is read. -
--nullabilityis refused in Management mode instead of accepted and ignored. It selects how generated Delivery models express nullability. Management models are uniformly nullable by contract - anullproperty is omitted from the upsert payload, which is how you leave an element untouched - so there was never anything for the flag to select there. The parameter table already documented it as Delivery-only; now the tool enforces it. -
--managementtogether with--baseRecordno longer emits code that cannot compile. The generated base record and its extender both carried a hardcodedusing Kontent.Ai.Delivery.Abstractions;. A project generated for the Management SDK has no reason to reference the Delivery SDK, so that line was aCS0246in a file the consumer never wrote. Neither it nor theusing System;beside it was referenced by the emitted code in either mode; both are gone.
Installation
dotnet add package Kontent.Ai.ModelGenerator --prereleaseFull changelog: src/model-generator/CHANGELOG.md
Kontent.Ai.Management 9.0.0-rc.2
Breaking changes
Referencemoved from the asset-folder and taxonomy-group PATCH bases onto the operations that need it, where it isrequired. Both bases declared a nullableReference, so aremove,rename,moveorreplaceoperation could be constructed without the reference the API demands — the compiler was fine with it and the request failed at the server. Each operation now declares its own:requiredon the ones that target something (AssetFolderRemovePatchModel,AssetFolderRenamePatchModel,TaxonomyGroupRemovePatchModel,TaxonomyGroupMovePatchModel,TaxonomyGroupReplacePatchModel), and still optional onaddInto, where it names the parent to add into and its absence means the root. This matches the collection patch models, which were already shaped this way. The wire format is unchanged; code that already setReferenceon these operations still compiles, and code that did not now fails to compile instead of failing at the API.
Changed
-
EnvironmentIdis no longer required when you only call subscription endpoints. Subscription-scoped endpoints resolve against/v2/subscriptions/{id}and never touch an environment, but validation demanded anEnvironmentIdregardless — so a subscription admin listing projects had to invent an environment GUID the SDK would never use. Each scope's client is now built only when its identifier is configured, andEnvironmentIdis validated for format only when supplied, exactly asSubscriptionIdalready was.Calling into a scope you did not configure fails immediately, naming the option — before the request is built, so you never see the API's
404for a path with an empty segment:EnvironmentId is not configured. Set ManagementOptions.EnvironmentId to call environment endpoints.Configuring neither identifier is still rejected at registration: that client could call nothing at all. Every existing configuration behaves exactly as before — this only accepts input that was previously refused.
Fixed
-
The doc samples assert success rather than that a result object exists. Forty of them ended in
Assert.NotNull(response)against anIManagementResult, which is never null — so a failed call passed. Replacing that withEnsureSuccess()immediately surfaced five sample fixtures that had drifted out of step with their models and could no longer deserialize; those are refreshed from the fixtures the domain tests use. -
The pass-through
CreateRefitSettingswrapper is gone, and the deliberateScheduleResponseModeldate divergence is now recorded so it is not "corrected" later. -
IntelliSense wording corrections. The single-item custom-app operations described themselves in the plural,
UpdatePreviewConfigurationAsyncwas documented as a "Modify" (this SDK's word forPATCH, which it is not) with a parameter described as project-scoped, and a subscription-user method read "Retrieve a user metadata". Two enum members had typos in their summaries. -
The unused
Microsoft.Extensions.Logging.Abstractionsreference is gone, so it no longer lands in the published package as a dependency nobody needs. -
The doc samples for importing content check their results. Every one of the nineteen discarded the
IManagementResultit received, so a failed call passed the test and the published sample taught ignoring the result pattern the SDK is built around. They now useEnsureSuccess(), which is both a real assertion and the idiomatic sample code — and each sample is backed by a response fixture that actually deserializes, so the assertion has something to check rather than passing on an empty body. -
The client factory no longer relabels an exception that came from your own registration.
Get(name)caughtInvalidOperationExceptionand reported it as a missing client — but the registration runs during resolution, so aconfigureHttpClientthat rejected its input came back as "No management client registered with name '…'", pointing at the wrong thing entirely. A genuinely missing registration still says so. -
A doc sample no longer reads a bare timestamp in the machine's time zone. Three samples fed
DateTime.Parseinto aDateTimeOffsetscheduling parameter, which is exactly the ambiguity the SDK's date convention exists to prevent — taught in code people copy. They now construct the offset explicitly, as the README sample already did. -
The README no longer offers a Refit-settings hook that was removed.
ManagementClientBuildercustomizes the resilience pipeline; the Refit hook it also advertised is gone. -
The
X-KC-SOURCEheader keeps naming the integration that made the call. Attribution matched the SDK assembly by full name, which carries the version — and nothing pinsAssemblyVersion, so the reference an integration recorded when it was built stopped matching on the first SDK release after that. The header then went silently missing for every consumer who had not rebuilt. Matching is now by simple name. -
The interface says what happens when you call into a scope you did not configure. Since
EnvironmentIdbecame optional for subscription-only clients, every environment operation throwsInvalidOperationExceptionwhen it is missing — the same guard the subscription operations already documented, but stated nowhere for the ~80 methods on the other side.IManagementClient's own remarks now describe both scopes and the guard once, rather than repeating an<exception>tag on every method. -
The documented error-handling model matches what the SDK does. The README, the upgrade guide and the
IManagementResult/ typed-variant IntelliSense all said network-level and serialization failures "still propagate as exceptions". They do not, and have not since the result pattern landed: a transport failure that never reached the server and a response whose body could not be read are both failed results, carrying the exception inError.Exception. A consumer following the old text wrote acatchthat never fires and skipped theIsSuccesscheck that would have caught the failure. The docs now state what actually throws — cancellation, argument and configuration validation,EnsureSuccess(), and a typed-variant projection onto a record that no longer matches the content type — and the behaviour is pinned by tests. -
The README now says how to configure a subscription-scoped call. It listed
SubscriptionIdin the options table and mentioned "an API key with subscription scope", but never said the Subscription API key is a different credential from the Management API key or where to get one. There is now a worked example and a pointer tohttps://app.kontent.ai/subscription/<subscription-id>/api-keys, which only a subscription admin can use.
Installation
dotnet add package Kontent.Ai.Management --prereleaseFull changelog: src/management/CHANGELOG.md
Kontent.Ai.Delivery 20.0.0-rc.2
Breaking changes
-
The caching package's registration class is renamed to
DeliveryCacheServiceCollectionExtensions. It and the Delivery SDK both declaredKontent.Ai.Delivery.ServiceCollectionExtensions, so two packages owned one full type name — and sinceKontent.Ai.Delivery.Cachingdepends onKontent.Ai.Delivery, every consumer has both and could name neither: referring to it wasCS0433, with no way to disambiguate. Nothing that compiled before stops compiling. The namespace is unchanged, sousing Kontent.Ai.Delivery;and everyservices.AddDeliveryMemoryCache(...)/AddDeliveryHybridCache(...)/AddDeliveryCacheManager(...)call is exactly as it was; only code that named the type explicitly is affected, and that could not have built. -
The source generator emits its marker attribute as
internal.ContentTypeCodenameAttributeis generated into each referencing compilation, so apublicone put the same type name into every assembly that uses the generator. Two such projects referencing each other stopped compiling withCS0436/CS0433, and the only fix available to the consumer was to drop a project reference. Emitting itinternal— standard practice for generated marker attributes — gives each assembly its own copy. Code that only applies the attribute to its own models is unaffected; code that exposed it across an assembly boundary was in the broken configuration already.
Added
-
ConfigureFusionCacheonDeliveryCacheOptions, fromKontent.Ai.Delivery.Caching, configures the underlying cache withFusionCacheOptionstyped:services.AddDeliveryMemoryCache(opts => opts .ConfigureFusionCache(fusion => fusion.DefaultEntryOptions.EagerRefreshThreshold = 0.8f));
The
ConfigureFusionCacheOptionsproperty it sets stays as it was,Action<object>?, because it is declared inKontent.Ai.Delivery.Abstractionsand that package deliberately references nothing. The extension lives where FusionCache is already referenced, so the cast happens once here instead of in every caller.
Fixed
-
DeliveryClientBuilder.Build()documents the exception it actually throws. It promisedInvalidOperationExceptionfor invalid configuration; the validation runs in the options pipeline, so what surfaces isOptionsValidationException. Now pinned by a test, and the inline note about why it fires during the build is corrected too. -
The caching package no longer re-registers the dependency extractor the SDK already registers, and the interface no longer describes a no-op implementation that does not exist — there is one implementation.
-
IDeliveryClientsays which queries are cached. Languages, single content elements and used-in queries always reach the API; that was a decision nowhere written down. -
DeliverySourceTrackingHeaderAttributeis sealed, and bothWithEnvironmentIdoverloads describe setting the environment rather than constructing the builder. -
A rich-text document is disposed once parsed. The AngleSharp document was left to finalization on every rich-text element mapped; the parsed blocks hold plain strings and lists rather than document nodes, so nothing needed it to stay alive.
-
A client name containing a tab or newline is rejected like one containing a space. The rule trimmed and then looked for spaces, so other whitespace passed validation and left a name that is invisible at the point of failure. The caching package also carried its own copy of the rule, which is now the shared one.
-
Dynamic queries carry their dependency keys, on every page.
GetItem/GetItemswithout a typed model returned results whoseDependencyKeyswerenull, while the typed queries forwarded them — so output-cache tagging, which those keys exist for, had nothing to tag with on the dynamic path. Paging through a dynamic listing dropped them the same way from the second page on. -
ImageUrlBuilderkeeps a query the asset URL already carries. Transformations were applied as a relative reference with its own query, which replaces the base URL's query outright. An asset URL produced by a default rendition preset therefore lost its rendition the moment any transformation was added. The two are merged now, with an explicit transformation winning where both set the same key. -
A cache miss in raw-JSON mode hydrates once instead of twice. The factory already builds the value to collect its dependency keys, and the payload it stored was then parsed and mapped a second time to answer the same call. The call that produced the value now reuses it; a cache hit or a background refresh still rehydrates, as it must.
-
The source generator no longer pins compilations in the IDE's incremental cache. Its pipeline model carried a
Location, which holds itsSourceTreealive — and pipeline values are retained for as long as the generator is loaded, so every edit accumulated another rooted syntax tree and the compilation behind it. The position is stored as a path and spans, and theLocationis rebuilt only when a diagnostic is reported. -
Options handed to the SDK prebuilt are copied by reflection rather than property by property.
DeliveryOptions.CopyTolisted the properties it carried, which keeps compiling when an option is added and silently stops carrying it — a value the caller set that the client never sees. It now uses the same copier the other SDKs do. -
The
X-KC-SOURCEheader keeps naming the integration that made the call. Attribution matched the SDK assembly by full name, which carries the version — and nothing pinsAssemblyVersion, so the reference an integration recorded when it was built stopped matching on the first SDK release after that. The header then went silently missing for every consumer who had not rebuilt. Matching is now by simple name. -
Rich-text tag resolvers keep their place in registration order, and the description is no longer dispatch.
WithHtmlNodeResolver(tagName, ...)registrations were lifted into a lookup consulted before any predicate resolver, so a tag resolver won however late it was registered — against the documented "evaluated in registration order, first match wins". Membership of that lookup was decided by whether the resolver's description started withTag=, so a predicate resolver a caller happened to describe that way was silently promoted into it. Registering the same tag twice threw anArgumentExceptionfromBuild(), where every other registration is resolved by order. All three now follow the one documented rule: one ordered pass, first match wins, tag registrations included. The public builder API is unchanged. -
Cache keys are scoped to the environment they were fetched from. A key was built from the query alone, so "the item
article" produced the same key in every environment. Two applications sharing one distributed cache and pointing at different environments served each other's content, silently and in both directions. The environment id is now part of the key prefix, ahead of which an explicitKeyPrefixstill separates clients within one environment. Existing distributed cache entries are not readable under the new keys and are simply missed once, then rewritten — nothing to migrate, but expect one cold start after upgrading. -
An application's own
JsonSerializerOptionsregistration is no longer taken over as the SDK's wire serializer.AddDeliveryClientlooked for a singleton registered underJsonSerializerOptionsand, finding one, used it to read every API response. Registering that type is an ordinary thing for an application to do, and the options it registers do not carryContentItemConverterFactory- without which no raw item JSON is captured, hydration has nothing to map from, and typed models come back empty. Nothing threw and nothing was logged. The SDK now keeps its serializer under a type only it names, so an application's registration stays the application's, and a registration made through a factory or under a service key no longer splits Refit and the mappers onto different serializers. -
A distributed cache no longer strips taxonomy and multiple-choice data out of content types. The distributed tier's serializer was built without the SDK's own converters, so it wrote content type elements by their declared type:
TaxonomyElement.TaxonomyGroupandMultipleChoiceElement.Optionswent in and never came out. A node reading such an entry back got a plainContentElement- anInvalidCastExceptionfor anything casting toITaxonomyElementorIMultipleChoiceElement, and missing data for anything that did not. The writing node was unaffected because it answers from its own memory tier, so this surfaced only on a second instance, which is the case a distributed cache exists for.ContentElementConverternow writes an element by its runtime type - the wire's owntypefield is the discriminator on the way back - and the distributed tier uses the SDK's serializer unless one is supplied. -
A request is bounded again when the SDK's own resilience pipeline is not the one installed.
HttpClient.Timeoutwas set toTimeout.InfiniteTimeSpanunconditionally, on the premise that the resilience pipeline owns timing - but the 30-second per-attempt timeout that premise rests on exists only whileEnableResilienceis left on and noconfigureResiliencehook replaces the default pipeline. SettingEnableResilience = false, or supplying a pipeline that adds no timeout of its own, therefore left a call with no attempt timeout, no overall timeout and no ceiling of any kind, so a connection that stopped responding hung the caller indefinitely. The ceiling is now lifted only for the default pipeline; otherwiseHttpClient's 100-second default applies, as it did before this SDK moved to a resilience pipeline. A custom pipeline that legitimately needs longer can raise it through `configure...
Kontent.Ai.AspNetCore 1.0.0-rc.2
Breaking changes
WebhookNotification.NotificationsisIReadOnlyList<WebhookModel>?instead ofWebhookModel[]?.WebhookNotificationis a record, so it compares by value — except that an array member compares by reference, which meant two notifications carrying identical payloads were never equal. The array was also handed out mutable, so a caller could rewrite a deserialized payload in place. Reading the collection is unaffected: indexing,foreach,Count(rather thanLength) and LINQ all work as before. Code that assigned an array to the property still compiles; code that declared the receiving variable asWebhookModel[]needsIReadOnlyList<WebhookModel>orvar.
Changed
- The modern signature header now wins when a request carries both.
X-Kontent-ai-Signatureis read first andX-KC-Signatureis the fallback, rather than the other way round. Both were always verified against the same secret, so this is not a security change — a request with only one header behaves exactly as before. It only settles which is authoritative when both are present, and it matches how the README and the header names themselves present the two.
Fixed
-
RichTextTagHelperuses a primary constructor, matching the other tag helpers in the package. -
A null
predicatepassed toUseWebhookSignatureValidatoris rejected at registration. Every other argument on those overloads was guarded; this one was dereferenced later byUseWhen, so the mistake surfaced away from the call that made it. -
The webhook signature is computed over the bytes as received. The body was decoded to a string and re-encoded before hashing. The decoder substitutes replacement characters for malformed input rather than failing, so that round trip could map two different request bodies onto the same bytes — and a comment claimed the opposite property, that a body which is not valid UTF-8 could not hash like one that is. Verification is fail-closed either way, so no invalid signature was ever accepted; the round trip and the comment are both gone.
Installation
dotnet add package Kontent.Ai.AspNetCore --prereleaseFull changelog: src/aspnetcore/CHANGELOG.md
Kontent.Ai.Sync 2.0.0-rc.1
Targets .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...
Kontent.Ai.ModelGenerator 11.0.0-rc.1
Targets .NET 10. Both packages move from net8.0 to net10.0, which is why this is a major release rather than a continuation of the 10.3.0 line. Generated output is unchanged.
Breaking changes
-
net8.0→net10.0.Kontent.Ai.ModelGenerator.Coreis a library, so a project on .NET 8 cannot reference this release at all — restore fails withNU1202. TheKontent.Ai.ModelGeneratorCLI likewise needs the .NET 10 runtime to run. Move to .NET 10 first. -
Two generator base-class properties became methods.
ClassCodeGenerator.Propertiesis nowGetProperties(), and the Delivery generator'sPropertyCodenameConstantsis nowGetPropertyCodenameConstants(). Both re-sort their input and build a fresh set of Roslyn syntax nodes on every access, so a property was misleading about the cost — two reads returned two different arrays.GetProperties()remainsvirtual, so overriding it still works; a derived generator changesoverride … Propertiestooverride … GetProperties(). Only affects code that subclasses these base classes. -
--withtypeprovider/-tandCodeGeneratorOptions.WithTypeProviderare removed, along with theTypeProviderCodeGeneratorthat backed them. The Delivery SDK generates its ownGeneratedTypeProviderat compile time fromKontent.Ai.Delivery.SourceGenerationand discovers it at runtime, so nothing needs a hand-written provider any more.The flag had in fact stopped doing anything before this release: the code path behind it lived on a method that hid its base rather than overriding it, and the CLI invokes the base, so passing
-tgenerated no provider and printed no warning. Passing it now fails withUnsupported parameter: -trather than being silently ignored. Remove it from your scripts and referenceKontent.Ai.Delivery.SourceGenerationfrom the project your models are generated into. -
CodeGeneratorBase.FilenameSuffixandGetFileClassNameare removed. The suffix has been the empty string since single-file generation landed, which madeGetFileClassName(name)an identity function. Generated file names are unchanged. Only affects code that subclassesCodeGeneratorBase. -
IOutputProvider.Outputreturnsboolinstead ofvoid—truewhen it wrote the file,falsewhen the file already existed andoverwriteExistingwas not set. The generator reports each file's outcome and had no way to tell the two apart. Only affects code that implementsIOutputProvider; a custom implementation adds areturn true;. -
The dropped custom-partial emission path is gone.
PartialClassCodeGenerator, thecustomPartialflag onIClassCodeGeneratorFactory.CreateClassCodeGenerator, andClassCodeGenerator.OverwriteExistingall existed to support emitting a second, user-extensible partial file. The CLI never asked for it — the flag was never passed astrue— so the generator was unreachable, andOverwriteExistingwas aGetType() != typeof(PartialClassCodeGenerator)check that could only ever answertrue. The factory method also took anIUserMessageLoggerit null-checked and never used; that parameter is gone too. -
IDeliveryElementServiceandDeliveryElementServiceare removed.GetElementType(string)returned its argument unchanged, and the injected options were never read — an interface, an implementation, a DI registration and an inheritance layer computing the identity function.DeliveryCodeGeneratornow readselement.Value.Typedirectly and derives fromCodeGeneratorBase;DeliveryCodeGeneratorBase, whose only purpose was carrying the service, is gone with it. -
The always-true emission seams are gone.
ClassCodeGenerator.IsRecordandUseFileScopedNamespacewerevirtualand defaulted tofalse, but every concrete generator overrode both totrue, so the class-emitting and block-namespace branches were unreachable.DeliveryClassCodeGeneratorBasehad one subclass left after the custom-partial removal and is folded intoDeliveryClassCodeGenerator, which is nowsealed. -
Dead public members are removed from
PropertyandTextHelpers. OnProperty:ObjectType,IsNullable,HasInitializer, the already-obsoleteRequiresDefaultInitializer, and theIsDateTimeElementType/IsRichTextElementType/IsModularContentElementTypepredicates — none reachable from any emission path. OnTextHelpers:GetEnumerableType, andGetUpperSnakeCasedIdentifierName, which despite its name producedPascal_Snake_Caserather than upper snake case and was called by nothing.Generated output is unaffected. Verified by generating against a live environment before and after: all 15 files, including the
--baserecordextender, are byte-identical. -
ClassDefinition.AddPropertyCodenameConstantis removed;AddPropertynow registers both the property and its codename constant. The two were always called as a pair, and calling them separately is what allowed a rejected property to leave its constant behind.CodeGeneratorBase.AddProperty(Property, ref ClassDefinition), which wrapped the pair and had no callers, is removed with it. Only affects code that drivesClassDefinitiondirectly.
Changed
-
Arguments are validated against the SDKs' own rules instead of a hand-written subset. The tool checked only that an environment id was present and non-blank, so
-i not-a-guidwas accepted and the run failed later against the API with a less obvious message. It now runs the validation the SDK options already declare — data annotations plusIValidatableObject— which is what the SDKs' own container-free constructors do. Every problem is reported at once rather than the first, so a run started with several bad arguments does not have to be repeated once per mistake.$ KontentModelGenerator -i not-a-guid The delivery configuration is not valid: - EnvironmentId: The environment ID must be a valid GUID. See http://bit.ly/k-params for more details on configuration.Configurations that were valid before remain valid. A configuration the tool used to accept and the API would then reject now fails at startup.
-
--baserecordno longer fetches the content model twice. Generating the base record re-read the whole content model rather than reusing what had just been fetched, so a run with-bmade every request twice — in management mode, both the content-type and snippet listings. The generated output was identical either way; only the number of API calls changes. -
Nothing about the code the generator emits, for any content model that generated valid code before. Model classes, enums and the mapping attributes are byte-identical to
10.3.0-beta-2, verified by the generator's own output assertions. Models that previously came out uncompilable are covered under Fixed.
Fixed
-
Element codenames that differ but produce the same C# identifier no longer emit uncompilable models. Duplicate detection compared raw codenames while emission used the PascalCased identifier, so
my_elementandmy__element— two codenames, one identifier — both got through. The generated record then declaredMyElementCodenametwice and did not compile. The same hole existed between the two kinds of member: an element namedtitleand one namedtitle_codenameproduced a constant and a property that were both calledTitleCodename, and that case emitted no warning at all. Everything a record declares is now checked against one registry of identifiers, and the offending element is skipped with a message naming both codenames and the identifier they collide on.Warning: Skipping element 'my__element'. Content type 'article': 'my__element' and 'my_element' both produce the identifier 'MyElement'. Rename one of the elements in Kontent.ai.A rejected element no longer half-registers either — the constant used to be recorded before the property could be refused, so skipping one element still corrupted the output.
-
An element that fails for an unanticipated reason is now reported instead of vanishing. Per-element failures were classified by a
switchwith arms for the three expected exception types and no default, so anything else was caught, matched nothing, and left the element out of the generated model with nothing written to the console. -
The tool no longer claims to have created a base record it did not write.
--baserecorddeliberately does not overwrite an existing file, so hand-written additions survive a rerun — but the run printed "<name>class was successfully created" either way. It now says the file was kept, andIOutputProvider.Outputreturns whether it wrote (see Breaking changes). -
The "no content type available" message names the environment in management mode. It read the Delivery options only, so a
--managementapirun against an empty environment reported the id as blank. -
A failure with more than one inner exception no longer exits silently.
Mainhad a special case forAggregateExceptionthat printed the message only when there was exactly one inner exception and otherwise returned exit code 1 with no output at all.awaitunwraps these anyway, so the case was vestigial; it is removed and the general handler reports every failure. -
Two content types that map to the same file no longer silently overwrite each other. Type codenames sanitize to a class name the same way element codenames do, so
my_typeandmy__typeboth wroteMyType.cs— the second overwrote the first, and the run reported both as created. The duplicate is now skipped with a warning, and the "N content type models were successfully created" count reflects what was actually written.
Dependencies
Shipped floors moved up:
Kontent.Ai.Delivery,Kontent.Ai.Delivery.Abstractions,Kontent.Ai.UrlsandKontent.Ai.Delivery.SourceGeneration19.4.0 → 20.0.0-rc.1, andKontent.Ai.Management9.0.0-beta-5...
Kontent.Ai.Management 9.0.0-rc.1
Targets .NET 10, completing the framework move that the 9.x line was always heading for, and upgrades Refit across four major versions. The result pattern, transport architecture and model conventions introduced in the earlier betas are unchanged — see the 9.0.0-beta-1 release notes for that overview.
Warning
Still a prerelease. Install with --prerelease — without that flag you get the stable 8.x API, which these notes do not describe.
Breaking changes
-
net8.0→net10.0. There is no multi-targeting, so a project on .NET 8 cannot install this release — restore fails withNU1202. Move to .NET 10 first. -
new FileContentSource(stream, …)now rejects a stream that cannot seek, with anArgumentExceptionnaming the parameter. The upload endpoint needs the size up front: without aContent-Lengththe request goes out chunked and is refused with "the file is bigger than the maximal allowed limit (2 GB)" — regardless of the actual size, and verified against the live API. A non-seekable stream has no length to declare, so this overload could never produce a successful upload; the error simply arrived from the server, describing the wrong problem. It is now refused where the stream is passed in.Nothing that worked stops working — there was no combination in which a non-seekable stream uploaded successfully. If you were passing one, buffer it first or use the
byte[]/file-path overload:// Before — always failed, with an error about a 2 GB limit var source = new FileContentSource(httpResponseStream, "photo.jpg", "image/jpeg"); // After — buffer, so the length is known using var buffered = new MemoryStream(); await httpResponseStream.CopyToAsync(buffered); var source = new FileContentSource(buffered.ToArray(), "photo.jpg", "image/jpeg");
-
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 holdingIManagementClientwas offered aDispose()that, on the container path, released nothing and must not be called: the container owns that lifetime.ManagementClientBuilder.Build()now returns the concreteManagementClient, which isIDisposableandIAsyncDisposable, so disposal stays available exactly where it means something.await using var client = ManagementClientBuilder…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 IManagementClient client = ManagementClientBuilder…Build(); client.Dispose(); // After - keep the concrete type, or just use var var client = ManagementClientBuilder…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.
-
The
configureRefitparameter is gone from all fiveAddManagementClientoverloads, andManagementClientBuilder.ConfigureRefitis removed. The hook handed out the transport library's settings object, but every value it could reach was load-bearing rather than configurable — the parameter-key formatter matches the API's casing, and the serializer options carry the converters, naming policy and nesting limit the wire format depends on. Overriding them broke requests silently. Delete the argument or the builder call; the SDK's own tests only ever used it to assert the callback fired. -
Enum values are now read case-sensitively. Only the exact wire token is accepted:
"modular_content"binds,"MODULAR_CONTENT"and the C# member name"LinkedItems"now throwJsonExceptioninstead of being coerced. The Management API emits canonical tokens andContentModelSnapshot.FromJsononly ever consumesToJsonoutput, so this affects hand-written JSON. Writing is unchanged, and numeric tokens are still rejected in both directions.
Changed
-
AddManagementClientgained the overloads its sibling SDKs already had, so the three register a client the same way: a pre-built options instance, and options configured with access to theIServiceProvider. Nothing was removed, and existing calls are unaffected — this closes gaps rather than reshaping the surface. -
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. This one matters on a write API: the request was
sent and the server may have applied it, so it is reported as a failed result carrying the exception,
never as the caller withdrawing a request that never happened. -
Transport failures report status
0.IManagementResultnow carries(HttpStatusCode)0for that case rather than an invented code. Responses that did arrive are unaffected. -
ManagementOptions.Timeoutsets the ceiling on one call, covering every retry attempt and the waits between them. Defaults to 30 minutes.
Fixed
- An empty pre-release label no longer produces a trailing hyphen in
X-KC-SOURCE. A package identifying itself with[assembly: SourceTrackingHeader("MyPackage", 2, 0, 0, "")]was reported asMyPackage;2.0.0-, which is not a valid SemVer version. An empty label now counts as no label, matching what passingnullalready did. GetFullFolderPathno longer starts a path with a separator. An ancestor folder with an empty name still contributed a segment, so a folder below it came back as\\Childrather thanChild. Unnamed ancestors are now skipped.- A long upload is no longer cut off at 100 seconds. This SDK deliberately configures no per-attempt timeout, because an asset upload takes as long as the file is large and the link is slow. But
HttpClient's own 100-second default bounds the whole call — every attempt and all the backoff between them — and nothing raised it, so it capped exactly the uploads the missing per-attempt timeout was meant to protect. It also silently truncated retries: a429carryingRetry-After: 60spent most of the budget before the next attempt began. The ceiling is nowManagementOptions.Timeout, defaulting to 30 minutes — sized against the documented 2 GB asset limit, which is roughly what that carries over a 10 Mbps link. - Uploads always declare a
Content-Length. A source that could not report its size sent the request chunked, and the endpoint rejects that outright — reporting "the file is bigger than the maximal allowed limit (2 GB)" no matter how small the file actually was. Every source now carries a length, so the request the SDK builds is one the API can accept. - 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. This matters when the endpoint's address changes underneath a process that stays up for days: a failover, a scale event, or any re-pointing upstream. Configuring your own primary handler viaconfigureHttpClientstill overrides this, as before.
Dependencies
Shipped floors on Kontent.Ai.Management moved up, all .NET 10 aligned:
Microsoft.Extensions.*(Configuration.Abstractions,Logging.Abstractions,Options.ConfigurationExtensions,Options.DataAnnotations) 9.0.15 → 10.0.10.Microsoft.Extensions.Http.Resilience9.6.0 → 10.8.0.RefitandRefit.HttpClientFactory10.2.0 → 14.0.1.
Internal
No consumer-visible effect:
- Enum wire tokens now travel on
[JsonStringEnumMemberName]and serialize through the built-inSystem.Text.Jsonconverter. The custom converter existed only because that attribute did not exist on .NET 8. All 140 members across 36 enums keep their exact tokens, verified by round-trip — including the ones that are not snake_case (light-purple,fullScreen,asc,modular_content). - Refit 14 builds request logic at compile time rather than by reflection; the Management interfaces generate completely and gained that with no changes.
Installation
dotnet add package Kontent.Ai.Management --prereleaseFull changelog: src/management/CHANGELOG.md
Kontent.Ai.Delivery 20.0.0-rc.1
Targets .NET 10. Every package in this product moves from net8.0 to net10.0, which is why this is a major release, and Refit's transport is upgraded across four major versions. Beyond the target framework the public API is almost untouched — one configuration hook is removed, and two request-building details change in ways that are visible in logs but not in results.
Breaking changes
-
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.Delivery is not compatible with net8.0. Move to .NET 10 first.Kontent.Ai.Delivery.SourceGenerationis the exception and staysnetstandard2.0, as Roslyn components must; it loads in every SDK from .NET 8.0.2xx onward, unchanged. -
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 holdingIDeliveryClientwas offered aDispose()that, on the container path, released nothing and must not be called: the container owns that lifetime.DeliveryClientBuilder.Build()now returns the concreteDeliveryClient, which isIDisposableandIAsyncDisposable, so disposal stays available exactly where it means something.await using var client = DeliveryClientBuilder…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 IDeliveryClient client = DeliveryClientBuilder…Build(); client.Dispose(); // After - keep the concrete type, or just use var var client = DeliveryClientBuilder…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.
-
The
configureRefitparameter is gone from all sixAddDeliveryClientoverloads, andRefitSettingsProvideris now internal. The hook exposed the transport library's settings object, but everything reachable through it was load-bearing rather than configurable: the parameter-key formatter is what matches the API's casing, and the serializer options carry the converters and nesting limit the wire format requires. Overriding any of them broke requests silently. If you passedconfigureRefit, delete the argument — the SDK's own tests never used it for anything but asserting the callback fired. Should a real need surface, it will return as an API named for what it does rather than for the transport library.
Changed
- Registering from
IConfigurationcan now customize the HTTP client and the resilience pipeline. The configuration-basedAddDeliveryClientoverloads took noconfigureHttpClient/configureResilience, so binding options from configuration and replacing the retry pipeline were mutually exclusive. The workaround — binding by hand inside anAction<DeliveryOptions>— 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. AddDeliveryClientgained the overloads its sibling SDKs already had, so the three register a client the same way: namedIConfigurationandIConfigurationSectionregistration. Nothing was removed, and existing calls are unaffected — this closes gaps rather than reshaping the surface.CacheResult<T>carriesFromFactory, saying whether the factory produced the value during this call or the cache served a stored one. Nothing else about the type changes and the constructor is untouched, so existing code compiles unchanged. A customIDeliveryCacheManagerthat builds its ownCacheResult<T>should set it — left at itsfalsedefault, every result it returns is treated as a cache hit.DeliveryOptions.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.- Repeated filter parameters are emitted in declaration order. Filters sharing an element used to be grouped together regardless of where they appeared, so
a,b,awas sent asa,a,b. Results are unaffected — filter parameters are AND-ed, and cache keys were already order-independent — but the request URL differs, which shows up in logs and traces. - 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. the result object now carries(HttpStatusCode)0for that case rather than an invented code. Responses that did arrive are unaffected.
Fixed
-
An element the model cannot map is now logged as a warning rather than at debug level. When a value fails to deserialize onto the generated property, the SDK logs and yields
null— butnullis also what an empty element gives, so the log is the only thing distinguishing the two. AtDebugit was absent from any normal production configuration, which made a model that had drifted from the content type look like missing content instead of a mismatch. The behaviour is unchanged; only the level is. -
Cache invalidation propagates reliably between nodes once a backplane is registered. With
AddDeliveryHybridCache, part of the invalidation state is held perIDeliveryCacheManagerinstance, so whether one node observes another's invalidation depends on the order the two nodes happened to read and invalidate in. In one measured ordering — node A caches an entry, node B reads it, then A invalidates — B keeps serving the evicted content until the entry expires on its own. Register anIFusionCacheBackplaneand the SDK now wires it up, so invalidations propagate regardless of ordering:services.AddStackExchangeRedisCache(o => o.Configuration = "localhost"); services.AddFusionCacheStackExchangeRedisBackplane(o => o.Configuration = "localhost"); services.AddDeliveryHybridCache();
Nothing changes for a single-instance application, which needs no backplane. FusionCache keeps an in-memory tier in front of the distributed one and uses it either way; the backplane is what keeps those tiers in step across nodes, so without one an invalidation still reaches only the node that performed it.
-
Cache invalidation no longer skips items whose codename looks like a component's. Components were told apart by the shape of their generated codename — a
_-separated group of four characters starting01, as inn373888cc_34e2_01e1_1820_3cb52ab1b2a1. Authored codenames collide with that:Product SKU 0123 Bluebecomesproduct_sku_0123_blue, whose third group is0123. Such an item was silently given noitem_dependency key, so a webhook naming it evicted nothing and the cached response kept being served until it expired — the failure was invisible and depended on how content was named.Components are now recognised from the response instead: the Delivery API gives every content item a
workflowandworkflow_stepand gives components neither. Where that signal is not available the item is tracked regardless, because the two mistakes are not equal — a dependency key for a component is one entry nobody ever looks up, while a missing key for an item is stale content. -
Eager refresh no longer misreports what a query returned. With
EagerRefreshThresholdset, the cache returns the stale-but-valid value immediately and refreshes it on a background thread. Each query builder decided whether it was serving a cache hit by reading variables its own cache factory wrote into — and the background refresh writes into those same variables, for a different call. Depending on how the two threads interleaved, an ordinary eager-refresh hit could come back asResponseSource.FailSafe, or be logged as a fresh fetch and wrapped with the background request's status code. The decision now comes from the cache, which is the only component that knows which value it handed back. Consumers who leaveEagerRefreshThresholdat its default of0were never affected. -
Rich-text parsing and resolution no longer risk deadlocking a caller that blocks on the task. Every
awaitin the SDK opts out of the caller's synchronization context — except the rich-text subsystem, which had drifted: 17 awaits across the parser, the HTML resolver and the default resolvers captured the context. On a host that has one (WPF, WinForms, legacy ASP.NET), code that blocks on the returned task —.Result,.Wait()— could deadlock. ASP.NET Core has no synchronization context and was never affected. CA2007 is now enforced across the SDK libraries so this cannot drift again. -
The configured retry pipeline can now run to completion.
HttpClient's 100-second default bounds the whole call, retries and backoff included, and...
Kontent.Ai.AspNetCore 1.0.0-rc.1
Targets .NET 10, moving from net8.0 to net10.0. Webhook signature verification is hardened in two ways
worth reading before you upgrade, and the public surface is tightened while the package is still pre-1.0:
types are sealed, the webhook payload models become immutable records, and two dependency-injected values
stop being public properties.
Breaking changes
- Every public type is
sealed, and the webhook payload models arerecords withinitproperties.WebhookNotification,WebhookModel,WebhookData,WebhookMessageandWebhookItemdescribe an inbound payload: nothing mutates one after it is bound, and comparing two by value is more often what you want than comparing by reference. Each keeps its public parameterless constructor, soSystem.Text.Jsonbinds them exactly as before, and property names and JSON attributes are unchanged. Code that constructs one with an object initializer still compiles; code that assigns a property after construction does not. Referenceis removed. A public model withById/ByCodename/ByExternalIdfactories that nothing in the package produced, consumed, or referenced — it was reachable only from its own unit test.SignatureMiddleware.WebhookOptionsandAssetTagHelper.ImageTransformationOptionsare no longer public properties. Both were dependency-injected values exposed for no scenario. The middleware's carried the shared webhook secret. The tag helper's was worse than redundant: Razor binds every public settable property on a tag helper to an HTML attribute unless told otherwise, so<img-asset>accepted animage-transformation-optionsattribute that was never meant to exist. Both are now constructor parameters held privately.UseWebhookSignatureValidatorno longer takes an optionalWebhookOptions. Three overloads accepting reference types meantUseWebhookSignatureValidator(predicate, null)could not be resolved. "Use the options from the container" is now its own two-argument overload, and theWebhookOptionsoverload takes a required, non-null instance. Calls that passed options, anAction<WebhookOptions>, a configuration section, or nothing at all are unaffected.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.AspNetCore is not compatible with net8.0. Move to .NET 10 first. This is a pre-1.0 package, so the minor version carries the break.
Security
-
Webhook signatures are compared in constant time. The check compared the Base64 signature strings with an ordinary ordinal comparison, which returns as soon as two characters differ. A caller able to time the response could recover the expected signature one character at a time and forge a request. The comparison now runs over the raw HMAC bytes via
CryptographicOperations.FixedTimeEquals. A signature that is not well-formed Base64, or that does not decode to exactly one HMAC-SHA256 digest, is rejected before any comparison — digest length is fixed, so rejecting on length leaks nothing. -
A missing webhook secret now fails loudly instead of accepting requests. With
WebhookOptions.Secretunset, the middleware hashed the body with an empty key and compared against that, so anything signed with the same empty key passed validation — a misconfigured deployment silently accepted forged webhooks, and a correctly-signed one looked like a bad signature. The middleware now throwsInvalidOperationExceptionnaming the missing setting.If you relied on running without a secret, set
WebhookOptions.Secretto the value shown in the webhook's settings in Kontent.ai. There is no configuration in which the previous behaviour was safe.
Fixed
- Webhook signature validation honors client disconnection. Reading the request body now observes
HttpContext.RequestAborted, so an aborted request stops the read instead of buffering the whole body first. <img-asset>readswidthandheightinvariantly, and no longer throws on values that are not numbers. The attributes were parsed withConvert.ToDoublein the server's culture whileImageUrlBuilderwrites the value back invariantly, so the round trip disagreed wherever.groups digits — on ade-DEserverwidth="1.5"produced?w=15, a silent tenfold resize. The same call threwFormatExceptionfor any value HTML allows but the image API has no equivalent for (100%,auto, a CSScalc), taking the render down with a 500. Such values now leave the transformation alone; the attribute still renders on the element, so it keeps working as plain HTML.
Dependencies
Shipped floors moved up:
Kontent.Ai.Delivery,Kontent.Ai.Delivery.AbstractionsandKontent.Ai.Urls19.4.0 → 20.0.0-rc.1. The19.xline targetsnet8.0, so leaving the floor there would let anet10.0package resolve a .NET 8 build of the SDK it is built on. Staying on Delivery19.xmeans staying onKontent.Ai.AspNetCore0.17.x.
This package has no direct Microsoft.Extensions.* references — it declares <FrameworkReference Include="Microsoft.AspNetCore.App" />, so those assemblies come from the ASP.NET Core shared framework rather than from a package.
Installation
dotnet add package Kontent.Ai.AspNetCore --prereleaseFull changelog: src/aspnetcore/CHANGELOG.md