Skip to content

Polymorphic Discriminator

Verified by tests

PolymorphicDiscriminatorAttributeTests — library CI run #31657041675 (2026-08-13)

The [PolymorphicDiscriminator] attribute marks a property as a type discriminator for polymorphic JSON data. The source generator creates an indexed physical column, enabling efficient SQL queries without parsing JSON at query time.

Overview

When your perspective models contain polymorphic types (abstract classes or [JsonPolymorphic] types), querying by derived type requires parsing JSON for every row. Discriminator columns solve this by storing the type information in an indexed column.

Query Type Without Discriminator With Discriminator
By derived type JSONB path query (slow) Indexed column (fast)
Performance O(n) JSON parsing O(log n) index lookup

Field Discriminator

Use [PolymorphicDiscriminator] on a string property that stores the type discriminator value:

Field Discriminator

public record FormFieldModel {
    [StreamId]
    public Guid FieldId { get; init; }

    // Discriminator column for the polymorphic Settings property
    [PolymorphicDiscriminator(ColumnName = "settings_type")]
    public string SettingsTypeName { get; init; }

    // Polymorphic property (abstract or [JsonPolymorphic])
    public AbstractFieldSettings Settings { get; init; }
}

Attribute Properties

Property Type Default Description
ColumnName string? null Custom column name (defaults to snake_case of property)

Generated Schema

The source generator creates:

  1. A physical column for the discriminator (e.g., settings_type)
  2. A B-tree index on the discriminator column
  3. Registration in the physical field registry

Generated Schema

CREATE TABLE wh_per_form_field (
    id UUID PRIMARY KEY,
    stream_id UUID NOT NULL,
    data JSONB NOT NULL,
    settings_type TEXT,  -- Discriminator column (TEXT: no max length, fits full type names)
    -- ... other columns
);

CREATE INDEX idx_form_field_settings_type ON wh_per_form_field(settings_type);

Querying with Discriminators

Direct Column Query

Query the discriminator column directly:

Direct Column Query

var textFields = await lens.Query
    .Where(r => r.Data.SettingsTypeName == "TextFieldSettings")
    .ToListAsync();

The discriminator property is registered as a physical field, so this query is translated to an indexed column lookup (WHERE settings_type = 'TextFieldSettings').

Type-Safe Polymorphic API

Planned

A WherePolymorphic(...).As<TDerived>(...) extension for type-safe polymorphic queries is planned but not shipped at this commit. Use the direct discriminator-column query above.

Setting the Discriminator Value

Set the discriminator value when applying events to your perspective:

Setting the Discriminator Value

public class FormFieldPerspective : IPerspectiveFor<FormFieldModel, FieldCreatedEvent> {
    public FormFieldModel Apply(FormFieldModel current, FieldCreatedEvent @event) {
        return current with {
            FieldId = @event.FieldId,
            Settings = @event.Settings,
            // Set discriminator to match the actual type
            SettingsTypeName = @event.Settings.GetType().Name
        };
    }
}

Using Full Type Names

For disambiguation, use fully qualified type names:

Using Full Type Names

SettingsTypeName = @event.Settings.GetType().FullName
// e.g., "MyApp.Forms.TextFieldSettings"

Collection Discriminators

For collections of polymorphic types, consider a separate perspective table:

Collection Discriminators

// Main form perspective
public record FormModel {
    [StreamId]
    public Guid FormId { get; init; }
    public string Title { get; init; }
}

// Separate perspective for fields (one row per field)
public record FormFieldModel {
    [StreamId]
    public Guid FieldId { get; init; }

    public Guid FormId { get; init; }

    [PolymorphicDiscriminator]
    public string FieldTypeName { get; init; }

    public AbstractFieldSettings Settings { get; init; }
}

This enables efficient queries like "find all text fields across all forms."

Best Practices

  1. Name discriminators clearly: Use {PropertyName}TypeName or {PropertyName}Discriminator
  2. Use consistent values: Either simple type names or fully qualified names, not both
  3. Index all discriminators: The attribute automatically creates an index
  4. Consider collection patterns: Use separate perspectives for collections of polymorphic types

Common Patterns

Multiple Polymorphic Properties

Multiple Polymorphic Properties

public record ConfigModel {
    [PolymorphicDiscriminator(ColumnName = "input_type")]
    public string InputSettingsType { get; init; }
    public AbstractInputSettings InputSettings { get; init; }

    [PolymorphicDiscriminator(ColumnName = "output_type")]
    public string OutputSettingsType { get; init; }
    public AbstractOutputSettings OutputSettings { get; init; }
}

Enum-Based Discriminators

While string discriminators are most flexible, you can use enums:

Enum-Based Discriminators

public record FormFieldModel {
    [PhysicalField(Indexed = true)]
    public FieldType FieldType { get; init; }

    public AbstractFieldSettings Settings { get; init; }
}

public enum FieldType {
    Text,
    Number,
    Date,
    Dropdown
}

See Also