Skip to main content

.NET

Comprehensive notes for fresher developers joining the team.


What is .NET?

.NET is a free, open-source, cross-platform framework developed by Microsoft for building modern applications — web APIs, desktop apps, cloud services, and more. It supports multiple languages, but the primary language used is C# (C-Sharp).

.NET consists of:

  • C# — the programming language
  • ASP.NET Core — the web framework for building APIs and web applications
  • CLR (Common Language Runtime) — the engine that runs your code
  • BCL (Base Class Library) — built-in utilities like collections, I/O, and networking

Why Use .NET?

FeatureDescription
Cross-PlatformRuns on Windows, macOS, and Linux
High PerformanceOne of the fastest web frameworks available
Strongly TypedC# catches type errors at compile time, reducing runtime bugs
Rich EcosystemNuGet packages, Microsoft support, extensive documentation
Unified PlatformOne framework for web, desktop, mobile, cloud, and microservices

Learning Flow

PhaseTopics
Phase 1 – C# CoreOOPs, Type system, Memory management, CLR fundamentals
Phase 2 – .NET FundamentalsProgram.cs, Middleware, DI, Async/Await, SOLID, Design Patterns
Phase 3 – Collections & LINQInterfaces, LINQ, Lambda expressions
Phase 4 – Controllers & RoutingControllers, Routing, Model binding, DTOs, Validation, Filters
Phase 5 – Middleware & FiltersCustom middleware, Built-in middleware, Filters vs Middleware
Phase 6 – API & RESTREST principles, HTTP, Web API, EF Core loading strategies
Phase 7 – Authentication & AuthorizationJWT, Middleware, Authorize attribute
Phase 8 – SecurityHTTPS, CORS, Secrets, SQL Injection
Phase 9 – Logging & Monitoring (Optional)ILogger, Serilog, NLog
Phase 10 – Performance Optimization (Optional)Caching, Async best practices

Phase 1 – C# Core

1. OOPs in C# — Classes, Objects, Inheritance, Polymorphism, Encapsulation, Abstraction

Object-Oriented Programming (OOP) is a programming paradigm that organises code around objects — instances of classes. C# is a fully object-oriented language and OOP is the backbone of how .NET applications are structured.

Classes and Objects

A class is a blueprint. An object is an instance of that blueprint.

// Class definition
public class Car
{
public string Brand { get; set; }
public int Year { get; set; }

public void StartEngine()
{
Console.WriteLine($"{Brand} engine started.");
}
}

// Creating objects
Car myCar = new Car();
myCar.Brand = "Toyota";
myCar.Year = 2022;
myCar.StartEngine(); // Output: Toyota engine started.

Constructors — special methods that run when an object is created:

public class Car
{
public string Brand { get; set; }
public int Year { get; set; }

// Constructor
public Car(string brand, int year)
{
Brand = brand;
Year = year;
}
}

Car myCar = new Car("Toyota", 2022);

Inheritance — a child class inherits properties and methods from a parent class, promoting code reuse.

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

public void Breathe()
{
Console.WriteLine("Breathing...");
}
}

public class Dog : Animal // Dog inherits from Animal
{
public void Bark()
{
Console.WriteLine("Woof!");
}
}

Dog dog = new Dog();
dog.Name = "Rex";
dog.Breathe(); // Inherited from Animal
dog.Bark(); // Defined in Dog

Use the base keyword to call the parent class constructor or method:

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

public Animal(string name)
{
Name = name;
}
}

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

Polymorphism — the ability of different classes to be treated as the same base type, while each class implements the behaviour differently.

  • Compile-time (Method Overloading): same method name, different parameters.
  • Runtime (Method Overriding): child class provides a different implementation of a parent method using virtual and override.
public class Shape
{
public virtual double Area()
{
return 0;
}
}

public class Circle : Shape
{
public double Radius { get; set; }

public override double Area()
{
return Math.PI * Radius * Radius;
}
}

public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }

public override double Area()
{
return Width * Height;
}
}

// Polymorphic usage
Shape shape1 = new Circle { Radius = 5 };
Shape shape2 = new Rectangle { Width = 4, Height = 6 };

Console.WriteLine(shape1.Area()); // 78.54
Console.WriteLine(shape2.Area()); // 24

Encapsulation — hiding internal details and exposing only what is necessary through access modifiers and properties.

ModifierAccessible From
publicAnywhere
privateInside the same class only
protectedSame class and derived classes
internalSame assembly/project
public class BankAccount
{
private decimal _balance; // private — hidden from outside

public decimal Balance
{
get { return _balance; }
private set { _balance = value; } // only this class can set it
}

public void Deposit(decimal amount)
{
if (amount > 0)
_balance += amount;
}
}

Abstraction — hiding complex implementation details and showing only the essential interface. Achieved using abstract classes and interfaces.

// Abstract class — cannot be instantiated directly
public abstract class Vehicle
{
public string Brand { get; set; }

public abstract void Drive(); // must be implemented by subclasses

public void Refuel()
{
Console.WriteLine("Refuelling...");
}
}

public class Car : Vehicle
{
public override void Drive()
{
Console.WriteLine("Car is driving.");
}
}

// Interface — pure contract, no implementation
public interface IPayable
{
void ProcessPayment(decimal amount);
}

public class Order : IPayable
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Payment of {amount:C} processed.");
}
}

Abstract Class vs Interface:

