Skip to content

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:

  • null itself, and DBNull — 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 nint and nuint — and a zero decimal amount (see the zeros flag below);
  • NaN ("not a number") and the infinities — no database column holds those;
  • placeholder dates such as 1900-01-01 and the year-one date, in DateOnly, DateTime and DateTimeOffset alike — 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 DateTime keeps its DateTimeKind (the tag saying whether it is local time or UTC), a DateTimeOffset keeps its offset, digits stay exactly as they were. ToNull only answers the question "is this a sentinel?" — it never converts. That is the difference from CkDateTime.ToDateTimeOrNull, which normalizes kinds while parsing: use the reader for parsing, the sentinel for save hooks.
  • Booleans and times have no sentinel. false and 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[], a TimeSpan, one of your own classes — anything not on the list above comes back as is. ToNull can never corrupt a value.
  • DateTimeOffset is judged like DateTime. 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.