Back to blog

Phase 1: C# Fundamentals — Getting Started with C#

csharpdotnetprogrammingfundamentalsbackend
Phase 1: C# Fundamentals — Getting Started with C#

Welcome to Phase 1

Welcome to the first phase of your C# learning journey! If you're coming from Python, Java, JavaScript, or another language, you'll find many familiar concepts here — with a number of pleasant surprises. C# has been evolving rapidly, and modern C# (version 10–12) is a genuinely elegant language.

The goal of this phase is to get you comfortable with C# syntax, understand how .NET approaches fundamental programming concepts, and set up a working development environment.

This is not a "what is a variable?" tutorial. We'll focus on C#-specific syntax, the .NET type system, and modern features that differentiate C# from other languages you may know.

Time commitment: 2 weeks, 1–2 hours daily
Prerequisite: Programming experience in any language

What You'll Learn

By the end of Phase 1, you'll be able to:

✅ Set up a .NET development environment
✅ Write and run C# programs with top-level statements
✅ Understand C#'s value types vs reference types
✅ Use nullable reference types for null safety
✅ Write control flow with modern switch expressions and pattern matching
✅ Define methods with various parameter modifiers
✅ Work with strings, string interpolation, and raw string literals
✅ Create basic classes, records, and properties

Setting Up Your Environment

1. Install .NET SDK

Download the .NET SDK from the official site:

Recommended: .NET 8 LTS (Long-Term Support)

# Verify installation
dotnet --version    # Should show 8.x.x
 
# List installed SDKs
dotnet --list-sdks

On macOS with Homebrew:

brew install dotnet

On Linux (Ubuntu/Debian):

sudo apt-get update && sudo apt-get install -y dotnet-sdk-8.0

2. Choose an IDE

Visual Studio Code + C# Dev Kit (recommended for cross-platform):

  • Free, lightweight, excellent .NET support
  • Install extension: C# Dev Kit (Microsoft)
  • Install extension: .NET Install Tool

Visual Studio 2022 (Windows/macOS):

JetBrains Rider (cross-platform):

  • Best refactoring and code analysis
  • Paid (30-day free trial)

3. Create Your First Project

# Create a new console application
dotnet new console -n MyFirstApp
cd MyFirstApp
 
# Run it
dotnet run

The generated Program.cs uses top-level statements (C# 9+):

// Program.cs — no class or Main method needed!
Console.WriteLine("Hello, C#!");

This is a major quality-of-life improvement over older C#, where you had to write:

// Old style (still valid, but verbose)
namespace MyFirstApp;
 
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, C#!");
    }
}

Both are valid. This series uses the modern style.

The .NET Type System

C# is statically typed and strongly typed. Every variable has a type, checked at compile time.

Value Types vs Reference Types

This is the most fundamental concept in C#. Unlike Python or JavaScript where everything is an object, C# distinguishes between two categories:

Value types store data directly on the stack. Copying a value type copies the data:

int a = 10;
int b = a;   // b gets a copy of the value
b = 20;
Console.WriteLine(a); // 10 — unchanged

Reference types store a reference (pointer) on the stack; the data lives on the heap. Copying a reference type copies the reference, not the data:

var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;   // list2 points to the same list
list2.Add(4);
Console.WriteLine(list1.Count); // 4 — both variables see the change

Built-in Value Types

// Integer types
byte   b = 255;          // 8-bit unsigned: 0 to 255
short  s = 32_000;       // 16-bit signed
int    i = 2_000_000;    // 32-bit signed (most common)
long   l = 10_000_000_000L; // 64-bit signed
uint   u = 4_000_000_000u;  // 32-bit unsigned
 
// Floating-point types
float  f = 3.14f;        // 32-bit (use 'f' suffix)
double d = 3.14159265;   // 64-bit (default for decimals)
decimal m = 19.99m;      // 128-bit, exact — use for money!
 
// Other value types
bool   flag = true;
char   c = 'A';          // Unicode character

Tip for Python/JS devs: Use int for integers, double for general decimals, and decimal for financial calculations (it avoids floating-point precision issues).