FeatureAbstract ClassInterface
Can have implementation✅ Yes❌ No (default methods are an exception in C# 8+)
Multiple inheritance❌ No✅ Yes (a class can implement many interfaces)
Constructors✅ Yes❌ No
Access modifiers✅ Yes❌ All public by default
Use whenSharing common base behaviourDefining a contract

2. IS / AS Operators — Type Checking and Safe Casting

is — checks if an object is of a specific type. Returns true or false.

object obj = "Hello";

if (obj is string)
{
Console.WriteLine("It is a string"); // ✅ prints this
}

// Pattern matching with 'is' (C# 7+)
if (obj is string text)
{
Console.WriteLine(text.ToUpper()); // HELLO
}

as — attempts to cast an object to a type. Returns null if the cast fails (instead of throwing an exception).

object obj = "Hello";

string text = obj as string; // ✅ "Hello"
int? number = obj as int?; // null — obj is not an int

if (text != null)
{
Console.WriteLine(text.Length); // 5
}

Comparison:

OperatorReturnsThrows on failure
isboolNever
asInstance or nullNever
(Type) castInstanceInvalidCastException

Rule: Use as when a failed cast is acceptable (nullable). Use (Type) cast only when you are certain of the type. Use is for type checks with pattern matching.


3. Sealed, Partial, Static Classes and Methods

sealed class — prevents other classes from inheriting from it.

public sealed class FinalClass
{
public void DoSomething() { }
}

// ❌ This will cause a compile error
public class DerivedClass : FinalClass { }

Use sealed to prevent unintended extension of a class (e.g., security-sensitive classes, or when you want to signal that the class is complete).

sealed method — prevents a derived class from overriding a method further. Must be used on an override method.

public class Base
{
public virtual void Display() { }
}

public class Middle : Base
{
public sealed override void Display() { } // no further overriding
}

public class Child : Middle
{
// ❌ Cannot override Display() here
}

partial class — splits a class definition across multiple files. All parts are merged by the compiler.

// File: Person.cs
public partial class Person
{
public string Name { get; set; }
}

// File: Person.Methods.cs
public partial class Person
{
public void Greet()
{
Console.WriteLine($"Hello, {Name}");
}
}

Useful in large classes, auto-generated code (like Entity Framework models), and separating designer code from logic code.


static class — cannot be instantiated. All members must also be static. Used for utility/helper methods.

public static class MathHelper
{
public static int Add(int a, int b) => a + b;
public static int Multiply(int a, int b) => a * b;
}

// Usage — no 'new' keyword needed
int result = MathHelper.Add(3, 4); // 7

static method — belongs to the class itself, not an instance.

public class Counter
{
public static int Count { get; private set; } = 0;

public Counter()
{
Count++;
}
}

new Counter();
new Counter();
Console.WriteLine(Counter.Count); // 2

4. Boxing & Unboxing

C# has two categories of types: value types (like int, bool, struct) and reference types (like class, string). Boxing and unboxing bridge the gap between them.

Boxing — converting a value type to a reference type (object). The value is wrapped in a heap-allocated object.

int number = 42;          // value type — stored on the stack
object boxed = number; // boxing — copied to heap as an object

Unboxing — extracting the value type back from the object. Requires an explicit cast.

object boxed = 42;
int unboxed = (int)boxed; // unboxing — must cast to the correct type

Why does it matter?

Boxing and unboxing have a performance cost — they involve heap allocation and type checking. Avoid them in performance-critical loops.

// ❌ Bad — boxes on every iteration
ArrayList list = new ArrayList();
for (int i = 0; i < 1000; i++)
list.Add(i); // each int is boxed

// ✅ Good — use generic collections instead
List<int> list = new List<int>();
for (int i = 0; i < 1000; i++)
list.Add(i); // no boxing

5. Strings — String vs StringBuilder, String Methods, Immutability

String Immutability

In C#, string is immutable — once created, its value cannot be changed. Every operation that appears to modify a string actually creates a new string in memory.

string name = "Hello";
name = name + " World"; // creates a brand new string, old one becomes garbage

This is why repeated string concatenation in a loop is inefficient.


String vs StringBuilder

Use StringBuilder when you need to build or modify strings in a loop, because it mutates the same buffer instead of creating new strings.

// ❌ Bad — creates a new string on each iteration (slow for large loops)
string result = "";
for (int i = 0; i < 1000; i++)
result += i;

// ✅ Good — StringBuilder modifies in place (efficient)
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
sb.Append(i);
string result = sb.ToString();
FeaturestringStringBuilder
Immutable✅ Yes❌ No (mutable)
MemoryNew object per changeSingle buffer
PerformancePoor for many changesExcellent for many changes
Use whenFew or no modificationsMany modifications or loops

Common String Methods

string s = "  Hello, World!  ";

s.Length // 18 — number of characters
s.Trim() // "Hello, World!" — removes leading/trailing whitespace
s.ToUpper() // " HELLO, WORLD! "
s.ToLower() // " hello, world! "
s.Contains("World") // true
s.StartsWith(" He") // true
s.EndsWith("! ") // true
s.Replace("World", "C#") // " Hello, C#! "
s.Split(',') // [" Hello", " World! "]
s.Substring(2, 5) // "Hello" — start index 2, length 5
s.IndexOf("World") // 9 — position of first match
s.Trim().ToUpper() // chaining: "HELLO, WORLD!"

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

// String.Format (older style)
string formatted = string.Format("Hello, {0}!", name);

// Verbatim string — @ prefix ignores escape characters
string path = @"C:\Users\Documents\file.txt";

// Check for null or empty
string.IsNullOrEmpty(s) // true if null or ""
string.IsNullOrWhiteSpace(s) // true if null, "", or whitespace only

6. Using Statement — Resource Management & IDisposable

Some objects hold unmanaged resources (file handles, database connections, network sockets) that must be explicitly released when done. The IDisposable interface provides the Dispose() method for this purpose.

The using statement ensures Dispose() is called automatically — even if an exception occurs.

// Without using — risky if an exception is thrown before Dispose()
StreamReader reader = new StreamReader("file.txt");
string content = reader.ReadToEnd();
reader.Dispose(); // might not be reached

// ✅ With using — Dispose() is always called
using (StreamReader reader = new StreamReader("file.txt"))
{
string content = reader.ReadToEnd();
} // Dispose() called automatically here

// ✅ Modern using declaration (C# 8+) — cleaner syntax
using StreamReader reader = new StreamReader("file.txt");
string content = reader.ReadToEnd();
// Dispose() called when reader goes out of scope

Implementing IDisposable in your own class:

public class DatabaseConnection : IDisposable
{
private bool _disposed = false;

public void Open() { /* open connection */ }

public void Dispose()
{
if (!_disposed)
{
// release resources
_disposed = true;
}
}
}

using var conn = new DatabaseConnection();
conn.Open();
// conn.Dispose() called automatically at end of scope

7. Finalize & Garbage Collection

Garbage Collection (GC)

The .NET Garbage Collector automatically manages memory for managed objects. You don't need to manually free memory — the GC periodically identifies objects with no references and reclaims their memory.

The GC works in generations:

  • Generation 0 — short-lived objects (most collections happen here)
  • Generation 1 — medium-lived objects (buffer between Gen 0 and Gen 2)
  • Generation 2 — long-lived objects (full GC, expensive)
// You rarely need to call GC directly, but you can:
GC.Collect(); // forces a collection (generally avoid this)
GC.SuppressFinalize(obj); // tell GC not to call finalizer (use in Dispose pattern)

Finalizer (Destructor)

A finalizer (~ClassName) is called by the GC before an object is collected. It is used as a safety net for releasing unmanaged resources. It is not deterministic — you don't control when it runs.

public class ResourceHolder
{
~ResourceHolder()
{
// cleanup unmanaged resources
// this runs at some point during GC — not immediately
}
}

Best Practice — Dispose Pattern:

Combine IDisposable with a finalizer to handle both deterministic (manual) and non-deterministic (GC) cleanup:

public class ResourceHolder : IDisposable
{
private bool _disposed = false;

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // no need for finalizer to run
}

protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// free managed resources
}
// free unmanaged resources
_disposed = true;
}
}

~ResourceHolder()
{
Dispose(false); // safety net
}
}

8. Continue & Break — Control Flow in Loops

break — exits the loop entirely.

for (int i = 0; i < 10; i++)
{
if (i == 5)
break; // stops the loop when i = 5
Console.WriteLine(i); // prints 0, 1, 2, 3, 4
}

continue — skips the current iteration and moves to the next one.

for (int i = 0; i < 10; i++)
{
if (i % 2 == 0)
continue; // skip even numbers
Console.WriteLine(i); // prints 1, 3, 5, 7, 9
}

In a foreach loop:

string[] names = { "Alice", "Bob", "", "Charlie" };

foreach (string name in names)
{
if (string.IsNullOrEmpty(name))
continue; // skip empty names

Console.WriteLine(name); // Alice, Bob, Charlie
}

In a while loop:

int count = 0;
while (true)
{
count++;
if (count >= 5)
break; // exit infinite loop when count reaches 5
}

9. CLR — Common Language Runtime, JIT Compilation, Managed Code

What is the CLR?

The Common Language Runtime (CLR) is the virtual machine component of .NET. It is the execution engine that manages running your C# code, providing services like:

  • Memory management via the Garbage Collector
  • Type safety — ensures code doesn't access wrong memory
  • Exception handling — structured try/catch across language boundaries
  • Thread management
  • Security

Compilation Flow:

C# Source Code (.cs)

C# Compiler (csc)

IL Code (MSIL) in .dll / .exe

CLR loads the assembly

JIT Compiler (Just-In-Time)

Native Machine Code

Runs

JIT (Just-In-Time) Compilation

C# code is first compiled to Intermediate Language (IL) — a CPU-independent bytecode. When your app runs, the CLR's JIT compiler converts IL to native machine code on demand — just before each method is first called. The result is cached so subsequent calls run native code directly.

This means:

  • Your app starts slightly slower (JIT overhead on first run)
  • But runs fast after warmup
  • The same IL runs on any OS/CPU where the CLR is installed

Managed vs Unmanaged Code:

Managed CodeUnmanaged Code
RuntimeRuns under CLR supervisionRuns directly on OS/CPU
MemoryGC handles itDeveloper handles it manually
SafetyType-safe, bounds-checkedCan cause buffer overflows
LanguageC#, VB.NET, F#C, C++

10. Value Types vs Reference Types

This is one of the most important concepts in C# — understanding how variables are stored and copied.

Value Types

Stored directly on the stack. When you assign one to another, a copy is made.

Examples: int, double, bool, char, struct, enum

int a = 10;
int b = a; // b is a separate copy
b = 20;

