Back to blog
Backend Systemsbeginner

C# Fundamentals: Your First Steps with a Modern Language

Learn C# from scratch: dotnet CLI, data types, control flow, string handling, nullable reference types, and building a working console calculator.

Asma HafeezApril 17, 202615 min read
csharpdotnetbeginnerconsoletypes
Share:𝕏

C# Fundamentals: Your First Steps with a Modern Language

C# is a statically typed, object-oriented language developed by Microsoft and maintained as part of the .NET ecosystem. It runs on Windows, Linux, and macOS, powers everything from web APIs to games (Unity), desktop apps, cloud services, and embedded systems. This guide walks you through the absolute fundamentals and finishes with a working console calculator.


Setting Up: dotnet CLI

Before writing a single line of code, install the .NET SDK from dot.net. Once installed, the dotnet CLI is your main tool.

Bash
# Check installed version
dotnet --version
# 9.0.100

# Create a new console project
dotnet new console -n MyFirstApp
cd MyFirstApp

# Run the project
dotnet run

The project scaffold creates two files:

MyFirstApp/
  MyFirstApp.csproj   ← project configuration (SDK, target framework, deps)
  Program.cs          ← your entry point

Open Program.cs and you will see:

C#
// Program.cs — generated by dotnet new console
Console.WriteLine("Hello, World!");

That single line is valid C# 10+. No using, no class, no static void Main. This is called a top-level statement file.


Top-Level Statements

Prior to C# 9, every program needed a boilerplate entry point:

C#
// Old style (still works, still common in larger codebases)
using System;

namespace MyApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}

C# 9 introduced top-level statements. Only one file in a project may use them. The compiler generates the Main method for you:

C#
// Modern style — no ceremony
Console.WriteLine("Hello, World!");
Console.WriteLine("The args array is still available:");

foreach (string arg in args)
{
    Console.WriteLine($"  arg: {arg}");
}

For learning and small projects, top-level statements are ideal. Larger projects often still use the explicit Main method for clarity.


Data Types

C# has two main categories of types: value types (stored on the stack, copied on assignment) and reference types (stored on the heap, copied by reference).

Numeric Types

C#
// Integer types
byte   b = 255;           // 0 to 255              (8-bit unsigned)
sbyte  sb = -128;         // -128 to 127           (8-bit signed)
short  s = 32_767;        // -32,768 to 32,767     (16-bit)
ushort us = 65_535;       // 0 to 65,535           (16-bit unsigned)
int    i = 2_147_483_647; // ≈ ±2.1 billion        (32-bit) ← most common
uint   ui = 4_294_967_295u;
long   l = 9_223_372_036_854_775_807L; // ≈ ±9.2 × 10^18  (64-bit)
ulong  ul = 18_446_744_073_709_551_615ul;

// Floating-point types
float  f = 3.14f;         // ~7 digits precision   (32-bit)
double d = 3.14159265358979; // ~15–17 digits       (64-bit) ← default for decimals
decimal m = 19.99m;       // 28–29 digits, exact   (128-bit) ← use for money!

Console.WriteLine($"int max: {int.MaxValue}");
Console.WriteLine($"long max: {long.MaxValue}");
Console.WriteLine($"decimal: {decimal.MaxValue}");

Key rule: Use decimal for financial calculations. double and float have rounding errors:

C#
double d1 = 0.1 + 0.2;
Console.WriteLine(d1);          // 0.30000000000000004  ← NOT 0.3!

decimal d2 = 0.1m + 0.2m;
Console.WriteLine(d2);          // 0.3  ← exact

Boolean

C#
bool isActive = true;
bool isDeleted = false;

// Logical operators
bool result = isActive && !isDeleted;  // AND, NOT
bool either = isActive || isDeleted;   // OR

Characters and Strings

C#
char letter = 'A';          // single quotes, single Unicode character
char newline = '\n';        // escape sequences
char tab = '\t';
char unicode = '\u0041';    // Unicode escape — 'A'

string name = "Asma";       // double quotes, sequence of chars
string empty = "";
string alsoEmpty = string.Empty;  // same as ""

