Skip to content

Releases: kontent-ai/dotnet

Kontent.Ai.Sync 2.0.0-rc.2

Pre-release

Choose a tag to compare

@net-release-bot net-release-bot released this 12 Aug 13:37

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 SyncOptions nobody 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 CreateRefitSettings wrapper 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 concrete SyncClient — that is what makes the client it hands back disposable.

  • ChangeType serializes 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) caught InvalidOperationException and reported it as a missing client, so a configureHttpClient that 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.Build copies 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-SOURCE header keeps naming the integration that made the call. Attribution matched the SDK assembly by full name, which carries the version — and nothing pins AssemblyVersion, 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.Timeout was set to Timeout.InfiniteTimeSpan unconditionally, on the premise that the resilience pipeline owns timing - but the 30-second per-attempt timeout that premise rests on exists only while EnableResilience is left on and no configureResilience hook replaces the default pipeline. Setting EnableResilience = 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; otherwise HttpClient's 100-second default applies, as it did in 1.0. A custom pipeline that legitimately needs longer can raise it through configureHttpClient.

  • 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 --prerelease

Full changelog: src/sync/CHANGELOG.md

Kontent.Ai.ModelGenerator 11.0.0-rc.2

Pre-release

Choose a tag to compare

Fixed

  • The tool no longer ships the Visual Basic compiler. Microsoft.CodeAnalysis is the meta-package; only the C# syntax and workspace formatting APIs are used, so it now references Microsoft.CodeAnalysis.CSharp.Workspaces directly.

  • A blank comment argument reports ArgumentException rather than ArgumentNullException. 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.json names the option the tool actually reads. It still listed BaseClass, which was renamed to BaseRecord.

  • 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.

  • IClassCodeGeneratorFactory covers 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.json as living beside the executable; the tool reads it from the working directory, and as a dotnet tool it 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}Codename for each element, plus the type's own ContentTypeCodename — regardless of mode. The Management emitter writes none of those, so the reservation only rejected valid input there: a type carrying both title and title_codename had the second skipped with a collision warning, and an element codenamed content_type_codename was renamed for no reason. Constant registration is now the Delivery emitter's, so Management mode has the whole identifier space its own output uses.

  • --baseRecord is rejected at startup when it is not a valid C# record name. -b "My-Base" wrote public partial record My-Base and 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 --management now 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 -m was accepted, dropped, and the run continued as a full Delivery generation - writing Delivery models over whatever was in the output directory and exiting 0. The reverse dropped the Delivery-only -p / --projectid in Management mode and then failed with a message about an empty EnvironmentId, 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:ApiKey without --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 - an appSettings.json carrying both sections is unaffected, and the section belonging to the mode you run is the one that is read.

  • --nullability is refused in Management mode instead of accepted and ignored. It selects how generated Delivery models express nullability. Management models are uniformly nullable by contract - a null property 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.

  • --management together with --baseRecord no longer emits code that cannot compile. The generated base record and its extender both carried a hardcoded using Kontent.Ai.Delivery.Abstractions;. A project generated for the Management SDK has no reason to reference the Delivery SDK, so that line was a CS0246 in a file the consumer never wrote. Neither it nor the using System; beside it was referenced by the emitted code in either mode; both are gone.

Installation

dotnet add package Kontent.Ai.ModelGenerator --prerelease

Full changelog: src/model-generator/CHANGELOG.md

Kontent.Ai.Management 9.0.0-rc.2

Pre-release

Choose a tag to compare

Breaking changes

  • Reference moved from the asset-folder and taxonomy-group PATCH bases onto the operations that need it, where it is required. Both bases declared a nullable Reference, so a remove, rename, move or replace operation 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: required on the ones that target something (AssetFolderRemovePatchModel, AssetFolderRenamePatchModel, TaxonomyGroupRemovePatchModel, TaxonomyGroupMovePatchModel, TaxonomyGroupReplacePatchModel), and still optional on addInto, 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 set Reference on these operations still compiles, and code that did not now fails to compile instead of failing at the API.