Console.WriteLine(a); // 10 — unchanged
Console.WriteLine(b); // 20

Reference Types

Store a reference (pointer) to data on the heap. When you assign one to another, both variables point to the same object.

Examples: class, string, array, interface, delegate

int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1; // arr2 points to the SAME array
arr2[0] = 99;

Console.WriteLine(arr1[0]); // 99 — arr1 was also affected!

Comparison:

FeatureValue TypeReference Type
StorageStackHeap
AssignmentCreates a copyCopies the reference (both point to same data)
Default valueZero/false/null structnull
Examplesint, bool, structclass, string, array
Nullable by default

null with value types:

Value types cannot be null by default. Use nullable types (int?) when needed:

int? age = null;  // nullable int
if (age.HasValue)
Console.WriteLine(age.Value);
else
Console.WriteLine("Age not set");

Phase 2 – .NET Fundamentals

1. Program.cs — Entry Point, Host Builder, App Configuration

In modern .NET (6+), Program.cs is the single entry point of your application. It uses top-level statements, eliminating the need for a boilerplate Main method.

// Program.cs — minimal Web API setup
var builder = WebApplication.CreateBuilder(args);

// Register services with the DI container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline (middleware)
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Two-phase structure:

  1. builder phase — register services and configuration before the app starts.
  2. app phase — configure the request pipeline (middleware) and run the app.

Configuration — read settings from appsettings.json:

// appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=...;Database=mydb;"
},
"JwtSettings": {
"SecretKey": "my-secret-key"
}
}
// Access configuration in Program.cs or via DI
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
var secretKey = builder.Configuration["JwtSettings:SecretKey"];

2. Middleware Pipeline — Request/Response Pipeline, Custom Middleware

What is Middleware?

Middleware is software that handles an HTTP request and response. Each piece of middleware can:

  • Process the incoming request
  • Pass it to the next middleware in the pipeline (calling next())
  • Process the outgoing response

Think of it as a chain of handlers — each one handles part of the request/response lifecycle.

HTTP Request

[Middleware 1] → (can short-circuit or pass to next)

[Middleware 2] → (can short-circuit or pass to next)

[Middleware 3] → (can short-circuit or pass to next)

Controller/Endpoint

[Middleware 3] ← (processes response on way back)

[Middleware 2] ←

[Middleware 1] ←

HTTP Response

Registering middleware — order matters:

app.UseHttpsRedirection();   // 1. Redirect HTTP to HTTPS
app.UseStaticFiles(); // 2. Serve static files
app.UseRouting(); // 3. Match request to a route
app.UseAuthentication(); // 4. Identify the user
app.UseAuthorization(); // 5. Check if user has permission
app.MapControllers(); // 6. Execute controller action

Custom Middleware (covered in detail in Phase 5).


3. Dependency Injection — IoC Container, Constructor Injection

What is Dependency Injection (DI)?

DI is a design pattern where a class receives its dependencies from outside rather than creating them itself. In .NET, an IoC (Inversion of Control) container manages object creation and wires dependencies together automatically.

Without DI (tightly coupled):

public class OrderService
{
private readonly EmailService _emailService;

public OrderService()
{
_emailService = new EmailService(); // ❌ tightly coupled — hard to test or swap
}
}

With DI (loosely coupled):

public interface IEmailService
{
void SendEmail(string to, string subject);
}

public class EmailService : IEmailService
{
public void SendEmail(string to, string subject)
{
// send email
}
}

public class OrderService
{
private readonly IEmailService _emailService;

// Constructor injection — the container provides the dependency
public OrderService(IEmailService emailService)
{
_emailService = emailService;
}
}

Registering services in Program.cs:

builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<OrderService>();

Now when OrderService is needed anywhere, the container automatically creates an EmailService and injects it. You can swap EmailService for a mock in tests without changing OrderService.


4. Service Lifetime — Singleton, Scoped, Transient

When registering services with the DI container, you specify how long the service instance lives:

LifetimeCreatedDestroyedUse Case
TransientEvery time it is requestedAfter each useLightweight, stateless services
ScopedOnce per HTTP requestAt end of the requestDatabase context, per-request state
SingletonOnce for the entire app lifetimeWhen app shuts downCaching, configuration, logging
builder.Services.AddTransient<IMyService, MyService>();   // new instance every time
builder.Services.AddScoped<IMyService, MyService>(); // one per HTTP request
builder.Services.AddSingleton<IMyService, MyService>(); // one for entire app

Choosing the right lifetime:

// ✅ Scoped — DbContext should be scoped (one per request)
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));

// ✅ Singleton — shared cache or configuration
builder.Services.AddSingleton<ICacheService, CacheService>();

// ✅ Transient — email sender, stateless utilities
builder.Services.AddTransient<IEmailSender, EmailSender>();

Warning: Never inject a Scoped service into a Singleton. The Scoped instance would be captured and live for the entire app lifetime, causing stale data bugs. The DI container will throw an error in development.


5. Serialization — JSON Serialization

Serialization converts a C# object to a string (e.g., JSON) for storage or transmission. Deserialization reverses the process.

.NET includes System.Text.Json (built-in, high-performance) and supports Newtonsoft.Json (popular third-party library with more features).

Using System.Text.Json:

using System.Text.Json;

public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}

// Serialize — object → JSON string
var product = new Product { Id = 1, Name = "Laptop", Price = 999.99m };
string json = JsonSerializer.Serialize(product);
// {"Id":1,"Name":"Laptop","Price":999.99}

// Deserialize — JSON string → object
string jsonInput = """{"Id":2,"Name":"Phone","Price":499.99}""";
var parsed = JsonSerializer.Deserialize<Product>(jsonInput);
Console.WriteLine(parsed.Name); // Phone

JsonSerializerOptions — customising behaviour:

var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // "name" instead of "Name"
WriteIndented = true, // pretty-print
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

string json = JsonSerializer.Serialize(product, options);

Data annotations on properties:

public class Product
{
[JsonPropertyName("product_id")] // custom JSON key
public int Id { get; set; }

[JsonIgnore] // exclude from serialization
public string InternalCode { get; set; }
}

In ASP.NET Core, JSON serialization/deserialization of request and response bodies happens automatically when you use [ApiController] with [HttpPost] / [HttpGet].


6. Async / Await — Asynchronous Programming, Task, Task<T>

Why Async?

Synchronous code blocks a thread while waiting for I/O (database queries, HTTP calls, file reads). In a web server this is wasteful — a blocked thread cannot serve other requests. Async/await lets the thread be freed while waiting, improving throughput and scalability.

Task and Task<T>

  • Task — represents an asynchronous operation that completes without a return value.
  • Task<T> — represents an asynchronous operation that returns a value of type T.
// Synchronous — blocks the thread
string content = File.ReadAllText("file.txt");

// Asynchronous — thread is freed while file is read
string content = await File.ReadAllTextAsync("file.txt");

Async method rules:

  • Must be marked with async
  • Should return Task, Task<T>, or void (avoid async void except for event handlers)
  • Use await to suspend until the awaited Task completes
public async Task<string> GetDataAsync(int id)
{
// Simulating a database call
await Task.Delay(100); // non-blocking delay
return $"Data for id {id}";
}

// Calling async method
string result = await GetDataAsync(42);
Console.WriteLine(result);

Real-world example — async controller action:

[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
var product = await _repository.GetByIdAsync(id);

if (product == null)
return NotFound();

return Ok(product);
}

Key rules:

  • If a method uses await, it must be async.
  • async propagates up — when you make a method async, its callers should also be async.
  • Use await rather than .Result or .Wait() — those block the thread and can cause deadlocks.

7. SOLID Principles

SOLID is a set of five design principles that make software more maintainable, testable, and extensible.

S — Single Responsibility Principle (SRP)

A class should have only one reason to change — it should do one thing only.

// ❌ Violates SRP — handles both order logic and email sending
public class OrderService
{
public void PlaceOrder(Order order) { /* ... */ }
public void SendConfirmationEmail(Order order) { /* ... */ }
}

