Module

Runtime-extensible custom fields.

Core.Properties enables any aggregate to support user-defined custom fields at runtime without schema changes. Properties are stored as JSONB, support 14 field types, and come with admin API, UI components, and automatic type discovery.

Overview

Dynamic Properties allow end users to add custom fields to any entity at runtime. A Contact can have a 'phone_number' today and a 'loyalty_tier' tomorrow — no database migration needed.

The system revolves around three concepts: PropertyDefinition (what the field looks like), PropertyGroup (how fields are organized), and IHasDynamicProperties (which entities support custom fields). Values are stored as strings in a JSONB column.

Package Purpose Key Contents
Sillium.Core.Properties Domain, application, infrastructure PropertyDefinition, PropertyGroup, CQRS commands/queries, EF configurations, type discovery
Sillium.Core.Properties.Api REST API endpoints Definition and group CRUD endpoints with authorization policies
Sillium.Core.Properties.Components Blazor UI components DynamicPropertyField, DynamicPropertyPanel, PropertyEditor

Property Types

The PropertyType enum defines 14 field types. Each type maps to a specific input component in the UI and determines validation behavior.

Type Value Rendered As Description
SingleLineText 1 TextInput Short text input
MultiLineText 2 TextArea Multi-line text area
Number 3 NumberInput Numeric input (supports PositiveNumbersOnly)
SingleCheckbox 4 Checkbox Boolean toggle
MultipleCheckboxes 5 Checkbox[] Multiple selection (stored as JSON array)
RadioSelect 6 RadioGroup Single selection from options
DropdownSelect 7 SelectInput Single selection via dropdown
RichText 8 TextArea Rich text area (6 rows)
PhoneNumber 9 TextInput (tel) Phone number input (tel type)
EmailAddress 10 TextInput (email) Email address input (email type)
DatePicker 11 DateInput Date selection (stored as yyyy-MM-dd)
DateTimePicker 12 DateTimeInput Date and time (stored as ISO 8601)
File 13 FileUpload File upload
Url 15 TextInput (url) URL input (url type)

Entity Integration

To make an aggregate support dynamic properties, implement the IHasDynamicProperties interface and configure the EF Core mapping.

IHasDynamicProperties Interface

The interface defines the contract for reading and writing dynamic property values.

Show code
public interface IHasDynamicProperties
{
    IReadOnlyDictionary<string, string?> DynamicProperties { get; }

    void SetProperty(string jsonName, string? value);
    string? GetProperty(string jsonName);
    T? GetProperty<T>(string jsonName);
    bool HasProperty(string jsonName);
    void RemoveProperty(string jsonName);
}

Implementation Example

Add a private Dictionary backing field and implement the interface methods.

Show code
public class Contact : AggregateRoot<ContactId>, IHasDynamicProperties
{
    private readonly Dictionary<string, string?> _dynamicProperties = new();

    public string Name { get; private set; }
    public string Email { get; private set; }

    // IHasDynamicProperties implementation
    public IReadOnlyDictionary<string, string?> DynamicProperties
        => _dynamicProperties;

    public void SetProperty(string jsonName, string? value)
        => _dynamicProperties[jsonName] = value;

    public string? GetProperty(string jsonName)
        => _dynamicProperties.GetValueOrDefault(jsonName);

    public T? GetProperty<T>(string jsonName) { /* type conversion */ }
    public bool HasProperty(string jsonName)
        => _dynamicProperties.ContainsKey(jsonName);
    public void RemoveProperty(string jsonName)
        => _dynamicProperties.Remove(jsonName);
}

EF Core Configuration

Use the extension method to configure the JSONB column mapping.

Show code
// In your EF Core entity configuration
public class ContactConfiguration : IEntityTypeConfiguration<Contact>
{
    public void Configure(EntityTypeBuilder<Contact> builder)
    {
        builder.ToTable("Contacts");
        builder.HasKey(x => x.Id);

        // Maps _dynamicProperties to JSONB column "PropertiesData"
        builder.ConfigureDynamicProperties<Contact>();
    }
}

Setup

Register Dynamic Properties services and map the admin API endpoints.

Service Registration

AddDynamicProperties registers type discovery, CQRS handlers, and authorization policies.

Show code
// Program.cs — Register Dynamic Properties
services.AddDynamicProperties(options =>
{
    // Assembly containing your domain aggregates
    options.DomainAssembly = typeof(Contact).Assembly;
});

Endpoint Mapping

Map admin endpoints for property definition and group management.

