Final Project

Reviewed & published by Brayan K

By the end of this capstone you'll have built a complete console Expense Tracker from scratch — classes, a List<T> store, a menu loop, LINQ reports, and a path to saving data — pulling together everything the whole C# course has taught you.

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'll Build

What You'll Practise — and where you learned it

This project is a victory lap. Each feature below maps straight back to a lesson you've already finished — nothing here is new, it's all combined.

Feature in the trackerC# skillLesson
The Expense type (data + constructor)Classes, properties, constructorsOOP
Storing many expensesList<T>, .Add, .CountCollections
The menu loopforeach, switchLoops & Control flow
Totals & category reportsSum, GroupBy, OrderBy, WhereLINQ
Rejecting bad inputthrow, try / catch, TryParseExceptions
Saving & loading dataFile, JSON serialisationFiles & JSON

💡 Real-World Analogy

Up to now you've been learning the individual instruments — the class drums, the List<T> bass, the LINQ keyboards. A finished program is the band playing together. The tune is the user's journey: open the app, see a menu, add an expense, ask for a report, save and quit. Each instrument you learned has a part to play, and your job in this capstone is to conduct them so they make one coherent piece of music — a real, usable tool.

How This Build Works

Real software is never written all at once — you start with a skeleton that runs, then grow it one feature at a time, testing after each. You'll do exactly that here, in three stages:

Stage 1 — The Starter Skeleton

Here is the foundation: an Expense class (the domain model), a List<Expense> to hold many of them, and a menu loop that reacts to choices. To keep the output predictable in this runner, the loop walks a fixed script instead of reading the keyboard — when you build the real app you'll swap that one line for Console.ReadLine(). Read every comment, run it, and notice the listing feature already works; the other features are yours to add.

using System;
using System.Collections.Generic;

// === EXPENSE TRACKER — the starter skeleton ===
// This is the foundation you'll build on. Read it, run it, then in the
// next two steps you'll add the "add expense" and "report" features.

// 1) The DOMAIN MODEL — one class describing a single expense.
//    (This is the OOP lesson: data + behaviour bundled into a type.)
class Expense
{
    public string Category { get; }   // e.g. "Food", "Travel"
    public decimal Amount { get; }    // money -> decimal, never double
    public DateTime Date { get; }     // when it was spent

    // A constructor fills every field in one line, so an Expense is
    // never left half-built (no invalid state).
    public Expense(string category, decimal amount, DateTime date)
    {
        Category = category;
        Amount = amount;
        Date = date;
    }

    // A computed line for printing — derived, no extra storage needed.
    public string Summary() => $"{Date:yyyy-MM-dd}  {Category,-10} £{Amount,8:F2}";
}

class Program
{
    // 2) The DATA STORE — a List<T> that grows as we add expenses.
    //    (This is the Collections lesson.)
    static readonly List<Expense> expenses = new List<Expense>();

    static void Main()
    {
        // 3) Seed a couple of expenses so reports have something to show.
        expenses.Add(new Expense("Food",   12.50m, new DateTime(2026, 6, 1)));
        expenses.Add(new Expense("Travel", 30.00m, new DateTime(2026, 6, 2)));

        // 4) The MENU LOOP — the heartbeat of any console app.
        //    In the live app Console.ReadLine() reads the user's choice;
        //    here we walk a fixed script so the output is predictable.
        string[] script = { "1", "2", "9" };   // pretend the user typed these
        foreach (string choice in script)
        {
            Console.WriteLine($"\n> You chose: {choice}");
            switch (choice)
            {
                case "1": ListExpenses(); break;   // already built below
                case "2": Console.WriteLine("(add expense — you build this next)"); break;
                case "9": Console.WriteLine("Goodbye! 👋"); break;
                default:  Console.WriteLine("Unknown option."); break;
            }
        }
    }

    // 5) A finished feature so you can see the shape of the others.
    static void ListExpenses()
    {
        Console.WriteLine("--- All expenses ---");
        foreach (Expense e in expenses)
            Console.WriteLine(e.Summary());
    }

    // ✅ Expected output (the dates/amounts come from the seeds above):
    //
    //    > You chose: 1
    //    --- All expenses ---
    //    2026-06-01  Food       £   12.50
    //    2026-06-02  Travel     £   30.00
    //
    //    > You chose: 2
    //    (add expense — you build this next)
    //
    //    > You chose: 9
    //    Goodbye! 👋
}

