Checkpoint: Types & Data
Reviewed & published by Brayan K
You've learned nullable types, pattern matching, records, structs, tuples, extension methods, DateTime and regex — let's combine them. This checkpoint isn't new material; it's a chance to prove to yourself that these pieces click together into the kind of small, clean module you'd write in a real C# project.
Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You've Learned
- Nullable types & the ?? ?. ??= operators
- Pattern matching & switch expressions
- Records — immutable data with value equality
- Structs — value types vs reference types
- Tuples & deconstruction
- Extension methods (the LINQ pattern)
- DateTime, TimeSpan & safe parsing
- Regular expressions for text patterns
💡 Why These Topics Belong Together
Notice the through-line: this whole track has been about modelling and handling data. Records and structs give data a shape; nullable operators handle missing data; tuples carry small groupings; pattern matching branches on data's shape; extension methods add fluent helpers; and DateTime and regex tame two of the messiest real-world data types — time and text. Master these together and you can model almost any domain cleanly.
Warm-Up
Run this first to shake off the rust. In a handful of lines it uses the ?? operator and a switch expression together — both should feel familiar now.
using System;
class Program
{
static void Main()
{
// A quick warm-up that touches several skills at once.
string? nickname = null;
string display = nickname ?? "Guest"; // ?? from Nullable
int score = 82;
string grade = score switch // switch expression
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
_ => "F"
};
Console.WriteLine($"{display}: grade {grade}"); // Guest: grade B
}
}
// ✅ Expected output:
// Guest: grade B🛠️ Build Challenge: an Order Module
Here's the main event — a multi-step build that ties the track together: a record for orders, an extension method that uses a switch expression with property patterns to compute shipping, a with update, and a LINQ revenue total. Fill in the three ___ blanks and run it. Try it yourself before peeking at the solution below.
using System;
using System.Collections.Generic;
using System.Linq;
// 🎯 BUILD CHALLENGE — a tiny order-processing module
// You will combine: records + pattern matching + a switch expression
// + an extension method. Fill in the ___ blanks.
// 1) A record for an order. Status is one of: "new", "paid", "shipped".
record Order(int Id, string Customer, decimal Total, string Status);
static class OrderExtensions
{
// 2) An EXTENSION METHOD that returns a friendly shipping fee using a
// SWITCH EXPRESSION with PROPERTY PATTERNS:
// cancelled-style "new" with Total > 100 => 0 (free)
// Total > 50 => 0 (free)
// otherwise => 4.99m
public static decimal ShippingFee(this Order o) => o switch
{
{ Total: > 50 } => 0m,
_ => ___ // 👉 4.99m
};
// 3) An extension that returns a one-line label.
public static string Label(this Order o) =>
$"#{o.Id} {o.Customer} — {o.Total:C} ({o.Status})";
}
class Program
{
static void Main()
{
var orders = new List<Order>
{
new(1, "Ana", 120m, "paid"),
new(2, "Ben", 30m, "new"),
new(3, "Cara", 75m, "shipped"),
};
// 4) Print each order's label and its shipping fee.
foreach (var o in orders)
Console.WriteLine($"{o.Label()} ship: {o.ShippingFee():C}");
// 5) Use 'with' to mark order #2 as paid (immutable update):
var paid = orders[1] with { Status = ___ }; // 👉 "paid"
Console.WriteLine($"Updated: {paid.Label()}");
// 6) Total revenue from PAID orders using LINQ:
decimal revenue = orders.Where(x => x.Status == "paid").Sum(x => x.Total);
Console.WriteLine($"Paid revenue: {revenue:C}");
}
}Reveal the Solution
Attempted it? Compare your version against a clean reference. Yours may differ slightly and still be correct — what matters is the same output.
using System.Globalization;
using System;
using System.Collections.Generic;
using System.Linq;
record Order(int Id, string Customer, decimal Total, string Status);
static class OrderExtensions
{
public static decimal ShippingFee(this Order o) => o switch
{
{ Total: > 50 } => 0m,
_ => 4.99m
};
public static string Label(this Order o) =>
$"#{o.Id} {o.Customer} — {o.Total:C} ({o.Status})";
}
class Program
{
static void Main()
{
// Currency formatting follows the thread's culture: {value:C} prints
// £ in London, $ in Boston and € in Paris. Pin it when the output has
// to be the same everywhere — as it does on a page that shows you the
// result.
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");
var orders = new List<Order>
{
new(1, "Ana", 120m, "paid"),
new(2, "Ben", 30m, "new"),
new(3, "Cara", 75m, "shipped"),
};
foreach (var o in orders)
Console.WriteLine($"{o.Label()} ship: {o.ShippingFee():C}");
var paid = orders[1] with { Status = "paid" };
Console.WriteLine($"Updated: {paid.Label()}");
decimal revenue = orders.Where(x => x.Status == "paid").Sum(x => x.Total);
Console.WriteLine($"Paid revenue: {revenue:C}");
}
}
/* Expected output:
#1 Ana — £120.00 (paid) ship: £0.00
#2 Ben — £30.00 (new) ship: £4.99
#3 Cara — £75.00 (shipped) ship: £0.00
Updated: #2 Ben — £30.00 (paid)
Paid revenue: £120.00
*/
// ✅ Expected output:
// #1 Ana — £120.00 (paid) ship: £0.00
// #2 Ben — £30.00 (new) ship: £4.99
// #3 Cara — £75.00 (shipped) ship: £0.00
// Updated: #2 Ben — £30.00 (paid)
// Paid revenue: £120.00Stretch Goals
- 💡 Add a DateTime Placed field to Order and an extension DaysAgo(DateTime today) returning (today - o.Placed).Days.
- 💡 Add a regex extension IsValidId(this string code) matching @"^ORD-\d{4}$".
- 💡 Return a tuple (int Count, decimal Total) from a method that summarises the orders.
- 💡 Convert Order to a readonly record struct and confirm the program still behaves identically.
📝 Checkpoint Quiz
Answer each before expanding. No peeking — these are exactly the things worth remembering.
1. What does x ?? y evaluate to when x is not null?
x. The right-hand side y is only used when x is null.
2. In a switch expression, what does the _ arm represent, and where should it go?
The discard/default — "match anything else". It should be the last arm so it catches everything not handled above and makes the expression exhaustive.
3. Two records have identical property values. Does a == b return True or False?
True — records use value equality, comparing every property. (A normal class would return False via reference equality.)
4. You assign one struct variable to another and change the copy. Does the original change? What about with a class?
For a struct, the original is unchanged (value types are copied). For a class, the original does change, because both variables reference the same object.
5. What makes a static method an extension method, and what's "hi".Shout() shorthand for?
The this modifier on its first parameter. "hi".Shout() is shorthand for StringExtensions.Shout("hi").
6. Why use @"\d+" instead of "\d+" for a regex, and what does ^\d+$ require?
A verbatim string @"..." keeps backslashes literal so the pattern is readable. ^\d+$ requires the whole string to be one or more digits (anchors at start and end).
If You Got Stuck...
- Shaky on ?? / ?.? Revisit Nullable Types & Null Operators.
- Switch arms confusing? Revisit Pattern Matching.
- Unsure about value equality or with? Revisit Records and Structs.
- Extension syntax? Revisit Extension Methods.
- Dates or text patterns? Revisit DateTime and Regular Expressions.
🎉 Checkpoint Complete
- ✅ You combined records, pattern matching, a switch expression and an extension method into one module
- ✅ You used with for immutable updates and LINQ for aggregation
- ✅ You can explain value vs reference semantics and value equality
- ✅ You handled missing data with null operators and validated text with regex
- ✅ You've finished the Types & Data track — fantastic work.
Practice quiz
What does the expression x ?? y evaluate to when x is NOT null?
- y
- null
- x
- false
Answer: x. The null-coalescing operator returns x when x is non-null; y is only used when x is null.
In a switch expression, what does the _ arm represent and where should it go?
- The discard/default 'match anything else'; it goes last
- The first case; it goes at the top
- A null check; placement is irrelevant
- An error case that throws
Answer: The discard/default 'match anything else'; it goes last. _ is the catch-all arm and must be last so the switch expression is exhaustive.
Two records have identical property values. Does a == b return true or false?
- false — reference equality
- It throws an exception
- Only if they are the same reference
- true — records use value equality
Answer: true — records use value equality. Records compare by value across all properties, so equal contents give true (unlike a normal class).
You assign one struct variable to another and change the copy. Does the original change?
- Yes, structs are shared by reference
- No, structs are value types and are copied
- Only if the struct has properties
- Structs cannot be copied
Answer: No, structs are value types and are copied. Structs are value types: assignment makes an independent copy, so the original is untouched.
What makes a static method an extension method?
- The 'this' modifier on its first parameter
- The static keyword on the class
- Returning the same type it extends
- Being placed in a namespace
Answer: The 'this' modifier on its first parameter. An extension method's first parameter is marked with 'this', letting you call it like an instance method.
What does the 'with' expression do on a record, as in order with { Status = "paid" }?
- Mutates the record in place
- Deletes the named property
- Creates a new record copy with the listed properties changed
- Compares two records
Answer: Creates a new record copy with the listed properties changed. 'with' performs a non-destructive copy, producing a new record that differs only in the named members.
Why use a verbatim string @"\d+" instead of "\d+" for a regex pattern?
- It runs faster
- It keeps backslashes literal so the pattern stays readable
- It is required by Regex.Match
- It makes the regex case-insensitive
Answer: It keeps backslashes literal so the pattern stays readable. A verbatim @ string does not process escape sequences, so regex backslashes are written once and read clearly.
What does the regex ^\d+$ require of a string?
- It contains at least one digit somewhere
- It starts with a digit
- It ends with a digit
- The whole string is one or more digits
Answer: The whole string is one or more digits. The ^ and $ anchors require the entire string to consist of one or more digits.
What is a tuple useful for in C#?
- Replacing all classes
- Carrying a small, lightweight grouping of values, often via deconstruction
- Storing key/value lookups
- Pattern matching only
Answer: Carrying a small, lightweight grouping of values, often via deconstruction. Tuples bundle a few related values together cheaply and can be deconstructed into separate variables.
Which LINQ call sums the Total of orders whose Status is "paid"?
- orders.Sum(x => x.Total)
- orders.Count(x => x.Status == "paid")
- orders.Where(x => x.Status == "paid").Sum(x => x.Total)
- orders.Select(x => x.Total).First()
Answer: orders.Where(x => x.Status == "paid").Sum(x => x.Total). Where filters to paid orders first, then Sum adds up their Total values.
Continue this course
- Previous: Regular Expressions
- Next: Operators — Arithmetic, comparison, logical, and null-coalescing operators in C#
- Quick reference: C# cheat sheet