Changed

  • EnvironmentId is 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 an EnvironmentId regardless — 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, and EnvironmentId is validated for format only when supplied, exactly as SubscriptionId already 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 404 for 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 an IManagementResult, which is never null — so a failed call passed. Replacing that with EnsureSuccess() 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 CreateRefitSettings wrapper is gone, and the deliberate ScheduleResponseModel date divergence is now recorded so it is not "corrected" later.

  • IntelliSense wording corrections. The single-item custom-app operations described themselves in the plural, UpdatePreviewConfigurationAsync was documented as a "Modify" (this SDK's word for PATCH, 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.Abstractions reference 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 IManagementResult it received, so a failed call passed the test and the published sample taught ignoring the result pattern the SDK is built around. They now use EnsureSuccess(), 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) caught InvalidOperationException and reported it as a missing client — but the registration runs during resolution, so a configureHttpClient that 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.Parse into a DateTimeOffset scheduling 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. ManagementClientBuilder customizes the resilience pipeline; the Refit hook it also advertised is gone.

  • The X-KC-SOURCE header keeps naming the integration that made the call. Attribution matched the SDK assembly by full name, which carries the version — and nothing pins AssemblyVersion, 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 EnvironmentId became optional for subscription-only clients, every environment operation throws InvalidOperationException when 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 in Error.Exception. A consumer following the old text wrote a catch that never fires and skipped the IsSuccess check 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 SubscriptionId in 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 to https://app.kontent.ai/subscription/<subscription-id>/api-keys, which only a subscription admin can use.

Installation

dotnet add package Kontent.Ai.Management --prerelease

Full changelog: src/management/CHANGELOG.md

Kontent.Ai.Delivery 20.0.0-rc.2

Pre-release

Choose a tag to compare

Breaking changes

  • The caching package's registration class is renamed to DeliveryCacheServiceCollectionExtensions. It and the Delivery SDK both declared Kontent.Ai.Delivery.ServiceCollectionExtensions, so two packages owned one full type name — and since Kontent.Ai.Delivery.Caching depends on Kontent.Ai.Delivery, every consumer has both and could name neither: referring to it was CS0433, with no way to disambiguate. Nothing that compiled before stops compiling. The namespace is unchanged, so using Kontent.Ai.Delivery; and every services.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. ContentTypeCodenameAttribute is generated into each referencing compilation, so a public one put the same type name into every assembly that uses the generator. Two such projects referencing each other stopped compiling with CS0436/CS0433, and the only fix available to the consumer was to drop a project reference. Emitting it internal — 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

  • ConfigureFusionCache on DeliveryCacheOptions, from Kontent.Ai.Delivery.Caching, configures the underlying cache with FusionCacheOptions typed:

    services.AddDeliveryMemoryCache(opts => opts
        .ConfigureFusionCache(fusion => fusion.DefaultEntryOptions.EagerRefreshThreshold = 0.8f));

    The ConfigureFusionCacheOptions property it sets stays as it was, Action<object>?, because it is declared in Kontent.Ai.Delivery.Abstractions and 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 promised InvalidOperationException for invalid configuration; the validation runs in the options pipeline, so what surfaces is OptionsValidationException. 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.

  • IDeliveryClient says which queries are cached. Languages, single content elements and used-in queries always reach the API; that was a decision nowhere written down.

  • DeliverySourceTrackingHeaderAttribute is sealed, and both WithEnvironmentId overloads 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/GetItems without a typed model returned results whose DependencyKeys were null, 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.

  • ImageUrlBuilder keeps 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 its SourceTree alive — 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 the Location is rebuilt only when a diagnostic is reported.

  • Options handed to the SDK prebuilt are copied by reflection rather than property by property. DeliveryOptions.CopyTo 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. It now uses the same copier the other SDKs do.

  • The X-KC-SOURCE header keeps naming the integration that made the call. Attribution matched the SDK assembly by full name, which carries the version — and nothing pins AssemblyVersion, 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 with Tag=, so a predicate resolver a caller happened to describe that way was silently promoted into it. Registering the same tag twice threw an ArgumentException from Build(), 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 explicit KeyPrefix still 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 JsonSerializerOptions registration is no longer taken over as the SDK's wire serializer. AddDeliveryClient looked for a singleton registered under JsonSerializerOptions and, 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 carry ContentItemConverterFactory - 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.TaxonomyGroup and MultipleChoiceElement.Options went in and never came out. A node reading such an entry back got a plain ContentElement - an InvalidCastException for anything casting to ITaxonomyElement or IMultipleChoiceElement, 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. ContentElementConverter now writes an element by its runtime type - the wire's own type field 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.Timeout was set to Timeout.InfiniteTimeSpan unconditionally, on the premise that the resilience pipeline owns timing - but the 30-second per-attempt timeout that premise rests on exists only while EnableResilience is left on and no configureResilience hook replaces the default pipeline. Setting EnableResilience = 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; otherwise HttpClient'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...

Read more

Kontent.Ai.AspNetCore 1.0.0-rc.2

Pre-release

Choose a tag to compare

Breaking changes

  • WebhookNotification.Notifications is IReadOnlyList<WebhookModel>? instead of WebhookModel[]?. WebhookNotification is 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 than Length) and LINQ all work as before. Code that assigned an array to the property still compiles; code that declared the receiving variable as WebhookModel[] needs IReadOnlyList<WebhookModel> or var.