// ✅ Follows SRP — separate responsibilities
public class OrderService
{
public void PlaceOrder(Order order) { /* ... */ }
}

public class EmailService
{
public void SendConfirmationEmail(Order order) { /* ... */ }
}

O — Open/Closed Principle (OCP)

Classes should be open for extension, closed for modification — add new behaviour by extending, not by changing existing code.

// ✅ Add new payment methods without modifying existing code
public interface IPaymentProcessor
{
void ProcessPayment(decimal amount);
}

public class CreditCardProcessor : IPaymentProcessor { /* ... */ }
public class PayPalProcessor : IPaymentProcessor { /* ... */ }
public class CryptoProcessor : IPaymentProcessor { /* ... */ } // new — no existing code changed

L — Liskov Substitution Principle (LSP)

A derived class should be substitutable for its base class without breaking behaviour.

// ✅ Bird and Penguin are substitutable where IBird is used
public interface IBird
{
void Eat();
}

public interface IFlyingBird : IBird
{
void Fly();
}

public class Eagle : IFlyingBird
{
public void Eat() { }
public void Fly() { }
}

public class Penguin : IBird // Penguin only implements IBird, not IFlyingBird
{
public void Eat() { }
}

I — Interface Segregation Principle (ISP)

Clients should not be forced to implement interfaces they don't use. Split large interfaces into smaller, focused ones.

// ❌ Violates ISP — forces all implementors to define Print and Scan
public interface IMultiFunction
{
void Print();
void Scan();
void Fax();
}

// ✅ Follows ISP — separate interfaces
public interface IPrinter { void Print(); }
public interface IScanner { void Scan(); }
public interface IFax { void Fax(); }

public class SimplePrinter : IPrinter
{
public void Print() { /* ... */ }
}

D — Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions (interfaces).

// ❌ Violates DIP — high-level class depends directly on low-level class
public class ReportService
{
private SqlDatabase _database = new SqlDatabase();
}

// ✅ Follows DIP — depends on abstraction
public interface IDatabase { void SaveReport(Report r); }

public class ReportService
{
private readonly IDatabase _database;
public ReportService(IDatabase database) { _database = database; }
}

8. Design Patterns — Repository, Factory, Singleton

Repository Pattern

Abstracts data access behind an interface, keeping the business logic free of database concerns.

public interface IProductRepository
{
Task<Product> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
Task AddAsync(Product product);
Task UpdateAsync(Product product);
Task DeleteAsync(int id);
}

public class ProductRepository : IProductRepository
{
private readonly AppDbContext _context;

public ProductRepository(AppDbContext context)
{
_context = context;
}

public async Task<Product> GetByIdAsync(int id)
=> await _context.Products.FindAsync(id);

public async Task<IEnumerable<Product>> GetAllAsync()
=> await _context.Products.ToListAsync();

// ... other methods
}

// Registration
builder.Services.AddScoped<IProductRepository, ProductRepository>();

Factory Pattern

Encapsulates object creation logic. Useful when the exact type to create depends on some condition.

public interface INotification
{
void Send(string message);
}

public class EmailNotification : INotification
{
public void Send(string message) => Console.WriteLine($"Email: {message}");
}

public class SmsNotification : INotification
{
public void Send(string message) => Console.WriteLine($"SMS: {message}");
}

public static class NotificationFactory
{
public static INotification Create(string type) => type switch
{
"email" => new EmailNotification(),
"sms" => new SmsNotification(),
_ => throw new ArgumentException($"Unknown type: {type}")
};
}

// Usage
var notification = NotificationFactory.Create("email");
notification.Send("Order confirmed!");

Singleton Pattern

Ensures only one instance of a class exists throughout the application. In .NET, prefer registering services as Singleton with the DI container rather than implementing the pattern manually.

// ✅ Preferred — use DI container
builder.Services.AddSingleton<IConfigurationService, ConfigurationService>();

// Manual implementation (if DI is not available)
public class AppConfig
{
private static AppConfig _instance;
private static readonly object _lock = new object();

private AppConfig() { }

public static AppConfig Instance
{
get
{
lock (_lock)
{
_instance ??= new AppConfig();
return _instance;
}
}
}
}

9. DSA in C# — Arrays, Lists, Stack, Queue, Dictionary Basics

Array — fixed-size, zero-indexed collection of the same type.

int[] numbers = new int[5];       // size 5, all zeros
int[] primes = { 2, 3, 5, 7 }; // initialiser syntax

primes[0] // 2
primes.Length // 4
Array.Sort(primes); // sorts in-place

List<T> — dynamic-size array. Most commonly used collection.

var names = new List<string>();
names.Add("Alice");
names.Add("Bob");
names.Remove("Bob");
names.Contains("Alice"); // true
names.Count; // 1
names[0]; // "Alice"

Stack<T> — Last-In First-Out (LIFO).

var stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
stack.Push(3);

stack.Pop(); // 3 — removes and returns top
stack.Peek(); // 2 — returns top without removing
stack.Count; // 2

Queue<T> — First-In First-Out (FIFO).

var queue = new Queue<string>();
queue.Enqueue("First");
queue.Enqueue("Second");
queue.Enqueue("Third");

queue.Dequeue(); // "First" — removes and returns front
queue.Peek(); // "Second" — returns front without removing
queue.Count; // 2

Dictionary<TKey, TValue> — key-value pairs, O(1) average lookup.

var scores = new Dictionary<string, int>();
scores["Alice"] = 95;
scores["Bob"] = 87;

scores["Alice"]; // 95
scores.ContainsKey("Bob"); // true
scores.Count; // 2

foreach (var kvp in scores)
Console.WriteLine($"{kvp.Key}: {kvp.Value}");

// Safe access with TryGetValue
if (scores.TryGetValue("Charlie", out int score))
Console.WriteLine(score);
else
Console.WriteLine("Not found");

Phase 3 – Collections & LINQ

1. IEnumerable — Deferred Execution, Iteration

IEnumerable<T> is the base interface for all collections in .NET. It provides only one capability: forward-only iteration via GetEnumerator(). It is the interface used by foreach.

Key characteristic — deferred execution:

LINQ queries on IEnumerable<T> are not executed when you define them. They execute when you actually iterate (with foreach, .ToList(), etc.).

IEnumerable<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// This query is NOT executed yet
var evenNumbers = numbers.Where(n => n % 2 == 0);

// Executed here — iterating materialises the query
foreach (var n in evenNumbers)
Console.WriteLine(n); // 2, 4

2. ICollection — Add, Remove, Count

ICollection<T> extends IEnumerable<T> and adds:

  • Count — number of items
  • Add(T item) — add an item
  • Remove(T item) — remove an item
  • Clear() — remove all items
  • Contains(T item) — check membership

List<T> and HashSet<T> implement ICollection<T>.

ICollection<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
names.Remove("Bob");
Console.WriteLine(names.Count); // 1

3. IList — Index-Based Access

IList<T> extends ICollection<T> and adds index-based access:

  • this[int index] — get or set by index
  • IndexOf(T item) — find position
  • Insert(int index, T item) — insert at position
  • RemoveAt(int index) — remove by position
IList<string> fruits = new List<string> { "Apple", "Banana", "Cherry" };
Console.WriteLine(fruits[1]); // Banana
fruits.Insert(1, "Avocado");
fruits.RemoveAt(0); // removes "Apple"

4. IDictionary — Key-Value Pairs

IDictionary<TKey, TValue> maps unique keys to values:

  • this[TKey key] — get or set by key
  • Add(TKey, TValue) — add a pair
  • Remove(TKey) — remove by key
  • ContainsKey(TKey) — check key existence
  • Keys / Values — collections of all keys or values
IDictionary<string, int> ages = new Dictionary<string, int>();
ages["Alice"] = 30;
ages["Bob"] = 25;

ages.ContainsKey("Alice"); // true
ages.Remove("Bob");

foreach (string key in ages.Keys)
Console.WriteLine($"{key}: {ages[key]}");

5. IQueryable — LINQ to Database, Expression Trees