Stage 2 — Your Turn: Add an Expense

First feature to build: adding an expense to the store. An AddExpense method needs to do two things — build a new Expense object, then append it to the list with .Add(...). Fill in the two ___ blanks using the hints, then run it; the program will add three expenses and report the count.

using System;
using System.Collections.Generic;

class Expense
{
    public string Category { get; }
    public decimal Amount { get; }
    public DateTime Date { get; }
    public Expense(string category, decimal amount, DateTime date)
    { Category = category; Amount = amount; Date = date; }
    public string Summary() => $"{Date:yyyy-MM-dd}  {Category,-10} £{Amount,8:F2}";
}

class Program
{
    static readonly List<Expense> expenses = new List<Expense>();

    // 🎯 YOUR TURN — build the "add expense" feature.
    static void AddExpense(string category, decimal amount, DateTime date)
    {
        // 1) Build a new Expense object from the three values.
        Expense item = new ___(category, amount, date);   // 👉 the class name: Expense

        // 2) Append it to the list so it's saved.
        expenses.___(item);                               // 👉 the List method: Add

        Console.WriteLine($"Added: {item.Summary()}");
    }

    static void Main()
    {
        // These three calls drive your method — don't change them.
        AddExpense("Food",    9.99m,  new DateTime(2026, 6, 3));
        AddExpense("Travel", 18.40m,  new DateTime(2026, 6, 4));
        AddExpense("Books",  24.00m,  new DateTime(2026, 6, 5));

        Console.WriteLine($"Total items stored: {expenses.Count}");

        // ✅ Expected output:
        //    Added: 2026-06-03  Food       £    9.99
        //    Added: 2026-06-04  Travel     £   18.40
        //    Added: 2026-06-05  Books      £   24.00
        //    Total items stored: 3
    }
}

Stage 2 — Your Turn: Build a LINQ Report

A list of expenses is only useful once you can summarise it. This is LINQ's moment: Sum gives a grand total, and GroupBy followed by Sum gives a per-category breakdown. Fill in the two ___ blanks (both are the same method!), then run it to see your spending ranked highest-first.

using System;
using System.Collections.Generic;
using System.Linq;                 // LINQ lives here

class Expense
{
    public string Category { get; }
    public decimal Amount { get; }
    public DateTime Date { get; }
    public Expense(string category, decimal amount, DateTime date)
    { Category = category; Amount = amount; Date = date; }
}

class Program
{
    static readonly List<Expense> expenses = new List<Expense>
    {
        new Expense("Food",   12.50m, new DateTime(2026, 6, 1)),
        new Expense("Travel", 30.00m, new DateTime(2026, 6, 2)),
        new Expense("Food",    7.50m, new DateTime(2026, 6, 3)),
        new Expense("Books",  24.00m, new DateTime(2026, 6, 4)),
    };

    static void Main()
    {
        // 🎯 YOUR TURN — build a spending report with LINQ.

        // 1) The grand total: Sum() up the Amount of every expense.
        decimal total = expenses.___(e => e.Amount);    // 👉 the LINQ method: Sum

        // 2) Group expenses by Category, then total each group.
        var byCategory = expenses
            .GroupBy(e => e.Category)                    // one group per category
            .Select(g => new { Category = g.Key, Total = g.___(x => x.Amount) });
                                                         // 👉 same method again: Sum

        Console.WriteLine("--- Spending by category ---");
        foreach (var row in byCategory.OrderByDescending(r => r.Total))
            Console.WriteLine($"{row.Category,-10} £{row.Total:F2}");

        Console.WriteLine($"TOTAL      £{total:F2}");

        // ✅ Expected output:
        //    --- Spending by category ---
        //    Travel     £30.00
        //    Books      £24.00
        //    Food       £20.00
        //    TOTAL      £74.00
    }
}

🔎 Deep Dive: how the pieces connect

It's worth seeing the whole data flow in one glance. The class is the shape of one record; the list is the store; the methods are the verbs that act on the store; LINQ is how you ask questions of it.

