Extensibility Guide
April 16, 2026 · View on GitHub
This guide covers how to customize and extend the Kontent.ai Delivery SDK to fit your specific needs.
Table of Contents
Overview
The SDK provides several extension points for customizing behavior:
- Type Providers - Map content type codenames to CLR types
- Rich Text Resolvers - Control how rich text elements are rendered to HTML
Type Providers
ITypeProvider Interface
The ITypeProvider interface allows the SDK to resolve the correct CLR type for each content item based on its content type codename.
public interface ITypeProvider
{
/// <summary>
/// Gets the CLR type for a content type codename.
/// </summary>
/// <param name="contentType">The content type codename.</param>
/// <returns>The CLR type, or null if not found.</returns>
Type? GetType(string contentType);
/// <summary>
/// Gets the content type codename for a CLR type.
/// </summary>
/// <param name="contentType">The CLR type.</param>
/// <returns>The content type codename, or null if not found.</returns>
string? GetCodename(Type contentType);
}
Creating a Custom Type Provider
Create a custom type provider when you want to control which model types are used for different content types:
public class CustomTypeProvider : ITypeProvider
{
private readonly Dictionary<string, Type> _typeMap = new(StringComparer.OrdinalIgnoreCase)
{
["article"] = typeof(Article),
["product"] = typeof(Product),
["homepage"] = typeof(HomePage),
["author"] = typeof(Author),
["category"] = typeof(Category)
};
private readonly Dictionary<Type, string> _codenameMap;
public CustomTypeProvider()
{
// Build reverse lookup
_codenameMap = _typeMap.ToDictionary(kvp => kvp.Value, kvp => kvp.Key);
}
public Type? GetType(string contentType)
{
return _typeMap.TryGetValue(contentType, out var type) ? type : null;
}
public string? GetCodename(Type contentType)
{
return _codenameMap.TryGetValue(contentType, out var codename) ? codename : null;
}
}
Source-Generated Type Provider (Recommended)
The SDK provides a Roslyn source generator that creates an ITypeProvider implementation at compile time. This approach offers several advantages:
- Compile-time validation - Duplicate codenames and invalid configurations are caught during build
- Auto-discovery - The SDK automatically finds the generated provider at runtime
- Automatic type filtering - Generic queries like
GetItems<Article>()automatically addsystem.type=articlefilter - No runtime reflection - Type mappings are generated as static dictionaries
Setup
Add the source generation package:
<PackageReference Include="Kontent.Ai.Delivery.SourceGeneration" Version="19.0.0" />
This package includes both the ContentTypeCodenameAttribute and the Roslyn source generator.
For predictable auto-discovery, keep attributed models in a single models project that references the source-generation package.
If models are intentionally split across multiple projects/compilations, use explicit ITypeProvider registration.
Usage
The Kontent.ai Model Generator automatically includes the [ContentTypeCodename] attribute on generated model classes:
// Generated by Kontent.ai Model Generator
using Kontent.Ai.Delivery.Attributes;
[ContentTypeCodename("article")]
public record Article
{
public string Title { get; init; }
public string Summary { get; init; }
public RichTextContent BodyCopy { get; init; }
}
[ContentTypeCodename("product")]
public record Product
{
public string Name { get; init; }
public decimal Price { get; init; }
}
[ContentTypeCodename("author")]
public record Author
{
public string Name { get; init; }
public string Bio { get; init; }
}
The source generator produces a GeneratedTypeProvider class at compile time:
// Auto-generated in: Kontent.Ai.Delivery.Generated namespace
public sealed class GeneratedTypeProvider : ITypeProvider
{
private static readonly Dictionary<string, Type> _codenameToType =
new(StringComparer.OrdinalIgnoreCase)
{
{ "article", typeof(Article) },
{ "author", typeof(Author) },
{ "product", typeof(Product) },
};
private static readonly Dictionary<Type, string> _typeToCodename = new()
{
{ typeof(Article), "article" },
{ typeof(Author), "author" },
{ typeof(Product), "product" },
};
public Type? GetType(string contentType)
=> _codenameToType.TryGetValue(contentType, out var type) ? type : null;
public string? GetCodename(Type contentType)
=> _typeToCodename.TryGetValue(contentType, out var codename) ? codename : null;
}
Compile-Time Diagnostics
The source generator reports errors during compilation:
| ID | Severity | Description |
|---|---|---|
KDSG001 | Error | Duplicate codename - two or more types have the same codename |
KDSG002 | Error | Invalid codename - null, empty, or whitespace |
KDSG003 | Error | Unsupported target - interfaces and abstract classes cannot be content types |
Example error:
error KDSG001: Duplicate codename 'article' used by: MyApp.Models.Article, MyApp.Models.BlogPost
Generating Models
Use the Kontent.ai Model Generator to generate model classes with the [ContentTypeCodename] attribute:
dotnet tool install -g Kontent.Ai.ModelGenerator
KontentModelGenerator --environmentid <your-environment-id> --outputdir Models
The generated models automatically participate in compile-time type provider generation when the source generation package is referenced.
Registering Type Providers
Auto-Discovery (Recommended)
When using source generation with [ContentTypeCodename] attributes, the SDK automatically discovers the GeneratedTypeProvider at runtime. No manual registration is needed:
// Just register the delivery client - type provider is auto-discovered
services.AddDeliveryClient(options =>
{
options.EnvironmentId = "your-environment-id";
});
Explicit Registration with Dependency Injection
If you need to override auto-discovery or use a custom implementation, register your type provider before AddDeliveryClient():
// Register your custom type provider (takes precedence over auto-discovery)
services.AddSingleton<ITypeProvider, CustomTypeProvider>();
// Or explicitly register the generated type provider
services.AddSingleton<ITypeProvider, GeneratedTypeProvider>();
// Then register the delivery client
services.AddDeliveryClient(options =>
{
options.EnvironmentId = "your-environment-id";
});
The SDK uses TryAddSingleton internally, so your registration takes precedence.
Without Dependency Injection
// Type provider is auto-discovered from source generation
await using var client = DeliveryClientBuilder
.WithOptions(builder => builder
.WithEnvironmentId("your-environment-id")
.Build())
.Build();
// Or explicitly provide a type provider
await using var client = DeliveryClientBuilder
.WithOptions(builder => builder
.WithEnvironmentId("your-environment-id")
.Build())
.WithTypeProvider(new CustomTypeProvider())
.Build();
Auto-Discovery
The SDK's default TypeProvider automatically discovers source-generated providers at runtime using this strategy:
- Entry assembly first - Checks the application's entry assembly for
Kontent.Ai.Delivery.Generated.GeneratedTypeProvider - Referenced assemblies - Checks assemblies referenced by the entry assembly
- Calling assembly fallback - For test scenarios, checks the calling assembly
This bounded search is deterministic and avoids scanning the entire AppDomain. If multiple providers are found, the entry assembly's provider takes precedence.
In practice, the recommended setup is one models project producing one generated provider.
If your solution produces multiple generated providers, use explicit ITypeProvider registration for deterministic behavior.
// The auto-discovery happens transparently when you use the SDK
var result = await client.GetItems<Article>().ExecuteAsync();
// ↑ SDK looks up "article" codename via auto-discovered GeneratedTypeProvider
// and automatically adds system.type=article filter
How Type Resolution Works
When the SDK deserializes content items:
- It reads the
system.typeproperty from the JSON - Calls
ITypeProvider.GetType(contentType)to get the CLR type - If a type is found, deserializes to that type
- If no type is found, falls back to dynamic elements (
IDynamicElements)
This enables:
- Linked items to be deserialized to their correct types
- Embedded content in rich text to use strongly-typed models
- Pattern matching on
IEmbeddedContent<T>to work correctly
Property Mapping Conventions
Property mapping customization via IPropertyMapper was removed during the API simplification.
Use model metadata to control mapping:
- Use
[JsonPropertyName("element_codename")]for element-to-property mapping. - Keep model properties writable for hydration.
- Use supported SDK element shapes (
IRichTextContent,IAsset,ITaxonomyTerm,IEmbeddedContent,IDateTimeContent) for complex element hydration.
Example:
public record Article
{
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("body_copy")]
public IRichTextContent? BodyCopy { get; set; }
[JsonPropertyName("related_articles")]
public IEnumerable<IEmbeddedContent>? RelatedArticles { get; set; }
}
Rich Text Resolvers
For customizing how rich text content is rendered to HTML, see the Rich Text Customization Guide.
Key extension points include:
- Content resolvers - Render embedded content items
- Link resolvers - Generate URLs for content item links
- HTML node resolvers - Transform specific HTML elements
Best Practices
1. Use Source-Generated Type Providers
For most projects, use the Kontent.ai Model Generator with source generation rather than creating type providers manually. The model generator automatically includes the [ContentTypeCodename] attribute on generated classes:
// Generated by Kontent.ai Model Generator
using Kontent.Ai.Delivery.Attributes;
[ContentTypeCodename("article")]
public record Article { /* ... */ }
This approach provides:
- Compile-time validation - Errors are caught during build, not at runtime
- Auto-discovery - No manual DI registration needed
- Automatic type filtering - Generic queries automatically filter by content type
- Synchronization - Type mappings are always in sync with your model definitions
2. Use Explicit JsonPropertyName Mapping
Prefer using [JsonPropertyName] attributes for explicit, stable element-to-property mapping:
public record Article
{
[JsonPropertyName("body_copy")]
public RichTextContent BodyCopy { get; init; }
}
3. Registration Order (When Not Using Auto-Discovery)
When using source generation, type provider registration is automatic. However, if you're registering custom implementations, register them before calling AddDeliveryClient:
// ✅ Correct order (when overriding auto-discovery)
services.AddSingleton<ITypeProvider, CustomTypeProvider>();
services.AddDeliveryClient(options => { ... });
// ❌ Wrong order - custom provider may not be used
services.AddDeliveryClient(options => { ... });
services.AddSingleton<ITypeProvider, CustomTypeProvider>();
Note
With source generation, you typically don't need to register the type provider at all - it's auto-discovered.
4. Test Custom Extensions
Write unit tests for custom type providers and property mappers:
[Fact]
public void GetType_ReturnsCorrectType_ForArticle()
{
var provider = new CustomTypeProvider();
var result = provider.GetType("article");
Assert.Equal(typeof(Article), result);
}
[Fact]
public void GetType_ReturnsNull_ForUnknownType()
{
var provider = new CustomTypeProvider();
var result = provider.GetType("unknown_type");
Assert.Null(result);
}
Related Documentation: