For the complete documentation index, see llms.txt. This page is also available as Markdown.

Primitives

The schemas you start a chain from — text, numbers, identifiers, dates, booleans, enums — and the rules that hang off each.

Every chain starts at a primitive. The primitive fixes the type; the rules after it narrow what that type is allowed to hold.

Text

Schema.Text accepts a string. Start with Trim() wherever the value came off a form — a trailing space is not something you want to reject over, and it is not something you want to store either.

public static readonly Schema<string, string> Sigil =
    Schema.Text.Trim().LengthBetween(3, 24);

// A shape with a fixed width says so, rather than bounding both ends at the
// same number.
public static readonly Schema<string, string> CountryCode =
    Schema.Text.Trim().Length(2);

// A closed set of spellings. Schema.Enum is the better home when the domain
// already models the set as an enumeration.
public static readonly Schema<string, string> Difficulty =
    Schema.Text.Trim()
          .OneOf(
               global::System.StringComparison.OrdinalIgnoreCase,
               "easy",
               "standard",
               "deadly");

Length rules read as what they are. Length(2) is a fixed width; LengthBetween, MinLength and MaxLength are bounds; NotEmpty is the one you will reach for most.

Patterns

Matches takes a Regex, not a pattern string. That is on purpose: it puts the choice of a match timeout in front of you rather than behind you.

Shapes with names

Some shapes are common enough to have their own rule. Each one is checked by a scan rather than an expression, so there is no pattern to get subtly wrong.

StartsWith and EndsWith take literals, not expressions. A dot or a bracket in one means itself.

Numbers

Four number schemas, one per type: Int32, Int64, Decimal and Double.

Inclusivity is in the name, and it matters.

Rule
Bound

GreaterThan, LessThan

Excluded

AtLeast, AtMost

Included

Between

Both included

Positive, Negative

Excludes zero

Prefer one rule over two where one says the same thing. Between(1, 6) reports a party of twelve as one failure; AtLeast(1).AtMost(6) reports the same thing and is just longer to read.

Identifiers

Schema.Uuid accepts a Guid. NotEmpty() rejects Guid.Empty, which is what an uninitialised field deserializes to and is almost never a value you meant to receive.

It is named for the standard rather than the role, because its rules are about the UUID layout. An identifier that is not a UUID starts at Schema.For<T>().

Checking the version

IsVersion4() requires the value to have been generated at random, which is what Guid.NewGuid() produces. Use it where the identifier must carry nothing a reader can mine — no creation time, and no ordering someone could walk.

IsVersion7() requires the other way round: version 7 leads with a millisecond timestamp, so a run of them sorts by creation order. That is what makes it a good database key and a bad choice where the creation time is a secret.

Both read the version digits and nothing else. A value with them set to 4 passes even if the rest was not random — nothing in a UUID records how it was really made. Both also reject Guid.Empty, whose version digits are zero, so adding NotEmpty() alongside one of them says nothing new.

IsVersion7() is on .NET 9 and later only, which is where Guid.CreateVersion7() arrived. A consumer on an earlier framework cannot produce one, so the package does not offer to check for one.

No other version has a rule, for the same reason: version 4 and version 7 are the only ones .NET creates.

Dates and times

Two schemas, and picking between them is picking what the value means.

  • Schema.Timestamp is a DateTimeOffset — a moment, so a time zone is part of the value.

  • Schema.Date is a DateOnly — a day, so a time of day would be noise.

Schema.Date is not available on netstandard2.0, because DateOnly is not.

Inclusivity is in the name here too. Before and After exclude the bound; OnOrBefore and OnOrAfter include it, which is what a closing date means.

Booleans

IsFalse is worth a second look when you write it. A flag that has to be clear usually reads better as the opposite flag that has to be set.

Enums

Schema.Enum<T>() rejects a value outside the declared members. That is not a theoretical case — a cast produces one, and so does a deserializer handed a number.

Anything else

Schema.For<T>() is the identity schema. It accepts any value of that type and gives Check and Transform somewhere to hang off.

Every primitive above is a Schema.For<T>() with rules already attached. Reach for the bare form when your type has none — or when you want a rule over a whole subject, which is how cross-field rules work.

Last updated

Was this helpful?