Expense (class)        ->  one record:  Category, Amount, Date
List<Expense> expenses ->  the store:    holds many Expense objects
AddExpense(...)        ->  a verb:       expenses.Add(new Expense(...))
ListExpenses()         ->  a verb:       foreach over expenses, print
Report() with LINQ     ->  a question:   expenses.GroupBy(...).Sum(...)
Save()/Load()          ->  persistence:  List <-> JSON file on disk

Every console app you ever write follows this same skeleton: a model, a store, verbs that change the store, and queries that read it. Learn it here and you can build almost any small tool.

Stage 3 — Extend It (on your own)

No blanks now — just a feature checklist in comments. Turn the starter into a finished, persistent app by adding these one at a time, running after each. Every technique you need comes from a lesson you've already completed, so reach back to those when you're stuck.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;                   // for file save/load
// using System.Text.Json;         // uncomment to serialise to JSON

// 🎯 EXTEND IT — turn the starter into a finished app, feature by feature.
// Pick them off one at a time and run after each. No solution is given —
// every skill you need is from a lesson you've already completed.

class Expense
{
    // Already yours from the worked example — reuse it as-is.
    public string Category { get; }
    public decimal Amount { get; }
    public DateTime Date { get; }
    public Expense(string category, decimal amount, DateTime date)
    { Category = category; Amount = amount; Date = date; }
    public string Summary() => $"{Date:yyyy-MM-dd}  {Category,-10} £{Amount,8:F2}";
}

class Program
{
    static readonly List<Expense> expenses = new List<Expense>();

    static void Main()
    {
        // FEATURE 1 — Real input (Variables / Methods lessons)
        //   Replace the fixed script with a real menu loop:
        //   read Console.ReadLine(), and use decimal.TryParse / DateTime.TryParse
        //   so a typo shows a message instead of crashing.
        //
        // FEATURE 2 — Filter by month (LINQ lesson)
        //   Add ExpensesInMonth(int year, int month) that returns
        //   expenses.Where(e => e.Date.Year == year && e.Date.Month == month).
        //
        // FEATURE 3 — Validation with exceptions (Exceptions lesson)
        //   In AddExpense, throw new ArgumentException(...) when amount <= 0,
        //   and catch it in the menu loop so the app keeps running.
        //
        // FEATURE 4 — Save & load (Files / JSON lesson)
        //   Save:  File.WriteAllText("expenses.json", JsonSerializer.Serialize(expenses));
        //   Load:  if (File.Exists(path)) deserialise it back into the list.
        //
        // FEATURE 5 — Top category report (LINQ lesson)
        //   Print the single category you spend the most on:
        //   GroupBy -> Select total -> OrderByDescending -> First().
        //
        // ✅ When all five work you have a complete, persistent expense tracker.

        Console.WriteLine("Build your features above, then test them here. 🚀");
    }
}

Tips for Building Real Projects

Common Pitfalls (and the fix)

📋 Quick Reference — the building blocks

TaskCode
Make a storevar list = new List<Expense>();
Add to itlist.Add(new Expense(...));
Count itemslist.Count
Grand totallist.Sum(e => e.Amount)
Filterlist.Where(e => e.Date.Month == 6)
Group & totallist.GroupBy(e => e.Category)
Safe number inputdecimal.TryParse(s, out var n)
Save to JSONJsonSerializer.Serialize(list)

Frequently Asked Questions

Q: Why does the starter use a fixed script instead of Console.ReadLine()?

So the example produces the same output every time in this in-page runner, which lets you self-check against the expected block. In your own real app on your machine, swap the scripted loop for while (true) reading Console.ReadLine() — that's the first feature in the Extend It step.

Q: This feels big — where do I start?

You already started: run the Stage 1 starter, then do the two guided steps. After that, add Extend It features one at a time and run after each. A program is just many small, tested steps stacked up.

Q: Could I build something other than an expense tracker?

Absolutely — the skeleton (model → list → menu → LINQ → save) fits a to-do list, a contacts book, a mini inventory, or a habit tracker. Swap the Expense class for your own type and keep the same shape.

Q: Why decimal for the amount and not double?

double can't represent values like 0.1 exactly, so money totals drift by tiny fractions. decimal is built for exact base-10 maths, which is what you want for currency. This came up back in the Variables lesson.

Q: How do I make the data survive after I close the app?