Changed

  • The modern signature header now wins when a request carries both. X-Kontent-ai-Signature is read first and X-KC-Signature is 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

  • RichTextTagHelper uses a primary constructor, matching the other tag helpers in the package.

  • A null predicate passed to UseWebhookSignatureValidator is rejected at registration. Every other argument on those overloads was guarded; this one was dereferenced later by UseWhen, 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 --prerelease

Full changelog: src/aspnetcore/CHANGELOG.md

Kontent.Ai.Sync 2.0.0-rc.1

Pre-release

Choose a tag to compare

@net-release-bot net-release-bot released this 07 Aug 15:51

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.Abstractions is gone; everything it held now ships in Kontent.Ai.Sync, in the Kontent.Ai.Sync namespace. The split existed so contracts could be referenced without the client, and nothing ever did that — the package's only consumer was Kontent.Ai.Sync itself. 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.Abstractions reference 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>, and Data is now typed. ISyncItem, ISyncType, ISyncLanguage and ISyncTaxonomy declared exactly the same two members and were backed by four identical records — while the thing that genuinely differs between them, the payload, was hidden behind object?. 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, SyncLanguageData and SyncTaxonomyData model each system object as the API documents it. They are deliberately not one type: a content item carries collection, language, type and workflow state, while a language carries three properties and no last_modified at all. SyncTypeData and SyncTaxonomyData happen to match today and are still separate, because the API may extend either alone.

    The deprecated sitemap_locations array is not modelled. It is scheduled for removal, and leaving it out means that removal changes nothing here.

  • net8.0net10.0. There is no multi-targeting, so a project on .NET 8 cannot install this release at all — restore fails with NU1202: Package Kontent.Ai.Sync is not compatible with net8.0. Move to .NET 10 first.

  • InitializeSyncAsync returns ISyncResult instead of ISyncResult<ISyncInitResponse>, and ISyncInitResponse is removed. Initialization establishes a starting point rather than returning content — the useful output has always been the token, on SyncToken. ISyncInitResponse was an interface with no members, so Value was an object you could hold but never read. ISyncResult is new and non-generic, and ISyncResult<T> now derives from it, adding only Value; this mirrors IManagementResult / 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

    GetDeltaAsync and EnumerateDeltaAsync are untouched, including page.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 holding ISyncClient was offered a Dispose() that, on the container path, released nothing and must not be called: the container owns that lifetime. SyncClientBuilder.Build() now returns the concrete SyncClient, which is IDisposable and IAsyncDisposable, 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 = SyncClientBuilderBuild();
    client.Dispose();
    
    // After - keep the concrete type, or just use var
    var client = SyncClientBuilderBuild();
    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.ConfigureServices is removed, replaced by WithResilience. Building a client outside dependency injection no longer stands up a private service container: the client constructs the handler chain directly and owns the resulting HttpClient, matching how ManagementClientBuilder already worked. ConfigureServices existed 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-class WithResilience(...), 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 its HttpClient, 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.

  • GetAllDeltaAsync is replaced by EnumerateDeltaAsync, which returns IAsyncEnumerable<ISyncResult<ISyncDeltaResponse>>. The old helper decided it had caught up when no collection in a response had reached 100 entries, a threshold published as SyncConstants.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, where Take or a break replaces maxPages.

    // 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>.SyncToken is no longer nullable, and a successful response without an X-Continuation header 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 covers InitializeSyncAsync, GetDeltaAsync and EnumerateDeltaAsync alike, with an InvalidOperationException naming the request. In exchange SyncToken is declared string rather than string?, matching Value: both are meaningful only when IsSuccess is true. Code that wrote result.SyncToken ?? previous can drop the fallback — it was dead once the guarantee existed. Nothing changes for callers that only read the token after checking IsSuccess.

  • ISyncResult<T>.HasMoreChanges, SyncConstants and ISyncAllDeltaResult are 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. Carry SyncToken from the last yielded result; when nothing is yielded, the token you passed in is still current.

  • The configureRefit parameter is gone from all three AddSyncClient overloads. 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 IConfiguration can now customize the HTTP client and the...