IQueryable<T> extends IEnumerable<T> and is designed for querying external data sources (like a SQL database via Entity Framework). Unlike IEnumerable<T>, which executes in memory, IQueryable<T> translates the query to the native query language (SQL) and executes it at the data source.

// IEnumerable — loads ALL products into memory, then filters in C#
IEnumerable<Product> products = _context.Products; // ❌ loads everything
var cheap = products.Where(p => p.Price < 100); // filters in memory

// IQueryable — builds a SQL WHERE clause, filters in the database
IQueryable<Product> query = _context.Products; // ✅ no data loaded yet
var cheap = query.Where(p => p.Price < 100); // adds SQL WHERE
var result = await cheap.ToListAsync(); // executes: SELECT ... WHERE Price < 100

Use IQueryable<T> when working with ORMs like Entity Framework Core to avoid pulling entire tables into memory.


6. LINQ — Where, Select, GroupBy, OrderBy, Query & Method Syntax

LINQ (Language Integrated Query) lets you query collections using a uniform syntax, whether the data is in memory, a database, XML, or elsewhere.

Method Syntax (most common):

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

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

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

// OrderBy / OrderByDescending
var sorted = numbers.OrderByDescending(n => n);
// 10, 9, 8, ...

// GroupBy
var people = new List<(string Name, string City)>
{
("Alice", "London"), ("Bob", "Paris"), ("Charlie", "London")
};
var byCity = people.GroupBy(p => p.City);
foreach (var group in byCity)
{
Console.WriteLine($"{group.Key}: {string.Join(", ", group.Select(p => p.Name))}");
}
// London: Alice, Charlie
// Paris: Bob

// First, FirstOrDefault, Single, SingleOrDefault
var first = numbers.FirstOrDefault(n => n > 5); // 6 — or default if not found

// Any, All, Count
bool hasLarge = numbers.Any(n => n > 8); // true
bool allPositive = numbers.All(n => n > 0); // true
int countEven = numbers.Count(n => n % 2 == 0); // 5

// Sum, Min, Max, Average
double avg = numbers.Average(); // 5.5
int total = numbers.Sum(); // 55

// ToList, ToArray — materialize
List<int> list = numbers.Where(n => n > 5).ToList();

Query Syntax (SQL-like, less common):

var result =
from n in numbers
where n % 2 == 0
orderby n descending
select n * n;

7. Lambda Expressions — Func<>, Action<>, Expression Lambdas

A lambda expression is an anonymous function — a function without a name, defined inline.

// Named method
int Add(int a, int b) => a + b;

// Equivalent lambda
Func<int, int, int> add = (a, b) => a + b;
int result = add(3, 4); // 7

Func<T> delegates — represent a method that returns a value:

Func<int, bool> isEven = n => n % 2 == 0;
Func<string, string> toUpper = s => s.ToUpper();
Func<int, int, int> multiply = (a, b) => a * b;

isEven(4); // true
toUpper("hello"); // "HELLO"
multiply(3, 5); // 15

Action<T> delegates — represent a method that returns nothing:

Action<string> print = message => Console.WriteLine(message);
Action<int, int> printSum = (a, b) => Console.WriteLine(a + b);

print("Hello"); // Hello
printSum(3, 4); // 7

Lambdas in LINQ:

var names = new List<string> { "Alice", "Bob", "Charlie" };

var longNames = names.Where(name => name.Length > 3)
.Select(name => name.ToUpper())
.ToList();
// ["ALICE", "CHARLIE"]

Multi-line lambda:

Func<int, int> factorial = n =>
{
int result = 1;
for (int i = 2; i <= n; i++)
result *= i;
return result;
};

factorial(5); // 120

Phase 4 – Controllers & Routing

1. Controllers — API Controller vs MVC Controller

API Controller — returns data (JSON/XML). Used for building REST APIs consumed by SPAs or mobile apps.

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult GetAll()
{
var products = new[] { new { Id = 1, Name = "Laptop" } };
return Ok(products); // returns 200 with JSON
}
}

MVC Controller — returns Views (HTML). Used for server-rendered web applications.

public class HomeController : Controller  // note: Controller, not ControllerBase
{
public IActionResult Index()
{
return View(); // returns an HTML view
}
}

Comparison:

FeatureControllerBase (API)Controller (MVC)
Inherits fromControllerBaseController (extends ControllerBase)
ReturnsData (JSON)Views (HTML) or Data
[ApiController]✅ Typically usedNot required
ViewBag / ViewData❌ Not available✅ Available
Use forREST APIsWeb apps with server-rendered HTML

2. Routing — Attribute Routing, Conventional Routing, Route Constraints

Attribute Routing — routes are defined directly on controllers and actions using attributes. Preferred for Web APIs.

[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpGet] // GET /api/products
public IActionResult GetAll() { }

[HttpGet("{id}")] // GET /api/products/5
public IActionResult GetById(int id) { }

[HttpPost] // POST /api/products
public IActionResult Create([FromBody] ProductDto dto) { }

[HttpPut("{id}")] // PUT /api/products/5
public IActionResult Update(int id, [FromBody] ProductDto dto) { }

[HttpDelete("{id}")] // DELETE /api/products/5
public IActionResult Delete(int id) { }
}

Route Constraints — restrict what values a route segment can match:

[HttpGet("{id:int}")]             // id must be an integer
[HttpGet("{name:alpha}")] // name must be letters only
[HttpGet("{id:int:min(1)}")] // id must be int >= 1
[HttpGet("{slug:regex(^[a-z]+$)}")] // slug matches regex

Conventional Routing — used in MVC apps, defined in Program.cs:

app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
// e.g. /Products/Details/5 → ProductsController.Details(5)

3. Model Binding — FromBody, FromQuery, FromRoute, FromHeader

Model binding maps incoming HTTP request data to action method parameters.

// [FromRoute] — from URL segment: GET /api/products/5
[HttpGet("{id}")]
public IActionResult GetById([FromRoute] int id) { }

// [FromQuery] — from query string: GET /api/products?page=2&size=10
[HttpGet]
public IActionResult GetAll([FromQuery] int page = 1, [FromQuery] int size = 10) { }

// [FromBody] — from JSON request body: POST /api/products
[HttpPost]
public IActionResult Create([FromBody] CreateProductDto dto) { }

// [FromHeader] — from HTTP header
[HttpGet]
public IActionResult GetWithToken([FromHeader(Name = "X-Api-Key")] string apiKey) { }

// [FromForm] — from HTML form data
[HttpPost("upload")]
public IActionResult Upload([FromForm] IFormFile file) { }

With [ApiController], [FromBody] is inferred for complex types in POST/PUT actions, and [FromQuery] is inferred for simple types. You can still be explicit for clarity.


4. DTO — Purpose, Mapping, AutoMapper Basics

DTO (Data Transfer Object) is a plain object used to carry data between layers. It:

  • Hides internal domain model properties (e.g., passwords, audit fields)
  • Controls exactly what data is sent in API requests/responses
  • Decouples the API contract from the database schema
// Domain model — internal
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string PasswordHash { get; set; } // ❌ should never be exposed
public DateTime CreatedAt { get; set; }
}

// DTO — what the API exposes
public class UserResponseDto
{
public int Id { get; set; }
public string Username { get; set; }
}

// Request DTO — what the client sends
public class CreateUserDto
{
public string Username { get; set; }
public string Password { get; set; }
}

Manual mapping:

var dto = new UserResponseDto
{
Id = user.Id,
Username = user.Username
};

AutoMapper — automatic mapping:

// Install: dotnet add package AutoMapper.Extensions.Microsoft.DependencyInjection

// Configure mapping profile
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<User, UserResponseDto>();
CreateMap<CreateUserDto, User>();
}
}

// Register in Program.cs
builder.Services.AddAutoMapper(typeof(MappingProfile));

// Use in controller
public class UsersController : ControllerBase
{
private readonly IMapper _mapper;

public UsersController(IMapper mapper)
{
_mapper = mapper;
}

[HttpGet("{id}")]
public IActionResult Get(int id)
{
var user = _userRepository.GetById(id);
var dto = _mapper.Map<UserResponseDto>(user);
return Ok(dto);
}
}