Show code
// Program.cs — Map admin endpoints
app.MapGroup("/api/admin")
   .RequireAuthorization()
   .MapDynamicPropertyEndpoints();
Auto-Discovery

When you provide DomainAssembly, the system automatically scans for classes implementing IHasDynamicProperties and converts their PascalCase names to snake_case aggregate type identifiers.

Class Name Aggregate Type
Contact contact
Company company
CompanyAccount company_account

Domain Model

The system uses two aggregate roots: PropertyDefinition describes a single custom field, and PropertyGroup organizes definitions into logical sections for UI display.

PropertyDefinition
Field Type Description
Id PropertyId Unique identifier
AggregateType PropertyAggregateType Target aggregate (e.g., 'contact', 'company')
GroupId PropertyGroupId Assigned PropertyGroup for UI grouping
DisplayName string User-facing field label (max 200 chars)
JsonName PropertyJsonName Immutable snake_case storage key (unique per aggregate type)
FieldType PropertyType One of 14 PropertyType values
FieldConfiguration PropertyFieldConfiguration Type-specific options and input rules (JSONB)
IsUnique bool Enforces value uniqueness across instances
IsDefault bool Built-in property — cannot be deleted
IsDeleted bool Soft-delete flag via ISoftDeletable
PropertyGroup

PropertyGroup has Id, AggregateType, Name, Description, and IsDeleted. Groups cannot be deleted if they still have active property definitions assigned.

API Reference

All endpoints are mapped under the configured admin route group and require authorization.

Property Definitions
Method Path Description
GET /property-definitions List definitions (filters: aggregateType, groupId, includeDeleted)
GET /property-definitions/{id} Get a single definition by ID
POST /property-definitions Create a new property definition
PUT /property-definitions/{id} Update display name, description, group, field type, uniqueness
DELETE /property-definitions/{id} Soft-delete a definition (fails for IsDefault)
POST /property-definitions/{id}/restore Restore a soft-deleted definition
Property Groups
Method Path Description
GET /property-groups List groups (filters: aggregateType, includeDeleted)
GET /property-groups/{id} Get a single group by ID
POST /property-groups Create a new property group
PUT /property-groups/{id} Update name and description
DELETE /property-groups/{id} Soft-delete a group (fails if definitions assigned)
POST /property-groups/{id}/restore Restore a soft-deleted group
Authorization Policies
Policy Protects
DefinitionsRead GET /property-definitions
DefinitionsCreate POST /property-definitions
DefinitionsUpdate PUT /property-definitions/{id}
DefinitionsDelete DELETE /property-definitions/{id}
GroupsRead GET /property-groups
GroupsCreate POST /property-groups
GroupsUpdate PUT /property-groups/{id}
GroupsDelete DELETE /property-groups/{id}

UI Components

Three Blazor components cover the full property lifecycle: displaying fields, showing grouped panels, and administering definitions.

Component Purpose Key Parameters
DynamicPropertyField Renders a single property input based on PropertyType PropertyType, DisplayName, Value, ValueChanged, Options, PositiveNumbersOnly
DynamicPropertyPanel Renders all properties grouped by PropertyGroup Definitions, Groups, Values, OnValueChanged, TwoColumns
PropertyEditor Admin UI for managing definitions and groups AggregateTypes (from IDynamicPropertyAggregateTypeProvider)

Panel Usage

Embed DynamicPropertyPanel in your entity edit page to show all custom fields.

Show code
<DynamicPropertyPanel
    Definitions="@_definitions"
    Groups="@_groups"
    Values="@_entity.DynamicProperties"
    OnValueChanged="HandlePropertyChanged"
    TwoColumns="true"
    Variant="Variant.Outlined"
    Color="Color.Light" />

@code {
    private void HandlePropertyChanged(PropertyValueChange change)
    {
        _entity.SetProperty(change.JsonName, change.Value);
    }
}

Query Filtering

When querying entities with dynamic properties via API, the system distinguishes property filters from built-in parameters by naming convention.

Convention Example Meaning
snake_case ?phone_number=+49... Filters by dynamic property value
camelCase ?page=1&pageSize=25 Built-in pagination/sorting parameter

Mixed Filtering

Combine property filters with built-in parameters in a single request.

Show code
// Filter contacts by dynamic property "phone_number"
// and built-in pagination parameters
GET /api/contacts?phone_number=+49123&city=berlin&page=1&pageSize=25

// snake_case params → property filters (phone_number, city)
// camelCase params → built-in (page, pageSize)
An unhandled error has occurred. Reload Dismiss