Read more

Kontent.Ai.ModelGenerator 11.0.0-rc.1

Pre-release

Choose a tag to compare

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.0net10.0. Kontent.Ai.ModelGenerator.Core is a library, so a project on .NET 8 cannot reference this release at all — restore fails with NU1202. The Kontent.Ai.ModelGenerator CLI likewise needs the .NET 10 runtime to run. Move to .NET 10 first.

  • Two generator base-class properties became methods. ClassCodeGenerator.Properties is now GetProperties(), and the Delivery generator's PropertyCodenameConstants is now GetPropertyCodenameConstants(). 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() remains virtual, so overriding it still works; a derived generator changes override … Properties to override … GetProperties(). Only affects code that subclasses these base classes.

  • --withtypeprovider / -t and CodeGeneratorOptions.WithTypeProvider are removed, along with the TypeProviderCodeGenerator that backed them. The Delivery SDK generates its own GeneratedTypeProvider at compile time from Kontent.Ai.Delivery.SourceGeneration and 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 -t generated no provider and printed no warning. Passing it now fails with Unsupported parameter: -t rather than being silently ignored. Remove it from your scripts and reference Kontent.Ai.Delivery.SourceGeneration from the project your models are generated into.

  • CodeGeneratorBase.FilenameSuffix and GetFileClassName are removed. The suffix has been the empty string since single-file generation landed, which made GetFileClassName(name) an identity function. Generated file names are unchanged. Only affects code that subclasses CodeGeneratorBase.

  • IOutputProvider.Output returns bool instead of voidtrue when it wrote the file, false when the file already existed and overwriteExisting was not set. The generator reports each file's outcome and had no way to tell the two apart. Only affects code that implements IOutputProvider; a custom implementation adds a return true;.

  • The dropped custom-partial emission path is gone. PartialClassCodeGenerator, the customPartial flag on IClassCodeGeneratorFactory.CreateClassCodeGenerator, and ClassCodeGenerator.OverwriteExisting all existed to support emitting a second, user-extensible partial file. The CLI never asked for it — the flag was never passed as true — so the generator was unreachable, and OverwriteExisting was a GetType() != typeof(PartialClassCodeGenerator) check that could only ever answer true. The factory method also took an IUserMessageLogger it null-checked and never used; that parameter is gone too.

  • IDeliveryElementService and DeliveryElementService are 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. DeliveryCodeGenerator now reads element.Value.Type directly and derives from CodeGeneratorBase; DeliveryCodeGeneratorBase, whose only purpose was carrying the service, is gone with it.

  • The always-true emission seams are gone. ClassCodeGenerator.IsRecord and UseFileScopedNamespace were virtual and defaulted to false, but every concrete generator overrode both to true, so the class-emitting and block-namespace branches were unreachable. DeliveryClassCodeGeneratorBase had one subclass left after the custom-partial removal and is folded into DeliveryClassCodeGenerator, which is now sealed.

  • Dead public members are removed from Property and TextHelpers. On Property: ObjectType, IsNullable, HasInitializer, the already-obsolete RequiresDefaultInitializer, and the IsDateTimeElementType / IsRichTextElementType / IsModularContentElementType predicates — none reachable from any emission path. On TextHelpers: GetEnumerableType, and GetUpperSnakeCasedIdentifierName, which despite its name produced Pascal_Snake_Case rather 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 --baserecord extender, are byte-identical.

  • ClassDefinition.AddPropertyCodenameConstant is removed; AddProperty now 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 drives ClassDefinition directly.

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-guid was 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 plus IValidatableObject — 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.

  • --baserecord no 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 -b made 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_element and my__element — two codenames, one identifier — both got through. The generated record then declared MyElementCodename twice and did not compile. The same hole existed between the two kinds of member: an element named title and one named title_codename produced a constant and a property that were both called TitleCodename, 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 switch with 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. --baserecord deliberately 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, and IOutputProvider.Output returns 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 --managementapi run against an empty environment reported the id as blank.

  • A failure with more than one inner exception no longer exits silently. Main had a special case for AggregateException that printed the message only when there was exactly one inner exception and otherwise returned exit code 1 with no output at all. await unwraps 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_type and my__type both wrote MyType.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.Urls and Kontent.Ai.Delivery.SourceGeneration 19.4.020.0.0-rc.1, and Kontent.Ai.Management 9.0.0-beta-5...
