/DB

Check DSL

Build type-safe, composable CHECK constraint expressions with IDbCheckExpression and the DbCheck builder.

updated 3 May 20263 min readv0.1.82

Overview

The [Check] attribute accepts either a raw SQL string or a class that implements IDbCheckExpression. The class-based approach is strongly preferred: it is type-safe, reusable across multiple models, composable with boolean operators, and refactoring-friendly.

IDbCheckExpression

public interface IDbCheckExpression
{
    DbCheckExpr Build(string? columnName);
}

DbCheckExpr is a lightweight struct that wraps the raw SQL fragment. Implement Build and return the result of composing DbCheck methods. The columnName parameter receives the snake_case column name when the attribute is on a property, or null when it is on the class.

Apply it with:

[Check(typeof(MyCheckExpression))]
public string Email { get; set; }
TIP
Share expression classes across multiple model classes. A ValidEmailExpression class defined once can be applied to UserLogin.Email, InviteCode.Email, and ContactForm.Email with a single attribute each.

Why Use the DSL Instead of Raw SQL

Raw SQL string DbCheck DSL
[Check("LENGTH(\"email\") < 200")] [Check(typeof(EmailLengthExpr))]
No refactoring support Rename property → update one class
Duplicated across models Defined once, referenced everywhere
Easy to misquote column names DbCheck.Value("Email") converts automatically

DbCheck Builder — All Methods

All methods return a DbCheckExpr value that can be further composed.

Column Reference Methods

Method SQL output Notes
DbCheck.Value("PropertyName") "snake_case_name" Converts the C# property name to snake_case
DbCheck.Column("raw_col") "raw_col" Uses the name as-is
WARNING
DbCheck.Value("PropertyName") converts the argument to snake_case automatically. If the column is already in snake_case (or uses a custom [Column("name")]), use DbCheck.Column("col_name") to avoid double-conversion.

String Methods

Method SQL output
DbCheck.StartsWith(col, "prefix") "col" LIKE 'prefix%'
DbCheck.EndsWith(col, "suffix") "col" LIKE '%suffix'
DbCheck.Contains(col, "sub") "col" LIKE '%sub%'
DbCheck.Regex(col, "pattern") "col" ~ 'pattern'

Length Method

DbCheck.Len(col, DbCheck.Operators.LessThan, 100)
// → length("col") < 100

The second argument is one of the DbCheck.Operators constants (see below).

Literal Values

DbCheck.Literal("value")  // → 'value'
DbCheck.Literal(42)       // → 42

Boolean Composition

Method SQL output
DbCheck.And(expr1, expr2, ...) (expr1 AND expr2 ...)
DbCheck.Or(expr1, expr2, ...) (expr1 OR expr2 ...)
DbCheck.Not(expr) NOT (expr)

DbCheck.Operators Constants

Constant SQL symbol
DbCheck.Operators.LessThan <
DbCheck.Operators.GreaterThan >
DbCheck.Operators.Equal =
DbCheck.Operators.NotEqual <>
DbCheck.Operators.LessOrEqual <=
DbCheck.Operators.GreaterOrEqual >=

Implementing an Expression Class

public class ValidUsernameExpression : IDbCheckExpression
{
    public DbCheckExpr Build(string? columnName)
    {
        var col = DbCheck.Column("username");

        return DbCheck.And(
            DbCheck.Len(col, DbCheck.Operators.GreaterOrEqual, 3),
            DbCheck.Len(col, DbCheck.Operators.LessOrEqual, 50),
            DbCheck.Regex(col, "^[a-zA-Z0-9_]+$")
        );
    }
}

Generated DDL:

CHECK ((length("username") >= 3 AND length("username") <= 50 AND "username" ~ '^[a-zA-Z0-9_]+$'))

Applied to a model:

[Table("user_logins")]
public partial class UserLogin
{
    [PrimaryKey, Default(DbDefaults.Guid.Random)]
    public Guid Id { get; set; }

    [Check(typeof(ValidUsernameExpression))]
    public string Username { get; set; }
}

Composing Expressions with And / Or / Not

Expression classes can call .Build() on each other to layer constraints:

public class ValidEmailExpression : IDbCheckExpression
{
    public DbCheckExpr Build(string? columnName)
    {
        var col = DbCheck.Column("email");

        return DbCheck.And(
            DbCheck.Len(col, DbCheck.Operators.LessThan, 256),
            DbCheck.Contains(col, "@")
        );
    }
}

public class NotDisposableEmailExpression : IDbCheckExpression
{
    public DbCheckExpr Build(string? columnName)
    {
        var col = DbCheck.Column("email");

        return DbCheck.And(
            new ValidEmailExpression().Build(columnName),
            DbCheck.Not(DbCheck.EndsWith(col, "@mailinator.com"))
        );
    }
}
[Check(typeof(NotDisposableEmailExpression))]
public string Email { get; set; }
NOTE
Expression classes have no dependency injection — they are instantiated with new(). Keep them as pure SQL-building helpers with no external dependencies.