Workshop · C# · Beginner
DN050 C# from zero
C# taught the way working teams use it: a small console project in every session, built and run from the terminal in your own VS Code seat with the AI tutor beside you. Forty short steps across eight sessions take you from one printed line to Tally, a tested command-line expense tracker that reads real files, reports by category and tells its caller how it went through exit codes. Every step ends with a check the machine runs on your own code.
Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick DN050 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.
Who it is for
Adults and teens who have never programmed and want a language that is typed, compiled and used in business software everywhere. You can type and follow a recipe; no maths beyond percentages. If you already code in another language, start at DN100 or take the placement step 8.5.
Where it leads. JV100 Java I · DN100 C# I: the working programmer’s toolkit · PY100 Python I · RS100 Rust I
What you will be able to do
- Create, build and run a console project from the terminal and read a compiler error back to the line it names
- Hold text, whole numbers, doubles and money in the right type, and explain why 0.1 + 0.2 is not 0.3 and why money is decimal
- Format a column of numbers with interpolated strings and format specifiers so a human can read it
- Choose with if/else and switch expressions and repeat with for, foreach and while, without an off-by-one
- Keep things in List<T> and Dictionary<K,V>, walk them, sort them and count them
- Write a static method with parameters and a return value, model data with a record, and test it with xUnit
- Read and write text and CSV files with using, catch the exceptions that matter, and survive a missing file
- Build a small command-line program with arguments and an exit-code contract
Syllabus
Every session ends in a lab the machine checks on your own code. Open a session's mini steps to read each lesson, demo and hands-on task; the badge shows how far the engine can verify it: checked · T3 a check runs over your files, proven · T4 the grader also tries inputs you did not see, tutored · T0 done on your word. Each step has its own link (for example #s1-1).
- 1. Your first programs
- 2. Data: text, numbers and money
- 3. Computing and formatting
- 4. Choices and repetition
- 5. Lists and dictionaries
- 6. Methods, records and tests
- 7. Files and errors
- 8. Capstone: Tally
Session 1 · Your first programs
Five steps that make the seat yours. What a C# program is: a project folder with a .csproj and a Program.cs the compiler turns into something the runtime executes. dotnet new console, dotnet run, and the rhythm of the course: run, read, change, run again. Top-level statements, Console.WriteLine, and reading a compiler error back to the file and line it names. Live in the seat: a project created from the terminal, a deliberate typo and its CS error code, three ways to break a line and what each says, and the first saved program with a comment explaining it.
Lab Create the project dotnet/dn050/s1 and make Program.cs print a three-line banner, a sentence with a computed number and a line built from two pieces of text joined together. It must build clean on the first try after you have read your own code once. Session 1 checkpoint.
5 mini steps · ~39 min
1.1A project, a build, a run8 minchecked · T3
Predict
Program.cs contains one line: Console.WriteLine("Hello"); What appears when you run dotnet run?
Lesson
A C# program is a project: a .csproj that describes it and Program.cs with the lines to run. dotnet new console makes one; dotnet run compiles it and runs it. You learn in one rhythm: run, read the output, change one thing, run again.
Demo
The tutor runs dotnet new console -o dotnet/dn050/demo/hello in the terminal, opens Program.cs, runs dotnet run, changes the text in the quotes, runs again, and points out the build line that appears before the output.
On screen
Terminal → dotnet new console -o dotnet/dn050/demo/hello → cd → dotnet run → edit Program.cs → dotnet run.
Hands-on
Create dotnet/dn050/s1 with dotnet new console. Replace the WriteLine text with words of your own (not Hello), run it, then add a second Console.WriteLine and run again so the terminal shows both runs.
Explain
In one sentence: what did dotnet run do with your project?
Check
s1 builds and prints two lines of your own — checked (T3):
seat_fileseat_run1.2Text and numbers are different things7 minchecked · T3
Predict
What does Console.WriteLine(2 + 3) print, and what does Console.WriteLine("2" + "3") print?
Lesson
C# gives every value a type the compiler knows: string for text, int for whole numbers, double for decimals. The same + adds ints but joins strings, and the compiler refuses to add a string to an int without being told how.
Demo
The tutor prints 2 + 3, "2" + "3", 7 / 2 and 7.0 / 2 in the demo project, then types "2" + 3 and reads the result (the int is converted to text), then 2 + "3" the same way, and names the type of each with .GetType().
Hands-on
In dotnet/dn050/s1/Program.cs replace the lines with four WriteLines: 40 + 2, "40" + "2", 7 / 2, and 7.0 / 2. Run it and compare with your predictions.
Explain
Why did 7 / 2 not give 3.5?
Check
Program prints 42, 402, 3 and 3.5 — checked (T3):
seat_run1.3Reading a compiler error8 minchecked · T3
Predict
A line has Console.WriteLine("Hi" with the closing bracket missing. What does dotnet run do?
Lesson
A compiler error is a map: Program.cs(3,22): error CS1026: ) expected. File, line, column, a code, a reason. Go to the line first, read it slowly, then think about the reason. Fix the first error; later ones often vanish.
Demo
The tutor breaks the demo three ways in turn: a missing bracket, WriteLin instead of WriteLine, "Total: " + 5 * "2"; runs each, reads the error line aloud and shows that fixing the first error removes the cascade.
Hands-on
Copy dotnet/dn050/samples/s1/Oops.cs over dotnet/dn050/s1/Program.cs. It has three errors. Fix them one at a time by reading the message, until it prints "Total: 12" and "Done".
Explain
Which part of a compiler error do you read first, and why?
Check
Program builds and prints Total: 12 and Done — checked (T3):
seat_buildseat_run1.4Variables, comments and the shape of a file7 minchecked · T3
Predict
int price = 5; price = 7; Console.WriteLine(price); What prints?
Lesson
A variable is a typed name for a value: string item = "Notebook"; int qty = 3; double unitPrice = 2.5;. A comment starts with // and is for people. A tidy file has a header comment, names that say what they hold, and one idea per line.
Demo
The tutor writes a header comment and three variables (item, qty, unitPrice), prints a line from them with a $ string, then renames unitPrice to x and back to show how the name reads a month later.
Hands-on
Make dotnet/dn050/s1/Program.cs a receipt line: a // comment on line 1, variables item (string), qty (int) and unitPrice (double); print one line like "3 x Notebook at 2.5" using $"...".
Explain
Why does the compiler ignore a comment?
Check
Program uses three typed variables and a comment — checked (T3):
seat_fileseat_run1.5Lab: the greeting banner9 minchecked · T3
Predict
Console.WriteLine("C" + "#" + " " + (2 + 3)); prints what?
Lesson
Everything from this session in one small program: a header comment, a banner, a number computed and shown in a sentence, two strings joined with +, and the discipline of reading your file once before you build it.
Demo
The tutor builds dotnet/dn050/demo/banner live: three banner lines of = and a title, a sentence with a computed number, a joined line; runs it; then shows the lab's check output so the learner knows what the grader reads.
Hands-on
Make dotnet/dn050/s1/Program.cs the lab: a // comment on line 1; a banner of exactly three lines (= signs, a title, = signs); one sentence that includes the number 5 computed as 2 + 3; and one line made by joining two string literals with +. Build and run clean on the first try.
Explain
Name one thing you now check in a file before you build it.
Check
Program: three-line banner, a computed 5, a joined line — checked (T3):
seat_fileseat_run
Session 2 · Data: text, numbers and money
Five steps on what a value is and how C# keeps it. string, int, double and decimal, and what each is for. The one fact that prevents a hundred bugs: 0.1 + 0.2 is not 0.3 in a double, so money is decimal. What survives when a number is squeezed into a smaller shape: (int) truncates, Math.Round rounds half to even unless you say otherwise, and decimal keeps the scale you typed. Strings you can measure, cut, upper-case and search. Live in the seat: 0.1 + 0.2 printed to 17 places, 1234.56 cast to an int, Math.Round(2.5) and Math.Round(3.5), and a receipt total that is right to the cent only in decimal.
Lab Write the receipt: three items with a decimal unit price and an int quantity, line totals, a subtotal, 8% tax rounded to cents away from zero, and a grand total, all printed with two decimals. The numbers must match the published receipt to the cent. Session 2 checkpoint.
5 mini steps · ~40 min
2.1Four types you will use every day7 minchecked · T3
Predict
int a = 7; double b = 7; Console.WriteLine(a / 2 + " " + b / 2); prints what?
Lesson
string holds text, int whole numbers, double approximate decimals for science and screens, decimal exact decimals for money. The type decides what / means: int division throws away the remainder; double and decimal keep it.
Demo
In dotnet/dn050/demo/types the tutor declares one of each type, prints them with .GetType().Name, divides 7 by 2 in all three numeric types, and shows the compiler refusing to put a double into an int without a cast.
Hands-on
In dotnet/dn050/s2/Program.cs (new console project) declare string name, int count, double ratio and decimal price; print each with its type name, then print 7 / 2, 7.0 / 2 and 7m / 2.
Explain
When would you pick decimal over double?
Check
Program prints the four types and three divisions — checked (T3):
seat_fileseat_run2.2Why 0.1 + 0.2 is not 0.38 minchecked · T3
Predict
Console.WriteLine(0.1 + 0.2 == 0.3); prints what?
Lesson
A double stores a binary fraction, and 0.1 has no finite binary form, so tiny errors creep in and add up. decimal stores base-ten digits, so 0.1m + 0.2m is exactly 0.3m. Rule: money and anything you compare for equality in base ten is decimal; measurements can be double.
Demo
The tutor prints 0.1 + 0.2 with the "R" and "F17" formats, adds 0.1 a thousand times in a double and in a decimal and prints both totals, then prints 0.1m + 0.2m == 0.3m.
Hands-on
In dotnet/dn050/s2/Program.cs print 0.1 + 0.2 == 0.3, then 0.1m + 0.2m == 0.3m, then a loop that adds 0.1 one thousand times as a double and as a decimal and prints both totals.
Explain
Why is decimal used for money here?
Check
Program shows False, True and the two thousand-sum totals — checked (T3):
seat_run2.3What survives: casts, Math.Round and scale9 minchecked · T3
Predict
decimal gross = 12345.67m; Console.WriteLine((int)gross); prints what?
Lesson
When a number moves into a smaller shape, the shape decides what survives. (int) drops the fraction. Math.Round rounds half to even by default (2.5 → 2, 3.5 → 4); MidpointRounding.AwayFromZero gives school rounding. A decimal keeps the scale you give it: 2.50m prints 2.50.
Demo
The tutor prints (int)12345.67m, Math.Round(2.5), Math.Round(3.5), Math.Round(2.675m, 2), Math.Round(2.675m, 2, MidpointRounding.AwayFromZero), then 2.5m and 2.50m to show the scale is kept.
Hands-on
In dotnet/dn050/s2/Program.cs declare decimal gross = 12345.67m and print: (int)gross, Math.Round(gross), Math.Round(gross, 1), Math.Round(2.5m), Math.Round(2.5m, MidpointRounding.AwayFromZero), and gross / 1000.
Explain
Why is the lost fraction in a cast silent?
Check
Program prints the six rounding results — checked (T3):
seat_run2.4Strings you can measure and cut7 minchecked · T3
Predict
string s = "Notebook"; Console.WriteLine(s.Length + " " + s.Substring(0, 4).ToUpper()); prints?
Lesson
A string knows its Length, can be cut with Substring, changed to upper or lower case, trimmed, searched with Contains and IndexOf, and split with Split. Strings never change in place: each method hands back a new one.
Demo
The tutor takes " Agile System Software " through Trim, Length, ToUpper, Contains("System"), IndexOf("S"), Split(' ') and shows the original is unchanged after all of it.
Hands-on
In dotnet/dn050/s2/Program.cs take the string " Pocket Notebook " and print: its trimmed form, the trimmed Length, the first word in upper case, whether it contains "Note", and the position of "book".
Explain
Why does s.ToUpper() not change s?
Check
Program prints the five string results — checked (T3):
seat_run2.5Lab: the receipt9 minchecked · T3
Predict
Subtotal 25.60m, tax 8% rounded to cents away from zero. What is the tax?
Lesson
The receipt is the money lesson in one program: decimal everywhere, line totals as price times quantity, tax rounded once at the end to cents with MidpointRounding.AwayFromZero, and :F2 so every amount shows two decimals.
Demo
The tutor builds the receipt in dotnet/dn050/demo/receipt with fixed items, prints each line with :F2, computes the tax two ways (unrounded, rounded) to show the cent that moves, and prints the grand total.
Hands-on
In dotnet/dn050/s2/Program.cs: items Notebook 2.50m × 3, Pen 1.20m × 5, Folder 4.95m × 2 as decimals and ints; print a line per item "Notebook 3 x 2.50 = 7.50", then "Subtotal 23.40", "Tax 1.87" (8%, rounded away from zero to cents) and "Total 25.27", all with F2.
Explain
Why round the tax once at the end instead of per item?
Check
The receipt matches to the cent — checked (T3):
seat_fileseat_run
Session 3 · Computing and formatting
Five steps on arithmetic and making output readable. Operators and precedence, remainder with %, and the Math class. Interpolated strings with format specifiers: :F2 for two decimals, :N0 for thousands separators, :P1 for percentages, and alignment with {value,10} so columns line up. Reading a number typed at the terminal with Console.ReadLine and turning it into a value with decimal.TryParse. Live in the seat: 3725 seconds becoming 1:02:05, a right-aligned column of prices, a percentage that was 0.085 printed as 8.5 %, and a tip calculator that refuses "abc" politely.
Lab Write the tip calculator: bill and tip percentage from the command line, tip rounded to cents, total, and a split across a number of people, printed as an aligned table. Wrong input prints a message and exits with code 2. Session 3 checkpoint.
5 mini steps · ~39 min
3.1Operators, precedence and remainder7 minchecked · T3
Predict
Console.WriteLine(7 + 3 * 2 % 4); prints what?
Lesson
* / and % bind tighter than + and -, and equals bind left to right; brackets win. % is the remainder, the tool for seconds into minutes, odd or even, and wrapping around. ++ and += are shortcuts you will read everywhere.
Demo
The tutor turns 3725 seconds into hours, minutes and seconds with / and %, prints 17 % 5 and -17 % 5, and shows (7 + 3) * 2 against 7 + 3 * 2.
Hands-on
New project dotnet/dn050/s3. Print 7 + 3 * 2, (7 + 3) * 2, 17 % 5, 3725 / 3600, and the H:MM:SS form of 3725 seconds using / and % only.
Explain
Why is % the right tool for the seconds that are left over?
Check
Program prints 13, 20, 2, 1 and 1:02:05 — checked (T3):
seat_run3.2Format specifiers8 minchecked · T3
Predict
decimal r = 0.085m; Console.WriteLine($"{r:P1}"); prints what?
Lesson
After a colon inside the braces comes a format: F2 two decimals, N0 thousands separators, P1 percent, D4 zero-padded, C currency by culture. Formatting rounds for display only; the value is unchanged. Say the culture when a file must be read by a machine (CultureInfo.InvariantCulture).
Demo
The tutor prints 1234567.891m with F2, N2, N0 and C, 0.085m with P1, 42 with D5, and then the same number with InvariantCulture to show the separators that do not move.
Hands-on
In dotnet/dn050/s3/Program.cs print 1234567.891m with :F2 and :N0, 0.085m with :P1, 7 with :D3, and 2.675m with :F2 using CultureInfo.InvariantCulture for all of them.
Explain
Does :F2 change the stored value?
Check
Program prints the five formatted values — checked (T3):
seat_fileseat_run3.3Columns that line up7 minchecked · T3
Predict
$"{"Pen",-8}|{1.2m,6:F2}|" prints what?
Lesson
A comma and a width inside the braces pads a value: {name,-12} left-aligns text in 12 columns, {amount,10:F2} right-aligns a number in 10. Names left, numbers right, header and rule lines the same width: a table a human can scan.
Demo
The tutor prints three items as an aligned table with a header, a dashed rule made with new string('-', 24), and a total row, then misaligns one width on purpose to show the column jump.
Hands-on
In dotnet/dn050/s3/Program.cs print a header "Item Qty Price", a rule of 24 dashes, and three rows for Notebook 3 2.50, Pen 5 1.20, Folder 2 4.95 with widths -12, 3 and 9 (F2).
Explain
Why are numbers right-aligned and names left-aligned?
Check
Program prints an aligned three-row table — checked (T3):
seat_run3.4Reading input without crashing8 minproven · T4
Predict
decimal.TryParse("abc", out var v) returns what, and what is v?
Lesson
Console.ReadLine gives you a string or null. decimal.Parse throws on bad text; decimal.TryParse returns false instead and sets an out variable on success. At a boundary with a human, TryParse plus a clear message beats a crash.
Demo
The tutor reads a line, parses it with Parse and types abc to show the exception, then switches to TryParse with an if/else that prints "That is not a number" and runs both paths.
Hands-on
In dotnet/dn050/s3/Program.cs read the first command-line argument as a decimal with TryParse and InvariantCulture; print "Amount: 12.50" style on success or "Not a number: <text>" otherwise.
Explain
Why prefer TryParse at a human boundary?
Check
Program parses or refuses the argument — proven (T4):
seat_fileseat_run3.5Lab: the tip calculator9 minproven · T4
Predict
Bill 48.00, tip 15%, 3 people. The tip is 7.20 and the total 55.20. What is each share, rounded to cents?
Lesson
The lab is three arguments in, a table out, and a contract: bill, percent and people must parse and be positive or the program prints one line to the error stream and returns 2. Math on decimal, round once per line to cents, align the table.
Demo
The tutor builds dotnet/dn050/demo/tip: TryParse all three, return 2 with a message on failure, compute tip, total and share with Math.Round(x, 2, AwayFromZero), print the aligned table, and runs it with good and bad arguments.
Hands-on
In dotnet/dn050/s3/Program.cs: args bill, percent, people. Print "Bill 48.00", "Tip 7.20", "Total 55.20", "Each 18.40" with the amount right-aligned in 8 columns (F2). Bad or missing input: print "usage: tip <bill> <percent> <people>" to Console.Error and return 2.
Explain
Why return 2 instead of printing an error and returning 0?
Check
The tip calculator tables and refuses correctly — proven (T4):
seat_fileseat_run
Session 4 · Choices and repetition
Five steps on control flow. if, else if and else with &&, || and ! and the trap of = against ==. The switch expression as the clean way to map a value to a result, with a default arm. for when you know how many, while when you do not, foreach over a collection, and the off-by-one that every programmer has written once. Live in the seat: a grade from a score three ways (if chain, switch expression, a wrong switch missing its default), a loop that runs nine times instead of ten, and a quiz that keeps score and stops when the questions run out.
Lab Write the quiz: five fixed questions with answers in the code, the player's answers read from the command line in order, one line per question saying right or wrong with the correct answer, and a final score line with a grade from a switch expression. Session 4 checkpoint.
5 mini steps · ~38 min
4.1if, else if, else7 minproven · T4
Predict
int score = 85; if (score >= 90) g = "A"; else if (score >= 80) g = "B"; else g = "C"; What is g?
Lesson
if tests a bool; else if adds the next test only if the earlier ones failed; else catches the rest. Combine tests with && (both), || (either) and ! (not). == compares; = assigns, and the compiler saves you from mixing them up only when the types do not fit.
Demo
The tutor writes the grade chain for 95, 85, 72 and 40, then a range test with && for "between 60 and 70", then shows the order of the chain matters by swapping two branches.
Hands-on
New project dotnet/dn050/s4. Read an int score from args[0] and print A for 90 and up, B for 80-89, C for 70-79, F below 70, and "out of range" for anything below 0 or above 100.
Explain
Why does the order of the else if branches matter?
Check
Program grades any score — proven (T4):
seat_fileseat_run4.2The switch expression7 minproven · T4
Predict
string d = 3 switch { 1 => "Mon", 2 => "Tue", _ => "?" }; What is d?
Lesson
A switch expression maps one value to one result: value switch { case => result, ..., _ => default }. Arms can be constants, ranges with relational patterns (>= 90 => "A"), or conditions with when. Leave out _ and the compiler warns that some input has no answer.
Demo
The tutor rewrites the grade chain as a switch expression with relational patterns, then maps month numbers to names, then removes the _ arm to show the warning and the exception at runtime.
Hands-on
In dotnet/dn050/s4/Program.cs rewrite the grade as a switch expression with >= patterns and a _ arm, and add a second switch that maps args[1] (1-7) to a weekday name with "unknown" as the default.
Explain
What does the _ arm protect you from?
Check
Program grades and names the day with switch expressions — proven (T4):
seat_fileseat_run4.3for, while and the off-by-one8 minchecked · T3
Predict
for (int i = 1; i < 10; i++) Console.WriteLine(i); How many lines print?
Lesson
for is for a known count: start, condition, step. while repeats as long as a condition holds and must change something inside or run forever. The off-by-one lives in < versus <= and in starting at 0 or 1: say the first and last value out loud before you write the loop.
Demo
The tutor prints 1 to 10 with < 10 and gets nine, fixes it, then writes a while loop that doubles a balance until it passes 1000 and prints the number of doublings, then shows the infinite loop a forgotten increment makes.
Hands-on
In dotnet/dn050/s4/Program.cs print the 7 times table from 7 x 1 to 7 x 12 with a for loop, then use a while loop to count how many times 1 must be doubled to exceed 1000 and print "Doublings: N".
Explain
How do you choose between for and while?
Check
Program prints 12 table rows and Doublings: 10 — checked (T3):
seat_fileseat_run4.4foreach and keeping score7 minproven · T4
Predict
string[] a = { "x", "y", "z" }; int n = 0; foreach (var s in a) n++; What is n?
Lesson
foreach walks a collection without a counter and cannot run past the end. A running score is a variable set before the loop and updated inside it; args is already a collection you can foreach over.
Demo
The tutor foreaches over args, printing each with its position via a separate counter, then counts how many of the arguments parse as numbers and prints "Numbers: N of M".
Hands-on
In dotnet/dn050/s4/Program.cs foreach over args: print "<i>: <arg>" for each (i from 1), count how many parse as int, and print "Numbers: N of M" at the end.
Explain
Why can a foreach never go out of range?
Check
Program lists and counts numeric arguments — proven (T4):
seat_fileseat_run4.5Lab: the quiz9 minproven · T4
Predict
Five questions, the player gets 4 right. Grade by switch: 5 => "Perfect", 4 => "Great", 3 => "Good", _ => "Keep going". What prints?
Lesson
The quiz is arrays of questions and answers, a for loop over them, an if comparing the player's answer from args to the stored one (case-insensitive), a score, and a switch expression for the grade. Fewer answers than questions count as wrong.
Demo
The tutor builds dotnet/dn050/demo/quiz with five capital-city questions, runs it with all right, some right and too few answers, and shows string.Equals with StringComparison.OrdinalIgnoreCase.
Hands-on
In dotnet/dn050/s4/Program.cs store five questions and answers (2+2 → 4; capital of France → Paris; days in a week → 7; colour of grass → green; 10 / 2 → 5). For each, print "Q1 right" or "Q1 wrong (4)"; then "Score: N/5" and the grade from a switch expression. Missing answers count wrong.
Explain
Why compare answers case-insensitively?
Check
The quiz scores any set of answers — proven (T4):
seat_fileseat_run
Session 5 · Lists and dictionaries
Five steps on collections. List<T>: Add, Remove, Count, indexing, Contains and the exception an index out of range throws. Sorting with Sort and OrderBy, and sorting by a key. Dictionary<string, int> for counting and looking up: TryGetValue, ContainsKey and the KeyNotFoundException. Walking a dictionary's keys and values. Live in the seat: a shopping list that grows and shrinks, a sort that changes the list in place against one that does not, a word counter over a sentence, and the lookup that throws because of a capital letter.
Lab Write the shopping list: items from the command line, deduplicated and counted in a dictionary, printed sorted by name with quantities, then the top item by quantity and the total count. Session 5 checkpoint.
5 mini steps · ~39 min
5.1List<T>: add, remove, index7 minchecked · T3
Predict
var xs = new List<int> { 5, 6, 7 }; xs.Remove(6); Console.WriteLine(xs[1]); prints?
Lesson
List<T> is a growable array of one type: Add appends, Remove deletes the first match, RemoveAt by position, Count tells the size, xs[i] indexes from 0. Reading xs[Count] throws ArgumentOutOfRangeException: the last item is xs[Count - 1] or xs[^1].
Demo
The tutor builds a List<string> of groceries, adds, removes, prints Count and the last item with ^1, then indexes past the end to show the exception and the line it names.
Hands-on
New project dotnet/dn050/s5. Make a List<string> of four items, Add a fifth, Remove the second, then print the Count, the first item and the last item (with ^1) on three lines.
Explain
Why is the last item at Count - 1?
Check
Program prints 4 and the first and last items — checked (T3):
seat_fileseat_run5.2Sorting7 minchecked · T3
Predict
var xs = new List<int> { 3, 1, 2 }; var ys = xs.OrderBy(x => x).ToList(); Console.WriteLine(xs[0]); prints?
Lesson
xs.Sort() orders the list in place; xs.OrderBy(key) gives a new ordered sequence and leaves the list alone. Sort by a key by giving OrderBy a lambda (x => x.Length), descending with OrderByDescending, and ThenBy for ties.
Demo
The tutor sorts a list of names with Sort and with OrderBy, prints both to show which one changed, then sorts by length then alphabetically and prints the result on one line with string.Join.
Hands-on
In dotnet/dn050/s5/Program.cs make the list pear, fig, banana, kiwi, apple; print it sorted alphabetically on one line (string.Join(", ")), then sorted by length then name, then the original order to prove OrderBy did not change it.
Explain
When would you use Sort instead of OrderBy?
Check
Program prints the three orderings — checked (T3):
seat_fileseat_run5.3Dictionary<K,V>: look things up8 minproven · T4
Predict
var d = new Dictionary<string, int> { ["pen"] = 2 }; Console.WriteLine(d["Pen"]); What happens?
Lesson
A dictionary maps a unique key to a value: d["pen"] = 2 sets, d["pen"] reads and throws if absent, TryGetValue reads safely, ContainsKey asks. Keys are compared exactly unless you pass StringComparer.OrdinalIgnoreCase to the constructor.
Demo
The tutor builds a price dictionary, reads a key that exists and one that does not, catches the exception, switches to TryGetValue, then rebuilds the dictionary with OrdinalIgnoreCase and reads "Pen".
Hands-on
In dotnet/dn050/s5/Program.cs build a Dictionary<string, decimal> of three prices with OrdinalIgnoreCase, then for the args print "<name>: <price>" with TryGetValue or "<name>: not stocked".
Explain
Why is TryGetValue safer than indexing?
Check
Program looks up prices case-insensitively — proven (T4):
seat_fileseat_run5.4Counting with a dictionary8 minproven · T4
Predict
Counting words in "a b a" with counts[w] = counts.GetValueOrDefault(w) + 1 gives counts["a"] = ?
Lesson
Counting is the classic dictionary loop: for each item, counts[item] = counts.GetValueOrDefault(item) + 1. Walk the result with foreach over the KeyValuePairs, or over Keys sorted, and pick the biggest with OrderByDescending(kv => kv.Value).First().
Demo
The tutor counts the words of a sentence, prints the pairs sorted by key, then prints the most frequent word and its count, then counts letters instead of words with the same five lines.
Hands-on
In dotnet/dn050/s5/Program.cs count the words in args (case-insensitive), print "<word> <count>" sorted by word, then "Top: <word> <count>".
Explain
Why does GetValueOrDefault remove the need for an if?
Check
Program counts words and names the top one — proven (T4):
seat_fileseat_run5.5Lab: the shopping list9 minproven · T4
Predict
Items: milk eggs milk bread eggs milk. Sorted by name, which line comes first and what is the top item?
Lesson
The lab joins a dictionary count, a sort by key, a top-by-value and a total: items in from args, "<item> x<n>" lines sorted by name, "Top: <item> x<n>", "Total: <n>" items. No items at all prints "Nothing to buy" and exits 0.
Demo
The tutor builds dotnet/dn050/demo/shopping, runs it with repeated items and with none, and shows how the total is the sum of the values, not the number of keys.
Hands-on
In dotnet/dn050/s5/Program.cs: count args case-insensitively, print "<item> x<n>" per distinct item sorted by name, then "Top: <item> x<n>" (ties: first alphabetically) and "Total: <sum>". No args → "Nothing to buy".
Explain
Why is Total the sum of the values rather than Count of the dictionary?
Check
The shopping list counts, sorts and totals — proven (T4):
seat_fileseat_run
Session 6 · Methods, records and tests
Five steps that turn scripts into pieces you can trust. Static methods with parameters and return values, and why a method that only computes is the easiest thing in the world to test. Records for data with named fields. A class library next to a console project, and an xUnit test project that references it: dotnet new xunit, [Fact], Assert.Equal, dotnet test. Writing the test first for once. Live in the seat: a Budget method extracted from the receipt, a Transaction record printed and compared, a solution with three projects, a red test turning green, and a test that fails because of a double where a decimal belonged.
Lab Build the Budget library: a Transaction record and static methods Total, ByCategory and Remaining, with xUnit tests for each, all green under dotnet test. The grader adds a hidden test of its own. Session 6 checkpoint.
5 mini steps · ~42 min
6.1Static methods with a return value8 minchecked · T3
Predict
static decimal Tax(decimal amount, decimal rate) => Math.Round(amount * rate, 2); Tax(100m, 0.08m) returns?
Lesson
A method names a piece of work: static decimal Tax(decimal amount, decimal rate) { return ...; }. Parameters come in, one value goes out. A method that depends only on its parameters and changes nothing outside is pure, and pure methods are what tests love.
Demo
The tutor extracts Tax and LineTotal from the session 2 receipt into methods below the top-level code, calls them in the receipt, and shows that nothing in the output changed while the top got shorter.
Hands-on
New project dotnet/dn050/s6. Write static decimal LineTotal(decimal price, int qty) and static decimal Tax(decimal subtotal, decimal rate) (rounded to cents away from zero); print LineTotal(2.50m, 3), Tax(23.40m, 0.08m) and Tax(25.60m, 0.08m).
Explain
What makes a method pure, and why does it matter for testing?
Check
Program prints 7.50, 1.87 and 2.05 from two methods — checked (T3):
seat_fileseat_run6.2Records: data with named fields7 minchecked · T3
Predict
record Tx(string Category, decimal Amount); var a = new Tx("food", 5m); var b = new Tx("food", 5m); Console.WriteLine(a == b);
Lesson
A record declares a bundle of named, read-only fields in one line: record Transaction(DateOnly Date, string Category, decimal Amount). You get a constructor, ToString that prints the fields, value equality and with-expressions for copies that change one field.
Demo
The tutor declares the Transaction record, creates two, prints one (the ToString shows the fields), compares them, makes a copy with { Amount = 9m } and shows the original unchanged.
Hands-on
In dotnet/dn050/s6/Program.cs declare record Transaction(string Category, decimal Amount); make a list of three, print each (its default ToString), then print whether the first equals a new Transaction with the same values.
Explain
Why is a record a better fit than three loose variables?
Check
Program prints three records and True — checked (T3):
seat_fileseat_run6.3A library, a console, a test project9 minchecked · T3
Predict
dotnet new xunit makes a project whose tests run with which command?
Lesson
A real solution splits into a class library (the methods), a console app (the user interface) and an xUnit test project, joined with project references. dotnet new classlib, dotnet new xunit, dotnet add reference, then dotnet test from the solution folder runs everything.
Demo
The tutor creates dotnet/dn050/demo/budget with Budget (classlib), Budget.Cli (console) and Budget.Tests (xunit), adds the two references, moves Tax into the library as a public static method, and runs dotnet test to see 1 passed.
Hands-on
Create dotnet/dn050/budget with three projects: Budget (classlib) holding public static class Money with Tax; Budget.Cli (console) that prints Money.Tax(23.40m, 0.08m); Budget.Tests (xunit) referencing Budget with one [Fact] asserting Tax(100m, 0.08m) == 8.00m. dotnet test must pass.
Explain
Why does the test project reference the library and not the console app?
Check
The budget solution builds and its first test passes — checked (T3):
seat_fileseat_buildseat_test6.4Test first8 minchecked · T3
Predict
You write a [Fact] calling Money.Total(...) before the method exists and run dotnet test. What happens?
Lesson
Test first: write the test that describes the behaviour, watch it fail, write the smallest method that passes, tidy up. Arrange, act, assert in every test; one behaviour per [Fact]; a name that reads like a sentence (Total_sums_amounts).
Demo
The tutor writes Total_sums_amounts against a Total that does not exist, runs dotnet test (red), adds a stub returning 0 (still red), writes the real sum (green), then adds Total_of_empty_is_zero and shows it already passes.
Hands-on
In dotnet/dn050/budget add Transaction(string Category, decimal Amount) to Budget and write two tests first: Total_sums_amounts (three transactions summing to 12.35m) and Total_of_empty_is_zero; then implement public static decimal Total(IEnumerable<Transaction> txs) until both pass.
Explain
Why watch the test fail before making it pass?
Check
Total tests pass — checked (T3):
seat_fileseat_test6.5Lab: the Budget library10 minproven · T4
Predict
Remaining(budget 100m, transactions totalling 120m) should return what, by the lab spec?
Lesson
The lab completes Money: ByCategory groups transactions into a Dictionary<string, decimal> of totals (case-insensitive keys), Remaining is budget minus Total and may be negative. Every method gets at least one test; the grader runs a hidden test with transactions you did not see.
Demo
The tutor writes ByCategory with a dictionary loop and GetValueOrDefault, a test with mixed-case categories, Remaining with an overspend test, runs dotnet test green, then shows the trx report file the grader reads.
Hands-on
In dotnet/dn050/budget add public static Dictionary<string, decimal> ByCategory(IEnumerable<Transaction>) (keys case-insensitive, totals per category) and public static decimal Remaining(decimal budget, IEnumerable<Transaction>), with tests ByCategory_groups_case_insensitively, ByCategory_of_empty_is_empty and Remaining_can_be_negative. All tests green.
Explain
Why does the grader add a test you did not see?
Check
Budget: Total, ByCategory and Remaining pass your tests and the hidden one — proven (T4):
seat_fileseat_test
Session 7 · Files and errors
Five steps on the world outside the program. File.ReadAllLines and File.WriteAllLines for small files; StreamReader inside a using block for big ones, so the file is closed even when something throws. Exceptions: FileNotFoundException, FormatException, and try/catch placed where you can do something about it. CSV by Split with a header row and the quoting rule you will respect by not putting commas in fields. Live in the seat: a ledger file read and summed, a missing file turning into one clear line instead of a stack trace, a bad amount on line 7 reported by line number, and a file written and read back byte for byte.
Lab Write the saved ledger: read transactions from a CSV path given on the command line, report the total and the count, append a new transaction given as arguments, and write the file back. A missing file is created; a malformed line is reported with its number and skipped. Session 7 checkpoint.
5 mini steps · ~40 min
7.1Reading a file7 minchecked · T3
Predict
File.ReadAllLines("notes.txt") on a three-line file returns what?
Lesson
File.ReadAllText gives the whole file as one string; File.ReadAllLines gives an array of lines; both open, read and close for you. Paths are relative to where the program runs, which is the project folder under dotnet run.
Demo
The tutor reads dotnet/dn050/samples/s7/ledger.csv with ReadAllLines, prints the line count and the first two lines, then foreaches the lines to print each with its number.
Hands-on
New project dotnet/dn050/s7. Read the path in args[0] with File.ReadAllLines, print "Lines: N", then each line prefixed with its 1-based number and a colon. Run it on samples/s7/ledger.csv.
Explain
Where does a relative path start from under dotnet run?
Check
Program numbers the lines of ledger.csv — checked (T3):
seat_fileseat_run7.2Splitting CSV lines8 minchecked · T3
Predict
"2026-03-04,food,12.50".Split(',') gives an array of how many parts, and what is parts[2]?
Lesson
A CSV line is fields separated by commas; Split(',') gives strings, and each field still needs parsing: DateOnly.Parse, decimal.Parse with InvariantCulture. Skip the header row. Fields with commas need quoting rules; this course avoids them and says so.
Demo
The tutor parses ledger.csv into Transaction records (date, category, amount), skipping the header with Skip(1), prints each record, and sums the amounts to the published total.
Hands-on
In dotnet/dn050/s7/Program.cs parse args[0] into records (DateOnly Date, string Category, decimal Amount) skipping the header; print "Count: N" and "Total: X" (F2, invariant culture).
Explain
Why must the amount be parsed with InvariantCulture?
Check
Program counts and totals ledger.csv — checked (T3):
seat_fileseat_run7.3When the file is not there8 minproven · T4
Predict
File.ReadAllLines("missing.csv") when the file does not exist does what?
Lesson
An exception unwinds the program until something catches it. Catch the specific type you can handle (FileNotFoundException, FormatException), print one clear line, and decide: create the file, skip the line, or exit with a code. Never catch Exception just to hide a bug.
Demo
The tutor runs the reader on a missing path to show the stack trace, wraps the read in try/catch (FileNotFoundException), prints "No ledger at <path>; starting empty", then catches FormatException around the parse to report "line 4: bad amount 'abc'".
Hands-on
In dotnet/dn050/s7/Program.cs catch FileNotFoundException and print "No ledger at <path>" and return 3; catch FormatException per line and print "line N: bad amount '<text>'" and skip it. Test on samples/s7/ledger_bad.csv.
Explain
Why catch FileNotFoundException and not Exception?
Check
Program survives a missing file and a bad line — proven (T4):
seat_fileseat_run7.4Writing a file and closing it7 minchecked · T3
Predict
using var w = new StreamWriter("out.txt"); w.WriteLine("a"); — when is the file closed?
Lesson
File.WriteAllLines writes an array and closes. For line-by-line work use a StreamWriter or StreamReader inside using so it is closed and flushed whatever happens. Append with File.AppendAllText. A written file is not real until it is closed: forget using and the last lines may be missing.
Demo
The tutor writes three lines with a StreamWriter without using, reads the file back (short), adds using, reads again (complete), then appends a line with AppendAllText and shows the count grow.
Hands-on
In dotnet/dn050/s7/Program.cs write the parsed records back to args[1] as CSV with a header using a StreamWriter in a using block, then read that file back with ReadAllLines and print "Written: N lines".
Explain
What can go wrong if a writer is not disposed?
Check
Program writes the ledger back and reads 6 lines — checked (T3):
seat_fileseat_runseat_file7.5Lab: the saved ledger10 minproven · T4
Predict
ledger add data.csv food 12.50 on a file with 5 rows totalling 92.15. What is the new total line?
Lesson
The ledger has two commands. "show <path>" prints Count and Total; "add <path> <category> <amount>" appends a row dated today and prints the new Count and Total. Missing file: created with a header. Bad lines: reported by number and skipped. Wrong usage: a usage line on stderr and exit 2.
Demo
The tutor builds dotnet/dn050/demo/ledger, runs show, add and show again, deletes the file and runs add to see the header created, then feeds a bad amount to see the line-number report.
Hands-on
In dotnet/dn050/s7/Program.cs implement show and add as specified; write with a using StreamWriter; use InvariantCulture for amounts and DateOnly.FromDateTime(DateTime.Today) formatted yyyy-MM-dd for the date; exit 2 on bad usage, 0 otherwise.
Explain
Why report a bad line by number instead of stopping?
Check
The ledger shows, adds, creates and reports — proven (T4):
seat_fileseat_run
Session 8 · Capstone: Tally
Five steps that build Tally, a command-line expense tracker, from a specification the way a small team would. Reading the spec and sketching the pieces before typing. A walking skeleton: every command recognised, every exit code wired, no business logic. The library of pure methods with tests. The report: totals by category, aligned, sorted by amount. Then the test files and the exit-code contract: 0 all rows good, 2 usage, 3 file problems. Live in the seat: the skeleton returning codes with empty output, a red-green pass on the report, and all five published test files run in a row with their expected outputs compared.
Lab Tally reads a CSV ledger and prints a category report with totals and a grand total, flags malformed rows by line number, and returns 0, 2 or 3 by the contract, on all five published test files and on files you did not see. Session 8 checkpoint and the course placement step.
5 mini steps · ~46 min
8.1Reading the spec8 minartefact · T2
Predict
The spec says "exit 3 when the ledger file cannot be read". A file with one malformed row returns which code?
Lesson
Before code, read the spec twice and write down: the commands, the inputs, the outputs line by line, the exit codes, and which pieces are pure (report maths) and which touch the world (files, console). That list is your plan and your test list.
Demo
The tutor reads dotnet/dn050/samples/s8/TALLY-SPEC.md aloud, builds a table of commands × exit codes on screen, and marks each spec line as library (pure) or CLI (I/O).
Hands-on
Read samples/s8/TALLY-SPEC.md. In dotnet/dn050/tally/PLAN.md write the command table, the exit-code table and a list of the pure methods you will test. The tutor comments, nothing is graded.
Explain
Which parts of Tally will you test with xUnit, and why those?
Check
PLAN.md exists with the two tables — artefact (T2):
seat_file8.2The walking skeleton9 minproven · T4
Predict
tally report with no path given returns which exit code in the skeleton?
Lesson
A walking skeleton is the whole shape with nothing inside: three projects, every command parsed, usage on stderr and exit 2, a file that cannot be opened gives exit 3, everything else prints nothing and returns 0. Now every later step is a small change to a program that already runs.
Demo
The tutor creates Tally (classlib), Tally.Cli (console) and Tally.Tests (xunit) under dotnet/dn050/demo/tally, wires the command switch, returns the codes, and runs the three cases to show the contract already holds.
Hands-on
Create dotnet/dn050/tally with Tally, Tally.Cli and Tally.Tests and references. Tally.Cli: no args or unknown command → usage on stderr, return 2; "report <path>" with a missing file → "Cannot read <path>" on stderr, return 3; otherwise return 0 printing nothing yet.
Explain
Why build the skeleton before any report logic?
Check
The skeleton honours the exit codes — proven (T4):
seat_buildseat_run8.3The library and its tests10 minchecked · T3
Predict
ParseRow("2026-01-05,food,abc", 4) in the spec returns what?
Lesson
The library is pure: ParseRow turns a line and its number into a Transaction or a ParseError (no exceptions escape), Summarise turns transactions into a Report record (totals by category, grand total, count). Tests for each, including the bad-row and empty cases, before the CLI uses them.
Demo
The tutor writes ParseRow tests (good row, bad amount, wrong field count), implements ParseRow, then Summarise tests and implementation, and runs dotnet test green.
Hands-on
In dotnet/dn050/tally/Tally add Transaction, ParseError(int Line, string Reason), ParseRow(string line, int number) and Summarise(IEnumerable<Transaction>) returning Report(Dictionary<string, decimal> ByCategory, decimal Total, int Count). Tests: ParseRow_reads_a_good_row, ParseRow_reports_bad_amount, ParseRow_reports_wrong_field_count, Summarise_totals_by_category, Summarise_of_empty_is_zero.
Explain
Why return a ParseError value instead of throwing?
Check
Library tests pass — checked (T3):
seat_fileseat_test8.4The report9 minchecked · T3
Predict
Categories food 40.00, rent 500.00, fun 12.50 sorted by amount descending: which line is second?
Lesson
The report is the spec's exact text: a header, one line per category with the amount right-aligned in 10 columns (F2), sorted by amount descending then name, a dashed rule, "Total" and "Rows" lines. Format with InvariantCulture so the grader can compare byte for byte.
Demo
The tutor implements Render(Report) in the library (pure: returns a string), tests it against the published expected text for t1.csv, wires report into the CLI, and diffs the output with the expected file.
Hands-on
Add public static string Render(Report r) to Tally producing the spec's layout, a test Render_matches_t1 using samples/s8/t1.expected.txt, and make "report <path>" in Tally.Cli parse, report bad rows to stderr as "line N: <reason>", and print Render.
Explain
Why is Render a pure method that returns a string rather than printing?
Check
report t1.csv matches the published text — checked (T3):
seat_testseat_run8.5Lab: the five test files and the contract10 minproven · T4
Predict
t3.csv has a header and no rows. What does report print and return?
Lesson
Run Tally against the five published files: t1 standard, t2 with two bad rows, t3 empty, t4 missing file, t5 mixed-case categories. Record exit code and output for each in TESTLOG.md. The grader does the same with two files you have not seen and a hidden test.
Demo
The tutor runs the five files in a row, compares each output with its expected text, fixes the category case bug that t5 reveals, and writes the test log.
Hands-on
Make all five published files produce their expected output and exit code; write dotnet/dn050/tally/TESTLOG.md with a row per file (file, exit code, matched yes/no). Categories are case-insensitive and reported in lower case.
Explain
Why does the certificate gate require inputs you did not see?
Check
Tally passes the five published files, two hidden files and the hidden test — proven (T4):
seat_fileseat_testseat_run
Outcome
By the end of the course the tutor's checks will have verified: a built, running program for every hands-on step; eight session labs that each grow the last — the greeting, the receipt, the tip calculator, the quiz, the shopping list, the tested budget methods, the saved ledger; and the Tally capstone, a command-line expense tracker whose totals, category report and exit codes match the published expected values on input files you did not see.
Certificate
Agile System Software Certificate — C# from zero (DN050) attests that you can write, build, test and debug a small command-line C# program that reads and writes files, with methods, records, collections and an exit-code contract, verified by a machine from your own seat.
To earn it:
- Pass the lab check of every session (steps 1.5, 2.5, 3.5, 4.5, 5.5, 6.5 and 7.5) on your own code.
- Pass the Tally capstone check (8.5) including the hidden input files and the hidden xUnit test the grader supplies.
- Score 80% or better on the 20-question reading quiz.
Gates (step · tier required):
Plus a 20-question reading quiz at 80%, retake after 48 h. Certificates are issued once the seat check runner is live; until then every step records what the engine saw.
Environment
A VS Code seat in your browser with the .NET 10 SDK, the console and xUnit templates and the C# extension, ready and offline; nothing to install. Your work lives in dotnet/dn050; the tutor's demo files live apart from yours and the tutor never edits your hands-on file while you are working on it.
Open Coding Tutor at codingtutor.agilesystemsoftware.com, pick DN050 from the course button in the chat and press Tutor me. Every lesson, demo and hands-on step is also published below.