Serialise the list to JSON and write it to a file on exit, then read and deserialise it on startup — Feature 4 in the Extend It step. That's the difference between a toy and a tool.

🎉 Course Complete — Congratulations!

You reached the end of the C# course — and you didn't just read about it, you built with it.

Look back at how far you've come. Every concept below is now a tool you can pick up and use, and in this capstone you wired them together into a working program:

You can now design a type, store many of them, drive a program with a loop, query data with LINQ, and save it to disk — that's the core of real software. Keep this expense tracker, finish the Extend It features, push it to GitHub, and make it your first portfolio piece. You're ready to build your own projects. 🚀

🏆 You've completed the entire C# course!

From variables and loops to OOP, LINQ, exceptions, and files — you've covered the full journey and finished by building something real.

Practice quiz

In the expense tracker, what is the role of the Expense class?

  • It stores many expenses
  • It drives the menu loop
  • It is the domain model describing a single expense (Category, Amount, Date)
  • It saves data to disk

Answer: It is the domain model describing a single expense (Category, Amount, Date). The Expense class is the domain model — one type bundling the data (Category, Amount, Date) for a single expense.

Which type holds many Expense objects and grows as you add more?

  • List<Expense>
  • Expense[]
  • Dictionary<string, Expense>
  • string

Answer: List<Expense>. A List<Expense> is the data store; it grows automatically as you Add expenses at runtime.

Why use decimal rather than double for the Amount?

  • decimal is faster
  • double cannot store negatives
  • decimal uses less memory
  • decimal is built for exact base-10 maths, so money totals don't drift

Answer: decimal is built for exact base-10 maths, so money totals don't drift. double can't represent values like 0.1 exactly, so money drifts by tiny fractions. decimal does exact base-10 arithmetic — correct for currency.

Which method appends a new Expense to the list?

  • expenses.Insert()
  • expenses.Add(item)
  • expenses.Append()
  • expenses.Push()

Answer: expenses.Add(item). List<T>.Add(item) appends the new Expense to the end of the list so it's stored.

Which LINQ method gives the grand total of every expense's Amount?

  • Sum(e => e.Amount)
  • Count(e => e.Amount)
  • GroupBy(e => e.Amount)
  • Max(e => e.Amount)

Answer: Sum(e => e.Amount). expenses.Sum(e => e.Amount) totals the Amount across every expense in one call.

To produce a per-category breakdown, you combine which two LINQ operations?

  • Where then Select
  • OrderBy then First
  • GroupBy then Sum
  • Distinct then Count

Answer: GroupBy then Sum. GroupBy(e => e.Category) buckets by category, then Sum on each group totals that category's spending.

Which using directive must be present to call Sum and GroupBy?

  • using System.IO;
  • using System.Linq;
  • using System.Text.Json;
  • using System.Collections.Generic;

Answer: using System.Linq;. Sum, Where, and GroupBy are LINQ extension methods in System.Linq — forgetting it causes CS1061 ('does not contain a definition for Sum').

Why use decimal.TryParse instead of decimal.Parse on user input?

  • TryParse is faster
  • Parse cannot read decimals
  • TryParse rounds the value
  • TryParse returns false on bad input instead of throwing, so a typo doesn't crash the app

Answer: TryParse returns false on bad input instead of throwing, so a typo doesn't crash the app. decimal.Parse("abc") throws; TryParse returns false for invalid input so you can show a message instead of crashing.

How do you make the expense data survive after the app closes?

  • Keep it in a static List forever
  • Serialise the list to JSON and write it to a file, then load it on startup
  • Print it to the console
  • Use a larger List capacity

Answer: Serialise the list to JSON and write it to a file, then load it on startup. Serialise the List to JSON, write it to a file on exit, and deserialise it on startup — that's the difference between a toy and a real tool.

Why is the List<Expense> declared as a static field rather than a local variable inside a method?

  • Static fields are faster
  • Because local lists can't hold objects
  • So every method (AddExpense, ListExpenses, reports) can see and share the same store
  • To force garbage collection

Answer: So every method (AddExpense, ListExpenses, reports) can see and share the same store. A local list declared in one method isn't visible in another (CS0103). A static field on the class lets every method share the one store.

Continue this course

Related lessons