Client Evaluated Default Value

ID

csharp.client_evaluated_default_value

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

CSharp

Tags

database, default-value, orm, reliability

Description

Reports a column default that the application computes rather than the database: HasDefaultValue in a fluent entity configuration, or defaultValue: in a migration, given DateTime.Now, DateTime.UtcNow, DateTime.Today, DateTimeOffset.Now, DateTimeOffset.UtcNow, Guid.NewGuid() or Guid.CreateVersion7().

Rationale

HasDefaultValue and defaultValue: take a value, not a rule for producing one. The expression is evaluated once — while the model is built, or while a developer scaffolds the migration — and the single value it produced is written into the generated DEFAULT clause. From then on the database hands that same frozen value to every row.

For a timestamp the result is a CreatedAt column that records when the process started rather than when the row was inserted, and that silently changes meaning on every redeploy. For an identifier it is worse: every row gets the same value, so if the column is a key or is uniquely indexed the second insert fails, and if it is not, the identifiers stop identifying anything. Nothing about the failure points at the mapping code; the column is populated, the data is simply wrong.

The database-side form solves this because it stores the expression instead of its result, leaving the server to evaluate it per insert. Constant defaults are untouched by this rule — a fixed number or string is exactly what these methods exist for.

using Microsoft.EntityFrameworkCore;

public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.Property(o => o.CreatedAt).HasDefaultValue(DateTime.UtcNow);   // FLAW
        builder.Property(o => o.PublicId).HasDefaultValue(Guid.NewGuid());     // FLAW

        builder.Property(o => o.ModifiedAt).HasDefaultValueSql("GETUTCDATE()"); // OK, the server evaluates it
        builder.Property(o => o.Status).HasDefaultValue("pending");             // OK, a real constant
    }
}

public partial class AddAuditColumns : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<DateTime>(
            name: "CreatedAt", table: "Orders", defaultValue: DateTime.Now);       // FLAW

        migrationBuilder.AddColumn<DateTime>(
            name: "ModifiedAt", table: "Orders", defaultValueSql: "GETDATE()");    // OK
    }
}

Remediation

Replace HasDefaultValue(expr) with HasDefaultValueSql("…​") and defaultValue: with defaultValueSql:, passing the database function that produces the value on each insert — GETUTCDATE(), SYSUTCDATETIME() or CURRENT_TIMESTAMP for a timestamp, NEWID() or NEWSEQUENTIALID() for an identifier. Use the function name your provider supplies, since the string is passed through to the server verbatim.

Where the value has to come from the application rather than the database, set it in code on the entity itself — in the constructor, in a property initializer, or in the save pipeline — so that the expression is evaluated once per row instead of once per model.