5. Model Validation — Data Annotations, FluentValidation Basics

Data Annotations — add validation rules directly to model properties using attributes.

public class CreateProductDto
{
[Required]
[StringLength(100, MinimumLength = 2)]
public string Name { get; set; }

[Range(0.01, 9999.99)]
public decimal Price { get; set; }

[EmailAddress]
public string ContactEmail { get; set; }

[RegularExpression(@"^\d{4}-\d{2}-\d{2}$", ErrorMessage = "Use YYYY-MM-DD format")]
public string LaunchDate { get; set; }
}

With [ApiController], validation is automatic — if the model is invalid, it returns a 400 Bad Request with validation errors before the action runs.

Manual validation check (without [ApiController]):

[HttpPost]
public IActionResult Create([FromBody] CreateProductDto dto)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);

// proceed
}

FluentValidation — keeps validation rules in a separate class, making them more readable and testable.

// Install: dotnet add package FluentValidation.AspNetCore

public class CreateProductValidator : AbstractValidator<CreateProductDto>
{
public CreateProductValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MinimumLength(2)
.MaximumLength(100);

RuleFor(x => x.Price)
.GreaterThan(0)
.LessThan(10000);
}
}

// Register in Program.cs
builder.Services.AddFluentValidation(fv =>
fv.RegisterValidatorsFromAssemblyContaining<CreateProductValidator>());

6. Action Filters — IActionFilter, OnActionExecuting, OnActionExecuted

Action Filters run code before or after an action method executes. They are applied as attributes on controllers or individual actions.

public class LogActionFilter : IActionFilter
{
private readonly ILogger<LogActionFilter> _logger;

public LogActionFilter(ILogger<LogActionFilter> logger)
{
_logger = logger;
}

// Runs BEFORE the action
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInformation($"Action starting: {context.ActionDescriptor.DisplayName}");
}

// Runs AFTER the action
public void OnActionExecuted(ActionExecutedContext context)
{
_logger.LogInformation($"Action finished: {context.ActionDescriptor.DisplayName}");
}
}

Registering and applying filters:

// Apply globally (all controllers and actions)
builder.Services.AddControllers(options =>
options.Filters.Add<LogActionFilter>());

// Apply to a specific controller or action
[ServiceFilter(typeof(LogActionFilter))]
public class ProductsController : ControllerBase { }

Short-circuiting in filters:

public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = new UnauthorizedResult(); // stops the action from running
}
}

7. MVC Pattern — Model, View, Controller Flow

MVC (Model-View-Controller) is an architectural pattern that separates an application into three components:

  • Model — represents data and business logic
  • View — the UI template (Razor .cshtml file for web apps)
  • Controller — handles user input, coordinates model and view

Request flow in ASP.NET Core MVC:

Browser Request: GET /Products/Details/5

Router → matches to ProductsController.Details(5)

Controller.Details(5)
→ fetches Product from Model/Repository
→ returns View("Details", product)

View (Details.cshtml)
→ renders HTML using Model data

HTML Response sent back to browser
// Model
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}

// Controller
public class ProductsController : Controller
{
[HttpGet("{id}")]
public IActionResult Details(int id)
{
var product = _repo.GetById(id); // fetch model
return View(product); // pass model to view
}
}

// View — Details.cshtml
@model Product
<h1>@Model.Name</h1>
<p>Price: @Model.Price.ToString("C")</p>

In API development (no HTML views), the Controller returns JSON directly — the MVC pattern still applies but the "View" is the serialised JSON response.


Phase 5 – Middleware & Filters

1. Custom Middleware — Writing and Registering Middleware

Custom middleware lets you inject reusable request/response logic into the pipeline.

Class-based middleware:

public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;

public RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}

public async Task InvokeAsync(HttpContext context)
{
var stopwatch = Stopwatch.StartNew();

await _next(context); // call the next middleware

stopwatch.Stop();
_logger.LogInformation(
$"{context.Request.Method} {context.Request.Path} — {stopwatch.ElapsedMilliseconds}ms");
}
}

// Extension method for clean registration
public static class RequestTimingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(this IApplicationBuilder app)
=> app.UseMiddleware<RequestTimingMiddleware>();
}

Registering in Program.cs:

app.UseRequestTiming(); // uses the extension method
// or
app.UseMiddleware<RequestTimingMiddleware>();

Inline middleware (simple cases):

app.Use(async (context, next) =>
{
Console.WriteLine($"Before: {context.Request.Path}");
await next();
Console.WriteLine($"After: {context.Response.StatusCode}");
});

Short-circuit middleware (skip the rest of the pipeline):

app.Use(async (context, next) =>
{
if (context.Request.Path == "/health")
{
await context.Response.WriteAsync("Healthy"); // respond immediately
return; // don't call next()
}
await next();
});

2. Built-in Middleware

ASP.NET Core includes many built-in middleware components:

MiddlewareMethodPurpose
HTTPS RedirectionUseHttpsRedirection()Redirect HTTP to HTTPS
Static FilesUseStaticFiles()Serve files from wwwroot
RoutingUseRouting()Match request to endpoint
CORSUseCors()Cross-origin request handling
AuthenticationUseAuthentication()Identify the user (who are you?)
AuthorizationUseAuthorization()Check user permissions (are you allowed?)
Exception HandlingUseExceptionHandler()Global error handling
Response CompressionUseResponseCompression()Compress responses (gzip/brotli)
Rate LimitingUseRateLimiter()Limit requests per time window

Exception handling middleware:

app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";

var error = context.Features.Get<IExceptionHandlerFeature>();
await context.Response.WriteAsJsonAsync(new
{
error = "An unexpected error occurred.",
detail = error?.Error.Message
});
});
});

3. Action Filters vs Middleware — When to Use Which

AspectMiddlewareAction Filters
ScopeEntire HTTP pipelineOnly for MVC/API actions
Access toHttpContext onlyHttpContext + Action arguments, Result
DI supportConstructor injectionConstructor injection
Short-circuitBy not calling next()By setting context.Result
Runs whenEvery requestOnly when a matching controller action runs
Use forAuth, logging, CORS, exceptions, rate limitingValidation, action-specific logging, response transformation

Rule of thumb:

  • Use Middleware for cross-cutting concerns that apply to all or most requests (authentication, logging, CORS).
  • Use Action Filters when you need access to the controller/action context or want to apply logic to specific actions only.

Phase 6 – API & REST

1. REST Principles — HTTP Verbs, Resource-Based URLs

REST (Representational State Transfer) is an architectural style for designing APIs around resources (nouns, not verbs).

Core Principles:

  1. Stateless — each request contains all necessary information; no session state on the server.
  2. Resource-based — URLs represent resources, not actions.
  3. Uniform interface — use standard HTTP methods consistently.
  4. Client-Server — client and server are independent.

Resource-based URL design:

Bad (verb-based)Good (resource-based)
/getProductsGET /products
/createProductPOST /products
/deleteProduct?id=5DELETE /products/5
/updateProduct/5PUT /products/5

2. HTTP Basics — Methods and Status Codes

HTTP Methods:

MethodPurposeHas BodyIdempotent
GETRead a resourceNo✅ Yes
POSTCreate a resourceYes❌ No
PUTReplace a resource entirelyYes✅ Yes
PATCHPartially update a resourceYes❌ No
DELETEDelete a resourceNo✅ Yes

Common Status Codes:

CodeNameWhen to Use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST (resource created)
204No ContentSuccessful DELETE (nothing to return)
400Bad RequestInvalid input / validation error
401UnauthorizedMissing or invalid authentication
403ForbiddenAuthenticated but not authorised
404Not FoundResource doesn't exist
409ConflictDuplicate resource, concurrency conflict
500Internal Server ErrorUnexpected server-side error

In ASP.NET Core controllers:

return Ok(data);          // 200
return Created(location, data); // 201
return NoContent(); // 204
return BadRequest(errors); // 400
return Unauthorized(); // 401
return Forbid(); // 403
return NotFound(); // 404
return Conflict(); // 409
return StatusCode(500); // 500