The var Keyword

var lets the compiler infer the type from the right-hand side. The type is still static — it is decided at compile time, not at runtime.

C#
var x = 42;             // int
var price = 9.99m;      // decimal
var name = "Learnixo";  // string
var flag = true;        // bool

// The type is locked — this would be a compile error:
// x = "hello";  // ERROR: cannot convert string to int

// var is particularly useful with long generic types:
var products = new Dictionary<string, List<int>>();
// vs:
Dictionary<string, List<int>> products2 = new Dictionary<string, List<int>>();

Use var when the type is obvious from context. Avoid it when it makes code harder to read:

C#
// Good — type is obvious
var message = "Hello";
var count = items.Count();

// Avoid — type is not obvious
var result = GetData();  // What does GetData() return?

String Interpolation

String interpolation (prefix $) is the modern way to embed expressions in strings:

C#
string firstName = "Asma";
string lastName = "Hafeez";
int age = 30;
decimal salary = 85_000.50m;

// Basic interpolation
string greeting = $"Hello, {firstName} {lastName}!";
Console.WriteLine(greeting);
// Hello, Asma Hafeez!

// Expressions inside {}
Console.WriteLine($"In 5 years you will be {age + 5}");
// In 5 years you will be 35

// Format specifiers after a colon
Console.WriteLine($"Salary: {salary:C}");         // Currency: $85,000.50
Console.WriteLine($"Pi: {Math.PI:F4}");           // 4 decimal places: 3.1416
Console.WriteLine($"Date: {DateTime.Now:yyyy-MM-dd}"); // 2026-04-17

// Multi-line interpolated string (C# 11+)
string report = $"""
    Employee: {firstName} {lastName}
    Age:      {age}
    Salary:   {salary:C}
    """;
Console.WriteLine(report);

Verbatim Strings

Prefix @ to treat backslashes as literal characters (no escape processing):

C#
// Without @: need to escape every backslash
string path1 = "C:\\Users\\Asma\\Documents\\file.txt";

// With @: write it naturally
string path2 = @"C:\Users\Asma\Documents\file.txt";

// Multi-line verbatim string
string sql = @"
    SELECT p.Name, p.Price
    FROM Products p
    WHERE p.IsActive = 1
    ORDER BY p.Name";

// Combine @ and $ for interpolated verbatim strings
string folder = "Documents";
string fullPath = $@"C:\Users\Asma\{folder}\report.txt";

Nullable Reference Types

In C# 8+, nullable reference types (NRT) are a major safety feature. Enable them in your .csproj:

XML
<PropertyGroup>
  <Nullable>enable</Nullable>
</PropertyGroup>

Once enabled, string means non-nullable (never null), and string? means nullable (might be null):

C#
// Non-nullable: compiler warns if you might assign null
string name = "Asma";
// name = null;  // WARNING: converting null to non-nullable

// Nullable: you explicitly acknowledge it might be null
string? middleName = null;
string? optionalTitle = GetTitle(); // might return null

// You must check before using a nullable value
if (middleName != null)
{
    Console.WriteLine(middleName.Length);  // safe here
}

// Or use null-conditional operator
Console.WriteLine(middleName?.Length);     // prints nothing if null
Console.WriteLine(middleName?.ToUpper());  // null if middleName is null

Null Coalescing (??)

Provide a default value when something is null:

C#
string? userInput = null;

// If userInput is null, use "Guest" instead
string displayName = userInput ?? "Guest";
Console.WriteLine(displayName);  // Guest

// Chaining
string? a = null;
string? b = null;
string c = "Found it";
string result = a ?? b ?? c;
Console.WriteLine(result);  // Found it

// Null coalescing assignment (??=)
string? config = null;
config ??= "default-value";  // assigns only if config is null
Console.WriteLine(config);   // default-value

Null Conditional (?. and ?[])

Safely navigate through potentially null objects:

C#
string? email = null;

// Without null conditional — throws NullReferenceException!
// int len = email.Length;

// With null conditional — returns null if email is null
int? len = email?.Length;
Console.WriteLine(len);  // (blank — null)

