Skip to content

Constant Key Detection: Non-Const References Cause Uncompilable Generated Code #190

Description

@dguisinger

Summary

When a key property uses expression-body (=>) or read-only auto-property ({ get; }) syntax with a reference to a static readonly field (rather than a const or string literal), the constant key detection silently fails. The generator then falls through to the normal code path, which emits property assignments in FromDynamoDb() that won't compile because the property has no setter.

This manifests as two user-facing problems:

  1. Property or indexer 'Entity.Sk' cannot be assigned to -- it is read only in FromDynamoDb()
  2. Generated CRUD methods (Update(), Get(), Delete()) still require the key parameter instead of omitting it

Reproduction

public static class DynamoDB
{
    public static readonly string DefaultSortkeyValue = "PROFILE"; // ❌ static readonly, NOT const
}

[DynamoDbTable("Users")]
public partial class User
{
    [PartitionKey(Prefix = "USER")]
    [DynamoDbAttribute("PK")]
    public string Pk { get; set; } = string.Empty;

    // Expression-body referencing a static readonly field
    [SortKey]
    [DynamoDbAttribute("SK")]
    public string Sk => DynamoDB.DefaultSortkeyValue;
}

Expected Behavior

Either:

  • Detection succeeds and the key is treated as constant (parameter omitted, no assignment in FromDynamoDb), OR
  • The generator emits a clear diagnostic explaining the requirement

Actual Behavior

  • DetectConstantKeyValue calls semanticModel.GetConstantValue(expr) which returns null for static readonly fields (only resolves compile-time constants)
  • IsConstantKey remains false
  • FromDynamoDb() generates entity.Sk = value; → compile error (no setter)
  • Update(pk, sk) is generated instead of Update(pk) → user must still pass the fixed SK value

Root Cause

Two separate issues:

Issue A: Detection Only Works with Compile-Time Constants

EntityAnalyzer.DetectConstantKeyValue uses SemanticModel.GetConstantValue() which only resolves:

  • String literals ("PROFILE")
  • References to const fields (const string X = "PROFILE")

It does NOT resolve:

  • static readonly fields
  • Property references
  • Method calls

This is a fundamental Roslyn limitation — GetConstantValue is specifically for compile-time constants.

Issue B: No Graceful Fallback for Read-Only Properties

When detection fails, the generator doesn't check whether the property actually HAS a setter before emitting assignment code. The normal FromDynamoDb path in MapperGenerator.GeneratePropertyFromAttributeValue unconditionally generates entity.Property = ... without verifying that a setter exists.

Proposed Fix

Fix A: Emit Diagnostic for Unresolvable Constant Key

When a key property is expression-body or read-only auto-property BUT GetConstantValue() returns null, emit a new diagnostic:

FDDB125: Key property '{PropertyName}' uses expression-body/read-only syntax but the value 
is not a compile-time constant. Use a string literal or const field reference. 
Static readonly fields are not supported.

This makes the requirement explicit instead of generating uncompilable code.

Fix B: Guard Assignment in FromDynamoDb

As a safety net, before generating property assignment code, check if the property has a setter. If it doesn't and IsConstantKey is false, emit FDDB125 instead of generating the assignment.

Detection logic in EntityAnalyzer.DetectConstantKeyValue:

// After both Case 1 and Case 2 fall through without setting ConstantKeyValue:
// Check if the property is read-only (no setter) — this means we have an
// unresolvable constant key that will generate uncompilable code
if (!propertyModel.IsConstantKey && IsReadOnlyProperty(propertyDecl))
{
    // Emit diagnostic: read-only key property with non-const value
    ReportDiagnostic(DiagnosticDescriptors.ConstantKeyNonConstReference,
        propertyDecl.GetLocation(),
        propertyModel.PropertyName);
}

Where IsReadOnlyProperty checks:

  • Expression-body (ExpressionBody != null) → always read-only
  • Only getter accessor with no set/init → read-only

User Guidance

Document clearly that constant key detection requires compile-time constants:

// ✅ String literal
public string Sk => "PROFILE";

// ✅ const field reference (same class or external)
public const string ProfileKey = "PROFILE";
public string Sk => ProfileKey;

// ✅ const from another class
public string Sk => Constants.ProfileKey; // where ProfileKey is const

// ❌ static readonly — NOT a compile-time constant
public static readonly string ProfileKey = "PROFILE";
public string Sk => ProfileKey; // Detection fails, generates bad code

// ❌ Property reference — NOT a compile-time constant
public string Sk => SomeClass.SomeProperty; // Detection fails

Acceptance Criteria

  1. New diagnostic FDDB125 emitted when a key property is read-only but value is not resolvable as a compile-time constant
  2. No uncompilable code generated for read-only key properties (assignment skipped or error emitted)
  3. Existing tests pass (entities with proper const values still work)
  4. Documentation updated to clarify the const requirement

Metadata

Metadata

Assignees

Labels

Type

Fields

No fields configured for Bug.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions