.NET & C# Development · Lesson 1 of 11
C# Fundamentals
What is C# and .NET?
C# (pronounced "C sharp") is a modern, strongly-typed, object-oriented language created by Microsoft. It runs on .NET — a free, open-source, cross-platform runtime that powers everything from web APIs to mobile apps to cloud services.
C# is used at Microsoft, Stack Overflow, Unity (game development), and thousands of enterprises worldwide. It's consistently ranked in the top 5 most-used programming languages.
What you'll be able to build after this guide:
- Console applications
- REST APIs with ASP.NET Core
- Background services and workers
- Data pipelines and automation scripts
Setting Up
# Install .NET SDK (free, cross-platform)
# Download from: https://dotnet.microsoft.com/download
# Verify installation
dotnet --version # should show 8.x or 9.x
# Create a new console app
dotnet new console -n MyFirstApp
cd MyFirstApp
dotnet runUse Visual Studio 2022 (Windows, full IDE) or VS Code + C# extension (cross-platform, lightweight).
Variables and Types
C# is strongly typed — every variable has a type, checked at compile time.
// Value types — stored directly
int age = 25;
double price = 9.99;
decimal total = 1234.56m; // m suffix for decimal
bool isActive = true;
char grade = 'A';
// String — reference type but behaves like value type
string name = "Alice";
string greeting = $"Hello, {name}!"; // string interpolation
// var — type inferred by compiler
var count = 42; // int
var message = "hello"; // string
var items = new List<string>();Value types vs Reference types:
- Value types (
int,bool,struct) are copied when assigned - Reference types (
class,string,array) hold a reference to the object
Nullable Types
// Non-nullable: compiler warns if you assign null
string name = "Alice";
// name = null; // CS8600 warning
// Nullable: explicitly allow null
string? email = null;
int? age = null;
// Check before use
if (email != null)
Console.WriteLine(email.ToUpper());
// Null-conditional operator
Console.WriteLine(email?.ToUpper()); // null if email is null
// Null-coalescing
string display = email ?? "No email";
// Null-coalescing assignment
email ??= "default@example.com";Enable nullable reference types in .csproj (default in .NET 6+):
<Nullable>enable</Nullable>Control Flow
// if / else
int score = 85;
if (score >= 90)
Console.WriteLine("A");
else if (score >= 80)
Console.WriteLine("B");
else
Console.WriteLine("C");
// switch expression (modern, concise)
string grade = score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
_ => "F" // default
};
// for loop
for (int i = 0; i < 10; i++)
Console.WriteLine(i);
// foreach
var names = new[] { "Alice", "Bob", "Carol" };
foreach (var n in names)
Console.WriteLine(n);
// while / do-while
int count = 0;
while (count < 5)
count++;
// Pattern matching in switch
object obj = 42;
switch (obj)
{
case int n when n > 0:
Console.WriteLine($"Positive int: {n}");
break;
case string s:
Console.WriteLine($"String: {s}");
break;
case null:
Console.WriteLine("Null");
break;
}Methods
// Basic method
static int Add(int a, int b) => a + b;
// Optional parameters (must be at end)
static string Greet(string name, string greeting = "Hello")
=> $"{greeting}, {name}!";
// Named arguments (can pass in any order)
Greet(greeting: "Hi", name: "Alice");
// params — variable number of arguments
static int Sum(params int[] numbers)
=> numbers.Sum();
Sum(1, 2, 3, 4, 5); // 15
// out parameter — returns multiple values
static bool TryParse(string input, out int result)
{
return int.TryParse(input, out result);
}
if (TryParse("42", out int n))
Console.WriteLine(n);
// ref — pass by reference
static void Swap(ref int a, ref int b)
{
(a, b) = (b, a); // tuple swap
}Classes and Objects
public class Customer
{
// Properties (preferred over public fields)
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; private set; } // only settable in class
public DateTime CreatedAt { get; } = DateTime.UtcNow; // init-only
// Constructor
public Customer(int id, string name, string email)
{
Id = id;
Name = name;
Email = email;
}
// Method
public string GetDisplayName() => $"{Name} ({Email})";
// Override ToString
public override string ToString() => $"Customer[{Id}]: {Name}";
}
// Usage
var customer = new Customer(1, "Alice Smith", "alice@example.com");
Console.WriteLine(customer.GetDisplayName());
// Object initializer (requires parameterless constructor or init setters)
var customer2 = new Customer
{
Id = 2,
Name = "Bob",
Email = "bob@example.com" // only if setter is public
};Record types (C# 9+)
Records are immutable value-object classes. Perfect for DTOs.
public record CustomerDto(int Id, string Name, string Email);
var dto = new CustomerDto(1, "Alice", "alice@example.com");
var dto2 = dto with { Name = "Bob" }; // non-destructive mutation
// Records have equality by value (not reference)
Console.WriteLine(dto == new CustomerDto(1, "Alice", "alice@example.com")); // TrueInheritance and Polymorphism
public abstract class Animal
{
public string Name { get; init; } = string.Empty;
// Abstract: must be overridden
public abstract string Speak();
// Virtual: can be overridden
public virtual string Describe() => $"I am {Name}";
}
public class Dog : Animal
{
public override string Speak() => "Woof!";
public override string Describe() => $"{base.Describe()}, a dog.";
}
public class Cat : Animal
{
public override string Speak() => "Meow!";
}
// Polymorphism
Animal[] animals = { new Dog { Name = "Rex" }, new Cat { Name = "Whiskers" } };
foreach (var a in animals)
Console.WriteLine($"{a.Name}: {a.Speak()}");Interfaces
Interfaces define contracts — what a type can do without specifying how.
public interface IEmailService
{
Task SendAsync(string to, string subject, string body);
Task<bool> ValidateAddressAsync(string email);
}
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
// Implementation
public class SmtpEmailService : IEmailService
{
public async Task SendAsync(string to, string subject, string body)
{
// ... SMTP implementation
await Task.CompletedTask;
}
public async Task<bool> ValidateAddressAsync(string email)
=> await Task.FromResult(email.Contains('@'));
}
// Depend on the interface, not the implementation
public class OrderService
{
private readonly IEmailService _emailService; // interface, not SmtpEmailService
public OrderService(IEmailService emailService)
=> _emailService = emailService;
public async Task PlaceOrderAsync(Order order)
{
// ... business logic ...
await _emailService.SendAsync(
order.CustomerEmail,
"Order Confirmed",
$"Order #{order.Id} is confirmed.");
}
}Generics
Write code that works with any type, with compile-time type safety.
// 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 item = _items[^1];
_items.RemoveAt(_items.Count - 1);
return item;
}
public int Count => _items.Count;
}
var intStack = new Stack<int>();
intStack.Push(1);
intStack.Push(2);
Console.WriteLine(intStack.Pop()); // 2
// Generic method
public static T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
Max(3, 7); // 7
Max("apple", "mango"); // mango
// Generic constraints
where T : class // must be reference type
where T : struct // must be value type
where T : new() // must have parameterless constructor
where T : IMyInterface // must implement interface
where T : BaseClass // must inherit from classCollections
// List<T> — ordered, resizable
var names = new List<string> { "Alice", "Bob", "Carol" };
names.Add("Dave");
names.Remove("Bob");
names.Sort();
int idx = names.IndexOf("Carol");
// Dictionary<TKey, TValue> — key-value pairs
var scores = new Dictionary<string, int>
{
["Alice"] = 95,
["Bob"] = 87
};
scores["Carol"] = 92;
scores.TryGetValue("Dave", out int score); // safe access
// HashSet<T> — unique values, fast lookup
var tags = new HashSet<string> { "sql", "csharp", "dotnet" };
tags.Add("sql"); // already exists, ignored
tags.Contains("sql"); // O(1)
// Queue<T> and Stack<T>
var queue = new Queue<int>();
queue.Enqueue(1); queue.Enqueue(2);
int next = queue.Dequeue(); // 1
// ImmutableList (System.Collections.Immutable)
using System.Collections.Immutable;
var immutable = ImmutableList.Create(1, 2, 3);
var updated = immutable.Add(4); // returns new list, original unchangedLINQ (Language Integrated Query)
LINQ lets you query any collection using a clean, SQL-like syntax.
var customers = new List<Customer>
{
new(1, "Alice", "alice@gmail.com"),
new(2, "Bob", "bob@yahoo.com"),
new(3, "Carol", "carol@gmail.com"),
};
// Method syntax (most common)
var gmailUsers = customers
.Where(c => c.Email.EndsWith("@gmail.com"))
.OrderBy(c => c.Name)
.Select(c => new { c.Name, c.Email })
.ToList();
// Aggregations
int total = customers.Count();
var firstAlpha = customers.MinBy(c => c.Name);
var emails = customers.Select(c => c.Email).ToArray();
// GroupBy
var byDomain = customers
.GroupBy(c => c.Email.Split('@')[1])
.Select(g => new { Domain = g.Key, Count = g.Count() });
// First / FirstOrDefault / Single
var alice = customers.FirstOrDefault(c => c.Name == "Alice");
// FirstOrDefault returns null if not found
// First throws if not found
// Single throws if more than one match
// Any / All / Count
bool hasGmail = customers.Any(c => c.Email.EndsWith("@gmail.com"));
bool allHaveEmail = customers.All(c => !string.IsNullOrEmpty(c.Email));
// Join
var orders = new List<Order> { /* ... */ };
var customerOrders = customers.Join(
orders,
c => c.Id,
o => o.CustomerId,
(c, o) => new { c.Name, o.Total }
);
// Deferred execution
var query = customers.Where(c => c.Name.StartsWith("A"));
// Query not executed yet
customers.Add(new(4, "Anna", "anna@example.com"));
foreach (var c in query) // executed now — includes Anna
Console.WriteLine(c.Name);Exception Handling
// try / catch / finally
try
{
var result = int.Parse("not a number");
}
catch (FormatException ex)
{
Console.WriteLine($"Format error: {ex.Message}");
}
catch (OverflowException ex)
{
Console.WriteLine($"Number too large: {ex.Message}");
}
catch (Exception ex) when (ex.Message.Contains("specific"))
{
// Exception filter
}
finally
{
// Always runs — use for cleanup
Console.WriteLine("Done");
}
// Custom exceptions
public class InsufficientFundsException : Exception
{
public decimal Amount { get; }
public InsufficientFundsException(decimal amount)
: base($"Insufficient funds: need {amount:C}")
=> Amount = amount;
}
// throw vs re-throw
catch (Exception ex)
{
throw; // re-throw, preserves stack trace
// throw ex; // BAD: resets stack trace
// throw new ApplicationException("msg", ex); // wrap with context
}Async/Await
The foundation of scalable .NET applications. Async code doesn't block threads while waiting.
// async method returns Task or Task<T>
public async Task<string> FetchUserAsync(int userId)
{
using var client = new HttpClient();
// await suspends the method, freeing the thread
string json = await client.GetStringAsync($"/api/users/{userId}");
return json;
}
// async void — ONLY for event handlers, never in libraries
// Always use Task in other cases
// Multiple concurrent operations
var task1 = FetchUserAsync(1);
var task2 = FetchUserAsync(2);
var (r1, r2) = await (task1, task2); // run in parallel
// Or:
var results = await Task.WhenAll(task1, task2);
// Cancellation
public async Task ProcessAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await DoWorkAsync();
await Task.Delay(1000, ct); // throws if cancelled
}
}
// ConfigureAwait(false) — in library code, don't capture sync context
string data = await client.GetStringAsync(url).ConfigureAwait(false);
// ValueTask — for hot paths that are often synchronous
public ValueTask<int> GetCachedCount()
{
if (_cache.TryGet("count", out int n)) return ValueTask.FromResult(n);
return new ValueTask<int>(FetchCountFromDbAsync());
}Delegates, Func, Action, Events
// Func<TIn, TOut> — method that returns a value
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 4)); // 7
// Action<T> — method that returns void
Action<string> log = msg => Console.WriteLine($"[LOG] {msg}");
log("Application started");
// Predicate<T> — Func<T, bool>
Predicate<string> isLong = s => s.Length > 10;
// Events
public class OrderProcessor
{
public event EventHandler<OrderEventArgs>? OrderPlaced;
public void PlaceOrder(Order order)
{
// ... process ...
OnOrderPlaced(new OrderEventArgs(order));
}
protected virtual void OnOrderPlaced(OrderEventArgs e)
=> OrderPlaced?.Invoke(this, e);
}
// Lambda expressions
var numbers = new[] { 1, 2, 3, 4, 5 };
var evens = numbers.Where(n => n % 2 == 0);
// Expression trees (used in EF Core and ORMs)
Expression<Func<Customer, bool>> filter = c => c.Name.StartsWith("A");
// EF Core translates this to SQL: WHERE name LIKE 'A%'Records and Pattern Matching (Modern C#)
// Pattern matching (C# 8+)
object shape = new Circle(5.0);
// is expression with pattern
if (shape is Circle c) Console.WriteLine($"Area: {Math.PI * c.Radius * c.Radius}");
// switch expression with patterns
string Describe(object obj) => obj switch
{
Circle c => $"Circle with radius {c.Radius}",
Rectangle r => $"Rectangle {r.Width}x{r.Height}",
string s when s.Length > 0 => $"Non-empty string: {s}",
null => "null",
_ => "Unknown"
};
// Positional pattern
record Point(double X, double Y);
string Quadrant(Point p) => p switch
{
( > 0, > 0) => "Q1",
( < 0, > 0) => "Q2",
( < 0, < 0) => "Q3",
_ => "Q4 or axis"
};
// List patterns (C# 11)
int[] arr = { 1, 2, 3 };
if (arr is [1, .., 3]) Console.WriteLine("Starts with 1, ends with 3");Dependency Injection
The core of .NET application architecture. Register services once, inject them everywhere.
// Program.cs (minimal hosting)
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
builder.Services.AddScoped<OrderService>();
builder.Services.AddSingleton<ILogger, ConsoleLogger>();
var app = builder.Build();
// Lifetimes:
// Transient — new instance per injection
// Scoped — one instance per HTTP request
// Singleton — one instance for entire app lifetime
// Constructor injection
public class OrderController
{
private readonly OrderService _orderService;
public OrderController(OrderService orderService)
=> _orderService = orderService;
}File I/O
// Write a file
await File.WriteAllTextAsync("output.txt", "Hello, world!");
// Read a file
string content = await File.ReadAllTextAsync("input.txt");
string[] lines = await File.ReadAllLinesAsync("data.txt");
// Stream-based (for large files)
await using var reader = new StreamReader("large.txt");
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
Console.WriteLine(line);
}
// JSON serialization
using System.Text.Json;
var customer = new Customer(1, "Alice", "alice@example.com");
string json = JsonSerializer.Serialize(customer);
var restored = JsonSerializer.Deserialize<Customer>(json);
// With options
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
};
string prettyJson = JsonSerializer.Serialize(customer, options);Real-World Example: Order Processing Service
// Domain model
public record Order(int Id, int CustomerId, decimal Total, DateTime PlacedAt);
public record OrderItem(int OrderId, int ProductId, int Qty, decimal UnitPrice);
// Repository interface
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(int id, CancellationToken ct = default);
Task<IReadOnlyList<Order>> GetByCustomerAsync(int customerId, CancellationToken ct = default);
Task<int> CreateAsync(Order order, IEnumerable<OrderItem> items, CancellationToken ct = default);
}
// Service
public class OrderService
{
private readonly IOrderRepository _orders;
private readonly IEmailService _email;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository orders,
IEmailService email,
ILogger<OrderService> logger)
{
_orders = orders;
_email = email;
_logger = logger;
}
public async Task<int> PlaceOrderAsync(
int customerId,
IEnumerable<OrderItem> items,
CancellationToken ct = default)
{
var total = items.Sum(i => i.Qty * i.UnitPrice);
var order = new Order(0, customerId, total, DateTime.UtcNow);
int orderId = await _orders.CreateAsync(order, items, ct);
_logger.LogInformation(
"Order {OrderId} placed for customer {CustomerId}, total {Total}",
orderId, customerId, total);
// Fire and forget — don't block order placement on email
_ = _email.SendAsync(
$"customer-{customerId}@example.com",
$"Order #{orderId} Confirmed",
$"Your order of {total:C} has been placed.");
return orderId;
}
}What to Learn Next
- ASP.NET Core Web API: Build production REST APIs
- Entity Framework Core: Database access done right
- Clean Architecture in .NET: How to structure real applications
- .NET Interview Questions (Junior): 100 questions for your first interview