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-sdksOn macOS with Homebrew:
brew install dotnetOn Linux (Ubuntu/Debian):
sudo apt-get update && sudo apt-get install -y dotnet-sdk-8.02. 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):
- Full-featured IDE, free Community edition
- Best debugging and refactoring experience
- Download: visualstudio.microsoft.com
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 runThe 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 — unchangedReference 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 changeBuilt-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 characterTip for Python/JS devs: Use
intfor integers,doublefor general decimals, anddecimalfor 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 thisNullable 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-coalescingThis 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#:
| Element | Convention | Example |
|---|---|---|
| Variables | camelCase | userName, totalCount |
| Constants | PascalCase | MaxRetries, DefaultTimeout |
| Methods | PascalCase | GetUser(), CalculateTotal() |
| Classes | PascalCase | UserService, OrderItem |
| Interfaces | IPascalCase | IRepository, ILogger |
| Private fields | _camelCase | _name, _count |
Key difference from Java: C# uses
PascalCasefor constants and public methods, notUPPER_SNAKE_CASEorcamelCase.
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); // 15The 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(" ") // trueStringBuilder 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
withexpressions 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)); // 5File-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
- C# Compiler (Roslyn) compiles
.csfiles to IL (Intermediate Language) - CLR (Common Language Runtime) loads the IL
- JIT Compiler converts IL to native machine code at runtime
- 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 boilerplateCommon 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 — exactPractice Exercises
Reinforce your learning with these exercises:
- Temperature Converter: Convert Celsius ↔ Fahrenheit using a
record Temperature(double Value, string Unit)and a switch expression - FizzBuzz with Pattern Matching: Use a switch expression with tuple patterns to implement FizzBuzz
- String Analyzer: Count words, sentences, and vowels in a string; use
StringBuilderfor output - Simple Bank Account: Implement a
BankAccountclass withDeposit,Withdraw, andTransfermethods; usedecimalfor money - 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/sealedkeywords - Interfaces, abstract classes, and polymorphism
- Encapsulation with access modifiers
- Advanced records and
withexpressions - 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.