Type Inference with var

C# supports local type inference:

var name = "Alice";         // string
var count = 42;             // int
var price = 9.99m;          // decimal
var items = new List<string>(); // List<string>

var is compile-time inference — the type is fixed at compile time. Use it when the type is obvious from the right-hand side. Avoid var when the type isn't clear:

var result = GetData(); // What type? Avoid this

Nullable Reference Types

One of C#'s most powerful modern features. Enable it in your .csproj:

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

Now the compiler tracks nullability:

string name = "Alice";      // Cannot be null
string? nickname = null;    // Can be null (note the '?')
 
// Compiler warns you before null dereferencing
Console.WriteLine(name.Length);      // OK
Console.WriteLine(nickname.Length);  // Warning: possible null reference
 
// Safe patterns
Console.WriteLine(nickname?.Length ?? 0); // Null-conditional + null-coalescing

This catches null reference exceptions at compile time rather than runtime — a huge improvement over Java or Python.

Variables and Constants

// Declaration and initialization
int count = 0;
string message = "Hello";
 
// Inferred
var user = "Alice";
 
// Constants (compile-time, must be a literal)
const int MaxRetries = 3;
const string AppName = "MyApp";
 
// Readonly (runtime, can be computed)
readonly DateTime startTime = DateTime.Now;

Naming conventions in C#:

ElementConventionExample
VariablescamelCaseuserName, totalCount
ConstantsPascalCaseMaxRetries, DefaultTimeout
MethodsPascalCaseGetUser(), CalculateTotal()
ClassesPascalCaseUserService, OrderItem
InterfacesIPascalCaseIRepository, ILogger
Private fields_camelCase_name, _count

Key difference from Java: C# uses PascalCase for constants and public methods, not UPPER_SNAKE_CASE or camelCase.

Control Flow

if / else

int score = 85;
 
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");

