Skip to content

Kontent.Ai.Delivery 20.0.0-rc.2

Pre-release
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 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.Delivery --prerelease

Full changelog: src/delivery/CHANGELOG.md