.NET Full Course with C#
A comprehensive guide — 29 chapters across 8 parts
Part I: C# Fundamentals
Introduction to .NET and C#
C# is a modern, type-safe, object-oriented programming language developed by Microsoft. It runs on the .NET platform and is widely used for web, desktop, mobile, cloud, and game development. .NET provides a comprehensive runtime environment (CoreCLR), a massive class library (BCL), and tools for building virtually any kind of application.
Key Components of .NET
- Common Language Runtime (CoreCLR): Execution engine managing memory, GC, type safety, and JIT compilation.
- Base Class Library (BCL): Reusable types for I/O, networking, collections, threading, cryptography, and more.
- ASP.NET Core: High-performance web framework for APIs, MVC, Blazor, and real-time apps.
- Entity Framework Core: Modern ORM for database access using .NET objects.
- .NET MAUI / WinForms / WPF: Frameworks for desktop and mobile UI.
- Languages: C#, F#, VB.NET — all compile to Intermediate Language (IL).
Hello, World!
The simplest C# program in modern .NET uses top-level statements (C# 9+):
csharpConsole.WriteLine("Hello, World!");
This single line is a complete program. The traditional approach with an explicit Main method:
csharpusing System;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
The C# compiler (Roslyn) compiles source code into Intermediate Language (IL) stored in an assembly. At runtime, the JIT compiler within CoreCLR compiles IL methods into native machine code as they are called. Modern .NET also supports Ahead-of-Time (AOT) compilation via Native AOT.
C# Version History
| Version | Released | Key Features |
|---|---|---|
| C# 1.0 | 2002 | Managed code, classes, structs, interfaces, events |
| C# 2.0 | 2005 | Generics, nullable types, anonymous methods, iterators |
| C# 3.0 | 2007 | LINQ, lambda expressions, extension methods, anonymous types |
| C# 4.0 | 2010 | Dynamic binding, named/optional params, co/contravariance |
| C# 5.0 | 2012 | Async/await, caller info attributes |
| C# 6.0 | 2015 | String interpolation, null-conditional, expression-bodied members |
| C# 7.0+ | 2017 | Tuples, pattern matching, ref locals/returns, local functions |
| C# 8.0 | 2019 | Nullable reference types, async streams, default interface methods |
| C# 9.0 | 2020 | Records, init-only setters, top-level statements, pattern matching |
| C# 10 | 2021 | Global usings, file-scoped namespaces, record structs |
| C# 11 | 2022 | Raw string literals, generic math, required members, list patterns |
| C# 12 | 2023 | Primary constructors, collection expressions, aliases for any type |
Key Takeaways
- .NET is a cross-platform, open-source runtime and library ecosystem.
- C# is the primary .NET language — modern, type-safe, and multi-paradigm.
- C# compiles to IL, which is JIT-compiled to native code at runtime.
- The language has evolved rapidly; stay current with the latest version.
Variables, Data Types, and Operators
C# has a unified type system where all types inherit from a common base type: object (System.Object). Types fall into two categories: value types (stored on the stack) and reference types (stored on the heap, GC-managed).
Value Types
| Category | Types | Size | Range |
|---|---|---|---|
| Integral | byte, sbyte | 8-bit | 0 to 255, -128 to 127 |
| Integral | short, ushort | 16-bit | -32,768 to 32,767; 0 to 65,535 |
| Integral | int, uint | 32-bit | ±2.1 billion; 0 to 4.3 billion |
| Integral | long, ulong | 64-bit | ±9.2 quintillion |
| Floating | float | 32-bit | ±1.5e-45 to ±3.4e38 (7 digits) |
| Floating | double | 64-bit | ±5.0e-324 to ±1.7e308 (15-16 digits) |
| Decimal | decimal | 128-bit | 28-29 digits (financial) |
| Boolean | bool | — | true / false |
| Character | char | 16-bit | Unicode (U+0000 to U+FFFF) |
Variables
csharpint age = 25;
string name = "Alice";
var price = 19.99m; // inferred as decimal
var message = "Hello"; // inferred as string
int x = 10, y = 20, z = 30; // multiple declarations
Operators
Arithmetic
csharpint sum = 10 + 5; // 15
int quotient = 10 / 3; // 3 (integer truncation)
double result = 10.0 / 3; // 3.333...
int remainder = 10 % 3; // 1
Null-Conditional (C# 6+)
csharpstring? name = GetName();
int length = name?.Length ?? 0; // null-conditional + coalescing
name ??= "Default"; // null-coalescing assignment (C# 8+)
Type Conversions
csharpint num = 100;
long big = num; // implicit
double d = num; // implicit
double pi = 3.14159;
int truncated = (int)pi; // explicit cast: 3
int parsed = int.Parse("123");
bool ok = int.TryParse("123", out int result); // safe parsing
Key Takeaways
- Value types stack-allocated; reference types heap-allocated (GC-managed).
- Use var when type is obvious from the right side.
- Prefer TryParse over Parse for user input.
- Use decimal for financial, double for scientific computations.
Control Flow and Iterations
Conditional Statements
if/else if/else
csharpint score = 85;
string grade = score >= 90 ? "A" : score >= 80 ? "B" : "C";
Switch Expression (C# 8+)
csharpstring category = day switch
{
"Saturday" or "Sunday" => "Weekend",
"Monday" => "Start",
"Friday" => "End",
_ => "Midweek"
};
Pattern Matching (C# 7+)
csharpstring Describe(object obj) => obj switch
{
int n when n < 0 => "Negative",
int n when n > 0 => "Positive",
int _ => "Zero",
string s => $"String: {s}",
null => "Null",
_ => "Unknown"
};
Loops
for
csharpfor (int i = 0; i < 5; i++)
Console.WriteLine(i);
foreach
csharpint[] numbers = { 1, 2, 3, 4, 5 };
foreach (int num in numbers)
Console.WriteLine(num);
while / do-while
csharpint count = 0;
while (count < 5) count++;
int num;
do { /* runs at least once */ }
while (condition);
Jump Statements
csharpfor (int i = 0; i < 10; i++)
{
if (i == 5) break; // exit loop
if (i % 2 == 0) continue; // skip even
Console.Write(i); // 1 3 5 7 9
}
Error Handling
csharptry
{
int result = Divide(10, 0);
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
finally
{
Console.WriteLine("Cleanup runs always");
}
int Divide(int a, int b) => a / b;
Key Takeaways
- Prefer switch expressions over switch statements for value-returning logic.
- Use pattern matching with when clauses for complex conditions.
- foreach is safer and more readable than indexed loops.
- Always handle specific exceptions before general ones.
Methods and Parameters
Methods promote code reuse and modularity. C# supports multiple parameter passing modes: by value, ref, out, in, and params.
Method Anatomy
csharppublic int Add(int a, int b) => a + b;
void PrintSum(int a, int b) => Console.WriteLine(a + b);
Parameter Passing
Pass by Value (default)
csharpvoid Modify(int x) { x = 100; } // not visible to caller
ref — reference to variable
csharpvoid Swap(ref int a, ref int b)
{
int t = a; a = b; b = t;
}
int x = 1, y = 2;
Swap(ref x, ref y); // x=2, y=1
out — must assign before return
csharpbool TryParseInt(string input, out int result)
=> int.TryParse(input, out result);
if (TryParseInt("42", out int n)) Console.WriteLine(n);
in — read-only reference
csharpvoid Display(in int value) => Console.WriteLine(value);
params — variable arguments
csharpint Sum(params int[] nums) => nums.Sum();
Sum(1, 2, 3, 4, 5); // 15
Named & Optional Parameters
csharpvoid CreateUser(string name, int age = 18, bool active = true) { }
CreateUser(name: "Alice", age: 30);
CreateUser("Bob", active: false);
Local Functions (C# 7+)
csharpint Fibonacci(int n)
{
int Fib(int a, int b, int count) =>
count == 0 ? a : Fib(b, a + b, count - 1);
return Fib(0, 1, n);
}
Console.WriteLine(Fibonacci(10)); // 55
Overloading
csharppublic int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;
Key Takeaways
- Use out for return-by-reference patterns; ref sparingly.
- Expression-bodied members keep simple methods concise.
- Local functions encapsulate helper logic within a method.
- Named arguments improve readability with many optional params.
Part II: Object-Oriented Programming
Classes and Objects
A class is a blueprint for creating objects. It defines data (fields, properties) and behaviors (methods, events). C# is fundamentally object-oriented, and classes are the primary building blocks.
Defining a Class
csharppublic class Person
{
private string _name;
private int _age;
public Person(string name, int age) { _name = name; _age = age; }
public string Name
{
get => _name;
set => _name = value ?? throw new ArgumentNullException();
}
public int Age
{
get => _age;
set => _age = value >= 0 ? value : throw new ArgumentException();
}
public string Email { get; set; } // auto-property
public DateTime CreatedAt { get; } = DateTime.UtcNow; // read-only
public string GetGreeting() => $"Hi, I'm {Name}, {Age}.";
public override string ToString() => $"{Name} ({Age})";
}
Creating Objects
csharpPerson alice = new Person("Alice", 30);
var bob = new Person("Bob", 25) { Email = "bob@example.com" };
Person carol = new("Carol", 28); // target-typed new (C# 9+)
Console.WriteLine(alice.GetGreeting());
Records (C# 9+)
Records provide value-based equality, immutability, and concise syntax:
csharppublic record PersonRecord(string Name, int Age);
var p1 = new PersonRecord("Alice", 30);
var p2 = new PersonRecord("Alice", 30);
Console.WriteLine(p1 == p2); // true (value equality)
var p3 = p1 with { Age = 31 }; // non-destructive mutation
// Record struct (C# 10):
public readonly record struct Point(int X, int Y);
Static Members
csharppublic class MathUtils
{
public static readonly double PI = 3.14159265;
public static int Add(int a, int b) => a + b;
}
Console.WriteLine(MathUtils.PI); // via type name
int sum = MathUtils.Add(3, 4); // 7
Key Takeaways
- Classes define object structure and behavior.
- Use properties (not public fields) for encapsulation.
- Records are ideal for immutable data carriers with value equality.
- Static members belong to the type, not instances.
Inheritance and Polymorphism
Inheritance allows a class to derive from another, reusing and extending behavior. C# supports single class inheritance but multiple interface implementation.
Base and Derived Classes
csharppublic class Vehicle
{
public string Make { get; set; }
public string Model { get; set; }
public int Year { get; set; }
public Vehicle(string make, string model, int year)
=> (Make, Model, Year) = (make, model, year);
public virtual void StartEngine()
=> Console.WriteLine("Engine started");
}
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public Car(string make, string model, int year, int doors)
: base(make, model, year) => NumberOfDoors = doors;
public override void StartEngine()
{
base.StartEngine();
Console.WriteLine("Car engine running smoothly");
}
}
Polymorphism
csharpList<Vehicle> vehicles = new()
{
new Car("Toyota", "Camry", 2022, 4),
new Bike("Harley", "Sportster", 2023, true),
new Truck("Ford", "F-150", 2021, 3000)
};
foreach (Vehicle v in vehicles)
v.StartEngine(); // polymorphic call
Abstract Classes
csharppublic abstract class Shape
{
public string Color { get; set; }
public Shape(string color) => Color = color;
public abstract double GetArea();
public virtual void Display()
=> Console.WriteLine($"{Color} shape area: {GetArea():F2}");
}
public class Circle : Shape
{
public double Radius { get; set; }
public Circle(string color, double r) : base(color) => Radius = r;
public override double GetArea() => Math.PI * Radius * Radius;
}
var shapes = new Shape[] { new Circle("Red", 5), new Circle("Blue", 3) };
foreach (var s in shapes) s.Display();
Access Modifiers
| Modifier | Visibility |
|---|---|
| public | Anywhere |
| private | Same class only |
| protected | Class + derived classes |
| internal | Same assembly |
| protected internal | Derived classes OR same assembly |
| private protected | Derived classes in same assembly (C# 7.2+) |
Key Takeaways
- Inheritance enables 'is-a' relationships and code reuse.
- Polymorphism treats derived objects through base type.
- Abstract classes define contracts with partial implementation.
- Prefer composition over inheritance for 'has-a' relationships.
Interfaces and Abstraction
An interface defines a contract — signatures without implementation. Classes implementing an interface must provide all member implementations. Interfaces enable polymorphism and loose coupling.
Defining and Implementing Interfaces
csharppublic interface IRepository<T>
{
T GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
public class InMemoryRepo<T> : IRepository<T> where T : class
{
private readonly Dictionary<int, T> _data = new();
private int _nextId = 1;
public T GetById(int id) =>
_data.TryGetValue(id, out var item) ? item : null;
public IEnumerable<T> GetAll() => _data.Values;
public void Add(T entity) { _data[_nextId++] = entity; }
public void Update(T entity) { /* find and replace */ }
public void Delete(int id) => _data.Remove(id);
}
Explicit Interface Implementation
csharppublic interface IWriter { void Write(string text); }
public interface ILogger { void Write(string msg); }
public class FileManager : IWriter, ILogger
{
void IWriter.Write(string text)
=> Console.WriteLine($"Writing: {text}");
void ILogger.Write(string msg)
=> Console.WriteLine($"Logging: {msg}");
public void WriteBoth(string content)
{
((IWriter)this).Write(content);
((ILogger)this).Write(content);
}
}
Default Interface Methods (C# 8+)
csharppublic interface INotifier
{
void Send(string message);
void SendWithLog(string message) // default implementation
{
Console.WriteLine($"Sending: {message}");
Send(message);
}
}
public class EmailNotifier : INotifier
{
public void Send(string message)
=> Console.WriteLine($"Email: {message}");
// SendWithLog inherited from interface
}
var n = new EmailNotifier();
((INotifier)n).SendWithLog("Hi");
Interface Segregation
csharp// Prefer small, focused interfaces:
public interface IPrinter { void Print(); }
public interface IScanner { void Scan(); }
public class MultiFunctionPrinter : IPrinter, IScanner
{
public void Print() => Console.WriteLine("Printing...");
public void Scan() => Console.WriteLine("Scanning...");
}
Key Takeaways
- Interfaces define contracts; classes implement them.
- Explicit implementation resolves naming conflicts.
- Default interface methods allow adding members without breaking implementers.
- Follow ISP — keep interfaces small and focused.
Generics
Generics enable type-safe, reusable code without boxing or casting. Before generics, collections stored object, causing boxing overhead and runtime type errors.
Generic Methods
csharppublic static T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) > 0 ? a : b;
int max = Max(5, 10); // 10
double d = Max(3.14, 2.71); // 3.14
string s = Max("apple", "zebra"); // "zebra"
Generic Classes
csharppublic class Stack<T>
{
private T[] _items = new T[4];
private int _count = 0;
public void Push(T item)
{
if (_count == _items.Length)
Array.Resize(ref _items, _items.Length * 2);
_items[_count++] = item;
}
public T Pop() => _count > 0
? _items[--_count]
: throw new InvalidOperationException("Empty");
public T Peek() => _count > 0 ? _items[_count - 1]
: throw new InvalidOperationException("Empty");
public int Count => _count;
}
var stack = new Stack<int>();
stack.Push(10); stack.Push(20);
Console.WriteLine(stack.Pop()); // 20
Generic Constraints
csharppublic class Repo<T> where T : class, IEntity, new() { }
// T must be: reference type + IEntity + parameterless ctor
// Constraint types:
// where T : struct — value type
// where T : class — reference type
// where T : notnull — non-nullable (C# 8+)
// where T : new() — parameterless ctor
// where T : Enum — enum (C# 7.3+)
// where T : Delegate — delegate (C# 7.3+)
// where T : unmanaged — unmanaged (C# 7.3+)
Covariance and Contravariance
csharp// Covariance (out) — T in output positions only
IEnumerable<string> strings = new[] { "a", "b" };
IEnumerable<object> objs = strings; // OK
// Contravariance (in) — T in input positions only
IComparer<object> objComp = /* ... */;
IComparer<string> strComp = objComp; // OK
Key Takeaways
- Generics provide type safety without boxing overhead.
- Constraints specify what types are valid for your generic.
- Covariance (out) and contravariance (in) enable flexible type relationships.
- Generic math (C# 11) enables type-safe arithmetic with INumber
.
Part III: Collections and LINQ
Arrays and Collections
Arrays are fixed-size collections. The System.Collections.Generic namespace provides dynamic collections: List, Dictionary, HashSet, Queue, Stack, and LinkedList.
Arrays
csharpint[] primes = { 2, 3, 5, 7, 11 };
Console.WriteLine(primes[0]); // 2
Array.Sort(primes);
Array.Reverse(primes);
int idx = Array.IndexOf(primes, 7);
bool exists = Array.Exists(primes, n => n > 10);
// Multidimensional
int[,] matrix = { { 1, 2 }, { 3, 4 } };
// Jagged
int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 } };
List
csharpList<string> fruits = new() { "Apple", "Banana" };
fruits.Add("Cherry");
fruits.AddRange(new[] { "Date", "Elderberry" });
fruits.Insert(1, "Blueberry");
fruits.Remove("Banana");
fruits.Sort();
int idx = fruits.IndexOf("Cherry");
bool has = fruits.Contains("Apple");
var filtered = fruits.Where(f => f.Length > 5).ToList();
Dictionary
csharpDictionary<string, int> ages = new()
{
{ "Alice", 30 }, { "Bob", 25 }
};
ages["Carol"] = 28;
if (ages.TryGetValue("Alice", out int age))
Console.WriteLine(age);
foreach (var (name, years) in ages)
Console.WriteLine($"{name}: {years}");
bool hasKey = ages.ContainsKey("David");
ages.Remove("Bob");
HashSet
csharpHashSet<int> set = new() { 1, 2, 3, 4, 5 };
set.Add(3); // no-op (already present)
bool added = set.Add(6); // true
HashSet<int> other = new() { 4, 5, 6, 7 };
set.UnionWith(other); // 1,2,3,4,5,6,7
set.IntersectWith(other); // 4,5,6
set.ExceptWith(other);
bool isSubset = set.IsSubsetOf(other);
Queue and Stack
csharpQueue<string> q = new();
q.Enqueue("First"); q.Enqueue("Second");
string next = q.Dequeue(); // "First"
string peek = q.Peek(); // "Second"
Stack<int> s = new();
s.Push(10); s.Push(20);
int top = s.Pop(); // 20
Performance Comparison
| Collection | Access | Search | Insert/Delete |
|---|---|---|---|
| Array | O(1) | O(n) | O(n) |
| List<T> | O(1) | O(n) | O(n) / O(1)* |
| Dictionary<K,V> | O(1) | O(1) | O(1) |
| HashSet<T> | O(1) | O(1) | O(1) |
| Queue<T> | O(1) peek | O(n) | O(1) |
| Stack<T> | O(1) peek | O(n) | O(1) |
| LinkedList<T> | O(n) | O(n) | O(1) |
Key Takeaways
- Choose the right collection for access, search, and mutation patterns.
- Dictionary and HashSet provide O(1) lookups via hash codes.
- List is the most versatile dynamic array.
- Consider LinkedList for frequent insertions in the middle.
LINQ Fundamentals
LINQ (Language Integrated Query) is a set of features that extends powerful query capabilities to the C# language syntax. LINQ enables you to query any data source that implements IEnumerable or IQueryable.
Query Expression Syntax
csharpint[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evenNumbers = from n in numbers
where n % 2 == 0
orderby n descending
select n * 2;
// Result: 20, 16, 12, 8, 4
Fluent Syntax (Method Syntax)
csharpvar result = numbers
.Where(n => n % 2 == 0)
.OrderByDescending(n => n)
.Select(n => n * 2)
.ToList();
Common LINQ Operators
Filtering
csharpvar adults = people.Where(p => p.Age >= 18);
var first = people.OfType<Student>(); // filter by type
Projection
csharpvar names = people.Select(p => p.Name);
var anon = people.Select(p => new { p.Name, p.Age });
Ordering
csharpvar sorted = people.OrderBy(p => p.Name)
.ThenByDescending(p => p.Age);
Aggregation
csharpint count = numbers.Count();
int sum = numbers.Sum();
double avg = numbers.Average();
int min = numbers.Min();
int max = numbers.Max();
Element Operators
csharpvar first = numbers.First();
var firstEven = numbers.First(n => n % 2 == 0);
var single = numbers.Single(n => n == 5); // throws if not exactly one
var last = numbers.Last();
var atPos = numbers.ElementAt(3);
Quantifiers
csharpbool any = numbers.Any(n => n < 0); // false
bool all = numbers.All(n => n > 0); // true
bool contains = numbers.Contains(5); // true
Deferred vs Immediate Execution
csharpvar query = numbers.Where(n => n > 5); // NOT executed yet
numbers[0] = 100; // changes affect the query!
var result = query.ToList(); // NOW executed (ToList forces it)
// Methods that force immediate execution:
// ToList(), ToArray(), ToDictionary(), ToHashSet()
// Count(), Sum(), Average(), Min(), Max()
// First(), Last(), Single(), Any(), All()
Key Takeaways
- LINQ provides a consistent query model across data sources.
- Deferred execution means queries run when enumerated, not when defined.
- Fluent and query syntax are equivalent; use what's more readable.
- Be aware of deferred execution when the data source changes.
Advanced LINQ
GroupBy
csharpvar students = new[]
{
new { Name = "Alice", Grade = "A", Age = 20 },
new { Name = "Bob", Grade = "B", Age = 19 },
new { Name = "Carol", Grade = "A", Age = 21 }
};
var byGrade = students.GroupBy(s => s.Grade);
foreach (var group in byGrade)
{
Console.WriteLine($"Grade {group.Key}: {group.Count()} students");
foreach (var s in group)
Console.WriteLine($" {s.Name}");
}
Join
csharpvar orders = new[] { /* order data */ };
var customers = new[] { /* customer data */ };
var query = from o in orders
join c in customers on o.CustomerId equals c.Id
select new { c.Name, o.Total, o.Date };
// Group join
var customersWithOrders = from c in customers
join o in orders on c.Id equals o.CustomerId into orderGroup
select new { c.Name, Orders = orderGroup };
SelectMany (Flatten)
csharpvar classes = new[]
{
new { Name = "Math", Students = new[] { "A", "B" } },
new { Name = "Science", Students = new[] { "C", "D" } }
};
var allStudents = classes.SelectMany(c => c.Students);
// "A", "B", "C", "D"
Zip
csharpvar names = new[] { "Alice", "Bob", "Carol" };
var scores = new[] { 85, 92, 78 };
var combined = names.Zip(scores, (n, s) => $"{n}: {s}");
// "Alice: 85", "Bob: 92", "Carol: 78"
Set Operations
csharpint[] set1 = { 1, 2, 3, 4 };
int[] set2 = { 3, 4, 5, 6 };
var union = set1.Union(set2); // 1,2,3,4,5,6
var intersect = set1.Intersect(set2); // 3,4
var except = set1.Except(set2); // 1,2
var distinct = set1.Distinct(); // remove duplicates
Aggregate
csharpint[] nums = { 1, 2, 3, 4, 5 };
// Manual sum using Aggregate
int sum = nums.Aggregate((acc, n) => acc + n); // 15
// With seed
int product = nums.Aggregate(1, (acc, n) => acc * n); // 120
// String building
string csv = nums.Aggregate("", (acc, n) => acc == ""
? n.ToString() : $"{acc},{n}"); // "1,2,3,4,5"
Deferred Execution Gotchas
csharp// ⚠ BAD: Query re-executed each time
var young = people.Where(p => p.Age < 30);
Console.WriteLine(young.Count()); // query runs here
Console.WriteLine(young.First()); // query runs AGAIN
// ✅ GOOD: Materialize once
var youngList = people.Where(p => p.Age < 30).ToList();
Console.WriteLine(youngList.Count);
Console.WriteLine(youngList[0]);
Key Takeaways
- GroupBy partitions data; Join combines two sources via key.
- SelectMany flattens nested collections into a single sequence.
- Aggregate performs custom accumulations.
- Always materialize (ToList/ToArray) if enumerating multiple times.
Part IV: Advanced C#
Delegates, Events, and Lambda Expressions
Delegates
A delegate is a type-safe function pointer. It defines a signature that methods must match to be called through the delegate.
csharp// Declare a delegate type
public delegate int Operation(int x, int y);
// Methods matching the signature
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
// Instantiate and invoke
Operation op = Add;
Console.WriteLine(op(3, 4)); // 7
op = Multiply;
Console.WriteLine(op(3, 4)); // 12
// Multicast delegate (chaining)
Operation multi = Add;
multi += Multiply;
int result = multi(3, 4); // returns 12 (last in chain)
Built-in Delegates
csharp// Func<TResult> — takes 0-16 params, returns value
Func<int, int, int> add = (a, b) => a + b;
// Action — takes 0-16 params, returns void
Action<string> print = msg => Console.WriteLine(msg);
// Predicate<T> — takes T, returns bool
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(add(5, 3)); // 8
print("Hello"); // Hello
Console.WriteLine(isEven(4)); // True
Lambda Expressions
csharp// Lambda: (parameters) => expression or block
Func<int, int> square = x => x * x;
Func<int, int, int> sum = (x, y) => x + y;
// Statement lambda (with block body)
Func<int, int, int> max = (x, y) =>
{
return x > y ? x : y;
};
// Lambda with discard (C# 9+)
Func<int, int, int> ignore = (_, _) => 42;
// Used heavily with LINQ
var evens = numbers.Where(n => n % 2 == 0).ToList();
Events
csharppublic class Button
{
// Event declaration (uses EventHandler<T> by convention)
public event EventHandler<EventArgs>? Clicked;
public void Click()
{
Console.WriteLine("Button clicked");
OnClicked();
}
protected virtual void OnClicked()
=> Clicked?.Invoke(this, EventArgs.Empty);
}
// Usage
var btn = new Button();
btn.Clicked += (sender, e) => Console.WriteLine("Event handled!");
btn.Click(); // "Button clicked" + "Event handled!"
Key Takeaways
- Delegates are type-safe function pointers; they enable callback mechanisms.
- Use Func/Action/Predicate instead of custom delegates for common patterns.
- Lambdas are concise inline functions, essential for LINQ.
- Events provide a publish-subscribe pattern built on delegates.
Exception Handling
Exceptions represent runtime errors. C# provides structured exception handling with try/catch/finally blocks.
Try/Catch/Finally
csharptry
{
string data = File.ReadAllText("config.json");
int value = int.Parse(data);
Console.WriteLine(value);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"File not found: {ex.FileName}");
}
catch (FormatException ex) when (ex.Message.Contains("Input"))
{
// Exception filter — catches only when condition is true
Console.WriteLine($"Bad format: {ex.Message}");
}
catch (Exception ex)
{
// Catch-all (rethrow if cannot handle)
Console.WriteLine($"Unexpected error: {ex.Message}");
throw; // preserves stack trace
}
finally
{
Console.WriteLine("This always executes");
}
Throwing Exceptions
csharppublic void Withdraw(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("Amount must be positive",
nameof(amount));
if (amount > Balance)
throw new InsufficientFundsException(Balance, amount);
Balance -= amount;
}
Custom Exceptions
csharppublic class InsufficientFundsException : Exception
{
public decimal Balance { get; }
public decimal Attempted { get; }
public InsufficientFundsException(decimal balance, decimal attempted)
: base($"Insufficient funds. Balance: {balance:C}, Attempted: {attempted:C}")
{
Balance = balance;
Attempted = attempted;
}
}
Best Practices
- Catch specific exceptions before general ones.
- Use exception filters (when) for conditional catching.
- Avoid throwing exceptions for control flow — use TryParse pattern.
- Always use finally for cleanup (or use statement for IDisposable).
- Log exceptions at the application boundary.
- Use throw; (not throw ex;) to preserve stack trace.
Key Takeaways
- Use try/catch/finally for structured error handling.
- Exception filters (when) enable conditional catching.
- Create custom exceptions for domain-specific errors.
- Fail fast — validate inputs early; avoid swallowing exceptions.
Asynchronous Programming with async/await
Asynchronous programming is essential for building responsive applications. The async/await pattern (C# 5+) allows non-blocking operations without manual thread management.
Basics of async/await
csharppublic async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
string result = await client.GetStringAsync(url);
return result;
}
// Calling an async method
string data = await FetchDataAsync("https://api.example.com/data");
Console.WriteLine(data);
Async Return Types
csharp// Task<T> — returns a value
public async Task<int> GetCountAsync() => await CountItemsAsync();
// Task — no return value (like void)
public async Task SaveDataAsync(string data)
{
await File.WriteAllTextAsync("data.txt", data);
}
// ValueTask<T> — avoids Task allocation when result is synchronous
public ValueTask<int> GetCachedValueAsync()
{
if (_cached.HasValue)
return new ValueTask<int>(_cached.Value);
return new ValueTask<int>(ComputeExpensiveAsync());
}
// void — only for event handlers (fire-and-forget)
public async void Button_Click(object sender, RoutedEventArgs e)
{
await DoWorkAsync();
}
Async Streams (C# 8+)
csharp// Producer
public async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100);
yield return i;
}
}
// Consumer
await foreach (int num in GenerateNumbersAsync())
{
Console.WriteLine(num);
}
Task Combinators
csharp// Wait for all to complete
Task task1 = DoWorkAsync();
Task task2 = DoOtherWorkAsync();
await Task.WhenAll(task1, task2);
// Wait for first to complete
Task<string> first = await Task.WhenAny(FetchA(), FetchB());
string result = await first;
// Sequential composition
var data = await FetchAsync()
.ContinueWith(t => Process(t.Result));
ConfigureAwait
csharp// In libraries (UI-agnostic), use ConfigureAwait(false)
public async Task<string> ReadFileAsync(string path)
{
using var stream = File.OpenRead(path);
byte[] buffer = new byte[stream.Length];
await stream.ReadAsync(buffer, 0, buffer.Length)
.ConfigureAwait(false);
return Encoding.UTF8.GetString(buffer);
}
Common Pitfalls
- Blocking on async: Never use .Result or .Wait() — causes deadlocks.
- Async all the way: Don't mix sync and async; go async from top to bottom.
- Fire-and-forget: Avoid async void except for event handlers.
- Captured context: Use ConfigureAwait(false) in libraries.
Key Takeaways
- async/await is non-blocking; no thread is consumed during I/O.
- Await returns the result; the method resumes on the captured context.
- Use Task.WhenAll for concurrency, Task.WhenAny for race conditions.
- Never block on async code — it causes deadlocks.
Reflection and Attributes
Attributes
Attributes add metadata to code elements (classes, methods, properties). They are used for serialization, validation, authorization, and more.
csharp[Serializable]
public class Person
{
[Obsolete("Use Name instead")]
public string FullName { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
[Range(0, 150)]
public int Age { get; set; }
}
// Custom attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public AuthorAttribute(string name) => Name = name;
}
[Author("Alice")]
public class Service { }
Reflection
csharpusing System.Reflection;
Type type = typeof(Person);
// Get all properties
foreach (PropertyInfo prop in type.GetProperties())
{
Console.WriteLine($"{prop.Name}: {prop.PropertyType.Name}");
}
// Get custom attributes
var authorAttr = type.GetCustomAttribute<AuthorAttribute>();
if (authorAttr != null)
Console.WriteLine($"Author: {authorAttr.Name}");
// Dynamic invocation
object instance = Activator.CreateInstance(typeof(Person))!;
PropertyInfo nameProp = type.GetProperty("Name")!;
nameProp.SetValue(instance, "Alice");
string name = (string)nameProp.GetValue(instance)!;
Console.WriteLine(name); // Alice
// Invoke method
MethodInfo method = type.GetMethod("GetGreeting")!;
string result = (string)method.Invoke(instance, null)!;
Source Generators (C# 9+)
Source Generators run at compile time, analyzing code and generating additional source files. They replace much of what Reflection was used for, with better performance.
csharp// Example: partial class generated by a source generator
// The generator reads [GenerateDto] attribute and creates DTO classes
// Common libraries: System.Text.Json source gen, Regex source gen
Key Takeaways
- Attributes add declarative metadata to code elements.
- Reflection inspects types and members at runtime.
- Use Reflection sparingly — it's slow and bypasses compile-time checks.
- Source Generators provide compile-time code generation as a faster alternative.
Part V: .NET Platform
Memory Management and Garbage Collection
.NET's automatic memory management frees developers from manual memory handling. The GC (Garbage Collector) manages allocation and deallocation of objects on the managed heap.
Generations
The managed heap is divided into three generations:
- Gen 0: Short-lived objects (e.g., temporary variables). Collected most frequently.
- Gen 1: Buffer between Gen 0 and Gen 2. Survivors from Gen 0 are promoted here.
- Gen 2: Long-lived objects (e.g., static data, singletons). Collected rarely.
- LOH (Large Object Heap): Objects ≥ 85 KB. Always Gen 2, not compacted by default.
GC Process
- Mark: Starting from root references (static fields, stack locals, CPU registers), the GC traverses object graphs to find live objects.
- Sweep: Dead objects' memory is freed. Adjacent dead objects become a single free block.
- Compact: Live objects are moved together to reduce fragmentation (optional, not done on LOH by default).
IDisposable Pattern
csharppublic class ResourceHolder : IDisposable
{
private IntPtr _nativeResource; // native resource (must release)
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // don't call finalizer
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// Release managed resources (IDisposable fields)
}
// Release native resources
if (_nativeResource != IntPtr.Zero)
{
Marshal.FreeHGlobal(_nativeResource);
_nativeResource = IntPtr.Zero;
}
_disposed = true;
}
~ResourceHolder() => Dispose(false); // finalizer
// Safe usage
public void UseResource() { /* ... */ }
}
// Usage: using statement ensures Dispose is called
using (var resource = new ResourceHolder())
{
resource.UseResource();
} // Dispose called here even if exception occurs
// Using declaration (C# 8+)
using var file = new StreamReader("data.txt");
// Dispose called when file goes out of scope
GC Modes
| Mode | Description |
|---|---|
| Workstation GC | Default for desktop apps; one heap per process. |
| Server GC | Default for ASP.NET Core; one heap per core, higher throughput. |
| Concurrent GC | Background collection minimizes pause times (default in .NET Core). |
| Non-concurrent GC | Suspends all threads during collection; shorter total time but longer pauses. |
Memory Best Practices
- Avoid allocations in hot paths — use object pooling (ArrayPool<T>, ObjectPool<T>).
- Use structs for small, immutable data — avoid heap allocations.
- Implement IDisposable for classes holding native resources.
- Use Span<T> and Memory<T> for stack-allocated memory views (C# 7.2+).
- Be careful with large object allocations — LOH is expensive to collect.
Key Takeaways
- GC manages heap memory automatically; generations optimize collection frequency.
- Implement IDisposable for native resource cleanup.
- Server GC scales with CPU cores (default for ASP.NET Core).
- Use Span
, structs, and pooling to reduce GC pressure.
File I/O and Serialization
File I/O
System.IO provides classes for reading/writing files, streams, and directories.
csharp// Reading files
string text = File.ReadAllText("data.txt");
string[] lines = File.ReadAllLines("data.txt");
// Writing files
File.WriteAllText("output.txt", "Hello, World!");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });
// Stream-based I/O (efficient for large files)
using var reader = new StreamReader("largefile.txt");
string? line;
while ((line = await reader.ReadLineAsync()) != null)
Process(line);
using var writer = new StreamWriter("log.txt", append: true);
await writer.WriteLineAsync($"{DateTime.UtcNow}: Event logged");
// File info
bool exists = File.Exists("data.txt");
FileInfo info = new("data.txt");
Console.WriteLine($"{info.Length} bytes, {info.LastWriteTime}");
Serialization
System.Text.Json (JSON)
csharpusing System.Text.Json;
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string? Email { get; set; }
}
// Serialize
var person = new Person { Name = "Alice", Age = 30 };
string json = JsonSerializer.Serialize(person);
// {"Name":"Alice","Age":30}
// Deserialize
Person? deserialized = JsonSerializer.Deserialize<Person>(json);
// Options
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
// Source generator (C# 9+, faster)
[JsonSerializable(typeof(Person))]
partial class AppJsonContext : JsonSerializerContext { }
XML Serialization
csharpusing System.Xml.Serialization;
var serializer = new XmlSerializer(typeof(Person));
using var writer = new StringWriter();
serializer.Serialize(writer, person);
string xml = writer.ToString();
using var reader = new StringReader(xml);
Person? deserialized = (Person?)serializer.Deserialize(reader);
Streams
csharp// MemoryStream — in-memory buffer
using var memStream = new MemoryStream();
using var writer = new StreamWriter(memStream);
writer.Write("Hello");
writer.Flush();
memStream.Position = 0;
using var reader = new StreamReader(memStream);
string text = reader.ReadToEnd();
// FileStream — file I/O
using var fs = new FileStream("data.bin", FileMode.OpenOrCreate);
byte[] buffer = new byte[1024];
int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length);
await fs.WriteAsync(buffer, 0, bytesRead);
// Pipe — streaming between producers and consumers
// System.IO.Pipelines for high-performance I/O
Binary Serialization (Protobuf)
csharp// Using protobuf-net (NuGet)
[ProtoContract]
public class Person
{
[ProtoMember(1)] public string Name { get; set; }
[ProtoMember(2)] public int Age { get; set; }
}
using var ms = new MemoryStream();
Serializer.Serialize(ms, person);
byte[] data = ms.ToArray();
ms.Position = 0;
var deserialized = Serializer.Deserialize<Person>(ms);
Key Takeaways
- Use async File I/O for non-blocking operations (ReadAsync/WriteAsync).
- System.Text.Json is the preferred JSON serializer (fast, no reflection).
- Streams provide a composable abstraction for byte-level I/O.
- Consider protocol buffers (protobuf) for high-performance binary serialization.
Dependency Injection
Dependency Injection (DI) is a design pattern where dependencies are provided (injected) to a class rather than created by the class itself. ASP.NET Core has built-in DI with first-class support.
Why DI?
- Loose coupling: Classes depend on abstractions (interfaces), not concrete implementations.
- Testability: Dependencies can be mocked or stubbed in unit tests.
- Maintainability: Changing implementations doesn't require changing consumers.
- Lifetime management: Container handles creation and disposal of services.
Registering Services
csharpvar builder = WebApplication.CreateBuilder(args);
// Transient: new instance every time (lightweight, stateless)
builder.Services.AddTransient<IEmailService, EmailService>();
// Scoped: one instance per HTTP request (DbContext, unit of work)
builder.Services.AddScoped<IUserRepository, UserRepository>();
// Singleton: one instance for the entire application lifetime
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
builder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
Constructor Injection
csharppublic class UserController : ControllerBase
{
private readonly IUserRepository _repo;
private readonly IEmailService _email;
private readonly ILogger<UserController> _logger;
// Dependencies are automatically resolved by the container
public UserController(
IUserRepository repo,
IEmailService email,
ILogger<UserController> logger)
{
_repo = repo;
_email = email;
_logger = logger;
}
[HttpGet("{id}")]
public async Task<ActionResult<User>> GetUser(int id)
{
_logger.LogInformation("Fetching user {Id}", id);
var user = await _repo.GetByIdAsync(id);
if (user == null) return NotFound();
return Ok(user);
}
}
Manual Resolution (Service Locator)
csharp// Avoid this pattern — prefer constructor injection
public class SomeClass
{
private readonly IServiceProvider _services;
public SomeClass(IServiceProvider services)
{
_services = services;
}
public void DoWork()
{
using var scope = _services.CreateScope();
var repo = scope.ServiceProvider
.GetRequiredService<IUserRepository>();
// ... use repo
}
}
DI Anti-Patterns
- Service Locator: Resolving from container directly (hides dependencies).
- Captive Dependency: Scoped service injected into singleton (stale data).
- Constructor Over-injection: Too many constructor parameters (violates SRP).
- Disposing container-managed services: Container manages disposal.
Key Takeaways
- DI enables loose coupling, testability, and maintainability.
- Choose lifetimes carefully: Transient, Scoped, or Singleton.
- ASP.NET Core has built-in DI — no third-party container needed.
- Inject interfaces, not concrete classes.
Part VI: Data Access
ADO.NET Fundamentals
ADO.NET is the low-level data access technology in .NET. It provides direct connection to databases, commands, and data readers. EF Core and Dapper are built on top of ADO.NET.
Connecting to a Database
csharpusing System.Data;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=MyDb;"
+ "User Id=sa;Password=MyPassword;TrustServerCertificate=true;";
using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
Console.WriteLine($"State: {connection.State}");
Executing Commands
csharp// ExecuteNonQuery — for INSERT, UPDATE, DELETE
using var cmd = new SqlCommand(
"INSERT INTO Users (Name, Email) VALUES (@name, @email)",
connection);
cmd.Parameters.AddWithValue("@name", "Alice");
cmd.Parameters.AddWithValue("@email", "alice@example.com");
int rows = await cmd.ExecuteNonQueryAsync();
Console.WriteLine($"{rows} row(s) affected");
// ExecuteScalar — returns single value
var scalarCmd = new SqlCommand("SELECT COUNT(*) FROM Users", connection);
int count = (int)(await scalarCmd.ExecuteScalarAsync())!;
Console.WriteLine($"Total users: {count}");
DataReader
csharpvar queryCmd = new SqlCommand(
"SELECT Id, Name, Email FROM Users WHERE IsActive = 1",
connection);
using var reader = await queryCmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
int id = reader.GetInt32(0);
string name = reader.GetString(1);
string email = reader.GetString(2);
Console.WriteLine($"#{id}: {name} <{email}>");
}
DataTable and DataAdapter
csharpusing var adapter = new SqlDataAdapter(
"SELECT * FROM Users", connection);
var table = new DataTable();
adapter.Fill(table);
foreach (DataRow row in table.Rows)
{
Console.WriteLine($"{row["Id"]}: {row["Name"]}");
}
Async Operations
csharpawait connection.OpenAsync();
await cmd.ExecuteNonQueryAsync();
await cmd.ExecuteScalarAsync();
await reader.ReadAsync();
// Each has a synchronous counterpart (without Async suffix)
Connection Pooling
ADO.NET pools connections by connection string. The pool is managed automatically. To benefit from pooling:
- Always close/dispose connections (using statement).
- Keep connection strings consistent (pool key is the string).
- Adjust pool size with Max Pool Size / Min Pool Size in the connection string.
Key Takeaways
- ADO.NET provides direct database access with full control.
- Always use parameters to prevent SQL injection.
- Use async methods for non-blocking database operations.
- Connection pooling is automatic — just dispose connections properly.
Entity Framework Core - Getting Started
Entity Framework Core (EF Core) is a modern, lightweight, extensible ORM that enables .NET developers to work with databases using .NET objects. It eliminates the need for most of the data-access code developers typically need to write.
Installing EF Core
csharpdotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.Tools
Defining the Model
csharppublic class Blog
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
// Navigation property
public List<Comment> Comments { get; set; } = new();
}
public class Comment
{
public int Id { get; set; }
public string Text { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public int BlogId { get; set; }
public Blog Blog { get; set; } = null!;
}
Creating the DbContext
csharppublic class AppDbContext : DbContext
{
public DbSet<Blog> Blogs => Set<Blog>();
public DbSet<Comment> Comments => Set<Comment>();
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Fluent API configuration
modelBuilder.Entity<Blog>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Title).HasMaxLength(200).IsRequired();
entity.HasMany(e => e.Comments)
.WithOne(e => e.Blog)
.HasForeignKey(e => e.BlogId)
.OnDelete(DeleteBehavior.Cascade);
});
}
}
Registering and Using
csharp// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration
.GetConnectionString("DefaultConnection")));
// Using in a service
public class BlogService
{
private readonly AppDbContext _db;
public BlogService(AppDbContext db) => _db = db;
public async Task<List<Blog>> GetAllBlogsAsync()
=> await _db.Blogs.OrderByDescending(b => b.CreatedAt)
.ToListAsync();
public async Task<Blog?> GetBlogAsync(int id)
=> await _db.Blogs.Include(b => b.Comments)
.FirstOrDefaultAsync(b => b.Id == id);
public async Task<Blog> CreateBlogAsync(string title, string content)
{
var blog = new Blog { Title = title, Content = content };
_db.Blogs.Add(blog);
await _db.SaveChangesAsync();
return blog;
}
}
Migrations
csharp# Create migration
dotnet ef migrations add InitialCreate
# Apply to database
dotnet ef database update
# Generate SQL script
dotnet ef migrations script -o script.sql
# Remove last migration
dotnet ef migrations remove
Key Takeaways
- EF Core translates LINQ queries to SQL automatically.
- Use migrations for version-controlled schema changes.
- Include() eager-loads related data, avoiding N+1 queries.
- Register DbContext as Scoped (default in ASP.NET Core).
EF Core - Advanced Concepts
Query Types
csharp// Tracking vs No-Tracking
var blogs = await _db.Blogs.AsNoTracking().ToListAsync();
// No-tracking is faster for read-only queries
// Raw SQL queries
var blogs = await _db.Blogs
.FromSql($"SELECT * FROM Blogs WHERE Title LIKE '%' + {term} + '%'")
.ToListAsync();
// Execute raw SQL
int affected = await _db.Database
.ExecuteSqlAsync($"UPDATE Blogs SET Views = Views + 1");
// Compiled queries (EF Core 6+)
private static readonly Func<AppDbContext, int, Blog?> GetBlogById =
EF.CompileQuery((AppDbContext ctx, int id) =>
ctx.Blogs.Include(b => b.Comments)
.FirstOrDefault(b => b.Id == id));
// Usage: var blog = GetBlogById(_db, 1);
Change Tracker
csharpvar blog = await _db.Blogs.FindAsync(1);
// Track state
Console.WriteLine(_db.Entry(blog).State); // Unchanged
blog.Title = "Updated";
Console.WriteLine(_db.Entry(blog).State); // Modified
_db.Blogs.Remove(blog);
Console.WriteLine(_db.Entry(blog).State); // Deleted
await _db.SaveChangesAsync(); // Generates SQL for all tracked changes
Concurrency Control
csharp// Model with concurrency token
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
[ConcurrencyCheck]
public int Version { get; set; }
}
// Or use row version (SQL Server)
// public byte[] RowVersion { get; set; }
// On conflict
try
{
await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// Reload and retry, or merge changes
var entry = ex.Entries.Single();
await entry.ReloadAsync();
}
Interceptors (EF Core 6+)
csharppublic class SlowQueryInterceptor : ISaveChangesInterceptor
{
public async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken ct = default)
{
var db = eventData.Context!;
foreach (var entry in db.ChangeTracker.Entries())
{
Console.WriteLine($"{entry.Entity.GetType().Name}: {entry.State}");
}
return result;
}
}
// Register in DbContext or Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.AddInterceptors(new SlowQueryInterceptor()));
Performance Tips
- Use AsNoTracking() for read-only queries.
- Use FindAsync() (checks local cache) instead of FirstOrDefault by PK.
- Batch operations with .ExecuteUpdate() / .ExecuteDelete() (EF Core 7+).
- Avoid N+1 queries by using Include() for related data.
- Use compiled queries for frequently executed queries.
- Enable SQL logging during development: options.LogTo(Console.WriteLine).
Key Takeaways
- No-tracking queries are faster for read-only scenarios.
- Change tracker detects entity state changes automatically.
- Concurrency tokens prevent lost updates in concurrent scenarios.
- Batch operations in EF Core 7+ reduce round-trips.
Dapper and Micro-ORMs
Dapper is a lightweight, high-performance micro-ORM by the Stack Overflow team. It extends IDbConnection with extension methods for executing queries and mapping results to objects, giving you SQL control with object convenience.
Getting Started
csharpusing Dapper;
using System.Data.SqlClient;
string connectionString = /* ... */;
using var connection = new SqlConnection(connectionString);
// Query and map to typed objects
var sql = "SELECT Id, Name, Email FROM Users WHERE IsActive = @active";
var users = await connection.QueryAsync<User>(sql, new { active = true });
foreach (var user in users)
Console.WriteLine($"{user.Name} <{user.Email}>");
Key Methods
csharp// QueryAsync — returns IEnumerable<T>
var users = await conn.QueryAsync<User>("SELECT * FROM Users");
// QueryFirstAsync — single result or exception
var user = await conn.QueryFirstAsync<User>(
"SELECT * FROM Users WHERE Id = @id", new { id = 1 });
// QueryFirstOrDefaultAsync — single result or null
var user = await conn.QueryFirstOrDefaultAsync<User>(
"SELECT * FROM Users WHERE Id = @id", new { id = 1 });
// QuerySingleAsync — exactly one result
var user = await conn.QuerySingleAsync<User>(
"SELECT * FROM Users WHERE Email = @email",
new { email = "alice@example.com" });
// ExecuteAsync — for INSERT, UPDATE, DELETE
int rows = await conn.ExecuteAsync(
"UPDATE Users SET Name = @name WHERE Id = @id",
new { name = "NewName", id = 1 });
// ExecuteScalarAsync — single value
int count = await conn.ExecuteScalarAsync<int>(
"SELECT COUNT(*) FROM Users");
Multiple Results
csharpvar sql = @"SELECT * FROM Users;
SELECT * FROM Orders WHERE UserId = @userId;";
using var multi = await conn.QueryMultipleAsync(sql, new { userId = 1 });
var users = await multi.ReadAsync<User>();
var orders = await multi.ReadAsync<Order>();
Transactions
csharpusing var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
using var tx = await connection.BeginTransactionAsync();
try
{
await connection.ExecuteAsync(
"INSERT INTO Orders (Product, Total) VALUES (@product, @total)",
new { product = "Widget", total = 9.99m }, transaction: tx);
await connection.ExecuteAsync(
"UPDATE Inventory SET Stock = Stock - 1 WHERE Product = @product",
new { product = "Widget" }, transaction: tx);
await tx.CommitAsync();
}
catch
{
await tx.RollbackAsync();
throw;
}
EF Core vs Dapper
| Aspect | EF Core | Dapper |
|---|---|---|
| ORM Type | Full, with change tracking | Micro, SQL-first |
| Performance | Slower (overhead) | Near-raw ADO.NET |
| SQL Control | Generated automatically | Full manual control |
| Learning Curve | Steeper (magic) | Shallow (just SQL) |
| Best For | CRUD apps, complex domain models | High-performance, queries, reports |
Key Takeaways
- Dapper is fast — essentially raw ADO.NET with object mapping.
- Write SQL manually — full control over queries.
- Great for high-performance scenarios, reports, and read-heavy apps.
- Use transactions explicitly when multiple operations must be atomic.
Part VII: Web Development
ASP.NET Core Fundamentals
ASP.NET Core is a cross-platform, high-performance framework for building modern, cloud-enabled web applications. It runs on .NET and is designed from the ground up for performance and flexibility.
WebApplication Builder
csharpvar builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Configuration
csharp// appsettings.json
// {
// "Database": { "ConnectionString": "..." },
// "Features": { "EnableLogging": true }
// }
// Reading configuration
string connStr = builder.Configuration
.GetConnectionString("DefaultConnection");
bool logging = builder.Configuration
.GetValue<bool>("Features:EnableLogging");
// Options pattern
public class DatabaseOptions
{
public string ConnectionString { get; set; } = "";
}
builder.Services.Configure<DatabaseOptions>(
builder.Configuration.GetSection("Database"));
// Environment-based config
// appsettings.Development.json, appsettings.Production.json
var env = builder.Environment.EnvironmentName;
Console.WriteLine($"Running in: {env}");
Middleware
csharp// Custom middleware component
app.Use(async (context, next) =>
{
var sw = Stopwatch.StartNew();
Console.WriteLine($">> {context.Request.Method} {context.Request.Path}");
await next(); // call next middleware
sw.Stop();
Console.WriteLine($"<< {context.Response.StatusCode} ({sw.ElapsedMilliseconds}ms)");
});
// Built-in middleware (typical order)
// app.UseExceptionHandler();
// app.UseHsts();
// app.UseHttpsRedirection();
// app.UseStaticFiles();
// app.UseRouting();
// app.UseCors();
// app.UseAuthentication();
// app.UseAuthorization();
// app.MapControllers();
Environment Management
csharpif (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else if (app.Environment.IsProduction())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
// Custom environment
// DOTNET_ENVIRONMENT=Staging in launchSettings.json
Key Takeaways
- ASP.NET Core is modular, cross-platform, and high-performance.
- Configuration system supports JSON, env vars, CLI args, and more.
- Middleware pipeline processes every request in configured order.
- Use environment-based configuration for dev/staging/production.
RESTful APIs with Minimal APIs
Minimal APIs (introduced in .NET 6) provide a streamlined way to build HTTP APIs with minimal ceremony. They are ideal for microservices, simple APIs, and learning.
Basic Endpoints
csharpvar app = WebApplication.Create(args);
// GET
app.MapGet("/api/items", async (AppDbContext db) =>
await db.Items.ToListAsync());
// GET with ID
app.MapGet("/api/items/{id}", async (int id, AppDbContext db) =>
await db.Items.FindAsync(id) is Item item
? Results.Ok(item)
: Results.NotFound());
// POST
app.MapPost("/api/items", async (Item item, AppDbContext db) =>
{
db.Items.Add(item);
await db.SaveChangesAsync();
return Results.Created($"/api/items/{item.Id}", item);
});
// PUT
app.MapPut("/api/items/{id}", async (int id, Item input, AppDbContext db) =>
{
var item = await db.Items.FindAsync(id);
if (item is null) return Results.NotFound();
item.Name = input.Name;
item.Price = input.Price;
await db.SaveChangesAsync();
return Results.NoContent();
});
// DELETE
app.MapDelete("/api/items/{id}", async (int id, AppDbContext db) =>
{
var item = await db.Items.FindAsync(id);
if (item is null) return Results.NotFound();
db.Items.Remove(item);
await db.SaveChangesAsync();
return Results.NoContent();
});
Route Groups
csharpvar items = app.MapGroup("/api/items")
.WithTags("Items")
.RequireAuthorization();
items.MapGet("/", GetAllItems);
items.MapGet("/{id}", GetItemById);
items.MapPost("/", CreateItem);
items.MapPut("/{id}", UpdateItem);
items.MapDelete("/{id}", DeleteItem);
Parameter Binding
csharp// Route parameters
app.MapGet("/users/{id}", (int id) => ...);
// Query parameters
app.MapGet("/users",
(int page, int pageSize) => ...);
// Request body (JSON)
app.MapPost("/users", (User user) => ...);
// Form values
app.MapPost("/upload",
(IFormFile file) => ...);
// Header values
app.MapGet("/check",
([FromHeader] string authorization) => ...);
// Services (DI)
app.MapGet("/data", (AppDbContext db, ILogger<Program> log) => ...);
// log and db are resolved from DI container
Structured Response
csharpapp.MapGet("/api/items/{id}", async (int id, AppDbContext db) =>
{
var item = await db.Items.FindAsync(id);
return item switch
{
null => Results.Problem(
statusCode: 404,
title: "Item not found",
detail: $"Item with ID {id} does not exist."),
_ => Results.Ok(new ItemResponse
{
Id = item.Id,
Name = item.Name,
Price = item.Price,
Links = new[]
{
new Link("self", $"/api/items/{item.Id}"),
new Link("update", $"/api/items/{item.Id}")
}
})
};
});
Key Takeaways
- Minimal APIs are concise and perfect for simple endpoints.
- Parameter binding automatically resolves from route, query, body, DI.
- Results helpers (Ok, NotFound, Created, NoContent, Problem) standardize responses.
- Route groups organize related endpoints with shared configuration.
MVC Pattern
MVC (Model-View-Controller) separates an application into three interconnected components: Model (data), View (UI), Controller (handles requests). ASP.NET Core MVC provides full-featured support.
Controller
csharp[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly AppDbContext _db;
public ProductsController(AppDbContext db) => _db = db;
[HttpGet]
public async Task<ActionResult<List<Product>>> GetAll()
{
return await _db.Products.ToListAsync();
}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetById(int id)
{
var product = await _db.Products.FindAsync(id);
if (product == null) return NotFound();
return product;
}
[HttpPost]
public async Task<ActionResult<Product>> Create(Product product)
{
_db.Products.Add(product);
await _db.SaveChangesAsync();
return CreatedAtAction(nameof(GetById),
new { id = product.Id }, product);
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, Product input)
{
var product = await _db.Products.FindAsync(id);
if (product == null) return NotFound();
product.Name = input.Name;
product.Price = input.Price;
await _db.SaveChangesAsync();
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
var product = await _db.Products.FindAsync(id);
if (product == null) return NotFound();
_db.Products.Remove(product);
await _db.SaveChangesAsync();
return NoContent();
}
[HttpGet("search")]
public async Task<ActionResult<List<Product>>> Search(
[FromQuery] string term)
{
return await _db.Products
.Where(p => p.Name.Contains(term))
.ToListAsync();
}
}
Model Binding and Validation
csharppublic class CreateProductRequest
{
[Required]
[StringLength(100, MinimumLength = 2)]
public string Name { get; set; } = "";
[Range(0.01, 999999.99)]
public decimal Price { get; set; }
[EmailAddress]
public string? ContactEmail { get; set; }
[Url]
public string? Website { get; set; }
[RegularExpression(@"^[A-Z]{2}\d{4}$")]
public string? Code { get; set; }
}
// In controller:
[HttpPost]
public async Task<ActionResult> Create(
[FromBody] CreateProductRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Process valid request...
}
Filter Pipeline
csharp// Custom action filter
public class LogActionFilter : IActionFilter
{
private readonly ILogger<LogActionFilter> _logger;
public LogActionFilter(ILogger<LogActionFilter> logger)
=> _logger = logger;
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInformation("Before action: {Action}",
context.ActionDescriptor.DisplayName);
}
public void OnActionExecuted(ActionExecutedContext context)
{
_logger.LogInformation("After action: {Status}",
context.Result?.GetType().Name);
}
}
// Global registration
builder.Services.AddControllers(options =>
{
options.Filters.Add<LogActionFilter>();
options.Filters.Add<ProducesResponseTypeAttribute>(304);
});
Key Takeaways
- MVC separates concerns: controllers handle requests, models represent data.
- Data annotations provide declarative validation on models.
- Action filters enable cross-cutting concerns (logging, caching, validation).
- Model binding automatically maps request data to action parameters.
Authentication and Authorization
ASP.NET Core provides a rich security system with authentication (who you are) and authorization (what you can do). JWT (JSON Web Tokens) is the most common approach for API authentication.
JWT Authentication Setup
csharpvar builder = WebApplication.CreateBuilder(args);
// Configure JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!))
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Generating Tokens
csharppublic class AuthController : ControllerBase
{
[HttpPost("login")]
public IActionResult Login([FromBody] LoginRequest request)
{
// Validate credentials (against DB, etc.)
if (request.Username != "admin" || request.Password != "pass")
return Unauthorized();
var claims = new[]
{
new Claim(ClaimTypes.Name, request.Username),
new Claim(ClaimTypes.Role, "Admin"),
new Claim("department", "Engineering")
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes("YourSecretKeyHere!123"));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: "myapp",
audience: "myapp",
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: creds
);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expires = token.ValidTo
});
}
}
Authorization
csharp// Role-based
[Authorize(Roles = "Admin")]
[HttpGet("admin-only")]
public IActionResult AdminEndpoint() => Ok();
// Policy-based
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireEngineering", policy =>
policy.RequireClaim("department", "Engineering"));
options.AddPolicy("Over21", policy =>
policy.RequireAssertion(context =>
{
var ageClaim = context.User.FindFirst("age")?.Value;
return int.TryParse(ageClaim, out int age) && age >= 21;
}));
});
[Authorize(Policy = "Over21")]
[HttpGet("age-restricted")]
public IActionResult Restricted() => Ok();
// Multiple policies
[Authorize(Roles = "Admin")]
[Authorize(Policy = "RequireEngineering")]
public IActionResult SuperSecure() => Ok();
Identity and User Management
csharpbuilder.Services.AddDefaultIdentity<IdentityUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>();
// With Identity, you get:
// UserManager, SignInManager, RoleManager
// Register, Login, Logout, Password management
// Two-factor authentication, Email confirmation
Key Takeaways
- Authentication verifies identity; authorization checks permissions.
- JWT is the standard for stateless API authentication.
- Use claims and policies for fine-grained authorization.
- ASP.NET Core Identity provides full user management out of the box.
Part VIII: Testing and Design Patterns
Unit Testing with xUnit
Unit testing ensures individual components work correctly. xUnit is the most popular testing framework for .NET, known for its extensibility and clean design.
Setting Up xUnit
csharp// Create a test project
// dotnet new xunit -n MyApp.Tests
// Add project reference
// dotnet add reference ../MyApp/MyApp.csproj
Writing Tests
csharpusing Xunit;
using FluentAssertions; // optional, for readable assertions
public class CalculatorTests
{
private readonly Calculator _sut = new(); // system under test
[Fact]
public void Add_ShouldReturnSum_WhenGivenTwoNumbers()
{
// Arrange
int a = 5, b = 3;
// Act
int result = _sut.Add(a, b);
// Assert
Assert.Equal(8, result);
// Or with FluentAssertions:
// result.Should().Be(8);
}
[Theory]
[InlineData(1, 2, 3)]
[InlineData(-1, -1, -2)]
[InlineData(0, 0, 0)]
public void Add_ShouldReturnCorrectSum_ForMultipleInputs(
int a, int b, int expected)
{
int result = _sut.Add(a, b);
Assert.Equal(expected, result);
}
}
Mocking with Moq
csharpusing Moq;
public class UserServiceTests
{
private readonly Mock<IUserRepository> _repoMock;
private readonly UserService _service;
public UserServiceTests()
{
_repoMock = new Mock<IUserRepository>();
_service = new UserService(_repoMock.Object);
}
[Fact]
public async Task GetUser_ShouldReturnUser_WhenExists()
{
// Arrange
var expectedUser = new User { Id = 1, Name = "Alice" };
_repoMock.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(expectedUser);
// Act
var result = await _service.GetUserAsync(1);
// Assert
Assert.Equal("Alice", result?.Name);
_repoMock.Verify(r => r.GetByIdAsync(1), Times.Once);
}
[Fact]
public async Task GetUser_ShouldThrow_WhenNotFound()
{
_repoMock.Setup(r => r.GetByIdAsync(It.IsAny<int>()))
.ReturnsAsync((User?)null);
await Assert.ThrowsAsync<NotFoundException>(() =>
_service.GetUserAsync(999));
}
}
Integration Testing with WebApplicationFactory
csharpusing Microsoft.AspNetCore.Mvc.Testing;
public class ApiIntegrationTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ApiIntegrationTests(WebApplicationFactory<Program> factory)
=> _factory = factory;
[Fact]
public async Task GetItems_ShouldReturnSuccess()
{
// Arrange
var client = _factory.CreateClient();
// Act
var response = await client.GetAsync("/api/items");
// Assert
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
Assert.Contains("items", json);
}
[Fact]
public async Task CreateItem_ShouldReturnCreated()
{
var client = _factory.CreateClient();
var newItem = new { Name = "Test", Price = 9.99 };
var content = new StringContent(
JsonSerializer.Serialize(newItem),
Encoding.UTF8,
"application/json");
var response = await client.PostAsync("/api/items", content);
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
}
Code Coverage
csharp# Run tests with coverage
dotnet test --collect:"XPlat Code Coverage"
# Generate report (install tool first)
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:*/coverage.cobertura.xml
-targetdir:coverage-report -reporttypes:Html
Key Takeaways
- xUnit is the standard .NET testing framework — use [Fact] and [Theory].
- Moq creates mock dependencies for isolated unit tests.
- WebApplicationFactory enables full integration tests without deployment.
- Aim for high code coverage, but focus on critical paths.
Design Patterns in C#
Design patterns are reusable solutions to common software design problems. Here are the most important patterns in C# with modern implementations.
Singleton
csharppublic sealed class Logger
{
private static readonly Logger _instance = new();
private Logger() { }
public static Logger Instance => _instance;
public void Log(string message) =>
Console.WriteLine($"[{DateTime.UtcNow:HH:mm:ss}] {message}");
}
// Usage
Logger.Instance.Log("Application started");
// Or with DI: services.AddSingleton<Logger>();
Factory Method
csharppublic interface IPaymentProcessor
{
Task<bool> ProcessPayment(decimal amount);
}
public class CreditCardProcessor : IPaymentProcessor { /* ... */ }
public class PayPalProcessor : IPaymentProcessor { /* ... */ }
public class PaymentProcessorFactory
{
public IPaymentProcessor Create(string method) => method switch
{
"CreditCard" => new CreditCardProcessor(),
"PayPal" => new PayPalProcessor(),
_ => throw new ArgumentException($"Unknown method: {method}")
};
}
Repository Pattern
csharppublic interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<List<T>> GetAllAsync();
Task AddAsync(T entity);
void Update(T entity);
void Delete(T entity);
Task<int> SaveChangesAsync();
}
public class EfRepository<T> : IRepository<T> where T : class
{
private readonly AppDbContext _db;
public EfRepository(AppDbContext db) => _db = db;
public async Task<T?> GetByIdAsync(int id)
=> await _db.Set<T>().FindAsync(id);
public async Task<List<T>> GetAllAsync()
=> await _db.Set<T>().ToListAsync();
public async Task AddAsync(T entity)
=> await _db.Set<T>().AddAsync(entity);
public void Update(T entity) => _db.Set<T>().Update(entity);
public void Delete(T entity) => _db.Set<T>().Remove(entity);
public async Task<int> SaveChangesAsync()
=> await _db.SaveChangesAsync();
}
Strategy Pattern
csharppublic interface IShippingStrategy
{
decimal CalculateCost(double weight, string destination);
}
public class StandardShipping : IShippingStrategy
{
public decimal CalculateCost(double weight, string dest)
=> (decimal)(weight * 0.5 + 5);
}
public class ExpressShipping : IShippingStrategy
{
public decimal CalculateCost(double weight, string dest)
=> (decimal)(weight * 1.5 + 15);
}
public class OrderService
{
private readonly IShippingStrategy _shipping;
public OrderService(IShippingStrategy shipping)
=> _shipping = shipping;
public decimal GetShippingCost(double weight, string dest)
=> _shipping.CalculateCost(weight, dest);
}
Observer Pattern (Events)
csharppublic class Stock
{
private decimal _price;
public event EventHandler<decimal>? PriceChanged;
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
_price = value;
OnPriceChanged();
}
}
}
protected virtual void OnPriceChanged()
=> PriceChanged?.Invoke(this, _price);
}
// Usage
var stock = new Stock();
stock.PriceChanged += (_, price) =>
Console.WriteLine($"New price: {price:C}");
stock.Price = 100.50m; // triggers event
Pattern Comparison
| Pattern | Type | Purpose |
|---|---|---|
| Singleton | Creational | Single instance globally |
| Factory | Creational | Encapsulate object creation |
| Repository | Structural | Abstract data access |
| Strategy | Behavioral | Swap algorithms at runtime |
| Observer | Behavioral | Notify subscribers of changes |
| DI | Structural | Inject dependencies |
Key Takeaways
- Design patterns provide proven solutions to recurring problems.
- Use creational patterns for object creation, structural for composition.
- Behavioral patterns define communication between objects.
- Don't force patterns — apply them where they solve a real problem.
Best Practices and Performance
Coding Conventions
- Naming: PascalCase for public members, camelCase for private fields, _camelCase for private fields (convention).
- Immutability: Prefer readonly fields, init-only setters, and records for data.
- Null safety: Enable nullable reference types (C# 8+); use nullable annotations properly.
- Pattern matching: Prefer switch expressions and pattern matching over if-else chains.
- Async: Use async/await throughout; avoid .Result/.Wait(); use ConfigureAwait(false) in libraries.
Performance Tips
- Minimize allocations: Use structs, Span<T>, ArrayPool in hot paths.
- Use StringBuilder for string concatenation in loops.
- Avoid LINQ in hot paths — use for/foreach for maximum performance.
- Pool connections: ADO.NET pools automatically; EF Core manages its own pool.
- Cache aggressively: Use IMemoryCache or IDistributedCache for repeated data.
- Use batch operations for bulk data (EF Core ExecuteUpdate, SqlBulkCopy).
- Profile first: Use BenchmarkDotNet, dotnet-trace, and Application Insights.
Security Best Practices
- SQL Injection: Always use parameters — never concatenate SQL strings.
- XSS: Encode output; use Content Security Policy headers.
- CSRF: Use anti-forgery tokens in MVC forms.
- Secrets: Use User Secrets in development, Azure Key Vault / environment variables in production.
- Authentication: Use ASP.NET Core Identity or OpenID Connect; store password hashes (bcrypt/Argon2).
- CORS: Restrict cross-origin requests to known origins.
- Rate limiting: Use built-in rate limiting (.NET 7+) or middleware.
Project Structure
textMyApp/
|-- src/
| |-- MyApp.Api/ # Web API / Controllers
| |-- MyApp.Application/ # Business logic, DTOs, interfaces
| |-- MyApp.Domain/ # Entities, value objects, domain events
| |-- MyApp.Infrastructure/ # Data access, external services
|-- tests/
| |-- MyApp.UnitTests/ # Unit tests (xUnit)
| |-- MyApp.IntegrationTests/ # Integration tests
|-- MyApp.sln
CI/CD Pipeline
csharp# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0'
- run: dotnet restore
- run: dotnet build --no-restore --configuration Release
- run: dotnet test --no-build --configuration Release
--collect:"XPlat Code Coverage"
- run: dotnet publish --configuration Release
- name: Deploy
run: # deploy to Azure / Cloudflare / etc.
Recommended Tools
| Category | Tools |
|---|---|
| IDE | Visual Studio, VS Code, JetBrains Rider |
| Testing | xUnit, FluentAssertions, Moq, TestContainers |
| Performance | BenchmarkDotNet, dotnet-trace, PerfView |
| Static Analysis | Roslyn analyzers, SonarCloud, StyleCop |
| Logging | Serilog, NLog, Application Insights |
| CI/CD | GitHub Actions, Azure DevOps, GitLab CI |
| Containers | Docker, Kubernetes, AKS |
Key Takeaways
- Write clean, readable code that follows team conventions.
- Profile before optimizing — measure, then improve.
- Security is everyone's responsibility, not just the security team.
- Automate everything: build, test, deploy, monitor.
Appendices
C# Version History
| Version | Released | Key Features |
|---|---|---|
| C# 1.0 | 2002 | Managed code, classes, structs, interfaces, events, properties, delegates |
| C# 2.0 | 2005 | Generics, nullable types, anonymous methods, iterators (yield), partial classes |
| C# 3.0 | 2007 | LINQ, lambda expressions, extension methods, anonymous types, expression trees |
| C# 4.0 | 2010 | Dynamic binding (dynamic), named/optional parameters, co/contravariance |
| C# 5.0 | 2012 | Async/await, caller info attributes (CallerMemberName, CallerFilePath, CallerLineNumber) |
| C# 6.0 | 2015 | String interpolation, null-conditional operators (?.), expression-bodied members, using static |
| C# 7.0 | 2017 | Tuples, pattern matching, out variables, local functions, ref locals/returns |
| C# 7.1 | 2017 | Async Main, default literal expressions, inferred tuple element names |
| C# 7.2 | 2017 | in parameters, readonly struct, private protected, Span<T> support |
| C# 7.3 | 2018 | Enum, delegate, and unmanaged constraints; ref local reassignment; stackalloc initializers |
| C# 8.0 | 2019 | Nullable reference types, async streams (IAsyncEnumerable), default interface methods, indices/ranges |
| C# 9.0 | 2020 | Records, init-only setters, top-level statements, pattern matching enhancements, function pointers |
| C# 10 | 2021 | Global usings, file-scoped namespaces, record structs, extended property patterns |
| C# 11 | 2022 | Raw string literals, generic math (INumber<T>), required members, list patterns, file-local types |
| C# 12 | 2023 | Primary constructors, collection expressions ([x, y, z]), aliases for any type, inline arrays |
.NET CLI Reference
Project Management
shelldotnet new console -n MyApp # Create console app
dotnet new webapi -n MyApi # Create web API
dotnet new classlib -n MyLib # Create class library
dotnet new xunit -n MyTests # Create test project
dotnet new sln -n MyApp # Create solution file
dotnet sln add src/MyApp/MyApp.csproj # Add project to solution
Build and Run
shelldotnet build # Build the project
dotnet run # Build and run
dotnet publish -c Release # Publish for deployment
dotnet clean # Clean build outputs
Testing
shelldotnet test # Run all tests
dotnet test --filter "Category=Integration"
dotnet test --collect:"XPlat Code Coverage" # With coverage
Packages
shelldotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Newtonsoft.Json --version 13.0.3
dotnet remove package PackageName
dotnet list package # List all packages
dotnet list package --outdated # Check for updates
Entity Framework
shelldotnet ef migrations add InitialCreate
dotnet ef database update
dotnet ef migrations remove
dotnet ef migrations list
dotnet ef dbcontext info
dotnet ef migrations script -o script.sql
Tool Management
shelldotnet tool install -g dotnet-ef # Install global tool
dotnet tool list -g # List global tools
dotnet tool update -g dotnet-ef # Update tool
dotnet tool uninstall -g dotnet-ef # Remove tool
SDK and Runtime
shelldotnet --version # SDK version
dotnet --list-sdks # List installed SDKs
dotnet --list-runtimes # List installed runtimes
dotnet --info # Full .NET info
.NET Full Course with C# — Generated with opencode.ai — July 05, 2026
This work is licensed under CC BY 4.0. Free to share and adapt with attribution.