How it works
Every class in CommonKit follows the same five rules. Learn them once and you know the whole library — and reading a value becomes one small decision, made once per value.
Rule 1 — nothing throws by default
You can pass anything to any method: null, an empty string, garbage text, the wrong
type, a huge input. The method never crashes your app. It answers with a rejection
value instead — a calm, predictable "this could not be used".
CkEmail.Address(null); // -> "" (no crash)
CkEmail.Address("not an email"); // -> ""
CkNumber.Int32("banana"); // -> 0 (the default fallback)
CkUuid.ToGuid("oops"); // -> Guid.Empty
The throw switch
Every throwIfInvalid parameter defaults to CkSettings.DefaultThrowIfInvalid, a constant
that ships as false. Set it to true and it overrides every call — throwIfInvalid: false
included — so every rejected value surfaces at once as a Ck<Class>Exception carrying the
input, what was expected and the reason, instead of slipping by as a fallback nobody checked.
That is a development switch: set it true while an app is being built, by you or by an AI
agent, and back to false to ship. It is a constant, so it takes a rebuild of the library,
which is why it is for teams that build from the source. Two things it never touches:
Explain (Rule 4) and the …OrNull readers (Rule 3) — the reason and null are their whole
answers.
Rule 2 — the rejection value depends on the return type
Every value you read — a quantity, a birthday, a price — is one of three calls: the plain
method, its …OrNull twin, or the plain method with throwIfInvalid: true. All three clean
and validate the value exactly the same way; the only difference is what comes back when the
value is bad. One table settles it:
| Your situation | Call | Bad input gives |
|---|---|---|
| I need a value no matter what — a non-nullable property, and a default is fine | the plain method, with a fallback |
your fallback |
| The column is nullable / I need to know whether it was really there | the …OrNull twin |
null |
| Bad data must STOP the show — an import, a job that must not continue | the plain method with throwIfInvalid: true |
that class's exception |
| I want the reason in words — a form, a validator that reports every problem | the class's Explain |
the reason, "" when the value passes |
Two words worth defining once: a non-nullable type cannot hold null — an int, a
DateOnly. A nullable type wears a ? — int?, DateOnly? — and can. Databases
call a missing value NULL, so nullable columns and the …OrNull twins go together.
What the plain method answers depends on what it returns:
| You call… | Bad input gives you… |
|---|---|
a string method (CkText.String, CkEmail.Address, …) |
"" — an empty string |
| a number or money method | your fallback parameter (default 0) |
any …OrNull method |
null |
CkUuid.ToGuid |
Guid.Empty |
a date reader (CkDate.ToDateOnly, CkDateTime.ToDateTime) |
year one — DateOnly.MinValue, DateTime.MinValue |
| a geo reader | the North Pole (90, 0); geo calculations give NaN |
a list or byte[] method |
an empty list/array — never null |
a display mask (SsnMask, CkEmail.Mask, …) |
a safe placeholder — never empty |
For string methods this gives you a simple test: non-empty in, empty out means the
value was rejected. Check for "" at the call site when it matters.
The plain method is for code that needs something to work with either way and a sensible default is fine. An order quantity, say — if the field is missing or unreadable, treating it as one item is a reasonable choice, and the program moves on:
int quantity = CkNumber.Int32(
input: raw,
fallback: 1
);
CkNumber.Int32(input: "3", fallback: 1); // -> 3
CkNumber.Int32(input: "", fallback: 1); // -> 1 (missing — the fallback steps in)
CkNumber.Int32(input: "banana", fallback: 1); // -> 1 (unreadable — same answer)
No if, no try, no null — the answer is always an int you can use. The cost: you
cannot tell a real 1 from a fallback 1. When that difference matters, pick one of the
other calls. The fallback goes through the exact same cleaning and validation as the input;
a bad fallback is rejected too. When you need to detect a rejection through a fallback,
choose one you can distinguish from real data — say -1 for a quantity that is never
negative.
CkCountry.Alpha2(input: dirty, fallback: "US"); // -> "US" when dirty is unusable
CkNumber.Int32(input: "banana", fallback: -1); // -> -1
Rule 3 — when you want null, use the OrNull family
Databases have NULL; the rejection values above are not NULL. So the readers that
feed nullable columns come in a second form: Int32OrNull, AmountOrNull,
ToDateTimeOrNull, ToDateOnlyOrNull, ToTimeOnlyOrNull, ToGuidOrNull,
LatitudeOrNull, LongitudeOrNull — and thirteen more numeric ones. Use one when
"nothing" is a real, honest answer you want to keep. An optional birthday, say — some people
gave one, some did not, and the nullable column can say which:
DateOnly? birthday = CkDate.ToDateOnlyOrNull(raw);
CkDate.ToDateOnlyOrNull("1985-04-12"); // -> 1985-04-12
CkDate.ToDateOnlyOrNull(""); // -> null (missing)
CkDate.ToDateOnlyOrNull("junk"); // -> null (unreadable)
CkDate.ToDateOnlyOrNull("1900-01-01"); // -> null (a placeholder date — "nobody filled this in")
One promise comes with the family: an OrNull method never throws — at all. It does not
even take a throwIfInvalid parameter, and the throw switch does not
reach it. Whatever goes wrong — a missing value, garbage text, a placeholder — null is the
answer, because null is exactly what you asked for by choosing this reader. When you want
errors instead, use the plain sibling (Int32, Amount, ToDateTime, …) with
throwIfInvalid: true.
They answer null for three kinds of input:
- A missing value —
nullorDBNull. - An unreadable value —
"banana"is not a number. - A sentinel — a value that technically parses but really means "nobody filled
this in": the number
0, placeholder dates like1900-01-01, the all-zeros UUID. This is on by default and switchable per call — each class names its own flag (treatZeroAsMissing: falsefor numbers and money,treatSentinelsAsMissing: falsefor dates,treatZerosAsMissing: falsefor UUIDs).
CkNumber.Int32OrNull("42"); // -> 42
CkNumber.Int32OrNull(""); // -> null (missing)
CkNumber.Int32OrNull("0"); // -> null (zero = sentinel)
CkNumber.Int32OrNull(input: "0",
treatZeroAsMissing: false); // -> 0 (you said zero is real)
CkDateTime.ToDateTimeOrNull("1900-01-01"); // -> null (placeholder date)
A yes-or-no value is where this family earns its keep, because a plain bool has no room
for "unknown": a missing consent answer would be stored as a refusal.
CkBool.FromValueOrNull keeps the three apart —
and a boolean has no sentinel to watch for: false is always something somebody said.
bool? optIn = CkBool.FromValueOrNull(raw);
CkBool.FromValueOrNull("yes"); // -> true
CkBool.FromValueOrNull("no"); // -> false (declined — a real answer)
CkBool.FromValueOrNull(""); // -> null (never answered)
Strings are different
String methods have no OrNull twins — with one exception, CkText.ToStringOrNull, the
nullable version of free-text cleaning:
CkText.ToStringOrNull(" hi there "); // -> "hi there"
CkText.ToStringOrNull(" "); // -> null (nothing left once cleaned)
For every other string method — CkEmail.Address, CkUS.Phone, CkText.Name, … — the
rejection value is "", and the bridge to null is CkText.NullIfEmpty:
string? email = CkText.NullIfEmpty(CkEmail.Address(raw));
CkText.NullIfEmpty(CkEmail.Address(" Mary.Smith@GMAIL.com ")); // -> "mary.smith@gmail.com"
CkText.NullIfEmpty(CkEmail.Address("not an email")); // -> null
Read it inside-out: CkEmail.Address cleans and validates, answering "" for a bad
address; CkText.NullIfEmpty turns that "" into the null your nullable column
wants. Two small calls, one honest value. Saving through EF Core, one hook can do this for
every property at once — CkSentinel.ToNull, wired up on
Using it with EF Core.
Rule 4 — when you want errors, ask with throwIfInvalid
Sometimes silence is wrong: an import that must not load garbage, a job that must stop at the
first bad row, a test. Every cleaning and converting method takes throwIfInvalid: true. Then
a value that cannot be used raises that class's own exception —
CkTextException,
CkNumberException,
CkDateException, … — all sealed, all deriving from
CkException, so one catch (CkException) covers them all when
you want a single net. Every one carries the Input that failed and a short Reason:
using SafeCoder.CommonKit.Exceptions;
foreach (string raw in importRows)
{
try
{
decimal amount = CkMoney.Amount(
input: raw,
throwIfInvalid: true
);
// ... save the amount ...
}
catch (CkMoneyException error)
{
Console.WriteLine(error.Message); // one log-safe line: the method, the input, the reason
Console.WriteLine(error.Reason); // short and lowercase — for raw = "ten dollars": letters in the amount
}
}
The row that says "ten dollars" stops its loop iteration with a reason you can log or show
an analyst, instead of quietly becoming 0.00 in the database.
A few members have nothing to throw about and take no flag: the display masks, the counters
(WordCount, CharacterCount), CkText.Join / Split / SameText / Similarity, the
In… allow-list checks (CkCountry.InCountries, CkMoney.InCurrencies, …),
CkDictionary.Has / MissingKeys, CkSentinel.ToNull — and Explain.
Explain — the reason without the exception
An exception stops at the first problem. A form, or a validator that reports everything wrong
with a payload, wants the reason for each field and no exception in the flow. Every class with
a reader has an Explain for that — CkEmail.Explain(input), CkDate.Explain(input),
CkUS.Explain(input, nameof(CkUS.Ssn)) where a class has several readers. The same rules,
the same words the exception would carry, "" when the value passes; it never throws, not
even with the throw switch on.
CkMoney.Explain("ten dollars"); // -> "letters in the amount"
CkMoney.Explain("$19.99"); // -> ""
Writing a validator builds a whole-payload check out of it.
Rule 5 — write data out the invariant way
Reading is half the job; the other half is writing values out — into JSON, a CSV, an
API call. Raw ToString() obeys the machine's locale: on a German or French server,
(1234.5).ToString() is "1234,5", and that comma quietly corrupts your payload.
CommonKit's writers always use the invariant culture — the same output on every machine:
CkText.NumberToString(1234.5); // -> "1234.5" (never "1234,5")
CkBool.Format(true); // -> "true" (what JSON, XML and URLs expect)
CkDateTime.Iso8601(new DateTime(2026, 9, 2, 10, 30, 0, DateTimeKind.Utc));
// -> "2026-09-02T10:30:00Z"
CkDate.Iso8601(new DateOnly(2026, 9, 2)); // -> "2026-09-02"
A boolean has more than one written form, so CkBool.Format
takes the spelling: lowercase true/false by default, 1/0 for a database flag,
Y/N for a legacy column. (.NET's own True — what true.ToString() and
CkText.BoolToString write — is not valid JSON.)
Rule of thumb: data leaving your process goes through a CommonKit writer, never raw
ToString().
Behind the rules
- Everything is length-capped. A ten-megabyte "name" is rejected, not processed.
Every cap and default is a public constant in
CkSettings(for exampleCkSettings.MaxTextInputLength), so you can read the limits in one place. A cap counts content, not padding: aCHAR(255)column hands"42"back followed by 253 spaces, andCkNumber.Int32OrNullstill reads 42. - Every method is static, pure and thread-safe. No setup, no state, no service to inject — call from anywhere.
- The invariant culture is used everywhere, reading and writing. A comma-decimal locale on the server changes nothing.
- Accents are composed on the way in. A base letter plus a combining mark, the way macOS writes "é", becomes the one composed letter in every reader of every class, so it counts, compares and stores like text typed anywhere else.
Where next
- Your first payload — a fifteen-minute tutorial: a messy order payload to clean, typed values, and the whole handler at the end.
- Common mistakes — the twelve traps everyone hits once.
- Writing a validator — every problem with a payload at once, built on
Explain. - Using it with EF Core — cleaning getters, one save hook, the query rule, column lengths.