Discriminators
Discriminators allow the source generator to distinguish between multiple entity types stored in the same DynamoDB table. When you use single-table design, a single partition key value can map to items of different types. Discriminators define how MatchesEntity determines which entity class should handle a given item.
Why Discriminators Exist
In single-table design, multiple entity types share one DynamoDB table. When you query a GSI or scan the table, the result set can contain items from different entity types. The generated MatchesEntity method on each entity uses discriminator patterns to claim items that belong to it and reject items that belong to other entities.
Without discriminators, every entity in a shared table would attempt to deserialize every item — leading to incorrect results or runtime errors.
Auto-Derived Patterns from Key Prefixes
The source generator inspects [PartitionKey(Prefix = "...")] and [SortKey(Prefix = "...")] attributes to automatically derive discriminator patterns. You don't need to specify patterns manually in most cases.
How It Works
When an entity declares a key prefix, the generator creates a pattern of the form PREFIX#* (using the configured separator). This pattern is used in the generated MatchesEntity method to check whether an item's key value starts with the expected prefix.
[DynamoDbTable("shared-table", IsDefault = true)]
public partial class Order
{
[PartitionKey(Prefix = "ORDER")]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[SortKey(Prefix = "META")]
[DynamoDbAttribute("sk")]
public string Sk { get; set; } = string.Empty;
[DynamoDbAttribute("total")]
public decimal Total { get; set; }
}
[DynamoDbTable("shared-table")]
public partial class Customer
{
[PartitionKey(Prefix = "CUSTOMER")]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[SortKey(Prefix = "PROFILE")]
[DynamoDbAttribute("sk")]
public string Sk { get; set; } = string.Empty;
[DynamoDbAttribute("name")]
public string Name { get; set; } = string.Empty;
}
The generator derives these discriminator patterns automatically:
| Entity | PK Pattern | SK Pattern |
|---|---|---|
Order | ORDER#* | META#* |
Customer | CUSTOMER#* | PROFILE#* |
The generated MatchesEntity for Order checks that the pk attribute starts with "ORDER#" and sk starts with "META#". No manual configuration is needed.
Auto-Derived Patterns from Computed Key Formats
When a key uses a [Computed] attribute with a Format string, the generator derives a discriminator pattern from the format's literal structure:
[DynamoDbTable("events")]
public partial class DailyEvent
{
[PartitionKey]
[DynamoDbAttribute("pk")]
[Computed("Region", "Year", "Month", Format = "EVT#{0}#{1}-{2}")]
public string Pk { get; set; } = string.Empty;
[Extracted("Pk", 0)]
public string Region { get; set; } = string.Empty;
[Extracted("Pk", 1)]
public int Year { get; set; }
[Extracted("Pk", 2)]
public int Month { get; set; }
}
The generator derives the pattern EVT#*#*-* from the format string "EVT#{0}#{1}-{2}", replacing each {N} placeholder with a wildcard *.
Overlapping Pattern Resolution
When two entities in the same table have patterns that could match the same key value, the source generator uses specificity scoring to determine precedence.
Specificity Scoring
Specificity is determined by the count and position of literal (non-wildcard) characters in a pattern. More literal characters mean a more specific pattern:
| Pattern | Literal Characters | Specificity |
|---|---|---|
ORDER#* | 6 (ORDER#) | Lower |
ORDER#PRIORITY#* | 15 (ORDER#PRIORITY#) | Higher |
Automatic Resolution
When patterns overlap but have different specificity scores, the generator resolves them automatically. The less-specific entity's MatchesEntity includes an exclusion guard that rejects items matching the more-specific pattern:
[DynamoDbTable("shared-table", IsDefault = true)]
public partial class AnyOrder
{
[PartitionKey(Prefix = "ORDER")]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[SortKey]
[DynamoDbAttribute("sk")]
public string Sk { get; set; } = string.Empty;
}
[DynamoDbTable("shared-table")]
public partial class PriorityOrder
{
[PartitionKey(Prefix = "ORDER")]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[SortKey(Prefix = "PRIORITY")]
[DynamoDbAttribute("sk")]
public string Sk { get; set; } = string.Empty;
}
Here both entities match pk values starting with "ORDER#". The generator resolves this by specificity:
PriorityOrderis more specific (matchespk = ORDER#*ANDsk = PRIORITY#*)AnyOrderis less specific (matchespk = ORDER#*with anysk)
The generated AnyOrder.MatchesEntity will include an exclusion guard: if sk starts with "PRIORITY#", reject the item (it belongs to PriorityOrder). This emits the informational DISC005 diagnostic confirming the resolution.
Exclusion Guards
Exclusion guards are MatchesEntity checks that reject items matching a more-specific sibling entity's pattern. They ensure each DynamoDB item is claimed by exactly one entity type.
Three-Tier MatchesEntity Logic
The generated MatchesEntity method follows this logic:
- Positive match — Does the item match this entity's pattern? If not, return
false. - Exclusion guards — Does the item match any more-specific sibling pattern? If yes, return
false. - Accept — Return
true.
// Conceptual generated code for AnyOrder.MatchesEntity:
public static bool MatchesEntity(Dictionary<string, AttributeValue> item)
{
// Step 1: Positive match
if (!item["pk"].S.StartsWith("ORDER#"))
return false;
// Step 2: Exclusion guard (PriorityOrder is more specific)
if (item["sk"].S.StartsWith("PRIORITY#"))
return false;
// Step 3: Accept
return true;
}
Explicit Discriminator Patterns
In most cases, auto-derivation handles patterns correctly. However, you can specify explicit patterns on GSI attributes using DiscriminatorPattern:
[DynamoDbTable("shared-table")]
public partial class SpecialOrder
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[GsiPartitionKey("gsi1",
DiscriminatorProperty = "entityType",
DiscriminatorPattern = "ORDER#SPECIAL*")]
[DynamoDbAttribute("gsi1pk")]
public string Gsi1Pk { get; set; } = string.Empty;
}
Prefer auto-derivation over explicit patterns. Explicit patterns are only needed when the key structure alone doesn't provide enough information to distinguish entities (for example, when discriminating by a non-key attribute on a GSI).
Diagnostics: FDDB100–FDDB104
These diagnostics relate to conflicts between key prefixes, computed formats, and discriminator patterns:
| Code | Severity | Description |
|---|---|---|
| FDDB100 | Error | Key prefix conflicts with explicit computed format — the [Computed] format string doesn't start with the expected prefix |
| FDDB101 | Error | Explicit discriminator pattern conflicts with key format — the manually specified DiscriminatorPattern contradicts the auto-derived pattern |
| FDDB102 | Warning | Overlapping auto-derived discriminator patterns — two entities share similar prefixes causing pattern overlap (still resolves correctly via exclusion guards) |
| FDDB103 | Info | Redundant explicit discriminator pattern — the explicit DiscriminatorPattern matches what auto-derivation would produce and can be removed |
| FDDB104 | Info | Compound discrimination resolved overlap — same-score overlap resolved by inspecting cross-key patterns |
FDDB100: Prefix/Format Conflict
Triggered when a key has both Prefix and [Computed(..., Format = "...")] and the format doesn't start with the prefix:
// ❌ Triggers FDDB100 — format doesn't start with "ORDER#"
[PartitionKey(Prefix = "ORDER")]
[DynamoDbAttribute("pk")]
[Computed("CustomerId", "OrderId", Format = "CUST#{0}#{1}")]
public string Pk { get; set; } = string.Empty;
// ✅ Fix — format starts with "ORDER#"
[PartitionKey(Prefix = "ORDER")]
[DynamoDbAttribute("pk")]
[Computed("CustomerId", "OrderId", Format = "ORDER#{0}#{1}")]
public string Pk { get; set; } = string.Empty;
FDDB101: Explicit Pattern vs Key Format
Triggered when DiscriminatorPattern contradicts what the key structure would derive:
// ❌ Triggers FDDB101 — pattern says "CUSTOMER#*" but prefix is "ORDER"
[DynamoDbTable("shared-table", DiscriminatorPattern = "CUSTOMER#*")]
public partial class Order
{
[PartitionKey(Prefix = "ORDER")]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
}
// ✅ Fix — remove explicit pattern, let auto-derivation handle it
[DynamoDbTable("shared-table")]
public partial class Order
{
[PartitionKey(Prefix = "ORDER")]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
}
FDDB103: Redundant Explicit Pattern
Triggered when the explicit pattern is identical to the auto-derived one:
// ⚠️ Triggers FDDB103 — "ORDER#*" would be auto-derived anyway
[DynamoDbTable("shared-table", DiscriminatorPattern = "ORDER#*")]
public partial class Order
{
[PartitionKey(Prefix = "ORDER")]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
}
// ✅ Fix — just remove the explicit pattern
[DynamoDbTable("shared-table")]
public partial class Order
{
[PartitionKey(Prefix = "ORDER")]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
}
Diagnostics: DISC004–DISC006
These diagnostics relate to discriminator pattern resolution on GSI attributes:
| Code | Severity | Description |
|---|---|---|
| DISC004 | Error | Ambiguous overlapping discriminator patterns — two patterns have the same specificity score and cannot be automatically resolved |
| DISC005 | Info | Overlapping discriminator pattern resolved — confirms that specificity-based resolution succeeded and an exclusion guard was generated |
| DISC006 | Error | Tautological exclusion guard detected — the exclusion guard would reject every item the entity's own pattern accepts, making MatchesEntity always return false |
DISC004: Ambiguous Overlap (Same Specificity)
When two patterns overlap and have the same specificity score, the generator cannot determine precedence:
// ❌ Triggers DISC004 — "ORD*A" and "ORD*B" have the same specificity
[DynamoDbTable("shared-table", IsDefault = true)]
public partial class OrderA
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[GsiPartitionKey("gsi1",
DiscriminatorProperty = "entityType",
DiscriminatorPattern = "ORD*A")]
[DynamoDbAttribute("gsi1pk")]
public string Gsi1Pk { get; set; } = string.Empty;
}
[DynamoDbTable("shared-table")]
public partial class OrderB
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[GsiPartitionKey("gsi1",
DiscriminatorProperty = "entityType",
DiscriminatorPattern = "ORD*B")]
[DynamoDbAttribute("gsi1pk")]
public string Gsi1Pk { get; set; } = string.Empty;
}
Fix: Use non-overlapping exact values instead of patterns:
// ✅ Fix — use DiscriminatorValue for exact matching
[GsiPartitionKey("gsi1",
DiscriminatorProperty = "entityType",
DiscriminatorValue = "ORDER_A")]
[DynamoDbAttribute("gsi1pk")]
public string Gsi1Pk { get; set; } = string.Empty;
DISC005: Successful Resolution (Informational)
This diagnostic confirms that overlapping patterns were resolved via specificity. No action is needed:
// Emits DISC005 (info) — "ORDER*" is less specific than "ORDER#PRIORITY*"
[DynamoDbTable("shared-table", IsDefault = true)]
public partial class AnyOrder
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[GsiPartitionKey("gsi1",
DiscriminatorProperty = "entityType",
DiscriminatorPattern = "ORDER*")]
[DynamoDbAttribute("gsi1pk")]
public string Gsi1Pk { get; set; } = string.Empty;
}
[DynamoDbTable("shared-table")]
public partial class PriorityOrder
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[GsiPartitionKey("gsi1",
DiscriminatorProperty = "entityType",
DiscriminatorPattern = "ORDER#PRIORITY*")]
[DynamoDbAttribute("gsi1pk")]
public string Gsi1Pk { get; set; } = string.Empty;
}
// AnyOrder.MatchesEntity automatically excludes items matching "ORDER#PRIORITY*"
DISC006: Tautological Exclusion
Triggered when two entities have patterns so similar that the exclusion guard would negate the entity's own positive match:
// ❌ Triggers DISC006 — both patterns are "*ITEM*" (identical)
[DynamoDbTable("shared-table", IsDefault = true)]
public partial class GenericItem
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[GsiPartitionKey("gsi1",
DiscriminatorProperty = "entityType",
DiscriminatorPattern = "*ITEM*")]
[DynamoDbAttribute("gsi1pk")]
public string Gsi1Pk { get; set; } = string.Empty;
}
[DynamoDbTable("shared-table")]
public partial class SpecificItem
{
[PartitionKey]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[GsiPartitionKey("gsi1",
DiscriminatorProperty = "entityType",
DiscriminatorPattern = "*ITEM*")]
[DynamoDbAttribute("gsi1pk")]
public string Gsi1Pk { get; set; } = string.Empty;
}
Fix: Make one pattern more specific so the generator can differentiate:
// ✅ Fix — use a distinct, more-specific pattern for SpecificItem
[GsiPartitionKey("gsi1",
DiscriminatorProperty = "entityType",
DiscriminatorPattern = "SPECIAL#ITEM*")]
[DynamoDbAttribute("gsi1pk")]
public string Gsi1Pk { get; set; } = string.Empty;
Compound Key Discrimination
When two entities on the same table have identical discriminator patterns on one key property (same specificity score), the source generator automatically attempts to resolve the overlap by inspecting the other key property's pattern. If the cross-key patterns differ, the generator promotes to a compound discriminator check that verifies both keys, suppresses FDDB102/DISC004 diagnostics, and emits an FDDB104 info diagnostic confirming the resolution.
When Compound Promotion Applies
Compound promotion resolves the scenario where entities share the same sort key prefix but differ by partition key structure (or vice versa):
[DynamoDbTable("capabilities", IsDefault = true)]
public partial class PlatformCapability
{
[PartitionKey(Prefix = "PLATFORM")]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[SortKey(Prefix = "CAP")]
[DynamoDbAttribute("sk")]
public string Sk { get; set; } = string.Empty;
}
[DynamoDbTable("capabilities")]
public partial class TenantCapability
{
[PartitionKey(Prefix = "TENANT")]
[DynamoDbAttribute("pk")]
public string Pk { get; set; } = string.Empty;
[SortKey(Prefix = "CAP")]
[DynamoDbAttribute("sk")]
public string Sk { get; set; } = string.Empty;
}
Both entities derive CAP#* as their sort key discriminator pattern — same score, same property. Without compound promotion, this would produce a DISC004 error (ambiguous same-score overlap). The compound promotion pass detects that the partition key patterns differ (PLATFORM#* vs TENANT#*) and resolves the overlap automatically.
Generated MatchesEntity with Compound Check
The generated MatchesEntity for each entity checks both keys:
// PlatformCapability.MatchesEntity checks:
// 1. sk starts with "CAP#" (primary discriminator)
// 2. pk starts with "PLATFORM#" (compound constraint)
// TenantCapability.MatchesEntity checks:
// 1. sk starts with "CAP#" (primary discriminator)
// 2. pk starts with "TENANT#" (compound constraint)
This ensures mutual exclusivity — any DynamoDB item with both keys present matches at most one entity.
Asymmetric Case: One Entity Has No Cross-Key Pattern
When one entity has a derivable cross-key pattern and the other has a bare key (no prefix), the generator uses an exclusion guard on the bare-key entity:
- The entity with a prefix gets a positive compound constraint: pk must start with
"PLATFORM#" - The bare-key entity gets an exclusion guard: if pk starts with
"PLATFORM#", return false (that item belongs to the prefixed entity)
Resolution Rules
| Cross-Key Patterns | Resolution |
|---|---|
| Both entities have different non-null patterns | Both get positive compound constraint (each checks its own PK/SK pattern) |
| One entity has pattern, other has null (bare key) | Pattern entity gets positive check; bare-key entity gets exclusion guard |
| Both entities have null patterns (both bare keys) | Not resolvable — FDDB102/DISC004 emitted |
| Both entities have identical patterns | Not resolvable — FDDB102/DISC004 emitted |
| Cross-key pattern has Complex strategy (multi-wildcard) | Treated as null — cannot be used for compound promotion |
Multi-Entity Groups
When three or more entities share the same discriminator pattern, compound promotion evaluates all unique pairs independently. If some pairs are resolvable and others are not, only the resolvable pairs are promoted; unresolved pairs still emit FDDB102/DISC004.
Diagnostic: FDDB104
When compound promotion resolves an overlap, the source generator emits an FDDB104 Info diagnostic:
Entity 'PlatformCapability' promoted to compound discrimination (sk: 'CAP#*' + pk: 'PLATFORM#*') to resolve overlap with 'TenantCapability'
This is informational only — no action is required.
Best Practices
-
Rely on auto-derivation — Define distinct key prefixes and let the generator handle pattern creation. This keeps entity definitions clean and avoids FDDB103.
-
Use distinct prefixes — Avoid sharing the same prefix across multiple entity types unless you also differentiate via sort key prefix. This avoids FDDB102.
-
Prefer longer, unique prefixes —
ORDERis better thanORDwhen other entities likeOrderReturnexist. Longer prefixes reduce the chance of overlap. -
Use
DiscriminatorValuefor exact matching — When patterns aren't sufficient (same GSI, different entity types with no key-based distinction), useDiscriminatorValuefor exact string matching instead of wildcard patterns. -
Check informational diagnostics — DISC005 and FDDB103 are informational but worth reviewing. They confirm the generator is making correct decisions about your entity hierarchy.