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.