Read more

Kontent.Ai.Management 9.0.0-rc.1

Pre-release

Choose a tag to compare

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.0net10.0. There is no multi-targeting, so a project on .NET 8 cannot install this release — restore fails with NU1202. Move to .NET 10 first.

  • new FileContentSource(stream, …) now rejects a stream that cannot seek, with an ArgumentException naming the parameter. The upload endpoint needs the size up front: without a Content-Length the 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 holding IManagementClient was offered a Dispose() that, on the container path, released nothing and must not be called: the container owns that lifetime. ManagementClientBuilder.Build() now returns the concrete ManagementClient, which is IDisposable and IAsyncDisposable, 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 = ManagementClientBuilderBuild();
    client.Dispose();
    
    // After - keep the concrete type, or just use var
    var client = ManagementClientBuilderBuild();
    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 configureRefit parameter is gone from all five AddManagementClient overloads, and ManagementClientBuilder.ConfigureRefit is 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 throw JsonException instead of being coerced. The Management API emits canonical tokens and ContentModelSnapshot.FromJson only ever consumes ToJson output, so this affects hand-written JSON. Writing is unchanged, and numeric tokens are still rejected in both directions.

Changed

  • AddManagementClient gained 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 the IServiceProvider. 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, the OperationCanceledException is rethrown,
    so Task.IsCanceled, Task.WhenAll and cancellation handlers behave as they do everywhere else in
    .NET. Previously all of these threw. An expired HttpClient.Timeout is not cancellation, even
    though .NET surfaces it as a TaskCanceledException. 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. IManagementResult now carries (HttpStatusCode)0 for that case rather than an invented code. Responses that did arrive are unaffected.

  • ManagementOptions.Timeout sets 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 as MyPackage;2.0.0-, which is not a valid SemVer version. An empty label now counts as no label, matching what passing null already did.
  • GetFullFolderPath no 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 \\Child rather than Child. 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: a 429 carrying Retry-After: 60 spent most of the budget before the next attempt began. The ceiling is now ManagementOptions.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 HttpClient from IHttpClientFactory once, so the handler chain it holds was never rotated — the factory only hands a fresh chain to a new CreateClient call. 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 via configureHttpClient still 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.1510.0.10.
  • Microsoft.Extensions.Http.Resilience 9.6.010.8.0.
  • Refit and Refit.HttpClientFactory 10.2.014.0.1.

Internal

No consumer-visible effect:

  • Enum wire tokens now travel on [JsonStringEnumMemberName] and serialize through the built-in System.Text.Json converter. 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 --prerelease

Full changelog: src/management/CHANGELOG.md

Kontent.Ai.Delivery 20.0.0-rc.1

Pre-release

