CkSentinel
Job: turn the library's stand-ins for "nothing" into a real null, for every type at
once — and pass everything real through completely untouched.
A sentinel is a placeholder value that means "nothing here": an empty string, a zero
amount, a 1900-01-01 date, an all-zeros UUID. In memory they are handy, but written to a
database or a JSON payload they become lies — a member who never left should have
"terminatedAt": null, not a year-one date. CkSentinel.ToNull is the one call you make at
that seam. It answers null for every sentinel and hands anything real back exactly as it
came in — same object, same type, nothing converted or cleaned. It is built for an EF Core
save hook that nulls the nullable columns in one place; the full recipe is on the
EF Core page.
Quick look
using SafeCoder.CommonKit;
CkSentinel.ToNull(""); // -> null
CkSentinel.ToNull(0m); // -> null (a zero amount is "no amount" by default)
CkSentinel.ToNull(Guid.Empty); // -> null (the all-zeros UUID)
CkSentinel.ToNull("Mary"); // -> "Mary" (real values pass through untouched)
Here null is not a rejection — it is the honest answer for a value that was only
pretending to be there. Nothing else about the value is ever changed.
Methods
ToNull
Every sentinel to an honest null, one verdict.
object? ToNull(
object? input = null,
bool treatZerosAsMissing = true
)
| Parameter | Type | Default | What it does |
|---|---|---|---|
input |
object? |
null |
The value of any type. |
treatZerosAsMissing |
bool |
true |
True (the default) reads a zero number or amount as missing, so it comes back as null too; pass false to keep zeros while text, dates and GUIDs are still checked. |
Returns
| When | You get |
|---|---|
The value is a sentinel: null itself or DBNull, empty or whitespace-only text, a zero number or amount (while the flag is on), NaN or an infinity, a placeholder or year-one date, Guid.Empty |
null |
| The value is real — or any type not on the list | The very same value, untouched, same box: 42 stays 42 |
It never throws — every input has an answer, so null here is honest, not a rejection.
Give it a value of any type (the parameter is input, an object?). If the value is one of
the library's sentinels, you get null; otherwise you get the very same value back. What
counts as a sentinel:
nullitself, andDBNull— the special value a database driver hands you for a NULL cell;- empty and whitespace-only text;
- zero in any number type — the thirteen fixed-size ones and the native-sized
nintandnuint— and a zerodecimalamount (see the zeros flag below); NaN("not a number") and the infinities — no database column holds those;- placeholder dates such as
1900-01-01and the year-one date, inDateOnly,DateTimeandDateTimeOffsetalike — the same dates CkDate treats as missing; - the all-zeros UUID,
Guid.Empty.
CkSentinel.ToNull(" "); // -> null (whitespace-only text)
CkSentinel.ToNull(DBNull.Value); // -> null (a database NULL)
CkSentinel.ToNull(0); // -> null
CkSentinel.ToNull(double.NaN); // -> null (never a real stored number)
CkSentinel.ToNull(new DateTime(1900, 1, 1)); // -> null (an SQL placeholder date)
CkSentinel.ToNull(DateOnly.MinValue); // -> null (the year-one sentinel)
CkSentinel.ToNull(42); // -> 42
CkSentinel.ToNull(false); // -> false (a boolean has no sentinel)
CkSentinel.ToNull(new TimeOnly(0, 0)); // -> 00:00 (midnight is a real time)
The result type is object?. That fits its natural home — an EF Core save hook, where the
values already travel as objects — so you rarely need to cast. The hook itself is one line
in your DbContext; see EF Core for the copy-paste version.
The zeros flag
Sometimes a zero is real — a balance of exactly 0.00, a count of zero items. Pass
treatZerosAsMissing: false (it defaults to true) and zeros are kept, while text, dates
and GUIDs are still checked.
CkSentinel.ToNull(
input: 0,
treatZerosAsMissing: false
); // -> 0 (a real zero, kept on request)
NaN and the infinities stay null even with the flag off — they can never be a real
stored value.
Good to know
- Real values come back untouched — same object, same everything. A
DateTimekeeps itsDateTimeKind(the tag saying whether it is local time or UTC), aDateTimeOffsetkeeps its offset, digits stay exactly as they were.ToNullonly answers the question "is this a sentinel?" — it never converts. That is the difference fromCkDateTime.ToDateTimeOrNull, which normalizes kinds while parsing: use the reader for parsing, the sentinel for save hooks. - Booleans and times have no sentinel.
falseand midnight are real values and always pass through — there is no boolean or time-of-day that means "nothing". - Types it does not recognize pass through unharmed. A
byte[], aTimeSpan, one of your own classes — anything not on the list above comes back as is.ToNullcan never corrupt a value. DateTimeOffsetis judged likeDateTime. Placeholder and year-one dates count as missing there too.
If you want errors instead
There is nothing to opt into here. ToNull cannot fail — every input has an answer — so it
takes no throwIfInvalid and has no exception type. It is one of the library's few
exception-free members by nature; the classes that clean and convert values, like
CkUuid, are where throwIfInvalid: true lives.