Console I/O and Top-Level Statements — Writing Your First Programs

Reading user input and writing formatted output are the mechanics of every interactive application. In .NET 6+ with top-level statements, you write a complete, working program without any class or Main method — just code directly in Program.cs. This makes the first programs easier to write and also mirrors how ASP.NET Core’s own Program.cs entry point is structured. This lesson builds a complete interactive calculator, covering Console I/O, safe input parsing with TryParse, the switch statement, loops, and local functions — patterns that appear throughout the full-stack application you will build in this series.

Console Input and Output

// ── Writing output ────────────────────────────────────────────────────────────
Console.WriteLine("Hello!");                      // text + newline
Console.Write("Enter name: ");                    // text WITHOUT newline
Console.WriteLine($"Count: {1_000_000:N0}");      // "Count: 1,000,000"

// Coloured output
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Success!");
Console.ResetColor();                             // ALWAYS reset after colour

// ── Reading input ──────────────────────────────────────────────────────────────
Console.Write("Enter your name: ");
string? raw   = Console.ReadLine();               // returns null when stdin closes
string  input = raw ?? string.Empty;              // treat null as empty string

// ── Safe numeric parsing with TryParse ────────────────────────────────────────
Console.Write("Enter your age: ");
string? ageInput = Console.ReadLine();

if (int.TryParse(ageInput, out int age))
{
    Console.WriteLine($"You are {age} years old.");
}
else
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine("Invalid age — please enter a whole number.");
    Console.ResetColor();
}

// TryParse works for all numeric and date types
double.TryParse("3.14",         out double pi);
decimal.TryParse("19.99",       out decimal price);
DateTime.TryParse("2025-06-15", out DateTime dt);
Note: Top-level statements are not less structured code — the C# compiler generates the full class and Main method from the simplified syntax. Top-level programs support args (command-line arguments), await (async/await from Chapter 13), local functions, and all other language features. Only the entry-point file uses top-level statements; all other classes are defined normally in separate files. This is exactly how ASP.NET Core Program.cs works — it is top-level statements configuring the web host builder.
Tip: Always prefer TryParse() over Parse() for user input or any external data you do not control. int.Parse("abc") throws a FormatException that crashes the program if not caught in a try/catch. int.TryParse("abc", out int n) returns false and sets n = 0, which you handle with a simple if statement. The out keyword passes a variable by reference so the called method can write to it — you can declare the variable inline in the call itself, as shown above.
Warning: Console.ReadLine() returns string? (nullable string) in .NET 6+ with nullable reference types enabled. Calling methods on the return value without a null check produces a compiler warning and risks a NullReferenceException if stdin is redirected from a file that reaches end-of-file. Always handle the null case: string input = Console.ReadLine() ?? string.Empty. This null-coalescing pattern appears throughout ASP.NET Core when reading from potentially-null configuration or request data sources.

Complete Calculator Program

// Program.cs — complete interactive calculator using top-level statements

Console.WriteLine("==============================");
Console.WriteLine("       C# Calculator v1.0     ");
Console.WriteLine("==============================");
Console.WriteLine("Operators: +  -  *  /  %");
Console.WriteLine("Type 'exit' to quit.");
Console.WriteLine();

while (true)
{
    // Read first number
    Console.Write("First number : ");
    string? firstInput = Console.ReadLine()?.Trim();

    if (string.Equals(firstInput, "exit", StringComparison.OrdinalIgnoreCase))
        break;

    if (!double.TryParse(firstInput, out double first))
    {
        PrintError("Please enter a valid number.");
        continue;
    }

    // Read operator
    Console.Write("Operator     : ");
    string op = Console.ReadLine()?.Trim() ?? "";

    // Read second number
    Console.Write("Second number: ");
    if (!double.TryParse(Console.ReadLine()?.Trim(), out double second))
    {
        PrintError("Please enter a valid number.");
        continue;
    }

    // Calculate using switch
    double result;

    switch (op)
    {
        case "+": result = first + second; break;
        case "-": result = first - second; break;
        case "*": result = first * second; break;
        case "%": result = first % second; break;
        case "/":
            if (second == 0) { PrintError("Cannot divide by zero."); continue; }
            result = first / second;
            break;
        default:
            PrintError($"Unknown operator '{op}'. Use +, -, *, /, or %.");
            continue;
    }

    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.WriteLine($"           = {result:G}");
    Console.ResetColor();
    Console.WriteLine();
}

Console.WriteLine("Goodbye!");

// Local function — defined after the top-level code, available throughout
void PrintError(string message)
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine($"  Error: {message}");
    Console.ResetColor();
    Console.WriteLine();
}

Common Mistakes

Mistake 1 — Using Parse() for user input (crashes on invalid data)

❌ Wrong:

int n = int.Parse(Console.ReadLine()!);   // FormatException if user types letters

✅ Correct:

if (!int.TryParse(Console.ReadLine(), out int n))
    Console.WriteLine("Please enter a valid integer.");

Mistake 2 — Not resetting console colour after setting it

❌ Wrong — all output after the error message stays red permanently.

✅ Correct — always call Console.ResetColor() immediately after coloured output, or encapsulate the pattern in a local function as shown in the calculator above.

🧠 Test Yourself

What does the out keyword do in int.TryParse(input, out int result), and why can you declare the variable inline in the call?