// Chain multiple levels
string? city = user?.Address?.City;

// Null conditional with method calls
string? upper = email?.ToUpper();

// Combine with ??
string display = email?.ToUpper() ?? "NO EMAIL";
Console.WriteLine(display);  // NO EMAIL

// Null conditional indexer
int[]? numbers = null;
int? first = numbers?[0];  // null, not an exception

Control Flow

if / else

C#
int score = 75;

if (score >= 90)
{
    Console.WriteLine("Grade: A");
}
else if (score >= 80)
{
    Console.WriteLine("Grade: B");
}
else if (score >= 70)
{
    Console.WriteLine("Grade: C");
}
else
{
    Console.WriteLine("Grade: F");
}

Ternary Operator

A shorthand for simple if/else that returns a value:

C#
int age = 20;
string status = age >= 18 ? "Adult" : "Minor";
Console.WriteLine(status);  // Adult

// Can nest ternaries (but avoid deep nesting — hard to read)
string grade = score >= 90 ? "A"
             : score >= 80 ? "B"
             : score >= 70 ? "C"
             : "F";

switch Statement

C#
string dayOfWeek = "Monday";

switch (dayOfWeek)
{
    case "Monday":
    case "Tuesday":
    case "Wednesday":
    case "Thursday":
    case "Friday":
        Console.WriteLine("Weekday");
        break;
    case "Saturday":
    case "Sunday":
        Console.WriteLine("Weekend");
        break;
    default:
        Console.WriteLine("Unknown");
        break;
}

