Skip to content

Your first payload

A hands-on tutorial: in about fifteen minutes you will build a tiny program that takes a messy order payload — extra spaces, wrong casing, a price with a dollar sign — and turns it into clean, typed values, without a single try/catch. You type, you run, you see the output. That's the whole plan.

You need the library referenced in a project first — two minutes on Getting started if you haven't done it.

Step 1 — a project to play in

dotnet new console -n PayloadTutorial
cd PayloadTutorial
dotnet add reference ../SafeCoder.CommonKit/SafeCoder.CommonKit.csproj

Open Program.cs, delete what's in it, and follow along — each step adds a few lines.

Step 2 — the messy payload

Real payloads are never clean. Here is the one we'll tame — paste this in:

using SafeCoder.CommonKit;

string body = """
{
  "customer": "  mary-jane  smith ",
  "email": " Mary.Smith@GMAIL.com ",
  "country": "usa",
  "quantity": "3",
  "price": "$19.99",
  "giftNote": "   "
}
""";

Every field has a problem: stray spaces, double spaces, wrong casing, a country that isn't a code, a number that arrived as text, a price wearing a dollar sign, and a "note" that is only whitespace.

Step 3 — wire text into a bag

One call turns the raw text into a dictionary — CommonKit calls it the bag:

Dictionary<string, object?> bag = CkPayload.FromJson(body);

Console.WriteLine($"fields: {bag.Count}");

Run it:

dotnet run
fields: 6

If the text had not been valid JSON you would get an empty bag — never a crash, never a null. We'll prove that in step 7.

Step 4 — typed values out of the bag

CkDictionary reads values out by key. Ask for the type you want:

string rawName  = CkDictionary.String(dictionary: bag, key: "customer");
int quantity    = CkDictionary.Int32(dictionary: bag, key: "quantity");

Console.WriteLine($"name: [{rawName}]");
Console.WriteLine($"quantity: {quantity}");
name: [mary-jane smith]
quantity: 3

Two things just happened quietly. The stray and doubled spaces are already gone — String cleans as it reads. And "3" — a number that arrived as text, like form and JSON values usually do — became a real int.

Step 5 — make each value the right kind of value

The domain classes each know one kind of data — names, emails, countries, money:

string name    = CkText.Name(rawName);
string email   = CkEmail.Address(CkDictionary.String(dictionary: bag, key: "email"));
string country = CkCountry.Alpha2(CkDictionary.String(dictionary: bag, key: "country"));
decimal price  = CkMoney.Amount(CkDictionary.Value(dictionary: bag, key: "price"));

Console.WriteLine($"{name} <{email}> from {country} pays {CkText.NumberToString(price)} each");
Mary-Jane Smith <mary.smith@gmail.com> from US pays 19.99 each

Look at what you did not write: no Trim(), no ToLower(), no Replace("$", ""), no decimal.Parse. Each class did its own job. The one wrapper you did write — CkText.NumberToString around the price — is there because a raw {price} obeys your machine's language settings and can print 19,99; step 8 turns that into a habit.

Notice the two ways a value came out of the bag. CkDictionary.String hands text to the text classes — CkText, CkEmail, CkCountry read a string, and a number in the JSON becomes its digits. CkDictionary.Value hands the value as it is to the classes that judge an object? — CkMoney, CkNumber, CkDate — so a JSON number stays a number.

Step 6 — the honest nothing

The gift note is three spaces — technically present, honestly nothing. CkText.NullIfEmpty gives you the honest answer, ready for a nullable column:

string? giftNote = CkText.NullIfEmpty(CkDictionary.String(dictionary: bag, key: "giftNote"));

Console.WriteLine($"gift note: {giftNote ?? "(none)"}");
gift note: (none)

Step 7 — now break it

The best part. Point the same code at garbage and watch nothing crash:

Dictionary<string, object?> broken = CkPayload.FromJson("{ this is not json");

Console.WriteLine($"broken fields: {broken.Count}");
Console.WriteLine($"broken name: [{CkText.Name(CkDictionary.String(dictionary: broken, key: "customer"))}]");
Console.WriteLine($"broken quantity: {CkDictionary.Int32(dictionary: broken, key: "quantity", fallback: 1)}");
broken fields: 0
broken name: []
broken quantity: 1

The bad JSON became an empty bag. The missing name became "". The missing quantity became the fallback you chose. Every value tells you what happened — and your program is still running.

Step 8 — write it out properly

One last habit — the one you glimpsed in step 5: values that leave your program go through the invariant writers, never raw ToString() — so the output is identical on every machine, no matter its language settings:

decimal total = quantity * price;

Console.WriteLine($"total: {CkText.NumberToString(total)}");
total: 59.97

Step 9 — the whole handler at a glance

That is the shape of every handler you will write with CommonKit. In a real signup endpoint the same steps collapse into one object initializer — here with a birthday added, the one kind of value the order did not have:

Dictionary<string, object?> bag = CkPayload.FromJson(requestBody);

var member = new Member
{
    Name     = CkText.Name(CkDictionary.String(dictionary: bag, key: "name")),
    Email    = CkEmail.Address(CkDictionary.String(dictionary: bag, key: "email")),
    Country  = CkCountry.Alpha2(CkDictionary.String(dictionary: bag, key: "country")),
    Birthday = CkDate.ToDateOnlyOrNull(CkDictionary.Value(dictionary: bag, key: "birthday")),
    Budget   = CkMoney.Amount(CkDictionary.Value(dictionary: bag, key: "monthlyBudget"))
};

Five lines of intake, and every value is clean, typed and safe — with not a single try/catch in sight. Two things to notice:

  • Birthday is a DateOnly? read with the …OrNull twin, so a nullable column gets an honest NULL when the field is missing, unreadable or a placeholder like 1900-01-01. A date such as "07/21/1990" reads month-first by default; pass a DateOrder when your users write day-first.
  • The strings are the plain readers: a bad email is "", and CkText.NullIfEmpty bridges it to null where the column is nullable — or, saving through EF Core, one hook does that for every property at once with CkSentinel.ToNull, wired up on Using it with EF Core.

If you want to reject the request when a required value fails, check the rejection values ("", null, your fallback) — or pass throwIfInvalid: true where a failure should stop the show. To answer the client with every problem at once, in the readers' own words, put a validator in front of this handler.

What you just learned

  • The pipeline: CkPayload gets text off the wire, CkDictionary gets typed values out of the bag, the domain classes make each one the right kind of value, and CkSentinel nulls the placeholders on the way to storage.
  • Nothing throws. Bad input answers "", your fallback, or null — you saw all three.
  • Data leaving the program goes through CkText.NumberToString and friends.

Where next