Capstone — Building a Small Project

Course 2 · Ch 8 — Capstone
Building a Small Project
A small library loan tracker combining nearly every chapter across both C# courses

Sixteen chapters, two courses — this capstone builds one small, real library loan tracker touching almost all of it: records for the data model, a generic repository queried with LINQ, an interface with a default method, an extension method, an async availability check, and an event for overdue notifications.

Designing the Data Model

public record Book(string Isbn, string Title, int CopiesAvailable); // csharp2-5's positional record public record Loan(string Isbn, string Borrower, DateTime DueDate);

Book and Loan are csharp2-5 records — one line each generates the constructor, equality, and formatting the whole rest of this capstone relies on.

A Custom Exception

public class BookNotAvailableException : Exception { // csharp1-7 — unchecked, like every C# exception public BookNotAvailableException(string isbn) : base($"No copies of book {isbn} are currently available.") {} }

A plain Exception subclass — csharp1-7 established there's no checked/unchecked split to navigate here, unlike Java's own capstone.

A Generic Repository, Queried with LINQ

public class Repository<T> { // csharp2-1's reified generics private readonly List<T> _items = new(); public void Add(T item) => _items.Add(item); public IEnumerable<T> All() => _items; public IEnumerable<T> Where(Func<T, bool> predicate) => _items.Where(predicate); // csharp2-2's LINQ }

One Repository<T> class serves both Repository<Book> and Repository<Loan>. Its own Where method just forwards to LINQ's real .Where()csharp2-7's own reveal, still an extension method underneath, even when wrapped in a method that merely delegates to it.

Interfaces & Default Methods

public interface ILoanNotifier { // csharp1-6 void Notify(string message); void NotifyOverdue(Loan loan) => Notify($"Overdue: {loan.Borrower}, due {loan.DueDate:d}"); // default method }

Extension Methods for Convenience

public static class LoanExtensions { // csharp2-7 public static bool IsOverdue(this Loan loan) => DateTime.Now > loan.DueDate; }

loan.IsOverdue() reads like a real instance method, but Loan is an immutable record — this extension method adds behavior without ever touching the type itself.

Async Availability Checks

public static async Task<bool> CheckAvailabilityAsync(Repository<Book> books, string isbn) { // csharp2-4 await Task.Delay(200); // simulating a remote inventory check var book = books.Where(b => b.Isbn == isbn).FirstOrDefault(); return book is { CopiesAvailable: > 0 }; // csharp2-5's property pattern }

Events for Overdue Notifications

public class LoanDesk { public event Action<Loan>? LoanOverdue; // csharp2-3's event + csharp2-6's nullable reference type public void CheckIn(Loan loan) { if (loan.IsOverdue()) LoanOverdue?.Invoke(loan); // csharp1-3's ?. guard against a null (unsubscribed) event } }

Putting It Together — A Small Run

var books = new Repository<Book>(); books.Add(new Book("001", "C# in Depth", 2)); if (!await CheckAvailabilityAsync(books, "001")) { throw new BookNotAvailableException("001"); } var desk = new LoanDesk(); desk.LoanOverdue += loan => Console.WriteLine($"Reminder needed for {loan.Borrower}"); var loan = new Loan("001", "Alice", DateTime.Now.AddDays(-3)); desk.CheckIn(loan); // prints "Reminder needed for Alice" — the loan is overdue

Chapter Attribution

Capstone pieceChapter
Book / Loan recordscsharp2-5
BookNotAvailableException (unchecked)csharp1-7
Repository<T> genericscsharp2-1
Repository.Where() forwarding to LINQcsharp2-2, csharp2-7
ILoanNotifier interface & default methodcsharp1-6
IsOverdue() extension methodcsharp2-7
CheckAvailabilityAsync + property patterncsharp2-4, csharp2-5
event LoanOverdue + ?.Invoke() guardcsharp2-3, csharp1-3, csharp2-6

What's Still Out of Scope

Honestly: Repository<T>'s internal List<T> isn't thread-safe — csharp2-4's own async work never guaranteed thread-safety, only non-blocking suspension, and this capstone has no genuine concurrent access to worry about. No real persistence — everything is in-memory. No real network call — CheckAvailabilityAsync simulates one with Task.Delay. No automated tests. This capstone proves the pieces fit together, not that the result is production-ready.

A capstone's value is in the seams, not the size
This project is deliberately small — the point was never scale, it was confirming that records, generics, LINQ, events, and async/await genuinely compose cleanly together in real code, not just in isolated chapter examples.
This is a teaching capstone, not a template for a real system
A real loan-tracking system would need persistence, a real inventory API, concurrency-safe storage, and tests — treat this as proof the language features fit together, not as production-ready code to copy.

Coding Challenges

Challenge 1

Add a Return(Loan loan) method to Repository<Loan>-backed code that removes a loan using LINQ to find it first, and demonstrate it removing the correct loan from a repository holding several.

📄 View solution
Challenge 2

Write a LINQ query over a Repository<Loan> that returns only overdue loans, using the IsOverdue() extension method inside the Where() predicate, and print the results using a record's own ToString().

📄 View solution
Challenge 3

Write a short paragraph (as a comment) explaining what would need to change in this capstone's Repository<T> to make it safe for concurrent access from multiple threads, referencing java2-8's own equivalent challenge and explaining any real differences the C# tools bring.

📄 View solution

Chapter 8 Quick Reference — C# Track Complete

  • Records model the domain with free equality, formatting, and deconstruction (csharp2-5)
  • A generic Repository<T> serves every entity type, backed by real reified generics (csharp2-1)
  • LINQ queries the repository declaratively (csharp2-2), itself powered by extension methods (csharp2-7)
  • Interfaces with default methods and a genuinely unchecked custom exception round out the OOP core (csharp1-6, csharp1-7)
  • async/await handles simulated I/O without blocking (csharp2-4); events with a nullable-safe ?.Invoke() guard handle notifications (csharp2-3, csharp2-6)
  • Concurrency safety, persistence, and testing are honestly named as still out of scope
  • Both C# courses are now complete — 16 chapters total, framed against Java, Kotlin, and TypeScript throughout.