switch Expression (C# 8+)

The modern, concise version that returns a value:

C#
string dayType = dayOfWeek switch
{
    "Saturday" or "Sunday" => "Weekend",
    "Monday" or "Tuesday" or "Wednesday" or "Thursday" or "Friday" => "Weekday",
    _ => "Unknown"  // default arm
};
Console.WriteLine(dayType);

// With pattern matching
int number = 42;
string description = number switch
{
    < 0 => "Negative",
    0 => "Zero",
    > 0 and < 10 => "Single digit",
    >= 10 and < 100 => "Double digit",
    _ => "Large number"
};
Console.WriteLine(description);  // Double digit

Arrays

Arrays in C# are fixed-size, zero-indexed collections of the same type:

C#
// Declaration and initialization
int[] numbers = new int[5];       // 5 zeros
int[] primes = { 2, 3, 5, 7, 11 };
int[] evens = new int[] { 2, 4, 6, 8 };

// Accessing elements
Console.WriteLine(primes[0]);  // 2 (first)
Console.WriteLine(primes[4]);  // 11 (last)
Console.WriteLine(primes[^1]); // 11 (last, using index from end)
Console.WriteLine(primes[^2]); // 7  (second from end)

// Modifying
numbers[0] = 10;
numbers[1] = 20;

// Length
Console.WriteLine(primes.Length);  // 5

// Iterating
foreach (int prime in primes)
{
    Console.Write($"{prime} ");  // 2 3 5 7 11
}
Console.WriteLine();

for (int i = 0; i < primes.Length; i++)
{
    Console.WriteLine($"primes[{i}] = {primes[i]}");
}

// Ranges (C# 8+)
int[] slice = primes[1..4];  // { 3, 5, 7 } — indices 1 to 3
int[] last3 = primes[^3..];  // { 5, 7, 11 }
int[] first2 = primes[..2];  // { 2, 3 }

// Multi-dimensional arrays
int[,] grid = new int[3, 3];
grid[0, 0] = 1;
grid[1, 1] = 5;
grid[2, 2] = 9;

// Jagged arrays (array of arrays)
int[][] jagged = new int[3][];
jagged[0] = new int[] { 1 };
jagged[1] = new int[] { 2, 3 };
jagged[2] = new int[] { 4, 5, 6 };

Console I/O

C#
// Writing output
Console.WriteLine("With newline");
Console.Write("Without newline");
Console.Write(" — stays on same line");
Console.WriteLine(); // blank line

// Writing with format (older style, still valid)
Console.WriteLine("Name: {0}, Age: {1}", "Asma", 30);

// Reading input
Console.Write("Enter your name: ");
string? input = Console.ReadLine();  // returns string? — can be null
string name = input ?? "Unknown";
Console.WriteLine($"Hello, {name}!");

// Reading a number
Console.Write("Enter a number: ");
string? raw = Console.ReadLine();
if (int.TryParse(raw, out int parsed))
{
    Console.WriteLine($"You entered: {parsed}");
    Console.WriteLine($"Doubled: {parsed * 2}");
}
else
{
    Console.WriteLine("That wasn't a valid number.");
}

int.TryParse vs int.Parse

C#
// int.Parse — throws FormatException if input is not a number
// int x = int.Parse("abc");  // CRASH

// int.TryParse — returns bool, safe
bool success = int.TryParse("42", out int value);  // success = true, value = 42
bool fail = int.TryParse("abc", out int bad);       // fail = false, bad = 0

// Other TryParse methods
double.TryParse("3.14", out double d);
decimal.TryParse("9.99", out decimal m);
bool.TryParse("true", out bool b);

Project: Console Calculator

Now let's put everything together to build a complete console calculator.

C#
// Program.cs
Console.WriteLine("=== Learnixo Calculator ===");
Console.WriteLine("Operations: +  -  *  /  %  pow");
Console.WriteLine("Type 'quit' to exit\n");

while (true)
{
    // Read first number
    Console.Write("Enter first number: ");
    string? input1 = Console.ReadLine();

    if (input1?.ToLower() == "quit") break;

    if (!double.TryParse(input1, out double num1))
    {
        Console.WriteLine("Invalid number. Try again.\n");
        continue;
    }

    // Read operator
    Console.Write("Enter operator (+, -, *, /, %, pow): ");
    string? op = Console.ReadLine()?.Trim();

    if (op == "quit") break;

    if (string.IsNullOrWhiteSpace(op))
    {
        Console.WriteLine("No operator entered. Try again.\n");
        continue;
    }

    // Read second number
    Console.Write("Enter second number: ");
    string? input2 = Console.ReadLine();

    if (input2?.ToLower() == "quit") break;

    if (!double.TryParse(input2, out double num2))
    {
        Console.WriteLine("Invalid number. Try again.\n");
        continue;
    }

    // Calculate using switch expression
    double? result = op switch
    {
        "+" => num1 + num2,
        "-" => num1 - num2,
        "*" => num1 * num2,
        "/" => num2 != 0 ? num1 / num2 : null,
        "%" => num2 != 0 ? num1 % num2 : null,
        "pow" => Math.Pow(num1, num2),
        _ => null
    };

    // Display result
    if (result is null && (op == "/" || op == "%"))
    {
        Console.WriteLine("Error: Division by zero!\n");
    }
    else if (result is null)
    {
        Console.WriteLine($"Unknown operator: {op}\n");
    }
    else
    {
        // Format nicely: remove trailing zeros for whole numbers
        string formatted = result.Value == Math.Floor(result.Value)
            ? result.Value.ToString("F0")
            : result.Value.ToString("G6");

        Console.WriteLine($"Result: {num1} {op} {num2} = {formatted}\n");
    }
}

Console.WriteLine("Goodbye!");

Sample session:

=== Learnixo Calculator ===
Operations: +  -  *  /  %  pow
Type 'quit' to exit

Enter first number: 10
Enter operator (+, -, *, /, %, pow): /
Enter second number: 3
Result: 10 / 3 = 3.33333

Enter first number: 5
Enter operator (+, -, *, /, %, pow): pow
Enter second number: 3
Result: 5 pow 3 = 125

Enter first number: 10
Enter operator (+, -, *, /, %, pow): /
Enter second number: 0
Error: Division by zero!

Extended Calculator: History Feature

Let's add a history feature using arrays and string interpolation:

C#
// CalculatorWithHistory.cs
const int MaxHistory = 10;
string[] history = new string[MaxHistory];
int historyCount = 0;

void AddToHistory(string entry)
{
    if (historyCount < MaxHistory)
    {
        history[historyCount++] = entry;
    }
    else
    {
        // Shift everything left to make room
        for (int i = 0; i < MaxHistory - 1; i++)
            history[i] = history[i + 1];
        history[MaxHistory - 1] = entry;
    }
}

void ShowHistory()
{
    if (historyCount == 0)
    {
        Console.WriteLine("No history yet.");
        return;
    }

    Console.WriteLine("\n--- Calculation History ---");
    int start = Math.Max(0, historyCount - MaxHistory);
    for (int i = 0; i < historyCount && i < MaxHistory; i++)
    {
        Console.WriteLine($"  {i + 1}. {history[i]}");
    }
    Console.WriteLine("---------------------------\n");
}

// Main loop
Console.WriteLine("=== Calculator with History ===");
Console.WriteLine("Commands: 'history' to view, 'quit' to exit\n");

while (true)
{
    Console.Write("> ");
    string? line = Console.ReadLine()?.Trim();

    if (line?.ToLower() == "quit") break;
    if (line?.ToLower() == "history") { ShowHistory(); continue; }

    // Parse expression like "10 + 5" or "10+5"
    // Simple tokenizer
    string[] parts = line?.Split(' ', StringSplitOptions.RemoveEmptyEntries)
                    ?? Array.Empty<string>();

    if (parts.Length != 3)
    {
        Console.WriteLine("Format: <number> <operator> <number>");
        continue;
    }

    if (!double.TryParse(parts[0], out double a) ||
        !double.TryParse(parts[2], out double b))
    {
        Console.WriteLine("Invalid numbers.");
        continue;
    }

    string oper = parts[1];
    double? res = oper switch
    {
        "+" => a + b,
        "-" => a - b,
        "*" => a * b,
        "/" when b != 0 => a / b,
        "%" when b != 0 => a % b,
        _ => null
    };

    if (res is null)
    {
        Console.WriteLine("Error (bad operator or division by zero).");
    }
    else
    {
        string entry = $"{a} {oper} {b} = {res:G6}";
        Console.WriteLine($"= {res:G6}");
        AddToHistory(entry);
    }
}

Common Mistakes

1. Integer Division

C#
int a = 7;
int b = 2;
int result = a / b;          // 3 — NOT 3.5! Integer division truncates
double correct = (double)a / b;  // 3.5 — cast one side to double first
double also = a / 2.0;           // 3.5 — literal 2.0 forces double division

2. Using == with strings (usually fine but know the rules)

C#
string s1 = "hello";
string s2 = "hello";
Console.WriteLine(s1 == s2);         // true — value comparison for strings
Console.WriteLine(s1.Equals(s2));    // true
Console.WriteLine(object.ReferenceEquals(s1, s2)); // might be true due to interning

// Case-insensitive comparison
Console.WriteLine(s1.Equals(s2, StringComparison.OrdinalIgnoreCase)); // true

3. Forgetting that Console.ReadLine() returns string?

C#
// This causes a nullable warning (and potential NullReferenceException at runtime):
string input = Console.ReadLine();  // WARNING in nullable-enabled context

// Correct:
string? input = Console.ReadLine();
string safe = input ?? "";
// or
string safe2 = Console.ReadLine() ?? "";

4. String concatenation in loops

C#
// Slow — creates a new string object every iteration
string result = "";
for (int i = 0; i < 1000; i++)
    result += i.ToString();  // avoid in loops

// Fast — use StringBuilder
var sb = new System.Text.StringBuilder();
for (int i = 0; i < 1000; i++)
    sb.Append(i);
string result2 = sb.ToString();

Key Takeaways

  • Top-level statements eliminate boilerplate for simple programs — great for learning and scripts.
  • Use decimal for money, double for scientific calculations, int/long for whole numbers.
  • var is still statically typed — the compiler infers the type at compile time.
  • String interpolation ($"...") is the modern and readable way to build strings.
  • Nullable reference types (?) prevent the billion-dollar mistake (null reference exceptions).
  • ?? provides fallback values; ?. safely navigates potentially null objects.
  • switch expressions return values and are more concise than switch statements.
  • TryParse is always safer than Parse when handling user input.

What's Next

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.