Skip to main content

ExtensibleEntity & JSON Configuration

Base class for entities that support dynamic, schema-free properties alongside their typed fields.

Overview

ExtensibleEntity provides a Properties dictionary that allows you to attach arbitrary strongly-typed data to any entity without changing its schema. This powers features like AI profile metadata, chat session annotations, and custom application data.

Serialized entities keep extensible values inside the nested Properties object. Root-level flattened extension data is not treated as valid extensible metadata.

Quick Reference

MethodWhen to Use
TryGet<T>(out T result)Read-only access — returns false when the key is missing (zero allocations)
GetOrCreate<T>()Read-write access — creates a new T when the key is missing
Alter<T>(Action<T>)Modify a stored object in-place (creates if missing)
Put<T>(T value)Store a strongly-typed object (key = type name)
Put(string name, object value)Store a value under a custom key
Has<T>()Check whether a key exists
Remove<T>()Remove a stored object

Prefer TryGet for Read-Only Access

When you only need to read a property and do not need a default instance, use TryGet<T>:

if (profile.TryGet<MyMetadata>(out var metadata))
{
// Use metadata — only entered when data is present.
Console.WriteLine(metadata.Label);
}

GetOrCreate<T>() allocates a new T every time the key is absent. Reserve it for cases where you intend to write back:

var metadata = profile.GetOrCreate<MyMetadata>();
metadata.Label = "Updated";
profile.Put(metadata);

Configuring JSON Serialization

All ExtensibleEntity property reads and writes go through a shared JsonSerializerOptions instance. The defaults work for most scenarios, but you can customize them.

Default Settings

SettingDefault Value
DefaultIgnoreConditionWhenWritingNull
PreferredObjectCreationHandlingPopulate
PropertyNameCaseInsensitivetrue
ReadCommentHandlingSkip
AllowTrailingCommastrue
WriteIndentedfalse
NumberHandlingAllowReadingFromString
Built-in convertersJsonStringEnumConverter

When you use the CrestApps framework with AddCrestAppsCore(), register your customizations through the standard options pattern:

builder.Services.AddCrestAppsCore(crestApps => crestApps
.AddAISuite(ai => ai
.AddOpenAI()
)
);

// Add a custom JSON converter for ExtensibleEntity properties.
builder.Services.Configure<ExtensibleEntityJsonOptions>(options =>
{
options.SerializerOptions.Converters.Add(new MyCustomJsonConverter());
});

The framework registers an IHostedService that reads IOptions<ExtensibleEntityJsonOptions> at startup and pushes the configured JsonSerializerOptions to ExtensibleEntityExtensions.JsonSerializerOptions. Your customizations are applied before any request processing begins.

Direct Static Property (Advanced)

If you are not using the CrestApps DI extensions (e.g., in a console app or test harness), set the static property directly before any serialization occurs:

var options = ExtensibleEntityJsonOptions.CreateDefaultSerializerOptions();
options.Converters.Add(new MyCustomJsonConverter());

ExtensibleEntityExtensions.JsonSerializerOptions = options;
warning

JsonSerializerOptions becomes immutable after the first serialization call (System.Text.Json caches internal metadata). Configure it during application startup, before any ExtensibleEntity methods are invoked.

Full Example

using CrestApps.Core;

// Define a custom metadata type.
public sealed class InvoiceMetadata
{
public string InvoiceNumber { get; set; }
public decimal Amount { get; set; }
public bool IsPaid { get; set; }
}

// Store metadata on an entity.
entity.Put(new InvoiceMetadata
{
InvoiceNumber = "INV-2025-001",
Amount = 149.99m,
IsPaid = false,
});

// Read-only check — no allocation if missing.
if (entity.TryGet<InvoiceMetadata>(out var invoice))
{
Console.WriteLine($"Invoice {invoice.InvoiceNumber}: ${invoice.Amount}");
}

// Modify in-place.
entity.Alter<InvoiceMetadata>(inv =>
{
inv.IsPaid = true;
});