Toolmingo
Guides21 min read

C# Tutorial for Beginners (2025): Learn C# Step by Step

Complete C# tutorial for absolute beginners. Learn C# syntax, OOP, async/await, LINQ, and build real projects with .NET 8. Free guide with code examples for 2025.

C# (pronounced "C sharp") is a modern, versatile programming language developed by Microsoft and used for building web APIs, desktop apps, mobile apps, games, and cloud services. As of 2025, C# consistently ranks in the top 5 most-used programming languages worldwide. This tutorial takes you from zero to writing real C# programs — no prior experience required.

What you'll learn

Topic What you'll be able to do
Setup Install .NET and write your first program
Syntax & variables Understand how C# code works
Data types Work with value types, strings, arrays
Control flow Use if/else, loops, switch expressions
Methods & functions Write reusable, clean code
OOP Build classes, interfaces, inheritance
Generics & collections Use List, Dictionary, LINQ
Async/await Handle asynchronous operations
Error handling Use try/catch, custom exceptions
Projects Build a to-do CLI, REST API, and more

Why learn C#?

Use case Example technologies
Web APIs & backends ASP.NET Core, Minimal APIs
Game development Unity (most popular game engine)
Desktop apps WPF, MAUI, Windows Forms
Mobile apps .NET MAUI (iOS + Android)
Cloud & microservices Azure, AWS Lambda with .NET
Enterprise applications Banking, healthcare, insurance
Machine learning ML.NET
IoT / embedded .NET nanoFramework

C# vs other languages quick table:

Language Typing Speed Use case strength
C# Static Fast (near C++) Web, games, enterprise
Java Static Fast Enterprise, Android
Python Dynamic Slow ML, scripting, data
JavaScript Dynamic Medium Browser, Node.js
Go Static Fast Cloud, DevOps
Rust Static Fastest Systems, WASM

1. Setup: Install .NET

Option 1: Download from microsoft.com

  1. Go to dotnet.microsoft.com/download
  2. Download .NET 8 SDK (LTS)
  3. Run the installer
  4. Verify in terminal:
dotnet --version
# 8.0.xxx

Option 2: Package manager

# macOS (Homebrew)
brew install dotnet

# Ubuntu / Debian
sudo apt install dotnet-sdk-8.0

# Windows (winget)
winget install Microsoft.DotNet.SDK.8

Recommended editor

Visual Studio Code with the C# Dev Kit extension (free, cross-platform) — or Visual Studio 2022 Community (Windows/Mac, free).

Create your first project

dotnet new console -n HelloWorld
cd HelloWorld
dotnet run

Output:

Hello, World!

Open Program.cs:

// Program.cs — top-level statements (C# 9+, no class/Main needed)
Console.WriteLine("Hello, World!");

That's a complete C# program. No boilerplate required since C# 9.

2. Variables and data types

Declaring variables

// Explicit type
string name = "Alice";
int age = 30;
double price = 9.99;
bool isActive = true;

// Type inference with var (type still fixed at compile time)
var city = "London";   // string
var count = 42;        // int
var ratio = 3.14;      // double

All primitive types

Type Size Range Example
byte 1 byte 0–255 byte b = 200;
sbyte 1 byte -128–127 sbyte sb = -5;
short 2 bytes -32,768–32,767 short s = 1000;
int 4 bytes -2B to 2B int n = 42;
long 8 bytes ±9.2 quintillion long l = 1_000_000L;
float 4 bytes ±3.4×10³⁸ float f = 3.14f;
double 8 bytes ±1.7×10³⁰⁸ double d = 3.14;
decimal 16 bytes 28–29 digits decimal m = 9.99m;
char 2 bytes Unicode character char c = 'A';
bool 1 byte true/false bool ok = true;

Constants

const double Pi = 3.14159265358979;
const int MaxRetries = 3;

String basics

string first = "Alice";
string last = "Smith";

// Concatenation
string full = first + " " + last;           // "Alice Smith"

// String interpolation (preferred)
string greeting = $"Hello, {first}!";       // "Hello, Alice!"

