◆ .NET Full Course
A comprehensive journey through C# and the .NET ecosystem — from fundamentals to advanced patterns, with clean examples and visual explanations.
📚 What You'll Learn
- C# language fundamentals: types, operators, control flow
- Object-Oriented Programming: classes, inheritance, polymorphism, interfaces
- Advanced C#: records, patterns, generics, LINQ, async/await
- .NET platform: DI, configuration, middleware, EF Core, ASP.NET Core
- Testing, design patterns, performance, and interview prep
🚀 Getting Started
Prerequisites: Install the .NET SDK from dotnet.microsoft.com. Then create your first project:
dotnet new console -n MyApp && cd MyApp
dotnet runAll code examples in this course are ready to copy and paste into a dotnet new console project targeting .NET 8+.
C# Basics — Variables, Types, Operators
C# is a modern, type-safe, object-oriented language. It runs on .NET and is used for web, desktop, mobile, cloud, and game development.
Hello World
Console.WriteLine("Hello, World!");Variables & Data Types
C# supports value types (stack) and reference types (heap).
int age = 25;
double price = 19.99;
decimal salary = 4500.00m;
string name = "Alice";
char grade = 'A';
bool isActive = true;
var inferred = "C# infers the type";Value Types vs Reference Types
Nullables, Operators & Keywords
int? maybe = null; // nullable value type
int sure = maybe ?? 0; // null-coalescing
int len = maybe?.ToString()?.Length ?? -1; // null-conditional
// is / as pattern matching
object obj = "hello";
if (obj is string str) Console.WriteLine(str.Length);
string? s = obj as string;
// ref / out / in
void Modify(ref int r) { r = 10; }
void Init(out int o) { o = 42; }
// const vs readonly
const int Months = 12; // compile-time
readonly int Id; // runtime, set in ctor== vs .Equals()
string a = "hello", b = "hello";
Console.WriteLine(a == b); // True (string overloads ==)
Console.WriteLine(a.Equals(b)); // True (same content)
Console.WriteLine((object)a == (object)b); // False! reference compareControl Flow — Conditions & Loops
Branching and iteration are the backbone of program logic. C# offers rich conditional and looping constructs.
If / Else If / Else
int temp = 30;
if (temp > 35) Console.WriteLine("Hot");
else if (temp > 20) Console.WriteLine("Warm");
else Console.WriteLine("Cold");Switch Expression (Pattern Matching)
string type = day switch
{
"Saturday" or "Sunday" => "Weekend",
"Monday" => "Start of work week",
_ => "Midweek"
};Loops
for (int i = 0; i < 5; i++) Console.WriteLine(i);
string[] fruits = ["Apple", "Banana", "Cherry"];
foreach (var f in fruits) Console.WriteLine(f);
int x = 0;
while (x < 3) { Console.WriteLine(x); x++; }
int y = 0;
do { Console.WriteLine(y); y++; } while (y < 3);Object-Oriented Programming
The four pillars of OOP: Encapsulation, Inheritance, Polymorphism, Abstraction.
Classes & Objects
public class Person
{
public string Name { get; set; }
public int Age { get; init; }
public Person(string name, int age) => (Name, Age) = (name, age);
public void Greet() => Console.WriteLine($"Hi, I'm {Name}");
}
var p = new Person("Bob", 30);
p.Greet();Inheritance & Polymorphism
public class Animal
{
public virtual void Speak() => Console.WriteLine("...");
}
public class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof!");
}
Animal a = new Dog();
a.Speak(); // Woof!Abstract Class vs Interface
| Feature | Abstract Class | Interface |
|---|---|---|
| Fields / State | Yes | No (C# 8+ static only) |
| Constructors | Yes | No |
| Default implementation | Yes | Yes (C# 8+) |
| Multiple inheritance | No (single) | Yes (many) |
| Access modifiers | All | Public (default impl can have other) |
| Use when | Shared state/behavior | Contract / capability |
Sealed & Partial Classes
public sealed class FinalClass { } // cannot be inherited
// Partial class: split across files
// File1.cs
public partial class Employee { public string Name { get; set; } }
// File2.cs
public partial class Employee { public decimal Salary { get; set; } }Advanced OOP — Interfaces, Records, Structs
Modern C# features for building robust, expressive types.
Interfaces
public interface ILogger { void Log(string message); }
public class ConsoleLogger : ILogger
{
public void Log(string msg) => Console.WriteLine($"[LOG] {msg}");
}
public class FileLogger : ILogger
{
public void Log(string msg) => File.AppendAllText("log.txt", msg);
}Records (Immutable by Default)
public record Person(string Name, int Age);
var p1 = new Person("Alice", 30);
var p2 = p1 with { Age = 31 }; // non-destructive mutation
Console.WriteLine(p1 == p2); // False (value equality)
Console.WriteLine(p1); // Person { Name = Alice, Age = 30 }Class vs Struct vs Record
Extension Methods
public static class StringExtensions
{
public static string Capitalize(this string s) =>
s.Length > 0 ? char.ToUpper(s[0]) + s[1..] : s;
}
Console.WriteLine("hello".Capitalize()); // HelloCollections & Generics
Type-safe data structures and generic programming.
Generic Constraints
public class Repository<T> where T : class, new() { }
// where T : struct | IComparable<T> | BaseClass | notnullCovariance & Contravariance
IEnumerable<string> strs = ["a","b"];
IEnumerable<object> objs = strs; // covariant (out T)
Action<object> broad = o => Console.WriteLine(o);
Action<string> narrow = broad; // contravariant (in T)Common Collections
IEnumerable<T> vs IQueryable<T>
IEnumerable<Product> inMemory = localList.Where(p => p.Price > 10);
// filter runs in .NET memory
IQueryable<Product> fromDb = ctx.Products.Where(p => p.Price > 10);
// translates to: SELECT * FROM Products WHERE Price > 10Yield Return
IEnumerable<int> GetEven(int max)
{
for (int i = 0; i <= max; i += 2)
yield return i;
}
foreach (var n in GetEven(10)) Console.WriteLine(n); // 0 2 4 6 8 10LINQ — Language Integrated Query
Declarative querying over collections, databases, XML, and more.
Deferred vs Immediate Execution
LINQ uses deferred execution by default: the query runs when you iterate, not when you define it.
Common LINQ Operators
var people = new[] {
new { Name = "Alice", Age = 30 },
new { Name = "Bob", Age = 25 },
new { Name = "Charlie", Age = 35 }
};
var adults = people.Where(p => p.Age >= 18);
var names = people.Select(p => p.Name);
var ordered = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
var grouped = people.GroupBy(p => p.Age / 10);
var first = people.FirstOrDefault(p => p.Name == "Bob");
var anyOver30 = people.Any(p => p.Age > 30);
var maxAge = people.Max(p => p.Age);
var flat = classes.SelectMany(c => c.Students); // flatteningDelegates, Events & Lambda Expressions
First-class functions and the event-driven paradigm in C#.
Delegates & Built-in Func/Action/Predicate
Func<int, int, int> add = (a, b) => a + b;
Action<string> print = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;
// Multicast delegate
Action notify = () => Console.WriteLine("First");
notify += () => Console.WriteLine("Second");
notify(); // "First", "Second"Events & Encapsulation
public class Button
{
public event EventHandler? Clicked;
public void Click()
{
Clicked?.Invoke(this, EventArgs.Empty); // only inside class
}
}
var btn = new Button();
btn.Clicked += (s, e) => Console.WriteLine("handled");
// btn.Clicked = null; // COMPILE ERROR: cannot assign
// btn.Clicked.Invoke(); // COMPILE ERROR: cannot invokeClosures (Captured Variables)
int factor = 3;
Func<int, int> multiplier = n => n * factor; // factor captured
Console.WriteLine(multiplier(5)); // 15Async / Await & Task-based Programming
Non-blocking, asynchronous code for I/O-bound operations.
Async Flow Diagram
Basic Async Method
async Task<string> FetchDataAsync(string url)
{
string result = await new HttpClient().GetStringAsync(url);
return result;
}
string data = await FetchDataAsync("https://api.example.com");ConfigureAwait & Deadlock Prevention
async Task<string> ReadFileAsync(string path)
{
using var stream = File.OpenRead(path);
byte[] buffer = new byte[4096];
int read = await stream.ReadAsync(buffer, 0, buffer.Length)
.ConfigureAwait(false); // no context capture
return Encoding.UTF8.GetString(buffer, 0, read);
}
// DEADLOCK: .Result or .Wait() in UI/ASP.NET context
// var data = GetDataAsync().Result; // deadlocks!
// Use async all the way up insteadValueTask vs Task
ValueTask avoids Task allocation when the result is synchronously available. Use in high-frequency sync-or-async paths.
public ValueTask<int> GetCachedAsync()
{
if (_cache is not null)
return new ValueTask<int>(_cache.Value); // sync, no allocation
return new ValueTask<int>(FetchAsync()); // async path
}Async Streams (IAsyncEnumerable)
async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 0; i < 5; i++)
{
await Task.Delay(100);
yield return i;
}
}
await foreach (var n in GetNumbersAsync()) Console.WriteLine(n);File I/O & Serialization
Reading, writing, and persisting data in multiple formats.
File Operations
await File.WriteAllTextAsync("example.txt", "Hello, File!");
string text = await File.ReadAllTextAsync("example.txt");
string[] lines = await File.ReadAllLinesAsync("data.csv");
// Streams for large files
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 person = new Person { Name = "Alice", Age = 30 };
string json = JsonSerializer.Serialize(person);
Person? deserialized = JsonSerializer.Deserialize<Person>(json);
// Options
var opts = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};Memory Management & Exception Handling
Understanding GC, disposal patterns, and robust error handling.
IDisposable & IAsyncDisposable
public class FileWriter : IDisposable
{
private FileStream _stream;
public FileWriter(string path) => _stream = new FileStream(path, FileMode.Create);
public void Dispose()
{
_stream?.Dispose();
GC.SuppressFinalize(this);
}
}
using var writer = new FileWriter("test.txt"); // auto-disposed
// Async variant
await using var db = new DatabaseWriter();Stack vs Heap & GC Generations
Exception Handling & throw vs throw ex
try { Divide(10, 0); }
catch (DivideByZeroException ex) { Console.WriteLine($"Math: {ex.Message}"); }
catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); }
finally { Console.WriteLine("Cleanup"); }
// throw vs throw ex
catch (Exception ex)
{
// throw ex; // BAD: resets stack trace
throw; // GOOD: preserves original stack trace
}.NET Core & Architecture
Project structure, dependency injection, and configuration.
Dependency Injection Lifetimes
| Lifetime | Instance | Best For |
|---|---|---|
| AddSingleton<T>() | Single instance for app lifetime | Configuration, cache, logging |
| AddScoped<T>() | Per HTTP request / scope | DbContext, request-scoped services |
| AddTransient<T>() | New instance every injection | Lightweight, stateless services |
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddSingleton<ICache, MemoryCache>();
builder.Services.AddTransient<IEmailService, EmailService>();Entity Framework Core
ORM for .NET: mapping objects to relational data.
DbContext & CRUD
public class Blog
{
public int Id { get; set; }
public string Title { get; set; } = "";
public List<Comment> Comments { get; set; } = [];
}
public class AppDbContext : DbContext
{
public DbSet<Blog> Blogs => Set<Blog>();
public AppDbContext(DbContextOptions<AppDbContext> opts) : base(opts) { }
}
// CRUD
context.Blogs.Add(blog); await context.SaveChangesAsync();
var blog = await context.Blogs.Include(b => b.Comments)
.FirstOrDefaultAsync(b => b.Id == id);
blog.Title = "Updated"; await context.SaveChangesAsync();
context.Blogs.Remove(blog); await context.SaveChangesAsync();Eager / Lazy / Explicit Loading
// Eager
var blogs = ctx.Blogs.Include(b => b.Posts)
.ThenInclude(p => p.Comments).ToList();
// Explicit
var blog = ctx.Blogs.First();
await ctx.Entry(blog).Collection(b => b.Posts).LoadAsync();
// Lazy: add Microsoft.EntityFrameworkCore.Proxies
// services.AddDbContext<Db>(o => o.UseLazyLoadingProxies());Add vs Attach vs Update vs Entry.State
ctx.Products.Add(product); // State = Added (INSERT)
ctx.Products.Attach(product); // State = Unchanged (known)
ctx.Products.Update(product); // State = Modified (UPDATE all)
ctx.Entry(product).State = EntityState.Deleted; // DELETEASP.NET Core Web API
Building RESTful APIs with Minimal APIs and Controllers.
Middleware Pipeline
Minimal API & JWT
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts => {
opts.Authority = "https://tenant.oauth.com";
opts.Audience = "api://default";
});
builder.Services.AddCors(opts => opts.AddPolicy("AllowFE",
p => p.WithOrigins("http://localhost:3000").AllowAnyHeader()));
var app = builder.Build();
app.UseCors("AllowFE"); app.UseAuthentication(); app.UseAuthorization();
app.MapGet("/api/hello", () => "Hello World");
app.MapPost("/api/data", (MyData data) => Results.Created($"/api/data/{data.Id}", data));
app.Run();Unit Testing with xUnit
Writing maintainable, verifiable tests for your code.
Facts, Theories & Mocking
[Fact]
public void Add_ReturnsSum()
{
var calc = new Calculator();
Assert.Equal(5, calc.Add(2, 3));
}
[Theory]
[InlineData(1, 2, 3)]
[InlineData(-1, 1, 0)]
public void Add_Various(int a, int b, int expected)
=> Assert.Equal(expected, new Calculator().Add(a, b));
// Moq: Stub vs Mock
var stub = new Mock<IRepository>();
stub.Setup(r => r.Get(1)).Returns(new User { Id = 1 }); // stub
var mock = new Mock<ILogger>();
service.DoWork();
mock.Verify(l => l.Log("done"), Times.Once); // mockBest Practices & Design Patterns
SOLID, Repository, CQRS, and performance tips.
SOLID Principles
| Letter | Principle | Meaning |
|---|---|---|
| S | Single Responsibility | One class, one reason to change |
| O | Open/Closed | Open for extension, closed for modification |
| L | Liskov Substitution | Subtypes must be substitutable for base |
| I | Interface Segregation | Small, focused interfaces |
| D | Dependency Inversion | Depend on abstractions, not concretions |
CQRS Pattern
public class CreateOrderCommand : IRequest<int> { ... }
public class GetOrdersQuery : IRequest<List<Order>> { ... }
// Typically used with MediatR libraryPerformance Tips
// StringBuilder for many concatenations
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++) sb.Append(i);
// Span<T> for zero-allocation slicing
ReadOnlySpan<char> slice = "hello world".AsSpan(0, 5);
// ArrayPool for large temporary buffers
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);
try { /* use */ } finally { ArrayPool<byte>.Shared.Return(buffer); }Deep Dives & Interview Topics
Advanced concepts, threading, reflection, and senior-level design.
Expression Trees
using System.Linq.Expressions;
Expression<Func<int, int, int>> expr = (a, b) => a + b;
var compiled = expr.Compile();
Console.WriteLine(compiled(2, 3)); // 5
// Used by: EF Core, AutoMapper, MVC ViewsReflection & Attributes
[AttributeUsage(AttributeTargets.Property)]
public class DisplayNameAttribute : Attribute
{
public string Name { get; }
public DisplayNameAttribute(string name) => Name = name;
}
// Reading at runtime
var props = typeof(Product).GetProperties();
foreach (var prop in props)
{
var attr = prop.GetCustomAttribute<DisplayNameAttribute>();
Console.WriteLine($"{prop.Name}: {attr?.Name}");
}Threading & Concurrent Collections
// lock
object _lock = new(); lock (_lock) { /* critical */ }
// SemaphoreSlim (async-aware)
var sem = new SemaphoreSlim(3, 3);
await sem.WaitAsync(); try { /* limited resource */ } finally { sem.Release(); }
// Concurrent collections
var dict = new ConcurrentDictionary<string, int>();
// Channel (producer-consumer)
var ch = System.Threading.Channels.Channel.Create<int>();
await ch.Writer.WriteAsync(42);
await foreach (var item in ch.Reader.ReadAllAsync()) Console.WriteLine(item);IComparable vs IComparer
public class Person : IComparable<Person>
{
public int CompareTo(Person? other) => Age.CompareTo(other?.Age);
}
public class NameComparer : IComparer<Person>
{
public int Compare(Person? x, Person? y) => string.Compare(x?.Name, y?.Name);
}
people.Sort(); // uses IComparable
people.Sort(new NameComparer()); // uses IComparerPrimary Constructors & Raw Strings (C# 11/12)
// Primary constructor (C# 12)
public class Person(string name, int age)
{
public string Name { get; } = name;
}
// required modifier (C# 11)
public class Config
{
public required string ConnectionString { get; set; }
}
// Raw string literals (C# 11)
string json = """
{
"name": "Alice",
"age": 30
}
""";Dispose vs Finalize & Memory Leaks
// Finalize (~destructor): non-deterministic, GC calls it
// Dispose: deterministic, called via using
// Common memory leaks:
// - Event handlers not unregistered (publisher keeps subscriber alive)
// - Static collections growing unbounded
// - Large Object Heap fragmentation
// - Improperly disposed DbContexts / HttpClients
// WeakReference: allow GC to collect while still having a reference
var cache = new WeakReference(new StringBuilder("data"));
if (cache.Target is StringBuilder sb) Console.WriteLine(sb);Design a Notification System
public interface INotificationChannel { Task SendAsync(Notification n); }
public class EmailChannel : INotificationChannel { /* SMTP */ }
public class SmsChannel : INotificationChannel { /* Twilio */ }
public class NotificationService(IEnumerable<INotificationChannel> channels)
{
public async Task SendAsync(Notification notif)
{
await Task.WhenAll(channels.Select(c => c.SendAsync(notif)));
}
}
// Considerations: retry, idempotency, templating, queuing