3. Building Web API — Minimal API vs Controller-Based

Controller-Based API — traditional approach, uses controller classes.

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repo;

public ProductsController(IProductRepository repo) => _repo = repo;

[HttpGet]
public async Task<IActionResult> GetAll()
=> Ok(await _repo.GetAllAsync());

[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id)
{
var product = await _repo.GetByIdAsync(id);
return product is null ? NotFound() : Ok(product);
}

[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateProductDto dto)
{
var product = await _repo.AddAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}

Minimal API — lightweight approach, defines endpoints directly in Program.cs. Best for small APIs or microservices.

var app = builder.Build();

app.MapGet("/api/products", async (IProductRepository repo) =>
Results.Ok(await repo.GetAllAsync()));

app.MapGet("/api/products/{id:int}", async (int id, IProductRepository repo) =>
{
var product = await repo.GetByIdAsync(id);
return product is null ? Results.NotFound() : Results.Ok(product);
});

app.MapPost("/api/products", async (CreateProductDto dto, IProductRepository repo) =>
{
var product = await repo.AddAsync(dto);
return Results.Created($"/api/products/{product.Id}", product);
});

Comparison:

FeatureController-BasedMinimal API
Code organisationOrganised in classesAll in Program.cs (can be split)
Filters / MiddlewareFull supportLimited filter support
Best forLarge, structured APIsSmall APIs, microservices
BoilerplateMoreLess

4. Lazy Loading vs Eager Loading — EF Core Navigation Properties

Entity Framework Core loads related entities (navigation properties) in two main ways.

Eager Loading — loads related data in the same query using .Include(). Recommended when you know you'll need the related data.

// One query with a SQL JOIN — loads Product and its Category together
var products = await _context.Products
.Include(p => p.Category)
.Include(p => p.Reviews)
.ToListAsync();

Console.WriteLine(products[0].Category.Name); // ✅ no extra query

Lazy Loading — loads related data on-demand when the property is first accessed. Requires a proxy package and virtual navigation properties.

// Install: dotnet add package Microsoft.EntityFrameworkCore.Proxies
// In DbContext setup: options.UseLazyLoadingProxies()

public class Product
{
public int Id { get; set; }
public virtual Category Category { get; set; } // virtual = lazy loading
}

var product = await _context.Products.FindAsync(1);
// Category is not loaded yet

var name = product.Category.Name; // triggers a NEW SQL query here

Explicit Loading — load related data on-demand manually (no proxy needed):

var product = await _context.Products.FindAsync(1);

await _context.Entry(product)
.Reference(p => p.Category)
.LoadAsync();

await _context.Entry(product)
.Collection(p => p.Reviews)
.LoadAsync();

Comparison:

StrategyWhen data loadsSQL queriesUse when
Eager LoadingWith main query (JOIN)One queryYou know you'll need related data
Lazy LoadingWhen property is accessedN+1 queries (can be a problem)Related data is rarely needed
Explicit LoadingWhen you call Load()Controlled additional queryNeed selective loading at runtime

N+1 Problem: Lazy loading in a loop causes one query per item — e.g., 100 products = 100 extra category queries. Always prefer eager loading in lists.


Phase 7 – Authentication & Authorization

1. Authentication vs Authorization

ConceptQuestionExample
AuthenticationWho are you?Verifying the JWT token and identifying the user
AuthorizationWhat are you allowed to do?Checking if the user has the "Admin" role

Authentication must happen before authorisation. A user must be identified before you can check their permissions.


2. JWT — Structure, Claims, Token Generation & Validation

JWT (JSON Web Token) is a compact, self-contained token used to securely transmit user identity. It consists of three Base64-encoded parts separated by dots:

header.payload.signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiQWxpY2UiLCJyb2xlIjoiQWRtaW4iLCJleHAiOjE3MjAwMDAwMDB9
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Header — algorithm and token type.
Payload — claims (user data): sub (subject), name, role, exp (expiry).
Signature — HMAC of header + payload, signed with a secret key. Verifies the token hasn't been tampered with.

Generating a JWT in ASP.NET Core:

// Install: dotnet add package System.IdentityModel.Tokens.Jwt
// Install: dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

public class TokenService
{
private readonly IConfiguration _config;

public TokenService(IConfiguration config) => _config = config;

public string GenerateToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.Role, user.Role)
};

var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["JwtSettings:SecretKey"]));

var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

var token = new JwtSecurityToken(
issuer: _config["JwtSettings:Issuer"],
audience: _config["JwtSettings:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: credentials
);

return new JwtSecurityTokenHandler().WriteToken(token);
}
}

Configuring JWT validation:

// Program.cs
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["JwtSettings:Issuer"],
ValidAudience = builder.Configuration["JwtSettings:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:SecretKey"]))
};
});

The client sends the token in the Authorization header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

3. Middleware for Auth — UseAuthentication, UseAuthorization

These two middleware components must be added to the pipeline in the correct order:

app.UseRouting();

app.UseAuthentication(); // 1. Identify who the user is (reads JWT, sets ClaimsPrincipal)
app.UseAuthorization(); // 2. Check if the identified user is allowed

app.MapControllers();

UseAuthentication reads the JWT from the Authorization header, validates it, and populates HttpContext.User with the user's claims.

UseAuthorization checks the [Authorize] attributes on controllers/actions and enforces access control.


4. [Authorize] Attribute — Role-Based and Policy-Based Authorization

Require authentication:

[Authorize] // user must be authenticated
public class ProductsController : ControllerBase { }

[AllowAnonymous] // override [Authorize] for a specific action
public IActionResult GetPublicProducts() { }

Role-based authorization:

[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult Delete(int id) { }

[Authorize(Roles = "Admin,Manager")] // multiple roles (OR)
public IActionResult Update(int id) { }

Policy-based authorization (more flexible):

// Define the policy in Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("CanEditProducts", policy =>
policy.RequireRole("Admin")
.RequireClaim("department", "sales", "product-team"));
});

// Apply the policy
[Authorize(Policy = "CanEditProducts")]
[HttpPut("{id}")]
public IActionResult Update(int id) { }

Phase 8 – Security

1. HTTPS & SSL Basics

HTTPS (HyperText Transfer Protocol Secure) encrypts all communication between the client and server using TLS (Transport Layer Security) — commonly still called SSL.

In ASP.NET Core:

// Force HTTPS redirect in Program.cs
app.UseHttpsRedirection();

// Enforce HTTPS in development via launchSettings.json
// Or use HSTS (HTTP Strict Transport Security) in production
app.UseHsts(); // tells browsers to only ever use HTTPS for this domain

In production, your web server (IIS, Nginx, Azure App Service) handles the TLS certificate. In development, use the .NET developer certificate:

dotnet dev-certs https --trust

2. CORS — Cross-Origin Resource Sharing

Browsers enforce the Same-Origin Policy — a webpage at https://myapp.com cannot call an API at https://api.myapp.com by default. CORS is an HTTP mechanism that allows servers to selectively permit cross-origin requests.

// Program.cs
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
policy.WithOrigins("https://myapp.com", "http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod());
});

app.UseCors("AllowFrontend"); // register before UseRouting

AllowAnyOrigin() with AllowCredentials() is not allowed — credentials require specific origins:

// ❌ Cannot combine AllowAnyOrigin with AllowCredentials
policy.AllowAnyOrigin().AllowCredentials();

// ✅ Use specific origins when credentials are needed
policy.WithOrigins("https://myapp.com").AllowCredentials();

3. Secrets Management — User Secrets, Environment Variables

Never hardcode secrets (connection strings, API keys, JWT secrets) in source code or appsettings.json committed to source control.

User Secrets (development) — stored outside the project directory, not committed to git:

# Enable user secrets for the project
dotnet user-secrets init

# Set a secret
dotnet user-secrets set "JwtSettings:SecretKey" "my-dev-secret-key"
// Access just like appsettings — configuration automatically merges them
var secretKey = builder.Configuration["JwtSettings:SecretKey"];