Choose a tag to compare

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.0net10.0. There is no multi-targeting, so a project on .NET 8 cannot install this release at all — restore fails with NU1202: Package Kontent.Ai.Delivery is not compatible with net8.0. Move to .NET 10 first. Kontent.Ai.Delivery.SourceGeneration is the exception and stays netstandard2.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 holding IDeliveryClient was offered a Dispose() that, on the container path, released nothing and must not be called: the container owns that lifetime. DeliveryClientBuilder.Build() now returns the concrete DeliveryClient, which is IDisposable and IAsyncDisposable, 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 = DeliveryClientBuilderBuild();
    client.Dispose();
    
    // After - keep the concrete type, or just use var
    var client = DeliveryClientBuilderBuild();
    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 configureRefit parameter is gone from all six AddDeliveryClient overloads, and RefitSettingsProvider is 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 passed configureRefit, 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 IConfiguration can now customize the HTTP client and the resilience pipeline. The configuration-based AddDeliveryClient overloads took no configureHttpClient / configureResilience, so binding options from configuration and replacing the retry pipeline were mutually exclusive. The workaround — binding by hand inside an Action<DeliveryOptions> — compiles and looks equivalent, but registers no change token, so IOptionsMonitor silently stops reloading. Both hooks are now available on every configuration overload, alongside the ones that already had them.
  • AddDeliveryClient gained the overloads its sibling SDKs already had, so the three register a client the same way: named IConfiguration and IConfigurationSection registration. Nothing was removed, and existing calls are unaffected — this closes gaps rather than reshaping the surface.
  • CacheResult<T> carries FromFactory, 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 custom IDeliveryCacheManager that builds its own CacheResult<T> should set it — left at its false default, every result it returns is treated as a cache hit.
  • DeliveryOptions.DefaultConfigurationSectionName exposes the section name the configuration overloads bind by default, matching ManagementOptions. 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, a was sent as a, 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, the OperationCanceledException is rethrown,
    so Task.IsCanceled, Task.WhenAll and cancellation handlers behave as they do everywhere else in
    .NET. Previously all of these threw. An expired HttpClient.Timeout is not cancellation, even
    though .NET surfaces it as a TaskCanceledException: 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)0 for 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 — but null is also what an empty element gives, so the log is the only thing distinguishing the two. At Debug it 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 per IDeliveryCacheManager instance, 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 an IFusionCacheBackplane and 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 starting 01, as in n373888cc_34e2_01e1_1820_3cb52ab1b2a1. Authored codenames collide with that: Product SKU 0123 Blue becomes product_sku_0123_blue, whose third group is 0123. Such an item was silently given no item_ 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 workflow and workflow_step and 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 EagerRefreshThreshold set, 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 as ResponseSource.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 leave EagerRefreshThreshold at its default of 0 were never affected.

  • Rich-text parsing and resolution no longer risk deadlocking a caller that blocks on the task. Every await in 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...

Read more

Kontent.Ai.AspNetCore 1.0.0-rc.1

Pre-release

Choose a tag to compare

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 are records with init properties. WebhookNotification, WebhookModel, WebhookData, WebhookMessage and WebhookItem describe 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, so System.Text.Json binds 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.
  • Reference is removed. A public model with ById / ByCodename / ByExternalId factories that nothing in the package produced, consumed, or referenced — it was reachable only from its own unit test.
  • SignatureMiddleware.WebhookOptions and AssetTagHelper.ImageTransformationOptions are 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 an image-transformation-options attribute that was never meant to exist. Both are now constructor parameters held privately.
  • UseWebhookSignatureValidator no longer takes an optional WebhookOptions. Three overloads accepting reference types meant UseWebhookSignatureValidator(predicate, null) could not be resolved. "Use the options from the container" is now its own two-argument overload, and the WebhookOptions overload takes a required, non-null instance. Calls that passed options, an Action<WebhookOptions>, a configuration section, or nothing at all are unaffected.
  • net8.0net10.0. There is no multi-targeting, so a project on .NET 8 cannot install this release at all — restore fails with NU1202: 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.Secret unset, 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 throws InvalidOperationException naming the missing setting.

    If you relied on running without a secret, set WebhookOptions.Secret to 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> reads width and height invariantly, and no longer throws on values that are not numbers. The attributes were parsed with Convert.ToDouble in the server's culture while ImageUrlBuilder writes the value back invariantly, so the round trip disagreed wherever . groups digits — on a de-DE server width="1.5" produced ?w=15, a silent tenfold resize. The same call threw FormatException for any value HTML allows but the image API has no equivalent for (100%, auto, a CSS calc), 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.Abstractions and Kontent.Ai.Urls 19.4.020.0.0-rc.1. The 19.x line targets net8.0, so leaving the floor there would let a net10.0 package resolve a .NET 8 build of the SDK it is built on. Staying on Delivery 19.x means staying on Kontent.Ai.AspNetCore 0.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 --prerelease

Full changelog: src/aspnetcore/CHANGELOG.md