// Multi-line (raw string literal, C# 11+)
string json = """
    {
        "name": "Alice",
        "age": 30
    }
    """;

// Common string methods
string msg = "  Hello, World!  ";
Console.WriteLine(msg.Trim());              // "Hello, World!"
Console.WriteLine(msg.ToUpper());           // "  HELLO, WORLD!  "
Console.WriteLine(msg.Contains("World"));   // True
Console.WriteLine(msg.Replace("World", "C#")); // "  Hello, C#!  "
Console.WriteLine(msg.Length);              // 17
Console.WriteLine(msg.Substring(8, 5));     // "World" (start, length)
Console.WriteLine(msg.Split(',')[0].Trim()); // "Hello"

Nullable value types

int? age = null;       // nullable int — can be null
double? price = null;  // nullable double

if (age.HasValue)
    Console.WriteLine(age.Value);

// Null-coalescing operator
int displayAge = age ?? 0;       // 0 if null

// Null-coalescing assignment
age ??= 18;                      // assign 18 if null

3. Control flow

if / else if / else

int score = 75;

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

// Ternary operator
string result = score >= 60 ? "Pass" : "Fail";

switch expression (C# 8+, preferred)

string grade = score switch
{
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    >= 60 => "D",
    _     => "F"   // default case
};
Console.WriteLine(grade); // C

for loop

for (int i = 0; i < 5; i++)
    Console.WriteLine(i); // 0 1 2 3 4

// Reverse
for (int i = 4; i >= 0; i--)
    Console.Write(i + " "); // 4 3 2 1 0

while / do-while

int n = 1;
while (n <= 5)
{
    Console.Write(n + " ");
    n++;
}
// 1 2 3 4 5

// do-while (always runs at least once)
do
{
    Console.Write(n + " ");
    n--;
} while (n > 0);

foreach

string[] fruits = { "apple", "banana", "cherry" };

foreach (string fruit in fruits)
    Console.WriteLine(fruit);

// With index using LINQ
foreach (var (fruit, i) in fruits.Select((f, i) => (f, i)))
    Console.WriteLine($"{i}: {fruit}");

break / continue

for (int i = 0; i < 10; i++)
{
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    Console.Write(i + " ");
}
// 0 1 2 4 5 6

4. Methods

// Basic method
static int Add(int a, int b)
{
    return a + b;
}

// Expression body (single expression)
static int Multiply(int a, int b) => a * b;

// Default parameters
static void Greet(string name, string greeting = "Hello")
    => Console.WriteLine($"{greeting}, {name}!");

// Named arguments
Greet(greeting: "Hi", name: "Bob");

// Params (variable number of arguments)
static int Sum(params int[] numbers)
    => numbers.Sum();

Console.WriteLine(Sum(1, 2, 3, 4, 5)); // 15

// Out parameters
static bool TryParse(string s, out int result)
{
    return int.TryParse(s, out result);
}

if (TryParse("42", out int value))
    Console.WriteLine(value); // 42

// Tuple return
static (string Name, int Age) GetPerson()
    => ("Alice", 30);

var (name, age) = GetPerson();
Console.WriteLine($"{name} is {age}"); // Alice is 30

5. Arrays and Collections

Arrays

// Fixed-size array
int[] numbers = { 1, 2, 3, 4, 5 };
string[] names = new string[3]; // ["", "", ""]

// Access
Console.WriteLine(numbers[0]); // 1
numbers[4] = 99;

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

// Multi-dimensional
int[,] matrix = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
Console.WriteLine(matrix[1, 0]); // 3

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

List

var fruits = new List<string> { "apple", "banana" };

fruits.Add("cherry");
fruits.Insert(0, "avocado"); // insert at index
fruits.Remove("banana");     // remove by value
fruits.RemoveAt(1);          // remove by index

Console.WriteLine(fruits.Count);        // 2
Console.WriteLine(fruits.Contains("apple")); // True

// Sort
fruits.Sort();

// Convert array ↔ List
int[] arr = { 3, 1, 2 };
var list = arr.ToList();
int[] back = list.ToArray();

Dictionary<TKey, TValue>

var ages = new Dictionary<string, int>
{
    ["Alice"] = 30,
    ["Bob"] = 25
};

ages["Charlie"] = 35;   // add
ages["Alice"] = 31;     // update

// Safe read
if (ages.TryGetValue("Dave", out int daveAge))
    Console.WriteLine(daveAge);
else
    Console.WriteLine("Not found");

// Iterate
foreach (var (key, val) in ages)
    Console.WriteLine($"{key}: {val}");

// Check key
Console.WriteLine(ages.ContainsKey("Bob")); // True
ages.Remove("Bob");

HashSet

var set = new HashSet<string> { "a", "b", "c" };
set.Add("b"); // ignored — no duplicates
set.Add("d");
Console.WriteLine(set.Count); // 4
Console.WriteLine(set.Contains("c")); // True

// Set operations
var other = new HashSet<string> { "b", "c", "e" };
set.IntersectWith(other); // { b, c }
set.UnionWith(other);     // original union
set.ExceptWith(other);    // set minus other

6. Object-Oriented Programming

Classes and objects

public class Person
{
    // Properties (preferred over public fields)
    public string Name { get; set; }
    public int Age { get; private set; }   // read-only outside class

    // Constructor
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    // Method
    public string Greet()
        => $"Hi, I'm {Name} and I'm {Age} years old.";

    // Override ToString
    public override string ToString()
        => $"Person(Name={Name}, Age={Age})";
}

// Usage
var p = new Person("Alice", 30);
Console.WriteLine(p.Greet());
Console.WriteLine(p);          // uses ToString()

Constructor shortcuts (primary constructors, C# 12)

// C# 12: primary constructor
public class Point(double x, double y)
{
    public double X { get; } = x;
    public double Y { get; } = y;
    public double Distance() => Math.Sqrt(X * X + Y * Y);
}

var pt = new Point(3, 4);
Console.WriteLine(pt.Distance()); // 5

Records (immutable data, C# 9+)

// Value-based equality, immutable by default
public record Product(string Name, decimal Price);

var p1 = new Product("Widget", 9.99m);
var p2 = new Product("Widget", 9.99m);
Console.WriteLine(p1 == p2); // True (value equality)

// Non-destructive mutation
var p3 = p1 with { Price = 14.99m };
Console.WriteLine(p3.Name);  // Widget
Console.WriteLine(p3.Price); // 14.99

Inheritance

public class Animal
{
    public string Name { get; init; }

    public Animal(string name) => Name = name;

    public virtual string Speak() => "...";

    public override string ToString() => $"{GetType().Name}({Name})";
}

public class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public override string Speak() => "Woof!";
}

public class Cat : Animal
{
    public Cat(string name) : base(name) { }

    public override string Speak() => "Meow!";
}

Animal[] animals = { new Dog("Rex"), new Cat("Luna") };

foreach (var animal in animals)
    Console.WriteLine($"{animal.Name} says: {animal.Speak()}");
// Rex says: Woof!
// Luna says: Meow!

Interfaces

public interface IShape
{
    double Area();
    double Perimeter();
    string Describe() => $"Area: {Area():F2}, Perimeter: {Perimeter():F2}";  // default impl
}

public class Circle : IShape
{
    private readonly double _radius;
    public Circle(double radius) => _radius = radius;
    public double Area() => Math.PI * _radius * _radius;
    public double Perimeter() => 2 * Math.PI * _radius;
}

public class Rectangle : IShape
{
    private readonly double _w, _h;
    public Rectangle(double w, double h) { _w = w; _h = h; }
    public double Area() => _w * _h;
    public double Perimeter() => 2 * (_w + _h);
}

IShape[] shapes = { new Circle(5), new Rectangle(4, 6) };
foreach (var shape in shapes)
    Console.WriteLine(shape.Describe());
// Area: 78.54, Perimeter: 31.42
// Area: 24.00, Perimeter: 20.00

Abstract classes

public abstract class Vehicle
{
    public string Model { get; }
    protected int Speed;

    protected Vehicle(string model) => Model = model;

    public abstract void Accelerate(int amount); // must override

    public virtual string Status()
        => $"{Model} going {Speed} km/h";
}

public class Car : Vehicle
{
    public Car(string model) : base(model) { }

    public override void Accelerate(int amount)
    {
        Speed = Math.Min(Speed + amount, 200); // top speed 200
    }
}

abstract class vs interface:

Feature Abstract class Interface
Multiple inheritance No (single) Yes (multiple)
Constructor Yes No
Fields Yes No
Default methods Yes Yes (C# 8+)
Access modifiers Yes Public only
State Yes No
Use when Shared base + state Contract / capability

Static members and singleton

public class Counter
{
    private static int _total = 0;       // shared across all instances
    private int _count = 0;

    public void Increment()
    {
        _count++;
        _total++;
    }

    public static int Total => _total;
    public int Count => _count;
}

var c1 = new Counter();
var c2 = new Counter();
c1.Increment();
c1.Increment();
c2.Increment();
Console.WriteLine(Counter.Total); // 3
Console.WriteLine(c1.Count);      // 2

7. Generics

// Generic method
static T Max<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) >= 0 ? a : b;

Console.WriteLine(Max(3, 7));       // 7
Console.WriteLine(Max("apple", "banana")); // banana

// Generic class
public class Stack<T>
{
    private readonly List<T> _items = new();

    public void Push(T item) => _items.Add(item);

    public T Pop()
    {
        if (_items.Count == 0) throw new InvalidOperationException("Stack is empty");
        var last = _items[^1];   // ^1 = last element (index from end)
        _items.RemoveAt(_items.Count - 1);
        return last;
    }

    public T Peek() => _items[^1];
    public int Count => _items.Count;
    public bool IsEmpty => _items.Count == 0;
}

var stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
stack.Push(3);
Console.WriteLine(stack.Pop()); // 3
Console.WriteLine(stack.Peek()); // 2

8. LINQ (Language Integrated Query)

LINQ lets you query collections with a readable, SQL-like syntax.

var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Filter
var evens = numbers.Where(n => n % 2 == 0);
// [2, 4, 6, 8, 10]

// Transform
var squares = numbers.Select(n => n * n);
// [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

// Chain
var result = numbers
    .Where(n => n % 2 == 0)
    .Select(n => n * n)
    .Take(3);
// [4, 16, 36]

// Aggregation
Console.WriteLine(numbers.Sum());        // 55
Console.WriteLine(numbers.Average());    // 5.5
Console.WriteLine(numbers.Max());        // 10
Console.WriteLine(numbers.Min());        // 1
Console.WriteLine(numbers.Count());      // 10

// Any / All
Console.WriteLine(numbers.Any(n => n > 9));   // True
Console.WriteLine(numbers.All(n => n > 0));   // True

// First / Single / Last
Console.WriteLine(numbers.First(n => n > 5)); // 6
Console.WriteLine(numbers.Last());             // 10
Console.WriteLine(numbers.FirstOrDefault(n => n > 100)); // 0 (default)

LINQ with objects

var products = new List<(string Name, string Category, decimal Price)>
{
    ("Widget", "Tools", 9.99m),
    ("Gadget", "Electronics", 49.99m),
    ("Doohickey", "Tools", 14.99m),
    ("Thingamajig", "Electronics", 99.99m),
    ("Gizmo", "Tools", 24.99m),
};

// Group by category
var byCategory = products
    .GroupBy(p => p.Category)
    .Select(g => new
    {
        Category = g.Key,
        Count = g.Count(),
        TotalValue = g.Sum(p => p.Price)
    });

foreach (var g in byCategory)
    Console.WriteLine($"{g.Category}: {g.Count} items, ${g.TotalValue:F2}");

// Sort
var cheapest = products
    .Where(p => p.Price < 30)
    .OrderBy(p => p.Price);

// Query syntax (alternative to method chaining)
var tools = from p in products
            where p.Category == "Tools"
            orderby p.Price
            select p;

9. Error Handling

// Basic try/catch/finally
try
{
    int result = 10 / 0;
    Console.WriteLine(result);
}
catch (DivideByZeroException ex)
{
    Console.WriteLine($"Error: {ex.Message}");
}
catch (Exception ex)  // catch-all
{
    Console.WriteLine($"Unexpected: {ex.Message}");
}
finally
{
    Console.WriteLine("Always runs");
}

// Multiple specific exceptions
static int ParseAge(string input)
{
    try
    {
        int age = int.Parse(input);
        if (age < 0 || age > 150)
            throw new ArgumentOutOfRangeException(nameof(input), "Age must be 0–150");
        return age;
    }
    catch (FormatException)
    {
        throw new ArgumentException($"'{input}' is not a valid age", nameof(input));
    }
}

// Custom exception
public class InsufficientFundsException : Exception
{
    public decimal Amount { get; }

    public InsufficientFundsException(decimal amount)
        : base($"Insufficient funds: need {amount:C} more")
    {
        Amount = amount;
    }
}

// Exception filters (C# 6+)
try
{
    // ...
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
    Console.WriteLine("Resource not found");
}

Common exceptions:

Exception When it occurs
ArgumentNullException Null passed where not allowed
ArgumentOutOfRangeException Value outside valid range
InvalidOperationException Object in wrong state
NullReferenceException Calling method on null object
IndexOutOfRangeException Array index too large/small
FormatException String in wrong format
IOException File/network I/O failure
OverflowException Arithmetic overflow (checked context)

10. File I/O

using System.IO;

// Write a file
await File.WriteAllTextAsync("notes.txt", "Hello from C#\nLine 2");

// Read entire file
string content = await File.ReadAllTextAsync("notes.txt");
Console.WriteLine(content);

// Read all lines
string[] lines = await File.ReadAllLinesAsync("notes.txt");
foreach (string line in lines)
    Console.WriteLine(line);

// Append
await File.AppendAllTextAsync("notes.txt", "\nLine 3");

// Check existence
if (File.Exists("notes.txt"))
    Console.WriteLine("File exists");

// Delete
File.Delete("notes.txt");

// Work with directories
Directory.CreateDirectory("mydir");
string[] files = Directory.GetFiles("mydir", "*.txt");

// Path operations
string path = Path.Combine("mydir", "notes.txt");
Console.WriteLine(Path.GetExtension(path));  // .txt
Console.WriteLine(Path.GetFileName(path));   // notes.txt
Console.WriteLine(Path.GetDirectoryName(path)); // mydir

11. Async / Await

C# has first-class async support. All I/O should be async to avoid blocking threads.

using System.Net.Http;
using System.Text.Json;

// Basic async method
static async Task<string> FetchDataAsync(string url)
{
    using var client = new HttpClient();
    string json = await client.GetStringAsync(url);
    return json;
}

// Run multiple tasks in parallel
static async Task Main()
{
    string[] urls = {
        "https://api.example.com/users",
        "https://api.example.com/posts",
        "https://api.example.com/comments"
    };

    // Sequential — slow
    foreach (var url in urls)
        await FetchDataAsync(url);

    // Parallel — fast (all at once)
    var tasks = urls.Select(FetchDataAsync);
    string[] results = await Task.WhenAll(tasks);

    Console.WriteLine($"Got {results.Length} responses");
}

// Return types
async Task DoWorkAsync()      { }  // no return value
async Task<int> GetCountAsync() { return 42; }  // returns int
async ValueTask<bool> CheckAsync() { return true; }  // ValueTask for hot paths

async/await rules:

Rule Explanation
async marks a method Enables await inside
await unwraps a Task<T> Gets the result, yields thread
Never use .Result or .Wait() Causes deadlocks
Return Task, not void Allows proper error propagation
Use ConfigureAwait(false) in libraries Avoid context capture
CancellationToken for cancellation Pass to all async methods
// Cancellation
static async Task LongOperationAsync(CancellationToken ct = default)
{
    for (int i = 0; i < 10; i++)
    {
        ct.ThrowIfCancellationRequested();
        await Task.Delay(1000, ct);
        Console.WriteLine($"Step {i + 1}");
    }
}

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
try
{
    await LongOperationAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Cancelled after 3 seconds");
}

12. Modern C# features

Pattern matching

// is pattern
object obj = 42;
if (obj is int number && number > 0)
    Console.WriteLine($"Positive int: {number}");

// switch expression with patterns
static string Describe(object value) => value switch
{
    int n when n < 0 => "negative int",
    int n            => $"int: {n}",
    string s         => $"string: '{s}'",
    null             => "null",
    _                => "unknown"
};

// List patterns (C# 11)
int[] arr = { 1, 2, 3 };
if (arr is [1, .., 3])
    Console.WriteLine("Starts with 1, ends with 3");

Records and init-only

// Record struct (value type, C# 10)
public record struct Coordinate(double Lat, double Lon);

// Init-only properties
public class Config
{
    public string Host { get; init; } = "localhost";
    public int Port { get; init; } = 8080;
}

var config = new Config { Host = "example.com", Port = 443 };
// config.Host = "other"; // compile error — init only

Nullable reference types (C# 8+)

#nullable enable

string name = "Alice";     // non-nullable — never null
string? maybeNull = null;  // nullable — can be null

// Compiler warns if you don't check
if (maybeNull != null)
    Console.WriteLine(maybeNull.Length); // safe

// Null-forgiving operator (use sparingly)
Console.WriteLine(maybeNull!.Length); // tells compiler "trust me"

// Null-conditional operator
int? len = maybeNull?.Length;    // null if maybeNull is null

Global using / File-scoped namespaces (C# 10)

// Program.cs — file-scoped namespace (no braces)
namespace MyApp;

public class App
{
    public static void Run() => Console.WriteLine("Running!");
}
// GlobalUsings.cs — apply to entire project
global using System;
global using System.Collections.Generic;
global using System.Linq;

13. Project: Command-Line To-Do Manager

// TodoManager.cs
using System.Text.Json;

namespace TodoApp;

record Todo(int Id, string Title, bool Done = false);

class TodoManager
{
    private List<Todo> _todos = new();
    private int _nextId = 1;
    private const string FilePath = "todos.json";

    public TodoManager()
    {
        Load();
    }

    public void Add(string title)
    {
        _todos.Add(new Todo(_nextId++, title));
        Save();
        Console.WriteLine($"Added: {title}");
    }

    public void Complete(int id)
    {
        var index = _todos.FindIndex(t => t.Id == id);
        if (index < 0)
        {
            Console.WriteLine($"Todo #{id} not found");
            return;
        }
        _todos[index] = _todos[index] with { Done = true };
        Save();
        Console.WriteLine($"Completed: {_todos[index].Title}");
    }

    public void Delete(int id)
    {
        var removed = _todos.RemoveAll(t => t.Id == id);
        if (removed == 0) Console.WriteLine($"Todo #{id} not found");
        else { Save(); Console.WriteLine($"Deleted #{id}"); }
    }

    public void List(bool? showDone = null)
    {
        var items = showDone.HasValue
            ? _todos.Where(t => t.Done == showDone.Value)
            : _todos;

        if (!items.Any())
        {
            Console.WriteLine("No todos found.");
            return;
        }

        foreach (var todo in items.OrderBy(t => t.Id))
        {
            string status = todo.Done ? "✓" : "○";
            Console.WriteLine($"[{status}] {todo.Id}. {todo.Title}");
        }
    }

    private void Save()
    {
        var json = JsonSerializer.Serialize(_todos, new JsonSerializerOptions { WriteIndented = true });
        File.WriteAllText(FilePath, json);
    }

    private void Load()
    {
        if (!File.Exists(FilePath)) return;
        var json = File.ReadAllText(FilePath);
        _todos = JsonSerializer.Deserialize<List<Todo>>(json) ?? new();
        _nextId = _todos.Count > 0 ? _todos.Max(t => t.Id) + 1 : 1;
    }
}

// Program.cs
var manager = new TodoManager();

if (args.Length == 0)
{
    Console.WriteLine("Usage: todo [add <title>] [done <id>] [delete <id>] [list]");
    return;
}

switch (args[0])
{
    case "add" when args.Length > 1:
        manager.Add(string.Join(" ", args[1..]));
        break;
    case "done" when args.Length > 1 && int.TryParse(args[1], out int doneId):
        manager.Complete(doneId);
        break;
    case "delete" when args.Length > 1 && int.TryParse(args[1], out int delId):
        manager.Delete(delId);
        break;
    case "list":
        manager.List();
        break;
    default:
        Console.WriteLine("Unknown command");
        break;
}

Run it:

dotnet run -- add "Buy groceries"
dotnet run -- add "Write unit tests"
dotnet run -- list
# [○] 1. Buy groceries
# [○] 2. Write unit tests

dotnet run -- done 1
dotnet run -- list
# [✓] 1. Buy groceries
# [○] 2. Write unit tests

14. Project: Simple REST API with ASP.NET Core

dotnet new webapi -n NotesApi --no-openapi
cd NotesApi
dotnet run

Replace Program.cs:

// Program.cs — Minimal API
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// In-memory store
var notes = new List<Note>();
var nextId = 1;

app.MapGet("/notes", () => notes);

app.MapGet("/notes/{id}", (int id) =>
{
    var note = notes.Find(n => n.Id == id);
    return note is not null ? Results.Ok(note) : Results.NotFound();
});

app.MapPost("/notes", (NoteRequest req) =>
{
    var note = new Note(nextId++, req.Title, req.Content);
    notes.Add(note);
    return Results.Created($"/notes/{note.Id}", note);
});

app.MapPut("/notes/{id}", (int id, NoteRequest req) =>
{
    var index = notes.FindIndex(n => n.Id == id);
    if (index < 0) return Results.NotFound();
    notes[index] = notes[index] with { Title = req.Title, Content = req.Content };
    return Results.Ok(notes[index]);
});

app.MapDelete("/notes/{id}", (int id) =>
{
    var removed = notes.RemoveAll(n => n.Id == id);
    return removed > 0 ? Results.NoContent() : Results.NotFound();
});

app.Run();

record Note(int Id, string Title, string Content);
record NoteRequest(string Title, string Content);

Test with curl:

# Create
curl -X POST http://localhost:5000/notes \
  -H "Content-Type: application/json" \
  -d '{"title":"Hello","content":"My first note"}'

# List all
curl http://localhost:5000/notes

# Get by ID
curl http://localhost:5000/notes/1

# Update
curl -X PUT http://localhost:5000/notes/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Updated","content":"Changed content"}'

# Delete
curl -X DELETE http://localhost:5000/notes/1

15. Project: CSV Data Analyzer

// DataAnalyzer.cs
using System.Globalization;

var path = args.Length > 0 ? args[0] : "data.csv";

if (!File.Exists(path))
{
    // Create sample data
    await File.WriteAllTextAsync(path, """
        Name,Category,Revenue
        Widget,Electronics,1250.50
        Gadget,Electronics,3400.00
        Doohickey,Tools,850.75
        Thingamajig,Electronics,2100.25
        Gizmo,Tools,1750.00
        Whatsit,Office,450.00
        Contraption,Tools,3200.50
        """);
    Console.WriteLine($"Created sample {path}");
}

var lines = await File.ReadAllLinesAsync(path);
var headers = lines[0].Split(',');

var records = lines[1..]
    .Where(line => !string.IsNullOrWhiteSpace(line))
    .Select(line =>
    {
        var cols = line.Split(',');
        return new
        {
            Name = cols[0].Trim(),
            Category = cols[1].Trim(),
            Revenue = decimal.Parse(cols[2].Trim(), CultureInfo.InvariantCulture)
        };
    })
    .ToList();

Console.WriteLine($"\nLoaded {records.Count} records\n");

// Summary stats
Console.WriteLine("=== Overall ===");
Console.WriteLine($"Total Revenue:   ${records.Sum(r => r.Revenue):N2}");
Console.WriteLine($"Average Revenue: ${records.Average(r => r.Revenue):N2}");
Console.WriteLine($"Max Revenue:     ${records.Max(r => r.Revenue):N2}");
Console.WriteLine($"Min Revenue:     ${records.Min(r => r.Revenue):N2}");

// By category
Console.WriteLine("\n=== By Category ===");
var byCategory = records
    .GroupBy(r => r.Category)
    .Select(g => new
    {
        Category = g.Key,
        Count = g.Count(),
        Total = g.Sum(r => r.Revenue),
        Avg = g.Average(r => r.Revenue)
    })
    .OrderByDescending(g => g.Total);

foreach (var g in byCategory)
    Console.WriteLine($"{g.Category,-15} Items: {g.Count}  Total: ${g.Total:N2}  Avg: ${g.Avg:N2}");

// Top 3
Console.WriteLine("\n=== Top 3 Items ===");
var top3 = records.OrderByDescending(r => r.Revenue).Take(3);
int rank = 1;
foreach (var item in top3)
    Console.WriteLine($"{rank++}. {item.Name} ({item.Category}): ${item.Revenue:N2}");

Learning path

Stage Duration Focus
Beginner 2–4 weeks Syntax, types, control flow, methods
OOP 2–4 weeks Classes, interfaces, inheritance, generics
Intermediate 4–6 weeks LINQ, async/await, collections, file I/O
Framework 4–8 weeks ASP.NET Core (web) or Unity (games)
Advanced Ongoing Design patterns, Entity Framework, testing
Specialization Ongoing Your specific domain (cloud, mobile, games)

Free resources:

Resource Best for
docs.microsoft.com/dotnet Official docs + tutorials
learn.microsoft.com Interactive C# courses
dotnet.microsoft.com Downloads, news
exercism.org/tracks/csharp Practice exercises
csharppad.com Online REPL

Common mistakes

Mistake Problem Fix
Using == to compare strings with null NPE risk Use string.IsNullOrEmpty() or is null
.Result or .Wait() on async Deadlock Always await
Forgetting await on async call Task not awaited, errors swallowed Enable warning CS4014
Mutating a foreach collection InvalidOperationException Collect to list first, then mutate
catch (Exception) with no rethrow Swallows errors silently Log and rethrow or handle specifically
public fields instead of properties Breaks encapsulation Use { get; set; }
Not using using for IDisposable Resource leaks Always using var for streams, clients
Magic strings everywhere Hard to maintain Use constants or enums

C# vs related terms

Term What it is
C# The programming language
.NET The platform/runtime C# runs on
CLR Common Language Runtime — executes .NET code
NuGet Package manager for .NET (like npm for Node.js)
MSBuild Build system for .NET projects
ASP.NET Core Web framework built on .NET
Entity Framework Core ORM for databases in .NET
Blazor C# web UI framework (runs in browser via WASM)
MAUI Cross-platform desktop/mobile framework
Unity Game engine that uses C# for scripting

FAQ

Q: Is C# only for Windows? No. Since .NET Core (2016) and .NET 5+ (2020), C# runs natively on Windows, macOS, and Linux. ASP.NET Core apps run everywhere.

Q: Should I learn C# or Java? Both are strongly typed, OOP languages with similar concepts. C# is stronger for game development (Unity), Windows/Azure ecosystems, and has more modern language features. Java dominates Android (legacy) and certain enterprise sectors. Either is a solid first language.

Q: Do I need Visual Studio? No. VS Code with the C# Dev Kit extension works great on all platforms. Visual Studio (Windows/Mac only) has more tooling but is heavier. Both are free.

Q: What's the difference between .NET Framework and .NET? .NET Framework (1.0–4.8) is Windows-only legacy. Modern .NET (5, 6, 7, 8) is cross-platform. Use .NET 8 for all new projects.

Q: How long does it take to learn C#? Basic syntax: 1–2 weeks. Writing useful programs: 1–2 months. Professional proficiency: 6–12 months with practice. The investment pays off — C# developers are well-paid across games, enterprise, and web.

Q: Can C# be used for machine learning? Yes, via ML.NET (Microsoft's ML framework) or by calling Python ML services from C#. Most ML teams still prefer Python, but ML.NET is viable for .NET-integrated ML workloads.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools