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:
- A physical column for the discriminator (e.g.,
settings_type) - A B-tree index on the discriminator column
- 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
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¶
- Name discriminators clearly: Use
{PropertyName}TypeNameor{PropertyName}Discriminator - Use consistent values: Either simple type names or fully qualified names, not both
- Index all discriminators: The attribute automatically creates an index
- 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¶
- Polymorphic Types - Analyzer for polymorphic detection
- Physical Fields - Physical column storage
- EF Core JSON Configuration - JSON polymorphic serialization