Switch Expressions (C# 8+)

Modern C# replaces the verbose switch statement with a concise expression:

// Old switch statement
string grade;
switch (score / 10)
{
    case 10:
    case 9: grade = "A"; break;
    case 8: grade = "B"; break;
    case 7: grade = "C"; break;
    default: grade = "F"; break;
}
 
// Modern switch expression — much cleaner
string grade = (score / 10) switch
{
    >= 9 => "A",
    8    => "B",
    7    => "C",
    _    => "F"   // _ is the discard/default pattern
};

Pattern Matching

C# pattern matching is more expressive than Java's instanceof:

object obj = "Hello, World!";
 
// Type pattern
if (obj is string text)
    Console.WriteLine($"String of length {text.Length}");
 
// Property pattern
if (obj is string { Length: > 5 } longText)
    Console.WriteLine($"Long string: {longText}");
 
// Switch with patterns
string description = obj switch
{
    int n when n > 0 => "positive integer",
    int n when n < 0 => "negative integer",
    string s         => $"string: {s}",
    null             => "null",
    _                => "something else"
};

Loops

// for loop
for (int i = 0; i < 5; i++)
    Console.WriteLine(i);
 
// foreach (most common for collections)
string[] fruits = ["Apple", "Banana", "Orange"]; // C# 12 collection expression
foreach (var fruit in fruits)
    Console.WriteLine(fruit);
 
// while
int count = 0;
while (count < 5)
{
    Console.WriteLine(count);
    count++;
}
 
// do-while
do
{
    Console.WriteLine("Runs at least once");
    count++;
} while (count < 5);

Methods

Methods in C# are always members of a type (class, struct, or record).

Basic Method Syntax

// File-scoped namespace (C# 10+) — less indentation
namespace LearningCSharp;
 
public class Calculator
{
    // Method with return value
    public int Add(int a, int b) => a + b;  // Expression body
 
    // Void method
    public void PrintSum(int a, int b)
    {
        Console.WriteLine($"Sum: {a + b}");
    }
 
    // Method with multiple parameters
    public double Average(int[] numbers)
    {
        if (numbers.Length == 0) return 0;
        return (double)numbers.Sum() / numbers.Length;
    }
}

Parameter Modifiers

C# has powerful parameter modifiers that Java and Python lack:

public class ParameterExamples
{
    // ref — passes by reference (must be initialized)
    public void DoubleIt(ref int value)
    {
        value *= 2;
    }
 
    // out — returns a value via parameter (doesn't need initialization)
    public bool TryDivide(int a, int b, out double result)
    {
        if (b == 0) { result = 0; return false; }
        result = (double)a / b;
        return true;
    }
 
    // in — read-only reference (for large structs, avoids copying)
    public double GetLength(in Vector3 v) => Math.Sqrt(v.X * v.X + v.Y * v.Y + v.Z * v.Z);
 
    // params — variable-length arguments
    public int Sum(params int[] numbers) => numbers.Sum();
}
 
// Usage
var calc = new ParameterExamples();
 
int x = 5;
calc.DoubleIt(ref x);
Console.WriteLine(x); // 10
 
if (calc.TryDivide(10, 3, out double quotient))
    Console.WriteLine(quotient); // 3.333...
 
int total = calc.Sum(1, 2, 3, 4, 5); // 15

The TryDivide pattern (returning bool + out parameter) is idiomatic C# and used throughout the .NET standard library (e.g., int.TryParse()).

Optional Parameters and Named Arguments

public void Greet(string name, string greeting = "Hello", bool uppercase = false)
{
    var message = $"{greeting}, {name}!";
    Console.WriteLine(uppercase ? message.ToUpper() : message);
}
 
// Usage
Greet("Alice");                              // "Hello, Alice!"
Greet("Bob", "Hi");                          // "Hi, Bob!"
Greet("Carol", uppercase: true);             // "HELLO, CAROL!"
Greet("Dave", greeting: "Hey", uppercase: true); // "HEY, DAVE!"

Method Overloading

public class Printer
{
    public void Print(int value)    => Console.WriteLine($"int: {value}");
    public void Print(double value) => Console.WriteLine($"double: {value}");
    public void Print(string value) => Console.WriteLine($"string: {value}");
}

Strings

Strings in C# are immutable reference types — they behave like value types in practice.

String Interpolation

string name = "Alice";
int age = 30;
 
// String interpolation (preferred)
string message = $"Name: {name}, Age: {age}";
 
// Formatting inside interpolation
double price = 9.99;
string formatted = $"Price: {price:C}";       // "Price: $9.99"
string padded   = $"{name,10}";               // "     Alice" (right-aligned, width 10)
string dateStr  = $"{DateTime.Now:yyyy-MM-dd}"; // "2026-02-25"

Raw String Literals (C# 11+)

For multi-line strings without escape headaches:

// Raw string literal — no need to escape backslashes or quotes
string json = """
    {
        "name": "Alice",
        "path": "C:\\Users\\Alice"
    }
    """;
 
// Interpolated raw string
string sql = $"""
    SELECT *
    FROM users
    WHERE name = '{name}'
    AND age > {age}
    """;

Common String Methods

string text = "  Hello, C#!  ";
 
text.ToUpper()                // "  HELLO, C#!  "
text.ToLower()                // "  hello, c#!  "
text.Trim()                   // "Hello, C#!"
text.TrimStart()              // "Hello, C#!  "
text.Contains("C#")           // true
text.StartsWith("  Hello")    // true
text.Replace("C#", ".NET")    // "  Hello, .NET!  "
text.Split(',')               // ["  Hello", " C#!  "]
text.Substring(2, 5)          // "Hello"
 
// String comparison (ordinal is fast; use for most cases)
string.Equals("hello", "HELLO", StringComparison.OrdinalIgnoreCase) // true
 
// Check for null or empty
string.IsNullOrEmpty(text)       // false
string.IsNullOrWhiteSpace("  ")  // true

StringBuilder for Concatenation

// Inefficient: creates many intermediate string objects
string result = "";
for (int i = 0; i < 1000; i++)
    result += i + ",";
 
// Efficient: mutable buffer
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
    sb.Append(i).Append(',');
string result = sb.ToString();

Records (C# 9+)

Records are one of C#'s most useful modern features — immutable data containers with value-based equality built in:

// Positional record — concise syntax
public record Person(string Name, int Age);
 
// Usage
var alice = new Person("Alice", 30);
var bob   = new Person("Bob", 25);
var alice2 = alice with { Age = 31 }; // Non-destructive mutation
 
Console.WriteLine(alice);   // Person { Name = Alice, Age = 30 }
Console.WriteLine(alice == new Person("Alice", 30)); // true — value equality!
Console.WriteLine(alice2);  // Person { Name = Alice, Age = 31 }

Records are perfect for DTOs, API responses, and domain value objects. They give you:

  • Immutability by default
  • ToString(), Equals(), GetHashCode(), and == implemented automatically
  • Deconstruction support
  • with expressions for non-destructive updates

Basic Classes and Properties

Auto-Properties

C# properties are cleaner than Java getters/setters:

public class BankAccount
{
    // Auto-property with private setter
    public string Owner { get; private set; }
 
    // Auto-property — readable and writable
    public decimal Balance { get; set; }
 
    // Init-only property (C# 9+) — set only during construction
    public string AccountNumber { get; init; }
 
    public BankAccount(string owner, string accountNumber)
    {
        Owner = owner;
        AccountNumber = accountNumber;
        Balance = 0m;
    }
 
    public void Deposit(decimal amount)
    {
        if (amount <= 0) throw new ArgumentException("Amount must be positive.");
        Balance += amount;
    }
}
 
// Usage
var account = new BankAccount("Alice", "ACC-001");
account.Deposit(100m);
Console.WriteLine(account.Balance); // 100
 
// Object initializer syntax
var account2 = new BankAccount("Bob", "ACC-002") { Balance = 500m };

Primary Constructors (C# 12+)

// Primary constructor — parameters are available throughout the class
public class Point(double x, double y)
{
    public double X { get; } = x;
    public double Y { get; } = y;
 
    public double DistanceTo(Point other)
    {
        var dx = X - other.X;
        var dy = Y - other.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }
 
    public override string ToString() => $"({X}, {Y})";
}
 
var p1 = new Point(0, 0);
var p2 = new Point(3, 4);
Console.WriteLine(p1.DistanceTo(p2)); // 5

File-Scoped Namespaces and Global Usings

Two modern C# features that dramatically reduce boilerplate:

File-Scoped Namespaces (C# 10+)

// Old style — everything indented inside namespace block
namespace MyApp.Services
{
    public class UserService
    {
        // ...
    }
}
 
// New style — namespace applies to the whole file, no braces needed
namespace MyApp.Services;
 
public class UserService
{
    // ...
}

Global Usings (C# 10+)

Add a GlobalUsings.cs file to your project:

// GlobalUsings.cs
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Text;
global using System.Threading.Tasks;

Now all files in your project have these namespaces available without explicit using statements.

Note: .NET 6+ projects have implicit global usings enabled by default. You can see what's included in obj/Debug/net8.0/MyApp.GlobalUsings.g.cs.

How .NET Compiles and Runs Code

The Compilation Pipeline

  1. C# Compiler (Roslyn) compiles .cs files to IL (Intermediate Language)
  2. CLR (Common Language Runtime) loads the IL
  3. JIT Compiler converts IL to native machine code at runtime
  4. On modern .NET, ReadyToRun (R2R) compilation pre-compiles IL for faster startup

This is similar to Java's JVM model, but .NET's AOT (Ahead-of-Time) compilation via dotnet publish -r <rid> --self-contained can produce native executables with no runtime dependency.

Project File (.csproj)

The .csproj file is simple XML:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>
  • <Nullable>enable</Nullable> — enables nullable reference types (always do this)
  • <ImplicitUsings>enable</ImplicitUsings> — enables default global usings

Best Practices for Phase 1

1. Always Enable Nullable Reference Types

<!-- In .csproj -->
<Nullable>enable</Nullable>

Treat compiler warnings about nullability as errors. Address them properly — don't suppress them.

2. Use Expression Bodies for Simple Methods

// Verbose
public int Add(int a, int b)
{
    return a + b;
}
 
// Concise — preferred for simple one-liners
public int Add(int a, int b) => a + b;

3. Use TryParse Instead of Parse

// Bad — throws exception on invalid input
int value = int.Parse(userInput);
 
// Good — returns false instead of throwing
if (int.TryParse(userInput, out int value))
    Console.WriteLine($"Valid: {value}");
else
    Console.WriteLine("Invalid input");

4. Prefer string.IsNullOrWhiteSpace Over != null

// Fragile — misses empty strings and whitespace-only strings
if (name != null) { ... }
 
// Robust
if (!string.IsNullOrWhiteSpace(name)) { ... }

5. Use Records for Data Containers

// Instead of a class with boilerplate getters/setters/equals:
public record Product(string Name, decimal Price, int StockCount);
 
// Immutable, equatable, printable — zero boilerplate

Common Pitfalls

❌ Using == for String Comparison with Different Cases

string a = "hello";
string b = "HELLO";
 
a == b; // false — case-sensitive comparison
 
// ✅ Use StringComparison
string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // true

❌ Not Handling Nulls with Nullable Reference Types Enabled

string? name = GetName(); // might return null
 
Console.WriteLine(name.Length); // Warning: possible null dereference
 
// ✅ Guard against null
if (name is not null)
    Console.WriteLine(name.Length);
 
// ✅ Or use null-conditional operator
Console.WriteLine(name?.Length ?? 0);

❌ Confusing Value Type Copy vs Reference Type Reference

// Value type — copy semantics
int a = 10;
int b = a;
b = 20;
Console.WriteLine(a); // 10 — unchanged
 
// Reference type — shared reference
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;
list2.Add(4);
Console.WriteLine(list1.Count); // 4 — list1 also changed!

❌ Using float for Financial Calculations

float price = 0.1f + 0.2f;
Console.WriteLine(price); // 0.3 — but actually 0.30000001192...
 
// ✅ Use decimal for money
decimal price = 0.1m + 0.2m;
Console.WriteLine(price); // 0.3 — exact

Practice Exercises

Reinforce your learning with these exercises:

  1. Temperature Converter: Convert Celsius ↔ Fahrenheit using a record Temperature(double Value, string Unit) and a switch expression
  2. FizzBuzz with Pattern Matching: Use a switch expression with tuple patterns to implement FizzBuzz
  3. String Analyzer: Count words, sentences, and vowels in a string; use StringBuilder for output
  4. Simple Bank Account: Implement a BankAccount class with Deposit, Withdraw, and Transfer methods; use decimal for money
  5. TryParse Practice: Write a program that reads integers from the console in a loop, using int.TryParse, and prints their sum when the user enters a non-number

Summary

You've covered the essential C# fundamentals:

✅ Set up .NET development environment with SDK and IDE
✅ Understand value types vs reference types and when each is used
✅ Use nullable reference types for compile-time null safety
✅ Write control flow with modern switch expressions and pattern matching
✅ Define methods with ref, out, params, and optional parameters
✅ Work with strings, interpolation, raw string literals, and StringBuilder
✅ Create basic classes with auto-properties and primary constructors
✅ Use records for immutable data containers
✅ Apply file-scoped namespaces and global usings to reduce boilerplate

Next Steps

You're ready to move to Phase 2: Object-Oriented Programming, where you'll learn:

  • Inheritance, method overriding, and the virtual/override/sealed keywords
  • Interfaces, abstract classes, and polymorphism
  • Encapsulation with access modifiers
  • Advanced records and with expressions
  • Extension methods and pattern matching deep-dive

🚀 Continue to Phase 2: Object-Oriented Programming →


Series: C# Learning Roadmap
Previous: C# Learning Roadmap ←
Next: Phase 2: Object-Oriented Programming →

Happy coding! 💜

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.