Check DSL
Build type-safe, composable CHECK constraint expressions with IDbCheckExpression and the DbCheck builder.
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; }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 |
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") < 100The second argument is one of the DbCheck.Operators constants (see below).
Literal Values
DbCheck.Literal("value") // → 'value'
DbCheck.Literal(42) // → 42Boolean 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; }new(). Keep them as pure SQL-building helpers with no external dependencies.