Environment Variables (production) — set at the OS/container/cloud level:

// Environment variable names use __ for hierarchy on Windows
// JwtSettings__SecretKey = "prod-secret-key"

var secretKey = builder.Configuration["JwtSettings:SecretKey"];
// automatically resolves from env var in production

In Azure — use Azure Key Vault or App Service Application Settings. In Docker, use environment variables or mounted secrets.


4. Common Vulnerabilities — SQL Injection

SQL Injection occurs when untrusted user input is concatenated into a SQL query, allowing attackers to modify the query.

// ❌ Vulnerable — direct string concatenation
string userInput = "'; DROP TABLE Users; --";
string sql = $"SELECT * FROM Users WHERE Username = '{userInput}'";
// Resulting SQL: SELECT * FROM Users WHERE Username = ''; DROP TABLE Users; --'

Prevention with parameterised queries:

// ✅ Safe — parameters are escaped automatically
using var command = new SqlCommand(
"SELECT * FROM Users WHERE Username = @username", connection);
command.Parameters.AddWithValue("@username", userInput);

Using Entity Framework Core (automatically parameterised):

// ✅ Safe — EF Core always uses parameterised queries
var user = await _context.Users
.Where(u => u.Username == username)
.FirstOrDefaultAsync();

If you must use raw SQL in EF Core, use parameters:

// ✅ Safe — FormattableString is parameterised
var user = await _context.Users
.FromSqlInterpolated($"SELECT * FROM Users WHERE Username = {username}")
.FirstOrDefaultAsync();

// ❌ Unsafe — FromSqlRaw with string concatenation
var user = await _context.Users
.FromSqlRaw($"SELECT * FROM Users WHERE Username = '{username}'")
.FirstOrDefaultAsync();

Phase 9 – Logging & Monitoring (Optional)

1. Built-in Logging — ILogger, Log Levels

ASP.NET Core includes a built-in logging framework via ILogger<T>. Inject it anywhere through DI.

Log levels (lowest to highest severity):

LevelMethodUse for
TraceLogTraceVery detailed, step-by-step diagnostics
DebugLogDebugDeveloper diagnostics
InformationLogInformationNormal application flow events
WarningLogWarningUnexpected but recoverable situations
ErrorLogErrorErrors that affect the current operation
CriticalLogCriticalFatal failures requiring immediate attention
public class OrderService
{
private readonly ILogger<OrderService> _logger;

public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}

public async Task PlaceOrder(Order order)
{
_logger.LogInformation("Placing order {OrderId} for user {UserId}",
order.Id, order.UserId);

try
{
// process order
_logger.LogInformation("Order {OrderId} placed successfully", order.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to place order {OrderId}", order.Id);
throw;
}
}
}

Configure log levels in appsettings.json:

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"MyApp.Services": "Debug"
}
}
}

2. Serilog / NLog — Structured Logging Basics

The built-in logger writes plain text. Serilog and NLog provide structured logging — logging with key-value properties that can be queried and filtered in log management tools (Seq, Elasticsearch, Application Insights).

Setting up Serilog:

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File
// Program.cs
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console()
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
.Enrich.FromLogContext()
.CreateLogger();

builder.Host.UseSerilog();

Structured log messages:

// The {OrderId} placeholder becomes a structured property, not just text
_logger.LogInformation("Order {OrderId} placed by {UserId}", order.Id, order.UserId);

This allows log queries like "show me all logs where OrderId = 42" in Seq or Kibana.


Phase 10 – Performance Optimization (Optional)

1. Caching — In-Memory Cache, Distributed Cache (Redis Overview)

Why cache? Avoid re-computing or re-fetching expensive data (database queries, API calls) on every request.

In-Memory Cache — stores data in the web server's memory. Fast, but not shared across multiple server instances.

// Register in Program.cs
builder.Services.AddMemoryCache();

// Use in a service
public class ProductService
{
private readonly IMemoryCache _cache;
private readonly IProductRepository _repo;

public ProductService(IMemoryCache cache, IProductRepository repo)
{
_cache = cache;
_repo = repo;
}

public async Task<IEnumerable<Product>> GetAllAsync()
{
const string cacheKey = "all_products";

if (!_cache.TryGetValue(cacheKey, out IEnumerable<Product> products))
{
products = await _repo.GetAllAsync();

var options = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
SlidingExpiration = TimeSpan.FromMinutes(2)
};

_cache.Set(cacheKey, products, options);
}

return products;
}
}

Distributed Cache (Redis) — stores data in a shared external cache. Ideal when running multiple server instances (load-balanced environments).

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
// Register in Program.cs
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration["Redis:ConnectionString"];
});

// Use via IDistributedCache
public class ProductService
{
private readonly IDistributedCache _cache;

public async Task<string> GetProductJsonAsync(int id)
{
string cacheKey = $"product:{id}";
string cached = await _cache.GetStringAsync(cacheKey);

if (cached is null)
{
var product = await _repo.GetByIdAsync(id);
cached = JsonSerializer.Serialize(product);

await _cache.SetStringAsync(cacheKey, cached, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
}

return cached;
}
}

2. Async Best Practices — Avoiding Deadlocks, ConfigureAwait

Avoid .Result and .Wait() — they block the current thread and can cause deadlocks in environments with a synchronisation context (ASP.NET Framework, WPF):

// ❌ Can deadlock in certain environments
var result = GetDataAsync().Result; // blocking
GetDataAsync().Wait(); // blocking

// ✅ Always await instead
var result = await GetDataAsync();

ConfigureAwait(false) — when writing library code, tells the awaiter not to resume on the original synchronisation context. This improves performance and avoids deadlocks in library code:

// Library code — use ConfigureAwait(false)
public async Task<string> FetchDataAsync()
{
var response = await httpClient.GetStringAsync(url).ConfigureAwait(false);
return response;
}

// ASP.NET Core application code — ConfigureAwait(false) is not strictly necessary
// (ASP.NET Core has no synchronisation context), but it can still help with performance

Avoid async void — exceptions thrown in async void methods cannot be caught by the caller and will crash the process:

// ❌ Avoid async void (except for event handlers)
public async void LoadData()
{
await FetchAsync(); // unhandled exceptions here = app crash
}

// ✅ Use async Task
public async Task LoadData()
{
await FetchAsync(); // caller can await and catch exceptions
}

Run independent tasks in parallel with Task.WhenAll:

// ❌ Sequential — each awaits before the next starts
var orders = await _orderRepo.GetAllAsync();
var products = await _productRepo.GetAllAsync();
var users = await _userRepo.GetAllAsync();

// ✅ Parallel — all three start simultaneously
var ordersTask = _orderRepo.GetAllAsync();
var productsTask = _productRepo.GetAllAsync();
var usersTask = _userRepo.GetAllAsync();

await Task.WhenAll(ordersTask, productsTask, usersTask);

var orders = await ordersTask;
var products = await productsTask;
var users = await usersTask;

Quick Reference

Common HTTP Status Codes

CodeMeaningWhen to Return
200 OKSuccessGET, PUT, PATCH success
201 CreatedResource createdPOST success
204 No ContentSuccess, no bodyDELETE success
400 Bad RequestInvalid inputValidation errors
401 UnauthorizedNot authenticatedMissing/invalid token
403 ForbiddenNot authorisedAuthenticated but no permission
404 Not FoundResource missingID not found in database
500 Internal Server ErrorServer crashedUnhandled exception

C# Collection Interfaces

InterfaceAdds Over PreviousTypical Implementation
IEnumerable<T>Iteration onlyAny collection
ICollection<T>Add, Remove, CountList<T>, HashSet<T>
IList<T>Index accessList<T>
IDictionary<K,V>Key-value accessDictionary<K,V>
IQueryable<T>DB query translationEF Core DbSet

Service Lifetimes

LifetimeInstance CreatedUse for
TransientEvery injectionStateless, lightweight services
ScopedOnce per requestDbContext, per-request services
SingletonOnce, app lifetimeCache, configuration, shared state