Column Defaults
All mechanisms for specifying PostgreSQL DEFAULT expressions in Socigy.OpenSource.DB v0.1.82 — C# initializers, [Default], DbDefaults constants, and how defaults interact with ExcludeAutoFields().
Overview
Socigy.OpenSource.DB provides three layered mechanisms for column defaults. Understanding all three — and how they interact — prevents the most common DDL and INSERT surprises.
- C# property initializer — the migration CLI reads the initializer value and emits a
DEFAULTexpression automatically. [Default]with no argument — flags the column as having a DB-side default without emitting a DDL expression (used with[AutoIncrement]and similar).[Default("expr")]or[Default(DbDefaults.Constant)]— emits an explicitDEFAULTexpression in the DDL.
Section 1: C# Initializer as Default
[Default] for simple literal defaults.When a property carries no [Default] attribute, the migration CLI inspects the C# property initializer. A non-zero, non-empty initializer is emitted directly as the DEFAULT expression in DDL.
[Table("courses")]
public partial class Course
{
[PrimaryKey, Default(DbDefaults.Guid.Random)]
public Guid Id { get; set; }
// No [Default] — CLI reads the initializer "DEFAULT NAME"
public string Name { get; set; } = "DEFAULT NAME";
public int Priority { get; set; } = 5;
public bool IsPublished { get; set; } = true;
}Generated DDL:
CREATE TABLE "courses" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL DEFAULT 'DEFAULT NAME',
"priority" INTEGER NOT NULL DEFAULT 5,
"is_published" BOOLEAN NOT NULL DEFAULT TRUE,
PRIMARY KEY ("id")
);What Produces a DEFAULT and What Does Not
| C# initializer | DDL DEFAULT emitted |
|---|---|
= "Hello" |
DEFAULT 'Hello' |
= 42 |
DEFAULT 42 |
= true |
DEFAULT TRUE |
= MyEnum.Value |
DEFAULT 0 (underlying integer) |
= MyDbEnum.Value |
DEFAULT 'Value' (PostgreSQL enum string — see below) |
= 0 |
(ignored — zero value) |
= false |
(ignored — zero value) |
= "" |
(ignored — empty string) |
= null |
(ignored) |
= Guid.Empty |
(ignored) |
Zero-values are the C# default for every value type, so the CLI cannot distinguish "you intentionally wrote = 0" from "you left the initializer at the default". Always use [Default(DbDefaults.Number.Zero)] when you want an explicit DEFAULT 0 in DDL.
Enum initializers
The behavior depends on whether the enum carries [DatabaseEnum]:
Regular enum (stored as INTEGER) — the initializer is emitted as the underlying integer:
public enum VersionState { Draft, Published, Archived }
[Table("project_versions")]
public partial class ProjectVersion
{
// Emits: DEFAULT 0
public VersionState State { get; set; } = VersionState.Draft;
}Generated DDL fragment:
"state" integer DEFAULT 0[DatabaseEnum] enum (stored as a PostgreSQL ENUM type) — the initializer is emitted as the quoted member name:
[DatabaseEnum]
public enum Priority { Low, Normal, High }
[Table("tasks")]
public partial class Task
{
// Emits: DEFAULT 'Normal'
public Priority Priority { get; set; } = Priority.Normal;
}Generated DDL fragment:
"priority" priority DEFAULT 'Normal'= VersionState.Draft is only emitted as a DEFAULT if Draft is not the first member (i.e. not 0). If Draft = 0, the initializer is treated as "no default" and no DEFAULT clause is emitted. Use [Default("0")] or [Default(DbDefaults.Number.Zero)] to force an explicit DEFAULT 0 in that case.= Test.First | Test.Fourth — are silently ignored and produce no DEFAULT clause. A combined value such as 9 is not a valid single row reference in the enum's database table. Use [Default("9")] to emit it verbatim if you intentionally store bitmasks in the column.Section 2: Naked `[Default]` — Flag Only, No DDL Expression
[Default] with no argument tells the framework that this column has a database-side default, but it does not emit any DDL expression. The column is still marked so that ExcludeAutoFields() skips it during INSERT.
This form is used with [AutoIncrement] (sequence-assigned values) or with a primary key whose value is generated outside the application:
[Table("user_login")]
public partial class UserLogin
{
// Naked [Default] — no DDL expression emitted.
// ExcludeAutoFields() will skip this column on INSERT.
[PrimaryKey, Default]
public Guid Id { get; set; }
public string Username { get; set; }
public string? PasswordHash { get; set; }
}The resulting DDL column is "id" UUID NOT NULL — no DEFAULT clause — because no expression was given. The application is expected to supply or assign the value through some other means (a trigger, an application-side call, etc.).
[Default(DbDefaults.Guid.Random)] instead of bare [Default].Section 3: `[Default("expr")]` — Raw SQL Expression
Pass a raw SQL expression as a string to emit it verbatim as the DEFAULT clause:
[Default("'pending'")]
public string Status { get; set; }
[Default("ARRAY[]::TEXT[]")]
public string[] Tags { get; set; } = [];
[Default("(CURRENT_DATE + INTERVAL '30 days')")]
public DateTime ExpiresAt { get; set; }Generated DDL fragments:
"status" TEXT NOT NULL DEFAULT 'pending',
"tags" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[],
"expires_at" TIMESTAMP NOT NULL DEFAULT (CURRENT_DATE + INTERVAL '30 days')"'pending'", not "pending").Section 4: `DbDefaults` Built-in Constants
DbDefaults provides named constants for the most common default expressions. Using these instead of raw strings makes your code DB-engine-agnostic and protected against typos.
| Constant | DDL expression emitted |
|---|---|
DbDefaults.Guid.Random |
DEFAULT gen_random_uuid() |
DbDefaults.Guid.Sequential |
DEFAULT uuid_generate_v1mc() |
DbDefaults.Time.Now |
DEFAULT timezone('utc', now()) |
DbDefaults.Time.NowLocal |
DEFAULT now() |
DbDefaults.Time.Date |
DEFAULT current_date |
DbDefaults.Bool.True |
DEFAULT TRUE |
DbDefaults.Bool.False |
DEFAULT FALSE |
DbDefaults.Number.Zero |
DEFAULT 0 |
DbDefaults.Number.One |
DEFAULT 1 |
DbDefaults.Text.Empty |
DEFAULT '' |
Usage:
[Table("test_items")]
public partial class TestItem
{
[PrimaryKey, Default(DbDefaults.Guid.Random)]
public Guid Id { get; set; }
public string Name { get; set; }
[Default(DbDefaults.Time.Now)]
public DateTime CreatedAt { get; set; }
[Default(DbDefaults.Bool.True)]
public bool IsActive { get; set; }
[Default(DbDefaults.Number.Zero)]
public int RetryCount { get; set; }
}Generated DDL:
CREATE TABLE "test_items" (
"id" UUID NOT NULL DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT timezone('utc', now()),
"is_active" BOOLEAN NOT NULL DEFAULT TRUE,
"retry_count" INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY ("id")
);uuid_generate_v1mc() (used by DbDefaults.Guid.Sequential) requires the uuid-ossp extension. Run CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; in your database before applying migrations that use this constant.Section 5: `ExcludeAutoFields()` — How Defaults Affect Inserts
The INSERT builder's ExcludeAutoFields() method skips any property that carries a [Default] attribute in any form. This allows the database to apply the DEFAULT expression instead of receiving an explicit value from the application.
// Id has [Default(DbDefaults.Guid.Random)] — excluded by ExcludeAutoFields()
// CreatedAt has [Default(DbDefaults.Time.Now)] — excluded by ExcludeAutoFields()
// Name has no [Default] — included
var item = new TestItem { Name = "Hello" };
await item.Insert()
.WithConnection(conn)
.ExcludeAutoFields() // skips Id and CreatedAt
.ExecuteAsync();
// Resulting SQL (approximate):
// INSERT INTO "test_items" ("name") VALUES (@name)
// The DB fills "id" and "created_at" from their DEFAULT expressions.The naked [Default] (no argument) form is specifically designed for [AutoIncrement] columns where the sequence generates the value:
[Table("test_counters")]
public partial class TestCounter
{
[PrimaryKey]
public Guid Id { get; set; }
[AutoIncrement] // implies the column has a DB-side sequence default
public int Seq { get; set; }
public string Label { get; set; }
}
var counter = new TestCounter { Id = Guid.NewGuid(), Label = "first" };
// Seq is excluded because [AutoIncrement] marks it as having a DB default.
// The sequence nextval() expression in DDL assigns the value.
await counter.Insert()
.WithConnection(conn)
.ExecuteAsync();Reading Back the DB-Assigned Value
Use WithValuePropagation() to execute the INSERT with RETURNING * and populate the instance's excluded fields from the returned row:
var counter = new TestCounter { Id = Guid.NewGuid(), Label = "VP" };
await counter.Insert()
.WithConnection(conn)
.WithValuePropagation()
.ExecuteAsync();
// counter.Seq is now populated with the sequence value assigned by PostgreSQL
Console.WriteLine(counter.Seq); // e.g. 42Section 6: `[Default]` vs C# Initializer — Precedence
When both a [Default] attribute and a C# initializer are present on the same property, the [Default] attribute always wins. The initializer is ignored for DDL purposes.
// [Default] wins — DDL emits DEFAULT gen_random_uuid(), not DEFAULT 'some-fixed-guid'
[Default(DbDefaults.Guid.Random)]
public Guid Id { get; set; } = Guid.Parse("00000000-0000-0000-0000-000000000001");
// [Default] wins — DDL emits DEFAULT timezone('utc', now()), not DEFAULT '2020-01-01'
[Default(DbDefaults.Time.Now)]
public DateTime CreatedAt { get; set; }The C# initializer is only consulted when no [Default] attribute is present at all.
Complete Example
using Socigy.OpenSource.DB.Attributes;
[Table("orders")]
public partial class Order
{
// DbDefaults constant — gen_random_uuid()
[PrimaryKey, Default(DbDefaults.Guid.Random)]
public Guid Id { get; set; }
// C# initializer — DEFAULT 'pending'
public string Status { get; set; } = "pending";
// C# initializer — DEFAULT 1
public int Priority { get; set; } = 1;
// C# initializer — DEFAULT TRUE
public bool RequiresShipping { get; set; } = true;
// C# initializer on a regular enum — DEFAULT 1 (underlying integer of Urgent)
// (assumes: public enum OrderUrgency { Normal, Urgent, Critical })
public OrderUrgency Urgency { get; set; } = OrderUrgency.Urgent;
// DbDefaults constant — DEFAULT timezone('utc', now())
[Default(DbDefaults.Time.Now)]
public DateTime CreatedAt { get; set; }
// Raw SQL expression — DEFAULT (CURRENT_DATE + INTERVAL '7 days')
[Default("(CURRENT_DATE + INTERVAL '7 days')")]
public DateTime DueDate { get; set; }
// Naked [Default] — no DDL expression; skipped by ExcludeAutoFields()
[Default]
public int InternalToken { get; set; }
public string? Notes { get; set; }
}