Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 1

SQL Server

This chapter covers various aspects of SQL Server, including performance optimization, query execution, and best practices.

Topics

Execution Plans

Execution plans are the roadmap that SQL Server uses to retrieve data. Understanding how to read and analyze execution plans is crucial for performance tuning.

Topics

Overview

An execution plan shows how SQL Server will execute a query, including:

  • Which indexes are used
  • Join methods
  • Scan types (table scan vs index scan/seek)
  • Sort operations
  • And various other operators

Why Execution Plans Matter

  • Performance Tuning: Identify bottlenecks in queries
  • Index Analysis: Understand if indexes are being used effectively
  • Query Optimization: See why a query is slow and how to improve it
  • Cardinality Estimates: Understand row count predictions

How to View Execution Plans

Using SSMS (SQL Server Management Studio)

  1. Enable “Include Actual Execution Plan” (Ctrl + M)
  2. Run your query
  3. View the Execution Plan tab

Using T-SQL

SET STATISTICS XML ON;
-- Your query here
SET STATISTICS XML OFF;

Common Operators

OperatorDescription
Table ScanReads all rows from a table
Index ScanReads all entries from an index
Index SeekUses index to find specific rows
Clustered Index ScanScans entire clustered index (entire table)
Nested LoopsJoins using nested iteration
Hash JoinJoins using hash table
Merge JoinJoins using sorted inputs
Compute ScalarCalculates new values from existing data
SortOrders data
FilterFilters rows based on condition

Further Reading

For detailed information about specific operators, see the topics in this section.

Compute Scalar

Compute Scalar is an operator that computes new values from existing data within a row. It does not read data from disk, only processes data in memory.

Common Use Cases

  • Calculations: Quantity * Price AS TotalAmount
  • Functions: UPPER(FirstName), YEAR(OrderDate)
  • Type Casting: CAST(OrderId AS VARCHAR)
  • Concatenation: FirstName + ' ' + LastName

Parameters Explained

1. Physical Operation / Logical Operation

Physical Operation: Compute Scalar
Logical Operation: Compute Scalar
  • Physical Operation: How SQL Server executes (compute scalar)
  • Logical Operation: Logical meaning (also compute scalar)

In this case, both are the same since the operator has no special variants.

2. Estimated Execution Mode

Estimated Execution Mode: Row
  • Row: Processes row-by-row
  • Batch: Processes in batch mode (usually with columnstore indexes)

This is a simple operator, always running in row mode.

3. Estimated Operator Cost

Estimated Operator Cost: 0.001982 (2%)

Most important!

  • 0.001982: Absolute cost of this operator
  • (2%): Relative cost compared to the entire query (2% of total cost)

Meaning:

  • This operator only takes 2% of the total query cost → NOT A BOTTLENECK
  • If this number is > 30-50%, you should consider optimization

4. Estimated I/O Cost

Estimated I/O Cost: 0
  • This is the I/O cost (reading from disk)
  • 0 means this operator does not read data from disk, only processes data already in memory

✅ This is good - no I/O here.

5. Estimated Subtree Cost

Estimated Subtree Cost: 0.117937
  • Total cost of the entire subtree from this operator downward
  • Includes the current operator and all child operators

Meaning:

  • This is the accumulated cost
  • In the plan, you’ll see this value increase as you go up the tree toward the root

6. Estimated CPU Cost

Estimated CPU Cost: 0.001982
  • CPU cost specifically for this operator
  • In this case, it equals the Estimated Operator Cost (0.001982) because there is no I/O

Comparison with I/O Cost:

CPU Cost = 0.001982
I/O Cost = 0
Total = 0.001982

7. Estimated Number of Executions

Estimated Number of Executions: 1
  • How many times this operator is executed
  • 1 is normal for top-level operators
  • If > 1, the operator may be in a loop (nested loop join) and could be a performance issue

8. Estimated Number of Rows

Estimated Number of Rows: 19820
  • SQL Server estimates this operator will process 19,820 rows
  • This is the cardinality estimate - very important for plan selection

Comparison with Actual Rows:

  • If Actual Rows differs significantly from Estimated Rows → statistics issue

9. Estimated Row Size

Estimated Row Size: 56 B
  • Each row takes approximately 56 bytes in memory
  • Used to estimate memory grant

Calculation:

56 bytes × 19,820 rows ≈ 1.1 MB (memory required)

10. Node ID

Node ID: 0
  • ID of the operator in the execution plan
  • Used for referencing, debugging, or searching in the XML plan

Overall Analysis

Based on these parameters, here’s the assessment:

✅ Good Points:

MetricValueAssessment
I/O Cost0No disk reads
CPU Cost0.001982Very small
% Cost2%Not a bottleneck
Executions1No loop repetition

⚠️ Needs Checking:

  • Estimated Rows = 19,820: Compare with Actual Rows in the actual plan (SET STATISTICS XML ON)
  • If Actual Rows >> 19,820 (e.g., 200,000) → Statistics outdated → Run UPDATE STATISTICS

Real-world Examples: When Compute Scalar Becomes an Issue?

Bad Case (bottleneck):

| Operator          | Cost    | Rows   | Executions |
|-------------------|---------|--------|------------|
| Compute Scalar    | 85%     | 1M     | 1000       |

Problem:

  • Takes 85% CPU, runs 1000 times (in a loop)
  • Consider: move calculation to application, use computed column, or optimize join logic

Your Case (good):

| Operator          | Cost    | Rows   | Executions |
|-------------------|---------|--------|------------|
| Compute Scalar    | 2%      | 19,820 | 1          |

No optimization needed, focus on other operators with higher cost.

How to View Actual vs Estimated in SSMS

To see actual metrics:

-- Enable Actual Execution Plan (Ctrl + M)
SET STATISTICS XML ON; -- Or use SSMS UI

-- Run your query
SELECT 
    OrderId,
    Quantity * Price AS TotalAmount, -- Compute Scalar will appear
    UPPER(CustomerName) AS UpperName
FROM Orders
WHERE OrderDate > '2024-01-01';

Then in the Execution Plan, hover over the operator to see both Estimated and Actual:

Actual Number of Rows: 19,820    (if it matches Estimated → good)
Actual Number of Executions: 1   (if > 1 → potential issue)

Summary

ParameterYour ValueMeaning
Operator Cost0.001982 (2%)✅ No concern, only 2% of query
I/O Cost0✅ No disk reads, CPU only
Estimated Rows19,820⚠️ Need to compare with Actual Rows
Executions1✅ No loop
Row Size56 B✅ Small, low memory footprint

Conclusion: This Compute Scalar operator is not a problem to optimize. If the query is slow, look for operators with higher cost (table scan, index scan, hash join) or check if Estimated Rows differs significantly from Actual Rows.

Clustered Index Scan

Clustered Index Scan scans the entire clustered index (meaning the entire table, since clustered index stores all data). This is the most expensive operator in the execution plan (taking 97% of the cost).

What is Clustered Index Scan?

When SQL Server performs a Clustered Index Scan, it reads through every row in the clustered index. Since the clustered index contains the entire table data (the data rows are stored in the leaf nodes of the index), a scan means reading the entire table.

This operator is typically the most expensive because it reads all rows, not just a subset.

Common Use Cases

  • No suitable index for the WHERE clause
  • No WHERE clause at all (SELECT * FROM table)
  • Non-sargable WHERE clause (e.g., WHERE YEAR(date) = 2024)
  • Fetching too many rows (SQL Server estimates scan is faster than seek + bookmark lookup)

Parameters Explained

1. Storage

Storage: RowStore
  • Data is stored in row format (horizontal) - traditional storage
  • RowStore vs ColumnStore: RowStore is good for OLTP (many inserts/updates), ColumnStore is good for OLAP (aggregates on many rows)

2. Number of Rows - VERY IMPORTANT

Number of Rows Read: 19820
Actual Number of Rows: 19820
Estimated Number of Rows: 19820
Estimated Number of Rows to be Read: 19820

✅ GOOD POINT:

  • Actual = Estimated = 19,820 rows
  • Statistics are accurate, no cardinality estimation issues

Meaning:

  • SQL Server correctly estimates the number of rows to process
  • Query optimizer selects a plan appropriate for actual data

3. Cost - THE MAIN ISSUE

Estimated Operator Cost: 0.113973 (97%)
Estimated I/O Cost: 0.0920139
Estimated CPU Cost: 0.021959

⚠️ KEY POINTS:

MetricValuePercentageAssessment
Operator Cost0.11397397%❌ Very high
I/O Cost0.092013981% of operator❌ Reading from disk
CPU Cost0.02195919% of operator⚠️ Significant

Analysis:

  • This operator takes 97% of the total query cost
  • 81% of cost is I/O → reading data from disk, not from cache
  • THIS IS THE MAIN BOTTLENECK of the query

4. Execution Parameters

Number of Executions: 1
Actual Rebinds: 0
Actual Rewinds: 0
Ordered: False
ParameterValueMeaning
Executions1Runs only once (not in a loop) ✅
Rebinds0Not re-initializing parameters ❌
Rewinds0No loop join rewind ✅
OrderedFalseResult is not sorted by clustered key

5. Row Size

Estimated Row Size: 47 B
  • Each row ~47 bytes
  • Total data: 47 × 19,820 ≈ 931 KB (less than 1 MB)

⚠️ Paradox:

  • Data is only ~1 MB, but still scanning everything?
  • The table may have more than 19,820 rows but the filter only selects 19,820 rows without a suitable index

Comparison with Compute Scalar

OperatorCostI/O CostCPU CostRows
Clustered Index Scan0.113973 (97%)0.0920140.02195919,820
Compute Scalar0.001982 (2%)00.00198219,820

Observation:

  • Clustered Index Scan is 57 times more expensive than Compute Scalar
  • If the query is slow, the issue is here, not in Compute Scalar

How to Optimize

Method 1: Check if there’s a WHERE clause

-- Current query (assuming)
SELECT * FROM Orders
WHERE CustomerId = 12345  -- If there's no index on CustomerId

Solution: Create a non-clustered index

CREATE INDEX IX_Orders_CustomerId ON Orders(CustomerId)

After that, the plan will become:

Index Seek (NonClustered) → Key Lookup (Clustered) → Compute Scalar

Method 2: If query has no WHERE clause (SELECT all)

SELECT * FROM Orders  -- Fetching all 19,820 rows

Assessment:

  • Scanning 20,000 rows is acceptable (I/O cost 0.09 is small)
  • No optimization needed if data is small

Method 3: Check WHERE clause sargability

-- BAD (non-sargable)
WHERE YEAR(OrderDate) = 2024

-- GOOD (sargable)
WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01'

Method 4: Use covering index to avoid Key Lookup

If the query only needs a few columns:

-- Instead of SELECT *
CREATE INDEX IX_Orders_CustomerId_Include 
ON Orders(CustomerId) 
INCLUDE (OrderDate, TotalAmount)

The plan will become:

Index Seek (NonClustered) → Compute Scalar
(No Key Lookup needed)

Overall Assessment

✅ Good Points:

MetricValueAssessment
Actual vs Estimated Rows19,820 = 19,820Statistics accurate
Executions1No loop
Row Size47 BSmall data

❌ Points to Improve:

MetricValueIssue
Operator Cost97%Takes almost entire cost
I/O Cost0.092Reading disk, not cache
StorageRowStoreAppropriate but scanning all

Questions to Determine If Optimization is Needed

  1. Does the query have a WHERE clause?

    • If yes: Need to create an index for the column in WHERE
    • If no: Scanning 20k rows is acceptable
  2. What is the total number of rows in the table?

    • If ≈ 20,000: Scan is OK
    • If >> 20,000 (e.g., 1 million): Scan is a serious issue
  3. How long does the query take?

    • If < 100ms: No optimization needed
    • If > 1s: Need to create an index

Summary

This Clustered Index Scan is the main bottleneck (97% cost).

Next steps:

  1. View query text (F4 or hover over operator) to see WHERE clause
  2. Check total rows in the table
  3. If there’s a valid WHERE clause → create non-clustered index
  4. If query actually runs slow (> 500ms) → optimize now
  5. If query is fast (< 100ms) and data is small → can leave as is

Principle: Clustered Index Scan on 20,000 rows is not always bad. What’s important is execution frequency and actual time.

C#/.NET

Giới thiệu

Tài liệu này tổng hợp các chủ đề quan trọng với vị trí C#/.NET Developer. Các câu hỏi được chia theo từng mảng kiến thức, từ nền tảng đến nâng cao.

Mục lục

#Mảng Kiến ThứcMô tả
1Nền Tảng C# và .NETNgôn ngữ C#, CLR, Garbage Collection, Value Types vs Reference Types
2ASP.NET Core Cốt lõiMiddleware, DI, Routing, Filters, Configuration
3Xây dựng Web APIRESTful, Authentication, Authorization, Swagger
4Truy cập Dữ liệu với EF CoreCode First, Migrations, N+1 Query, Transactions
5Kiến trúc Phần mềmSOLID, Design Patterns, Clean Architecture, DDD, CQRS
6Hiệu suất và Xử lý Bất đồng bộCaching, Rate Limiting, Load Balancing
7Hệ thống Phân tánMessage Queue, Docker, Kubernetes
8Kiểm thửUnit Test, Integration Test với xUnit
9Câu hỏi Phân biệtSo sánh các công nghệ và concepts

Hướng dẫn sử dụng

  1. Đọc theo thứ tự: Bắt đầu từ phần 1 (Nền tảng) và tiến dần đến các phần nâng cao
  2. Thực hành: Mỗi chủ đề cần có code example đi kèm
  3. Ôn tập lại

Note: Đây là tài liệu tổng hợp. Bạn có thể click vào từng chủ đề để xem chi tiết và bổ sung nội dung cụ thể.

1. Nền Tảng C# và .NET

Giới thiệu

Phần này trình bày các kiến thức nền tảng về ngôn ngữ C# và .NET Runtime, bao gồm:

Nội dung chính

C# Cơ bản

  • Cú pháp cơ bản - Kiểu dữ liệu, biến, toán tử, luồng điều khiển, phương thức

Lập trình hướng đối tượng

  • OOP - Class, Inheritance, Polymorphism, Encapsulation, Interface

Hệ thống kiểu & Generics

  • Types & Generics - Generic types, nullable types, type conversion, boxing/unboxing

Delegates, Events & Lambda

Collections

  • Collections - Arrays, List, Dictionary, HashSet và các collection interfaces

Xử lý chuỗi

  • Strings - String vs StringBuilder, string interpolation, string methods

Async/Await & LINQ

Exception Handling & IDisposable

Pattern Matching, Records & Reflection

CLR & Bộ nhớ


Câu hỏi phỏng vấn (Sắp xếp từ dễ đến khó)

Mức độ: Dễ (Junior)

  1. Sự khác biệt giữa value typesreference types trong C#?
  2. constreadonly khác nhau như thế nào?
  3. String vs StringBuilder - khi nào nên dùng cái nào?
  4. Sự khác biệt giữa for, while, và foreach loops?
  5. break, continue, và return khác nhau như thế nào?
  6. ArrayList<T> khác nhau gì?
  7. ref, out, in parameters khác nhau như thế nào?
  8. null-coalescing operator (??) và null-conditional operator (?.) dùng để làm gì?

Mức độ: Trung bình (Mid-level)

  1. abstract classinterface khác nhau như thế nào? Khi nào dùng cái nào?
  2. Giải thích inheritance, polymorphism, và encapsulation trong OOP?
  3. Sự khác biệt giữa delegate, event, và lambda expression?
  4. So sánh List<T>, Dictionary<TKey, TValue>, và HashSet<T> - khi nào dùng cái nào?
  5. Khi nào nên sử dụng generic types và cách áp dụng constraints?
  6. BoxingUnboxing là gì? Tại sao nên tránh?
  7. Deferred Execution trong LINQ là gì?
  8. Sự khác biệt giữa IEnumerableIQueryable - khi nào dùng cái nào?
  9. Giải thích try-catch-finally hoạt động thế nào? throwthrow ex khác nhau gì?
  10. IDisposable là gì? Tại sao cần implement nó?
  11. using statement hoạt động như thế nào với IDisposable?

Mức độ: Khó (Senior)

  1. Giải thích async/await hoạt động thế nào dưới the hood?
  2. Sự khác biệt giữa IEnumerableIAsyncEnumerable?
  3. Cách tối ưu truy vấn LINQ trên tập dữ liệu lớn?
  4. Garbage Collection (GC) hoạt động như thế nào?
  5. Các thế hệ (Generations) trong GC là gì?
  6. Cách tối ưu để giảm áp lực lên GC?
  7. Value Types (struct) vs Reference Types (class) – cách lưu trữ trên Stack/Heap và tác động đến hiệu năng?
  8. Reflection là gì và khi nào nên sử dụng? Performance considerations?
  9. Attributes trong C# dùng để làm gì? Cho ví dụ về custom attribute.
  10. Pattern Matching trong C# 9+ có gì mới?
  11. Records khác Classes như thế nào? Khi nào nên dùng Records?
  12. Tương tác giữa try-catch-finally và IDisposable - using statement tương đương với try-finally như thế nào?
  13. Tại sao không nên throw exception từ Dispose method?
  14. ConfigureAwait(false) dùng để làm gì? Khi nào nên sử dụng?

C# Cơ bản

Cú pháp cơ bản

Kiểu dữ liệu

C# có hai loại kiểu dữ liệu chính:

// Value types (lưu trên stack)
int number = 42;
double price = 19.99;
bool isActive = true;
char letter = 'A';
DateTime now = DateTime.Now;

// Reference types (lưu trên heap)
string name = "John";
object obj = new object();
int[] numbers = new int[] { 1, 2, 3 };

Biến và Hằng số

// Biến thông thường
var message = "Hello"; // Type inference
string name = "Alice";

// Hằng số
const int MaxUsers = 100;
const string AppName = "MyApp";

// Readonly (chỉ gán trong constructor)
readonly string _connectionString;

Toán tử

// Toán tử số học
int sum = 5 + 3;      // 8
int diff = 10 - 4;    // 6
int product = 6 * 7;  // 42
int quotient = 15 / 3; // 5
int remainder = 10 % 3; // 1

// Toán tử so sánh
bool isEqual = (5 == 5);      // true
bool notEqual = (5 != 3);     // true
bool greaterThan = (10 > 5);  // true
bool lessThan = (3 < 7);      // true

// Toán tử logic
bool and = (true && false);   // false
bool or = (true || false);    // true
bool not = !true;             // false

// Toán tử ternary
int score = 85;
string grade = score >= 60 ? "Pass" : "Fail"; // "Pass"

Luồng điều khiển

// if-else
if (age >= 18)
{
    Console.WriteLine("Adult");
}
else if (age >= 13)
{
    Console.WriteLine("Teenager");
}
else
{
    Console.WriteLine("Child");
}

// switch
string day = "Monday";
switch (day)
{
    case "Monday":
        Console.WriteLine("Start of week");
        break;
    case "Friday":
        Console.WriteLine("Weekend is near");
        break;
    default:
        Console.WriteLine("Midweek");
        break;
}

// Vòng lặp
for (int i = 0; i < 10; i++)
{
    Console.WriteLine(i);
}

int count = 0;
while (count < 5)
{
    Console.WriteLine(count);
    count++;
}

do
{
    Console.WriteLine("At least once");
} while (false);

// foreach
string[] names = { "Alice", "Bob", "Charlie" };
foreach (string name in names)
{
    Console.WriteLine(name);
}

Phương thức

// Method với parameters và return type
public int Add(int a, int b)
{
    return a + b;
}

// Optional parameters
public void Greet(string name = "Guest")
{
    Console.WriteLine($"Hello, {name}!");
}

// Method overloading
public int Multiply(int a, int b) => a * b;
public double Multiply(double a, double b) => a * b;

// ref, out, in parameters
public void Swap(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}

public bool TryParse(string input, out int result)
{
    return int.TryParse(input, out result);
}

public double Calculate(in double value)
{
    // value cannot be modified
    return value * 2;
}

Lập trình hướng đối tượng (OOP)

Class và Object

public class Person
{
    // Fields
    private string _name;
    private int _age;
    
    // Properties
    public string Name
    {
        get => _name;
        set => _name = value;
    }
    
    public int Age
    {
        get => _age;
        set
        {
            if (value >= 0)
                _age = value;
        }
    }
    
    // Auto-property
    public string Email { get; set; }
    
    // Constructor
    public Person(string name, int age)
    {
        _name = name;
        _age = age;
    }
    
    // Method
    public void Introduce()
    {
        Console.WriteLine($"Hi, I'm {_name}, {_age} years old.");
    }
}

// Sử dụng
var person = new Person("Alice", 30);
person.Introduce();

Inheritance (Kế thừa)

public class Animal
{
    public string Name { get; set; }
    
    public virtual void MakeSound()
    {
        Console.WriteLine("Some sound");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
    
    public void Fetch()
    {
        Console.WriteLine("Fetching ball...");
    }
}

// Sử dụng
Animal myDog = new Dog();
myDog.MakeSound(); // "Woof!"

Polymorphism (Đa hình)

public abstract class Shape
{
    public abstract double CalculateArea();
}

public class Circle : Shape
{
    public double Radius { get; set; }
    
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
    
    public override double CalculateArea()
    {
        return Width * Height;
    }
}

// Sử dụng đa hình
List<Shape> shapes = new List<Shape>
{
    new Circle { Radius = 5 },
    new Rectangle { Width = 4, Height = 6 }
};

foreach (var shape in shapes)
{
    Console.WriteLine($"Area: {shape.CalculateArea()}");
}

Encapsulation (Đóng gói)

public class BankAccount
{
    // Private field - encapsulated
    private decimal _balance;
    
    // Public property với validation
    public decimal Balance
    {
        get => _balance;
        private set
        {
            if (value >= 0)
                _balance = value;
        }
    }
    
    public void Deposit(decimal amount)
    {
        if (amount > 0)
            Balance += amount;
    }
    
    public bool Withdraw(decimal amount)
    {
        if (amount > 0 && amount <= Balance)
        {
            Balance -= amount;
            return true;
        }
        return false;
    }
}

Interface

public interface ILogger
{
    void Log(string message);
    void LogError(string error);
}

public interface IDisposable
{
    void Dispose();
}

public class FileLogger : ILogger, IDisposable
{
    public void Log(string message)
    {
        File.AppendAllText("log.txt", $"{DateTime.Now}: {message}\n");
    }
    
    public void LogError(string error)
    {
        Log($"ERROR: {error}");
    }
    
    public void Dispose()
    {
        // Cleanup resources
    }
}

Abstract Class vs Interface

FeatureAbstract ClassInterface
Constructor✅ Có❌ Không
Fields✅ Có❌ Không (chỉ properties)
Method implementation✅ Có (có thể có cả abstract và concrete)❌ Không (C# 8+ có default implementation)
Multiple inheritance❌ Không✅ Có
Access modifiers✅ Có (public, protected, private)❌ Mặc định public

Hệ thống kiểu & Generics

Generics

// Generic class
public class Repository<T> where T : class
{
    private List<T> _items = new List<T>();
    
    public void Add(T item)
    {
        _items.Add(item);
    }
    
    public T GetById(int id)
    {
        // Implementation
        return default(T);
    }
}

// Generic method
public T Max<T>(T a, T b) where T : IComparable<T>
{
    return a.CompareTo(b) > 0 ? a : b;
}

// Sử dụng
var repo = new Repository<Product>();
repo.Add(new Product());

int max = Max(10, 20); // 20

Nullable Types

// Nullable value types
int? nullableInt = null;
DateTime? nullableDate = null;

if (nullableInt.HasValue)
{
    int value = nullableInt.Value;
}

// Null-coalescing operator
int safeValue = nullableInt ?? 0;

// Null-conditional operator
string name = person?.Name?.ToUpper() ?? "Unknown";

// Null-forgiving operator (C# 8+)
string notNull = possiblyNull!; // Assert not null

Type Conversion

// Implicit conversion
int small = 10;
long large = small; // OK

// Explicit conversion (cast)
double d = 9.8;
int i = (int)d; // 9

// as operator (returns null if fails)
object obj = "Hello";
string str = obj as string; // "Hello"
int? number = obj as int?; // null

// is operator
if (obj is string)
{
    Console.WriteLine("It's a string");
}

// Pattern matching with is
if (obj is string s)
{
    Console.WriteLine($"Length: {s.Length}");
}

Boxing và Unboxing

// Boxing - value type → reference type
int value = 42;
object boxed = value; // Boxing

// Unboxing - reference type → value type
int unboxed = (int)boxed; // Unboxing

// Performance impact
// Boxing allocates memory on heap
// Unboxing requires type check and copy

Delegates, Events & Lambda

Delegates

// Delegate declaration
public delegate void Notify(string message);

// Delegate instance
Notify notifier = SendEmail;

// Multicast delegate
notifier += SendSMS;
notifier += LogMessage;

// Invoke
notifier("Hello!");

// Built-in delegates
Action<string> action = Console.WriteLine;
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0;

void SendEmail(string msg) { /* ... */ }
void SendSMS(string msg) { /* ... */ }
void LogMessage(string msg) { /* ... */ }

Events

public class Button
{
    // Event declaration
    public event EventHandler Clicked;
    
    public void Click()
    {
        // Raise event
        Clicked?.Invoke(this, EventArgs.Empty);
    }
}

// Sử dụng
var button = new Button();
button.Clicked += (sender, e) => Console.WriteLine("Button clicked!");

// Event với custom EventArgs
public class OrderPlacedEventArgs : EventArgs
{
    public int OrderId { get; set; }
    public decimal Total { get; set; }
}

public class OrderService
{
    public event EventHandler<OrderPlacedEventArgs> OrderPlaced;
    
    public void PlaceOrder(Order order)
    {
        // Process order
        OrderPlaced?.Invoke(this, new OrderPlacedEventArgs
        {
            OrderId = order.Id,
            Total = order.Total
        });
    }
}

Lambda Expressions

// Expression lambda
Func<int, int> square = x => x * x;

// Statement lambda
Action<int> print = x =>
{
    Console.WriteLine($"Value: {x}");
    Console.WriteLine($"Square: {x * x}");
};

// Lambda với multiple parameters
Func<int, int, int> multiply = (x, y) => x * y;

// Sử dụng trong LINQ
var evenNumbers = numbers.Where(n => n % 2 == 0);

Mối quan hệ giữa Delegate, Event và Lambda Expression

Ba khái niệm này có mối quan hệ chặt chẽ và thường bị nhầm lẫn. Hiểu được bản chất của chúng sẽ giúp bạn sử dụng đúng cách.

Bản chất

Khái niệmBản chấtVai trò
DelegateKiểu dữ liệu (type-safe function pointer)Định nghĩa “hợp đồng” cho phương thức
Lambda ExpressionCú pháp (syntax)Cách viết ngắn gọn cho anonymous method
EventCơ chế bảo vệ (wrapper)Giới hạn truy cập delegate, chỉ cho phép +=-=

Mối quan hệ

┌─────────────────────────────────────────────────────────────┐
│                     Mối quan hệ                              │
│                                                             │
│   Lambda Expression ──► tạo ra ──► Delegate Instance         │
│                              │                              │
│                              ▼                              │
│   Event ──► bao bọc (wraps) ──► Delegate Field              │
│                                                             │
│   Kết quả: Event + Lambda = Pattern phổ biến trong C#       │
└─────────────────────────────────────────────────────────────┘

Giải thích chi tiết

1. Lambda Expression thực chất là Delegate

Khi bạn viết một lambda expression, compiler sẽ chuyển nó thành delegate:

// Lambda expression
Func<int, int> square = x => x * x;

// Compiler tạo ra tương đương:
Func<int, int> square = delegate(int x) { return x * x; };

// Hoặc thậm chí là một phương thức private:
// private static int <Main>b__0_0(int x) { return x * x; }

2. Event là “wrapper” bảo vệ cho Delegate

Event không phải là một kiểu riêng biệt - nó là một delegate được bảo vệ:

public class Publisher
{
    // Delegate field (private)
    private EventHandler _clicked;
    
    // Event - chỉ cho phép += và -= từ bên ngoài
    public event EventHandler Clicked
    {
        add { _clicked += value; }
        remove { _clicked -= value; }
    }
    
    // Bên trong class, có thể Invoke
    public void Raise() => _clicked?.Invoke(this, EventArgs.Empty);
}

3. Lambda + Event = Pattern phổ biến

// Lambda expression được dùng để tạo delegate handler
button.Clicked += (sender, e) => Console.WriteLine("Clicked!");
//                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                Lambda expression → EventHandler delegate

Ví dụ tổng hợp

// 1. Định nghĩa delegate (kiểu)
public delegate void MessageHandler(string message);

public class ChatRoom
{
    // 2. Event bao bọc delegate
    public event MessageHandler MessageReceived;
    
    public void SendMessage(string from, string message)
    {
        // 3. Lambda expression tạo delegate instance để invoke
        MessageReceived?.Invoke($"{from}: {message}");
    }
}

// 4. Sử dụng lambda để subscribe event
var chat = new ChatRoom();
chat.MessageReceived += msg => Console.WriteLine(msg);
//                           ^^^^^^^^^^^^^^^^^^^^^^^^^^
//                           Lambda → MessageHandler delegate

chat.SendMessage("Alice", "Hello!");

Khi nào dùng gì?

Tình huốngSử dụng
Truyền method như parameterFunc<>, Action<>, hoặc custom delegate
LINQ queriesLambda expression
Callback không cần exposeLambda → Delegate
Publisher-Subscriber patternEvent
Cần giới hạn truy cập (chỉ +=/-=)Event
Cần Invoke từ bên ngoàiDelegate (không dùng event)

Lưu ý quan trọng

  1. Event không thể Invoke từ bên ngoài class - Đây là sự khác biệt chính so với delegate
  2. Lambda expression không có kiểu riêng - Nó phải được gán cho một delegate type
  3. Event tự động generate add/remove - Tương tự property tự động generate getter/setter

Collections

Arrays

// Single-dimensional array
int[] numbers = new int[5];
numbers[0] = 1;

int[] initialized = { 1, 2, 3, 4, 5 };

// Multi-dimensional array
int[,] matrix = new int[3, 3];
matrix[0, 0] = 1;

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

List

List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");
names.Add("Charlie");

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

// Common operations
bool contains = names.Contains("Alice"); // true
int index = names.IndexOf("Bob"); // 1
names.Remove("Charlie");
names.Sort();

// Capacity management
names.Capacity = 100; // Pre-allocate
names.TrimExcess(); // Reduce capacity

Dictionary<TKey, TValue>

Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Alice"] = 30;
ages["Bob"] = 25;

// Safe access
if (ages.TryGetValue("Alice", out int age))
{
    Console.WriteLine($"Alice is {age} years old");
}

// Initialization
var scores = new Dictionary<string, int>
{
    ["Alice"] = 95,
    ["Bob"] = 87,
    ["Charlie"] = 92
};

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

HashSet

HashSet<int> uniqueNumbers = new HashSet<int>();
uniqueNumbers.Add(1);
uniqueNumbers.Add(2);
uniqueNumbers.Add(1); // Duplicate - ignored

// Set operations
var set1 = new HashSet<int> { 1, 2, 3 };
var set2 = new HashSet<int> { 3, 4, 5 };

set1.UnionWith(set2); // {1, 2, 3, 4, 5}
set1.IntersectWith(set2); // {3}
set1.ExceptWith(set2); // {1, 2}

Queue và Stack

// Queue (FIFO)
Queue<string> queue = new Queue<string>();
queue.Enqueue("First");
queue.Enqueue("Second");
string first = queue.Dequeue(); // "First"

// Stack (LIFO)
Stack<string> stack = new Stack<string>();
stack.Push("First");
stack.Push("Second");
string last = stack.Pop(); // "Second"

Collection Interfaces

InterfaceMô tảVí dụ
IEnumerable<T>Chỉ hỗ trợ iterationLINQ queries
ICollection<T>Thêm Count, Add, Remove, ClearList<T>, HashSet<T>
IList<T>Thêm index-based accessList<T>, arrays
IDictionary<TKey, TValue>Key-value pairsDictionary<TKey, TValue>
IReadOnlyCollection<T>Chỉ đọc với CountIEnumerable<T>.ToList()

Span và Memory - High-Performance Memory Views

Lưu ý: Span<T>Memory<T> không phải là collections theo nghĩa truyền thống. Chúng là các ref struct cung cấp view an toàn, zero-allocation trên bộ nhớ liên tục.

Span là gì?

Span<T> là một stack-only type (ref struct) cho phép truy cập an toàn vào một vùng bộ nhớ liên tục mà không cần allocation trên heap.

// Tạo Span từ array
int[] array = { 1, 2, 3, 4, 5 };
Span<int> span = array.AsSpan();

// Slice - tạo view mà không copy dữ liệu
Span<int> slice = span[1..3]; // { 2, 3 }

// Truy cập phần tử
span[0] = 10; // array[0] cũng thay đổi

// Tạo Span từ stack
Span<int> stackSpan = stackalloc int[3] { 1, 2, 3 };

// Tạo Span từ string (readonly)
ReadOnlySpan<char> text = "Hello World".AsSpan();

Span vs Collections

Đặc điểmSpan<T>Collections (List<T>, T[])
Kiểuref struct (stack-only)Reference type hoặc array
AllocationZero heap allocationHeap allocation
Lưu trữKhông thể là field của classCó thể là field
AsyncKhông hỗ trợ (ref struct limitation)Hỗ trợ đầy đủ
IEnumerableKhông implementCó implement
PerformanceCao nhấtThấp hơn do allocation
Use caseParsing, slicing, buffer manipulationGeneral purpose storage

Memory - Phiên bản linh hoạt hơn

Memory<T> không phải ref struct nên có thể lưu trữ trong class và sử dụng với async:

public class DataProcessor
{
    // Memory<T> có thể là field của class
    private Memory<byte> _buffer;
    
    public DataProcessor(Memory<byte> buffer)
    {
        _buffer = buffer;
    }
    
    // Có thể dùng với async
    public async Task ProcessAsync()
    {
        // Span không thể dùng trong async method
        Span<byte> span = _buffer.Span;
        // ... process
    }
}

// Sử dụng
var memory = new Memory<byte>(new byte[1024]);
var span = memory.Span; // Lấy Span từ Memory

Khi nào sử dụng Span và Memory?

Tình huốngKhuyến nghị
Parsing string/byte bufferSpan<T>
Slicing array mà không copySpan<T>
High-performance buffer manipulationSpan<T>
Cần lưu trữ trong classMemory<T>
Cần sử dụng với async/awaitMemory<T>
General purpose data storageList<T>, T[]

Ví dụ thực tế: Parsing với Span

// Không allocation khi parsing
int ParseNumbers(ReadOnlySpan<char> input)
{
    int sum = 0;
    while (!input.IsEmpty)
    {
        int commaIndex = input.IndexOf(',');
        if (commaIndex == -1)
        {
            sum += int.Parse(input);
            break;
        }
        
        sum += int.Parse(input[..commaIndex]);
        input = input[(commaIndex + 1)..];
    }
    return sum;
}

// Sử dụng
var csv = "1,2,3,4,5";
int total = ParseNumbers(csv.AsSpan()); // total = 15

Tại sao Span không phải là Collection?

  1. Không implement IEnumerable<T> - Không thể dùng foreach trực tiếp (phải dùng foreach(ref var item in span))
  2. Không implement ICollection<T> - Không có Add, Remove, Clear
  3. Là view, không phải storage - Span<T> không sở hữu dữ liệu, nó chỉ “nhìn” vào vùng bộ nhớ có sẵn
  4. Stack-only - Không thể boxing, không thể lưu trong heap

Tóm lại: Span<T>Memory<T>memory views, không phải collections. Chúng bổ sung cho collections bằng cách cung cấp hiệu suất cao cho các thao tác trên bộ nhớ liên tục.

Xử lý chuỗi

String vs StringBuilder

// String - immutable
string s1 = "Hello";
string s2 = s1 + " World"; // Tạo string mới

// StringBuilder - mutable (hiệu quả cho nhiều thao tác)
StringBuilder sb = new StringBuilder();
sb.Append("Hello");
sb.Append(" ");
sb.Append("World");
string result = sb.ToString(); // "Hello World"

// Khi nào dùng StringBuilder?
// - Nhiều string concatenations trong loop
// - Xây dựng string động từ nhiều phần
// - Hiệu suất quan trọng

String Interpolation

string name = "Alice";
int age = 30;

// String interpolation (C# 6+)
string message = $"Hello, {name}. You are {age} years old.";

// Format specifiers
decimal price = 19.99m;
string formatted = $"Price: {price:C}"; // "Price: $19.99"
string percent = $"Discount: {0.15:P}"; // "Discount: 15.00%"

// Expression trong interpolation
string status = $"User is {(age >= 18 ? "adult" : "minor")}";

String Methods

string text = "  Hello World  ";

// Common operations
string trimmed = text.Trim(); // "Hello World"
string upper = text.ToUpper(); // "  HELLO WORLD  "
string lower = text.ToLower(); // "  hello world  "

// Searching
bool contains = text.Contains("Hello"); // true
int index = text.IndexOf("World"); // 9
bool startsWith = text.StartsWith("  Hello"); // true
bool endsWith = text.EndsWith("World  "); // true

// Splitting và joining
string[] parts = "a,b,c".Split(',');
string joined = string.Join("-", parts); // "a-b-c"

// Formatting
string formatted = string.Format("{0} is {1} years old", name, age);

Async/Await & Collections Nâng cao

Async/Await

Cơ chế hoạt động

async/await là cú pháp cho phép viết code bất đồng bộ một cách đồng bộ (synchronous-looking).

public async Task<string> FetchDataAsync()
{
    using var client = new HttpClient();
    var result = await client.GetStringAsync("https://api.example.com/data");
    return result;
}

Luồng xử lý

  1. Khi gọi method async, thread hiện tại không bị block
  2. Task được tạo và chạy trên Thread Pool
  3. Khi async operation hoàn thành, continuation được schedule lại
  4. Kết quả được trả về cho caller

Lưu ý quan trọng

  • async method luôn trả về Task, Task<T>, hoặc void (chỉ dùng cho event handlers)
  • Không nên dùng .Result hoặc .Wait() vì sẽ gây deadlock
  • Sử dụng ConfigureAwait(false) để tránh context capture

IEnumerable vs IAsyncEnumerable

IEnumerable

public IEnumerable<Product> GetProducts()
{
    return _context.Products; // Deferred execution
}
  • Synchronous - load all data vào memory
  • Deferred Execution - không thực thi cho đến khi được enumerate
  • Phù hợp cho tập dữ liệu nhỏ

IAsyncEnumerable (.NET Core 2.1+)

public async IAsyncEnumerable<Product> GetProductsAsync()
{
    await foreach (var product in _context.Products.AsAsyncEnumerable())
    {
        yield return product;
    }
}
  • Asynchronous - stream data từ database
  • Non-blocking - không block thread
  • Phù hợp cho tập dữ liệu lớn hoặc real-time streaming

So sánh

FeatureIEnumerableIAsyncEnumerable
ExecutionSynchronousAsynchronous
MemoryLoad allStream
PerformanceChậm với large dataTốt với large data
Use caseSmall datasetsLarge datasets, real-time

LINQ

Deferred Execution

var query = products.Where(p => p.Price > 100); // Chưa execute
var result = query.ToList(); // Execute tại đây
  • Query chỉ được thực thi khi:
    • Gọi .ToList(), .ToArray(), .Count(), .First(), etc.
    • Sử dụng foreach

IEnumerable vs IQueryable

// IEnumerable - Thực thi trong memory
IEnumerable<Product> products = _context.Products.ToList();
var filtered = products.Where(p => p.Price > 100); // Filter in memory

// IQueryable - Thực thi trên database
IQueryable<Product> query = _context.Products;
var filteredQuery = query.Where(p => p.Price > 100); // Filter in SQL
FeatureIEnumerableIQueryable
ExecutionIn-memoryDatabase/Provider
Deferred Execution✅ Yes✅ Yes
Query CompositionLINQ to ObjectsLINQ to Entities/SQL
PerformanceLoad all data firstFilter at database
Use caseSmall datasets, in-memoryDatabase queries, large datasets

Khi nào dùng IQueryable?

// ✅ Tốt - Filter ở database
var expensiveProducts = _context.Products
    .Where(p => p.Price > 100)
    .ToList(); // SQL: SELECT * FROM Products WHERE Price > 100

// ❌ Không tốt - Load all rồi filter
var allProducts = _context.Products.ToList(); // SELECT * FROM Products
var expensive = allProducts.Where(p => p.Price > 100); // Filter in memory

Tối ưu LINQ trên tập dữ liệu lớn

  1. Sử dụng IQueryable thay vì IEnumerable cho database queries

    public IQueryable<Product> GetProducts() => _context.Products;
    
  2. Avoid multiple enumerations

    // Bad - Multiple enumerations (deferred execution)
    var query = products.Where(x => x.Active); // IEnumerable, chưa execute
    var count = query.Count();  // First enumeration - executes query
    var list = query.ToList();  // Second enumeration - executes query AGAIN!
    
    // Good - Materialize once
    var list = products.Where(x => x.Active).ToList(); // Materialize once
    var count = list.Count; // Use cached list, no re-enumeration
    
  3. Use pagination

    Offset-based pagination (phù hợp cho admin dashboard, báo cáo):

    // ✅ Offset-based - đơn giản, dễ implement
    var page = products
        .OrderBy(p => p.Id)
        .Skip((pageNumber - 1) * pageSize)
        .Take(pageSize)
        .ToList();
    

    Cursor-based pagination (phù hợp cho infinite scroll, mobile app, real-time feed):

    // ✅ Cursor-based - hiệu suất cao, không bị "drift" khi data thay đổi
    // Client gửi cursor = Id của item cuối cùng đã load
    var page = products
        .Where(p => p.Id < lastSeenId) // hoặc > nếu sort ascending
        .OrderByDescending(p => p.Id)
        .Take(pageSize)
        .ToList();
    
    // Trả về cursor cho client (Id của item cuối cùng)
    var nextCursor = page.LastOrDefault()?.Id;
    
    LoạiKhi nào dùngƯu điểmNhược điểm
    Offset-basedAdmin dashboard, báo cáo, cần nhảy đến page cụ thểĐơn giản, dễ implement, hỗ trợ “jump to page N”Chậm với large offset, bị “drift” khi data thay đổi
    Cursor-basedInfinite scroll, mobile app, real-time feed (Facebook, Twitter)Hiệu suất cao (dùng index), không bị driftKhông nhảy page được, chỉ “next/prev”
  4. Select only needed columns

    var names = products.Select(p => p.Name).ToList(); // Not full entity
    
  5. Eager loading vs Lazy loading

    // Lazy loading (nhiều queries)
    var products = context.Products.ToList();
    foreach (var p in products)
    {
        var category = p.Category; // Additional query mỗi lần
    }
    
    // Eager loading (1 query với join)
    var products = context.Products
        .Include(p => p.Category)
        .ToList();
    

Exception Handling & IDisposable

Try-Catch-Finally

Cấu trúc cơ bản

try
{
    // Code có thể throw exception
    var result = PerformOperation();
}
catch (ArgumentException ex)
{
    // Handle specific exception
    Log.Error(ex, "Invalid argument");
    throw; // Re-throw với original stack trace
}
catch (Exception ex)
{
    // Handle general exception
    throw new CustomException("Error occurred", ex); // Wrap exception
}
finally
{
    // Cleanup - luôn chạy dù có exception hay không
    Cleanup();
}

Finally block luôn được thực thi

try
{
    Console.WriteLine("In try block");
    return;
}
finally
{
    Console.WriteLine("In finally block"); // Vẫn được thực thi!
}
// Output:
// In try block
// In finally block

Multiple Catch Blocks

try
{
    // Risky operation
}
catch (FileNotFoundException ex)
{
    // Handle file not found
    Console.WriteLine($"File missing: {ex.FileName}");
}
catch (IOException ex)
{
    // Handle IO errors
    Console.WriteLine($"IO error: {ex.Message}");
}
catch (Exception ex)
{
    // Handle all other exceptions
    Console.WriteLine($"Unexpected error: {ex.Message}");
}

Exception Filters (C# 6+)

try
{
    // Risky operation
}
catch (Exception ex) when (ex is IOException)
{
    // Handle IO exceptions
}
catch (Exception ex) when (ex.Message.Contains("timeout"))
{
    // Handle timeout specifically
}

Anti-patterns cần tránh

// ❌ Bad - Swallow exception
catch (Exception ex)
{
    // Do nothing
}

// ✅ Good - Always log or handle
catch (Exception ex)
{
    Log.Error(ex);
    throw;
}

// ❌ Bad - Lose stack trace
catch (Exception ex)
{
    throw ex; // Resets stack trace
}

// ✅ Good - Preserve stack trace
catch (Exception ex)
{
    throw; // Preserves stack trace
}

Tại sao throw ex reset stack trace?

Khi sử dụng throw ex, CLR (Common Language Runtime) coi đây là một exception mới được ném từ vị trí hiện tại, thay vì tiếp tục truyền exception gốc lên trên. Điều này dẫn đến việc stack trace bị reset và chỉ bắt đầu từ vị trí throw ex.

Cơ chế hoạt động

void MethodA()
{
    MethodB();
}

void MethodB()
{
    MethodC();
}

void MethodC()
{
    throw new InvalidOperationException("Error in C");
}

try
{
    MethodA();
}
catch (Exception ex)
{
    // ❌ throw ex - Stack trace bị reset
    throw ex;
    
    // Stack trace chỉ còn:
    // at Program.Main() in Program.cs:line XX
    // (Mất thông tin MethodA -> MethodB -> MethodC)
}
try
{
    MethodA();
}
catch (Exception ex)
{
    // ✅ throw - Stack trace được giữ nguyên
    throw;
    
    // Stack trace đầy đủ:
    // at Program.MethodC() in Program.cs:line XX
    // at Program.MethodB() in Program.cs:line XX
    // at Program.MethodA() in Program.cs:line XX
    // at Program.Main() in Program.cs:line XX
}

Giải thích chi tiết

Aspectthrow exthrow
Stack traceBị reset từ vị trí throwGiữ nguyên stack trace gốc
Exception objectTạo mới (về mặt stack trace)Giữ nguyên object gốc
DebuggingKhó tìm root causeDễ dàng trace ngược lại
IL Codethrow instructionrethrow instruction

Khi nào dùng throw ex?

Thực tế, hầu như không nên dùng throw ex. Tuy nhiên, có một số trường hợp đặc biệt:

// Trường hợp duy nhất chấp nhận được:
// Khi bạn muốn wrap exception vào một exception khác
catch (Exception ex)
{
    throw new CustomException("Context-specific message", ex);
    // InnerException giữ nguyên stack trace gốc
}

ExceptionDispatchInfo - Giữ stack trace khi re-throw

Nếu bạn cần re-throw exception ở một vị trí khác (ví dụ: trong async scenarios), sử dụng ExceptionDispatchInfo:

using System.Runtime.ExceptionServices;

ExceptionDispatchInfo capturedException = null;

try
{
    // Some operation
}
catch (Exception ex)
{
    capturedException = ExceptionDispatchInfo.Capture(ex);
}

// Later, possibly in a different location
capturedException?.Throw();
// Stack trace vẫn được giữ nguyên!

Rule of thumb: Luôn sử dụng throw; thay vì throw ex; để giữ nguyên stack trace gốc, giúp việc debugging dễ dàng hơn.


IDisposable Interface

Interface Definition

public interface IDisposable
{
    void Dispose();
}

Implementing IDisposable

public class ResourceHolder : IDisposable
{
    private bool _disposed = false;
    private FileStream _fileStream;
    
    public ResourceHolder(string path)
    {
        _fileStream = new FileStream(path, FileMode.Open);
    }
    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                // Dispose managed resources
                _fileStream?.Dispose();
            }
            
            // Free unmanaged resources
            // (if any)
            
            _disposed = true;
        }
    }
    
    ~ResourceHolder()
    {
        Dispose(false);
    }
}

Using Statement

// Traditional using statement
using (var stream = new FileStream("file.txt", FileMode.Open))
{
    // Use stream
} // Dispose() called automatically

// C# 8+ using declaration
using var stream = new FileStream("file.txt", FileMode.Open);
// Use stream
// Dispose() called at end of scope

Tương tác giữa Try-Catch-Finally và IDisposable

Using vs Try-Finally

// Using statement (recommended)
using var resource = new ResourceHolder("file.txt");
resource.DoWork();
// Dispose() called automatically, even if exception occurs

// Equivalent try-finally
ResourceHolder resource = null;
try
{
    resource = new ResourceHolder("file.txt");
    resource.DoWork();
}
finally
{
    resource?.Dispose();
}

Multiple Resources với Using

// Multiple using statements
using var file1 = new FileStream("file1.txt", FileMode.Open);
using var file2 = new FileStream("file2.txt", FileMode.Open);
using var reader = new StreamReader(file1);
using var writer = new StreamWriter(file2);

// All resources disposed in reverse order

Exception trong Dispose

public class SafeResource : IDisposable
{
    private bool _disposed = false;
    
    public void Dispose()
    {
        try
        {
            Dispose(true);
        }
        catch
        {
            // Log but don't throw - Dispose should not throw
            Log.Error("Error during dispose");
        }
        finally
        {
            GC.SuppressFinalize(this);
            _disposed = true;
        }
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Cleanup managed resources
        }
    }
}

Best Practices

  1. Luôn sử dụng using cho IDisposable resources

    // ✅ Good
    using var connection = new SqlConnection(connectionString);
    connection.Open();
    
    // ❌ Bad
    var connection = new SqlConnection(connectionString);
    connection.Open();
    // Forgot to dispose!
    
  2. Không throw exception từ Dispose

    public void Dispose()
    {
        try
        {
            Cleanup();
        }
        catch (Exception ex)
        {
            Log.Error(ex); // Log, don't throw
        }
    }
    
  3. Implement Dispose pattern đúng cách

    public class MyResource : IDisposable
    {
        private bool _disposed = false;
        
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    // Dispose managed resources
                }
                // Free unmanaged resources
                _disposed = true;
            }
        }
    }
    
  4. Kết hợp try-catch với using

    try
    {
        using var resource = new ResourceHolder("file.txt");
        resource.DoWork();
    }
    catch (IOException ex)
    {
        Log.Error(ex, "IO error during operation");
        throw;
    }
    // resource.Dispose() called automatically
    

Khi nào implement IDisposable?

  • Khi class sử dụng unmanaged resources (file handles, database connections, etc.)
  • Khi class chứa các IDisposable members
  • Khi cần deterministic cleanup

Khi nào KHÔNG cần IDisposable?

  • Chỉ sử dụng managed resources
  • Không cần cleanup đặc biệt
  • Object lifetime ngắn và GC có thể handle

Pattern Matching, Records & Reflection

Pattern Matching

C# 9+ Pattern Matching

// Type pattern
if (obj is Product product)
{
    Console.WriteLine(product.Name);
}

// Switch expression
var message = obj switch
{
    null => "Empty",
    int i when i > 0 => "Positive",
    int i => "Negative",
    string s => $"String: {s}",
    _ => "Unknown"
};

// Relational patterns (C# 9)
var category = score switch
{
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    _ => "F"
};

Records (C# 9+)

Value Equality

public record Product(string Name, decimal Price);

// So sánh bằng giá trị, không phải reference
var p1 = new Product("Apple", 1.99m);
var p2 = new Product("Apple", 1.99m);

Console.WriteLine(p1 == p2); // True!

With Expression

var p1 = new Product("Apple", 1.99m);
var p2 = p1 with { Price = 2.99m }; // Tạo bản sao với Price mới

// Non-destructive mutation

Positional Records

public record Product(string Name, decimal Price);

// Constructor và Deconstruct tự động
var product = new Product("Apple", 1.99m);
var (name, price) = product;

Attributes & Reflection

Attributes (Thuộc tính)

Attributes cung cấp metadata cho code elements (classes, methods, properties, etc.).

// Built-in attributes
[Serializable]
public class Product { }

[Obsolete("Use NewMethod instead")]
public void OldMethod() { }

[Conditional("DEBUG")]
public void DebugLog(string message) { }

// Custom attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
    public string Name { get; }
    public string Version { get; set; }
    
    public AuthorAttribute(string name)
    {
        Name = name;
    }
}

// Sử dụng custom attribute
[Author("John Doe", Version = "1.0")]
public class Calculator
{
    [Author("Jane Smith")]
    public int Add(int a, int b) => a + b;
}

Common Built-in Attributes

AttributeMục đíchVí dụ
[Serializable]Cho phép object serialization[Serializable] class Data
[Obsolete]Đánh dấu method/class deprecated[Obsolete("Use v2")]
[Conditional]Chỉ compile khi symbol defined[Conditional("DEBUG")]
[DllImport]Import native DLL function[DllImport("user32.dll")]
[Required]Data validation (ASP.NET Core)[Required] string Name
[Range]Value range validation[Range(1, 100)] int Age

Reflection

Reflection cho phép inspect và thao tác với types tại runtime.

using System.Reflection;

// Lấy Type information
Type type = typeof(Product);
Console.WriteLine($"Type: {type.Name}");
Console.WriteLine($"Namespace: {type.Namespace}");
Console.WriteLine($"Is Class: {type.IsClass}");

// Lấy properties
PropertyInfo[] properties = type.GetProperties();
foreach (var prop in properties)
{
    Console.WriteLine($"Property: {prop.Name} ({prop.PropertyType.Name})");
}

// Lấy methods
MethodInfo[] methods = type.GetMethods();
foreach (var method in methods)
{
    Console.WriteLine($"Method: {method.Name}");
}

// Kiểm tra attributes
var attributes = type.GetCustomAttributes(typeof(AuthorAttribute), false);
foreach (AuthorAttribute attr in attributes)
{
    Console.WriteLine($"Author: {attr.Name}, Version: {attr.Version}");
}

Dynamic Type Creation & Invocation

// Tạo instance dynamically
Type calculatorType = typeof(Calculator);
object calculator = Activator.CreateInstance(calculatorType);

// Gọi method dynamically
MethodInfo addMethod = calculatorType.GetMethod("Add");
object result = addMethod.Invoke(calculator, new object[] { 5, 3 });
Console.WriteLine($"Result: {result}"); // 8

// Get/Set property values
PropertyInfo nameProperty = calculatorType.GetProperty("Name");
nameProperty.SetValue(calculator, "MyCalculator");
string name = (string)nameProperty.GetValue(calculator);

Performance Considerations

// ❌ Chậm - Reflection mỗi lần
for (int i = 0; i < 1000; i++)
{
    MethodInfo method = obj.GetType().GetMethod("Process");
    method.Invoke(obj, null);
}

// ✅ Tốt hơn - Cache MethodInfo
MethodInfo cachedMethod = obj.GetType().GetMethod("Process");
for (int i = 0; i < 1000; i++)
{
    cachedMethod.Invoke(obj, null);
}

// ✅ Tốt nhất - Delegate (Expression Trees)
var method = obj.GetType().GetMethod("Process");
var delegate = (Action)Delegate.CreateDelegate(typeof(Action), obj, method);
for (int i = 0; i < 1000; i++)
{
    delegate();
}

Use Cases cho Reflection

  1. Dependency Injection Frameworks - Tìm và register services
  2. ORM Frameworks - Map database columns to properties
  3. Serialization/Deserialization - Inspect object structure
  4. Testing Frameworks - Tìm và chạy test methods
  5. Plugin Systems - Load và instantiate plugins dynamically

C# 12 Features

Primary Constructors (C# 12)

public class Point(int X, int Y)
{
    public int Sum() => X + Y;
}

var point = new Point(3, 4);
Console.WriteLine(point.Sum()); // 7

Collection Expressions

// Array
int[] numbers = [1, 2, 3, 4, 5];

// Span
Span<int> span = [1, 2, 3];

// List
List<string> names = ["Alice", "Bob"];

Default Lambda Parameters

Func<int, int, int> add = (int a, int b = 10) => a + b;
Console.WriteLine(add(5)); // 15

Alias Any Type

using IntList = List<int>;
using Point3D = (int x, int y, int z);

CLR & Bộ nhớ

Garbage Collection (GC)

GC hoạt động như thế nào?

Garbage Collection là quản lý bộ nhớ tự động trong .NET, hoạt động theo cơ chế:

// Khi không còn reference, object sẽ được GC thu hồi
public void CreateObject()
{
    var obj = new MyClass(); // Allocate trên Heap
    // Khi method kết thúc, obj không còn reference
    // GC sẽ thu hồi bộ nhớ
}

GC Cycle

  1. Allocation: Khi object mới được tạo, nó được allocate trên Heap
  2. Mark Phase: GC đánh dấu tất cả objects có reference (root objects)
  3. Sweep Phase: Xóa các objects không có reference
  4. Compact Phase: Di chuyển các objects còn lại để giảm fragmentation

Generations

┌─────────────────────────────────────────────────────────────┐
│                        LARGE OBJECT HEAP                    │
│                    (>85KB objects)                          │
├─────────────────────────────────────────────────────────────┤
│  Generation 2 (Long-lived)    │ Gen 1 (Intermediate)       │
│  ┌─────────────────────────┐   │ ┌───────────────────────┐  │
│  │ Static variables        │   │ │ Objects that survived│  │
│  │ Singleton services      │   │ │ Gen 1 collection     │  │
│  └─────────────────────────┘   │ └───────────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│  Generation 0 (Short-lived)                                │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ Local variables, temporary objects                    │  │
│  │ Created and destroyed frequently                      │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
GenerationMô tảTần suất GC
Gen 0Short-lived objects (local variables)Thường xuyên nhất
Gen 1Intermediate objectsÍt hơn Gen 0
Gen 2Long-lived objects (static, singletons)Ít nhất

Tối ưu để giảm áp lực lên GC

1. Sử dụng StringBuilder thay vì String

// ❌ Tạo nhiều string objects
string result = "";
for (int i = 0; i < 1000; i++)
{
    result += i.ToString(); // Mỗi lần tạo new string
}

// ✅ Sử dụng StringBuilder
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
    sb.Append(i);
}
string result = sb.ToString();

2. Sử dụng struct khi phù hợp

// Value type - lưu trên Stack
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

// Reference type - lưu trên Heap
public class PointClass
{
    public int X { get; set; }
    public int int Y { get; set; }
}

3. Tránh boxing/unboxing

// ❌ Boxing - chuyển value type sang object
int i = 10;
object o = i; // Boxing

// ✅ Tránh boxing
int i = 10;
long l = i; // Không boxing

4. Sử dụng Span và Memory

// Tránh allocation khi xử lý arrays
Span<int> span = stackalloc int[100]; // Stack allocation
span[0] = 42;

// Hoặc slice without allocation
var slice = span.Slice(0, 10);

5. Dispose objects sử dụng using

// ✅ Tự động gọi Dispose
using var file = new StreamReader("file.txt");
var content = file.ReadToEnd();

// ✅ Hoặc using statement truyền thống
using (var file = new StreamReader("file.txt"))
{
    var content = file.ReadToEnd();
}

6. GC.Collect() - Khi nào nên dùng?

// ❌ Không nên gọi thủ công trong production
GC.Collect();

// ✅ Có thể dùng sau khi giải phóng large objects
public void Cleanup()
{
    largeObject = null;
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

Value Types vs Reference Types

So sánh cách lưu trữ

┌─────────────────────────────────────────────────────────────┐
│                      STACK                │      HEAP       │
├────────────────────────────────────────────┼─────────────────┤
│                                            │                 │
│  int x = 10;           │ 10                 │                 │
│  int y = x;            │ 10  (copy)         │                 │
│                                            │                 │
│  var p1 = new Point(); │ 0x1234 (ref)──────►│ Point { x: 0 }  │
│  var p2 = p1;          │ 0x1234 (ref)──────►│ Point { x: 0 }  │
│                                            │                 │
└────────────────────────────────────────────┴─────────────────┘

Khi nào sử dụng struct?

  • Kích thước nhỏ (<16 bytes)
  • Immutable (không thay đổi sau khi tạo)
  • Không cần inheritance
  • Tạo và hủy thường xuyên (trong loops)
// ✅ Nên dùng struct
public readonly struct Point
{
    public double X { get; }
    public double Y { get; }
    
    public Point(double x, double y) => (X, Y) = (x, y);
}

// ❌ Không nên dùng struct
public struct LargeObject
{
    public byte[] Data { get; set; } // Array - vẫn allocate trên Heap!
}

Performance Impact

OperationValue TypeReference Type
AllocationStack (nhanh)Heap (chậm hơn)
CopyCopy toàn bộCopy reference
PassingCopy toàn bộCopy reference (4/8 bytes)
GC PressureKhông

ref, out, in modifiers

ref - Pass by Reference

public void Swap(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}

int x = 5, y = 10;
Swap(ref x, ref y);
// x = 10, y = 5
  • Caller phải khởi tạo biến trước khi pass
  • Có thể đọc và ghi

out - Output Parameter

public bool TryParse(string input, out int result)
{
    if (int.TryParse(input, out result))
    {
        return true;
    }
    result = 0; // Phải gán trước khi return
    return false;
}

if (TryParse("42", out int number))
{
    Console.WriteLine(number);
}
  • Caller KHÔNG cần khởi tạo
  • Phải gán giá trị trước khi method return
  • Thường dùng cho “output” values

in - Read-only Reference

public void Print(in Point point)
{
    // point.X = 10; // ❌ Lỗi - readonly
    Console.WriteLine(point.X); // ✅ Đọc được
}
  • Không thể modify giá trị
  • Tránh copying khi passing large structs
  • Cải thiện performance cho large structs

So sánh

ModifierCaller initializeCan modifyUse case
ref✅ Yes✅ YesIn-place modification
out❌ No✅ YesReturn multiple values
in✅ Yes❌ NoRead-only, performance

2. ASP.NET Core Cốt lõi

Giới thiệu

Phần này trình bày các kiến thức cốt lõi về ASP.NET Core framework.

Nội dung chính

Khởi tạo ứng dụng

Middleware

Dependency Injection

Routing

Filters

Cấu hình & Logging

Xử lý lỗi

Model Binding

Model Validation

ActionResult Types

Static Files & File Handling

Background Tasks

Health Checks

Minimal APIs Advanced

Content Negotiation

Output Caching

Kestrel Configuration

Localization (i18n)

Real-time Communication

gRPC

Khởi tạo Ứng dụng

Program.cs vs Startup.cs

Minimal APIs (.NET 6+)

// Program.cs - Minimal APIs
var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

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

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

app.Run();

Startup.cs (Trước .NET 6)

// Startup.cs - Traditional pattern
public class Startup
{
    public IConfiguration Configuration { get; }
    
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<AppDbContext>();
        services.AddControllers();
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthorization();
        
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

// Program.cs
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
    
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

So sánh

AspectMinimal APIsStartup.cs
Code linesÍt hơnNhiều hơn
ComplexityĐơn giảnPhức tạp hơn
FlexibilityHạn chếLinh hoạt
TestabilityKhó test hơnDễ test hơn
Use caseMicroservices, small APIsLarge applications
DI ConfigurationTrong Program.csTrong ConfigureServices

Các phương thức mở rộng thường dùng

Service Registration

// Singleton - Một instance duy nhất cho toàn bộ lifetime của app
builder.Services.AddSingleton<IService, Service>();

// Scoped - Một instance per request
builder.Services.AddScoped<IService, Service>();

// Transient - Instance mới mỗi lần được yêu cầu
builder.Services.AddTransient<IService, Service>();

Middleware Registration

var app = builder.Build();

// Run - Kết thúc pipeline (không gọi next)
app.Run(async context => 
{
    await context.Response.WriteAsync("Hello");
});

// Use - Có thể gọi next middleware
app.Use(async (context, next) =>
{
    // Do something before
    await next();
    // Do something after
});

// Map - Route-based middleware
app.Map("/api", appBuilder =>
{
    appBuilder.UseRouting();
});

Environment-based Configuration

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

if (app.Environment.IsProduction())
{
    app.UseExceptionHandler("/Error");
}

Middleware

Khái niệm

Middleware là các component nằm trong pipeline xử lý request/response. Mỗi middleware có thể:

  • Xử lý request trước khi chuyển cho middleware tiếp theo
  • Xử lý response sau khi các middleware trước đó đã xử lý
  • Quyết định không chuyển request cho middleware tiếp theo (short-circuit)

Pipeline Execution Order

┌────────────────────────────────────────────────────────────────────┐
│                        REQUEST PIPELINE                           │
├────────────────────────────────────────────────────────────────────┤
│                                                                    │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    │
│  │  Logging │───▶│ Routing  │───▶│ Auth     │───▶│ Endpoint │    │
│  │ Middleware   │ Middleware   │ Middleware   │ (Controller) │    │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘    │
│       ▲                                                    │       │
│       │                                                    ▼       │
│       │                   ┌──────────┐    ┌──────────┐           │
│       └───────────────────│ Response │◀───│ Error    │           │
│                           │ Middleware   │ Middleware   │           │
│                           └──────────┘    └──────────┘           │
│                                                                    │
└────────────────────────────────────────────────────────────────────┘

Thứ tự quan trọng

  1. Exception Handling - Đầu tiên để bắt exceptions
  2. Security (CORS, Authentication, Authorization)
  3. Static Files - Nếu cần
  4. Routing - Xác định endpoint
  5. Endpoints - Controller/Action
  6. Custom Middleware

Cách viết Custom Middleware

1. Conventional Middleware Class

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();
        
        // Before next middleware
        _logger.LogInformation("Request started: {Path}", context.Request.Path);
        
        await _next(context); // Call next middleware
        
        // After next middleware
        stopwatch.Stop();
        _logger.LogInformation(
            "Request completed in {ElapsedMs}ms", 
            stopwatch.ElapsedMilliseconds);
    }
}

// Extension method để dễ đăng ký
public static class RequestTimingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestTiming(
        this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestTimingMiddleware>();
    }
}

// Đăng ký trong Program.cs
var app = builder.Build();
app.UseRequestTiming();

2. Inline Middleware (Minimal API)

var app = builder.Build();

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    
    await next();
    
    stopwatch.Stop();
    Console.WriteLine($"Request took {stopwatch.ElapsedMilliseconds}ms");
});

app.MapGet("/", () => "Hello World");
app.Run();

3. Middleware with Dependencies

public class CustomMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _config;

    public CustomMiddleware(
        RequestDelegate next,
        IConfiguration config)
    {
        _next = next;
        _config = config;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        // Sử dụng injected dependencies
        var timeout = _config.GetValue<int>("App:Timeout");
        
        await _next(context);
    }
}

Middleware vs Filters

Middleware

  • Hoạt động trên toàn bộ request pipeline
  • Thực thi trước khi routing xác định endpoint
  • Phù hợp cho: Logging, Authentication, CORS, Error handling

Filters (MVC/Web API)

  • Chỉ hoạt động cho MVC/Razor Pages actions
  • Có access đến ActionContext
  • Phù hợp cho: Model validation, Result caching, Exception handling cụ thể

Ví dụ so sánh

// Middleware - Cho toàn bộ app
app.Use(async (context, next) =>
{
    // Log mọi request
    await next();
});

// Filter - Chỉ cho MVC actions
public class ActionLogFilter : IActionFilter
{
    public void OnActionExecuting(ActionExecutingContext context)
    {
        // Log trước khi action chạy
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        // Log sau khi action chạy
    }
}

Dependency Injection

3 Loại Lifecycle

Singleton

// Một instance duy nhất cho toàn bộ application lifetime
builder.Services.AddSingleton<IEmailService, EmailService>();

// Ví dụ sử dụng
public class ProductService
{
    private readonly IEmailService _emailService;
    
    public ProductService(IEmailService emailService)
    {
        _emailService = emailService; // Cùng instance xuyên suốt
    }
}

Khi nào dùng:

  • Configuration services
  • Logger (ILogger)
  • Caching services
  • Services không có state hoặc share state toàn cục

Scoped

// Một instance per HTTP request
builder.Services.AddScoped<IProductRepository, ProductRepository>();

// Ví dụ sử dụng
public class OrderService
{
    private readonly IProductRepository _productRepo;
    
    public OrderService(IProductRepository productRepo)
    {
        _productRepo = productRepo; // Cùng instance trong 1 request
    }
}

Khi nào dùng:

  • DbContext (EF Core)
  • Services cần per-request state
  • Business logic services

Transient

// Instance mới mỗi lần được yêu cầu
builder.Services.AddTransient<IReportGenerator, ReportGenerator>();

// Ví dụ sử dụng
public class DashboardService
{
    private readonly IReportGenerator _reportGenerator;
    
    public DashboardService(IReportGenerator reportGenerator)
    {
        _reportGenerator = reportGenerator; // Instance mới mỗi lần
    }
}

Khi nào dùng:

  • Lightweight, stateless services
  • Services với expensive initialization
  • Khi cần instance riêng biệt cho mỗi lần sử dụng

Ví dụ thực tế

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Singleton - Configuration, Logger
builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
builder.Services.AddSingleton<ILogger<Program>, Logger<Program>>();

// Scoped - DbContext, Repositories
builder.Services.AddScoped<AppDbContext>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();

// Transient - Small, stateless services
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.AddTransient<IDateTimeProvider, DateTimeProvider>();

var app = builder.Build();

Lifecycle Diagram

┌─────────────────────────────────────────────────────────────────┐
│                    APPLICATION LIFETIME                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                    SINGLETON                            │   │
│   │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐   │   │
│   │  │ Request │  │ Request │  │ Request │  │ Request │   │   │
│   │  │    1    │  │    2    │  │    3    │  │    4    │   │   │
│   │  └─────────┘  └─────────┘  └─────────┘  └─────────┘   │   │
│   │       │            │            │            │        │   │
│   │       └────────────┴────────────┴────────────┘        │   │
│   │       (Cùng một instance cho tất cả requests)         │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                    SCOPED                               │   │
│   │  ┌─────────┐              ┌─────────┐                  │   │
│   │  │ Request │              │ Request │                  │   │
│   │  │    1    │              │    2    │                  │   │
│   │  │ ┌─────┐ │              │ ┌─────┐ │                  │   │
│   │  │ │ Svc │ │              │ │ Svc │ │                  │   │
│   │  │ └─────┘ │              │ └─────┘ │                  │   │
│   │  └─────────┘              └─────────┘                  │   │
│   │ (Instance mới cho mỗi request, reuse trong request)    │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │                   TRANSIENT                             │   │
│   │  ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐            │   │
│   │  │ S1 │ │ S2 │ │ S3 │ │ S4 │ │ S5 │ │ S6 │            │   │
│   │  └────┘ └────┘ └────┘ └────┘ └────┘ └────┘            │   │
│   │  (Instance mới mỗi lần được request)                   │   │
│   └─────────────────────────────────────────────────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Anti-Pattern: Scoped vào Singleton

Tại sao KHÔNG NÊN?

// ❌ BAD - Injecting Scoped service vào Singleton
builder.Services.AddSingleton<BadSingletonService>();

public class BadSingletonService
{
    private readonly AppDbContext _context;
    
    public BadSingletonService(AppDbContext context) // Scoped!
    {
        _context = context; // Sẽ gây lỗi!
    }
}

Vấn đề: Singleton tồn tại xuyên suốt application lifetime, nhưng Scoped service (DbContext) chỉ valid trong một HTTP request. Khi request kết thúc, DbContext bị disposed nhưng Singleton vẫn giữ reference, gây ObjectDisposedException.

Giải pháp

// ✅ GOOD - Inject IServiceProvider vào Singleton
builder.Services.AddSingleton<GoodSingletonService>();

public class GoodSingletonService
{
    private readonly IServiceProvider _serviceProvider;
    
    public GoodSingletonService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public void DoWork()
    {
        // Tạo scope mới để resolve scoped services
        using var scope = _serviceProvider.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        
        // Sử dụng context trong scope
        var data = context.Products.ToList();
    }
}

Best Practices

  1. Tuân thủ Service Lifetime Hierarchy:

    • ✅ Singleton → Singleton
    • ✅ Singleton → Scoped (thông qua IServiceProvider)
    • ✅ Scoped → Scoped
    • ✅ Scoped → Transient
    • ✅ Transient → Transient
    • ❌ Scoped → Singleton (không có vấn đề về mặt kỹ thuật)
    • ❌ Singleton → Scoped (không nên)
    • ❌ Singleton → Transient (có thể accept)
  2. Register services với interface:

    // ✅ Tốt
    builder.Services.AddScoped<IProductRepository, ProductRepository>();
    
    // ❌ Tránh
    builder.Services.AddScoped<ProductRepository>();
    
  3. Constructor Injection thay vì Property Injection:

    // ✅ Tốt
    public class Service
    {
        private readonly IOtherService _other;
        public Service(IOtherService other) => _other = other;
    }
    
    // ❌ Tránh
    public class Service
    {
        public IOtherService Other { get; set; }
    }
    

Routing

Conventional Routing vs Attribute Routing

Conventional Routing (MVC)

Định nghĩa routes tập trung trong Program.cs hoặc Startup.cs:

// Program.cs
app.UseRouting();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.MapControllerRoute(
    name: "blog",
    pattern: "blog/{*slug}",
    defaults: new { controller = "Blog", action = "Post" });

// Hoặc routes phân cấp
app.MapControllerRoute(
    name: "products",
    pattern: "products/{category}/{action}",
    defaults: new { controller = "Products" });

Controller:

public class ProductsController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
    
    public IActionResult Details(int id)
    {
        return View(id);
    }
}

Attribute Routing (Web API)

Định nghĩa routes trực tiếp trên controller/action:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll()
    {
        return Ok();
    }
    
    [HttpGet("{id:int}")]
    public IActionResult GetById(int id)
    {
        return Ok(id);
    }
    
    [HttpPost]
    public IActionResult Create([FromBody] Product product)
    {
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
    
    [HttpPut("{id:int}")]
    public IActionResult Update(int id, [FromBody] Product product)
    {
        return NoContent();
    }
    
    [HttpDelete("{id:int}")]
    public IActionResult Delete(int id)
    {
        return NoContent();
    }
}

So sánh

AspectConventional RoutingAttribute Routing
DefinitionTập trung trong configTrực tiếp trên method
FlexibilityLinh hoạt với patternsChi tiết, rõ ràng
RESTfulKhó tạo RESTfulDễ dàng
ConventionTheo quy ước đặt tênTùy chỉnh
Use caseMVC với ViewsWeb API

Route Templates

Basic Templates

[HttpGet("/products")]                    // Exact: /products
[HttpGet("products/{id}")]                 // Parameter: /products/1
[HttpGet("products/{id:int}")]             // Typed: /products/1 (chấp nhận số)
[HttpGet("products/{name:alpha}")]         // Constraint: chỉ chữ cái
[HttpGet("products/{*slug}")]              // Catch-all: /products/a/b/c

Route Constraints

ConstraintExampleDescription
int{id:int}Chỉ số nguyên
bool{active:bool}Chỉ true/false
datetime{date:datetime}Giá trị datetime
guid{id:guid}GUID
length{name:length(3,10)}Độ dài cụ thể
range{age:range(18,100)}Khoảng giá trị
regex{code:regex(^\\d{3}$)}Regex pattern

Optional Parameters

[HttpGet("products/{category?}")]
public IActionResult GetByCategory(string? category)
{
    // category có thể null hoặc có giá trị
}

Default Values

[HttpGet("products")]
public IActionResult GetAll([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
    // Default values
}

Route Ordering

Explicit Order với [Route] attribute

[ApiController]
[Route("api/[controller]")]
public class ItemsController : ControllerBase
{
    // Đặt specific routes trước generic routes
    [HttpGet("best-selling")]  // ✅ Matches /api/items/best-selling
    public IActionResult GetBestSelling()
    {
        return Ok();
    }
    
    [HttpGet("{id}")]          // ❌ Sẽ không bao giờ được gọi nếu đặt sau
    public IActionResult GetById(int id)
    {
        return Ok(id);
    }
}

Route Precedence

ASP.NET Core ưu tiên:

  1. Static segments (e.g., api/products)
  2. Route parameters có constraints
  3. Route parameters không constraints
  4. Catch-all parameters

Filters

Các loại Filter

Filters cho phép chạy code tại các điểm cụ thể trong pipeline execution của MVC.

┌─────────────────────────────────────────────────────────────────────┐
│                    MVC REQUEST PIPELINE                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. Authorization Filter                                             │
│     ↓                                                               │
│  2. Resource Filter (OnExecuting)                                   │
│     ↓                                                               │
│  3. Model Binding                                                   │
│     ↓                                                               │
│  4. Action Filter (OnExecuting)                                     │
│     ↓                                                               │
│  5. Action executes                                                 │
│     ↓                                                               │
│  6. Action Filter (OnExecuted)                                      │
│     ↓                                                               │
│  7. Result Filter (OnExecuting)                                     │
│     ↓                                                               │
│  8. Result executes                                                 │
│     ↓                                                               │
│  9. Result Filter (OnExecuted)                                      │
│     ↓                                                               │
│ 10. Resource Filter (OnExecuted)                                   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

1. Authorization Filter

public class CustomAuthorizationFilter : IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var user = context.HttpContext.User;
        
        if (!user.Identity.IsAuthenticated)
        {
            context.Result = new UnauthorizedResult();
        }
    }
}

// Sử dụng
[ServiceFilter(typeof(CustomAuthorizationFilter))]
public class ProductsController : ControllerBase { }

2. Resource Filter

public class TrackRequestFilter : IAsyncResourceFilter
{
    private readonly ILogger<TrackRequestFilter> _logger;

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

    public async Task OnResourceExecutingAsync(ResourceExecutingContext context)
    {
        _logger.LogInformation("Resource executing");
        // Thực thi trước khi resource (controller) được gọi
        context.HttpContext.Items["StartTime"] = DateTime.UtcNow;
    }

    public async Task OnResourceExecutedAsync(ResourceExecutedContext context)
    {
        var startTime = (DateTime)context.HttpContext.Items["StartTime"];
        _logger.LogInformation($"Resource executed in {(DateTime.UtcNow - startTime).TotalMilliseconds}ms");
    }
}

3. 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("Action executing: {Controller}.{Action}", 
            context.Controller.GetType().Name,
            context.ActionDescriptor.Name);
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
        _logger.LogInformation("Action executed");
    }
}

4. Exception Filter

public class GlobalExceptionFilter : IExceptionFilter
{
    private readonly ILogger<GlobalExceptionFilter> _logger;

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

    public void OnException(ExceptionContext context)
    {
        _logger.LogError(context.Exception, "Unhandled exception");

        var result = new
        {
            error = "An error occurred",
            message = context.Exception.Message,
            traceId = context.HttpContext.TraceIdentifier
        };

        context.Result = new JsonResult(result)
        {
            StatusCode = 500
        };
        
        context.ExceptionHandled = true;
    }
}

5. Result Filter

public class CacheResultFilter : IResultFilter
{
    public void OnResultExecuting(ResultExecutingContext context)
    {
        if (!context.HttpContext.Response.HasStarted)
        {
            context.HttpContext.Response.Headers["Cache-Control"] = "no-cache";
        }
    }

    public void OnResultExecuted(ResultExecutedContext context)
    {
        // Post-processing
    }
}

Filter Types Comparison

Filter TypeImplementsRunsUse Case
AuthorizationIAuthorizationFilterĐầu tiênCheck authentication/authorization
ResourceIResourceFilterTrước & sau model bindingCaching, performance tracking
ActionIActionFilterTrước & sau actionLogging, validation
ExceptionIExceptionFilterKhi exception xảy raError handling
ResultIResultFilterTrước & sau result executionOutput formatting, caching

Filters vs Middleware

Filters

  • ✅ Chỉ áp dụng cho MVC/Razor Pages
  • ✅ Có access đến ActionContext (model binding results, etc.)
  • ✅ Có thể bind services từ DI
  • ✅ Thực thi sau khi route đã được xác định

Middleware

  • ✅ Áp dụng cho toàn bộ pipeline (bao gồm static files)
  • ✅ Thực thi trước khi routing xác định endpoint
  • ✅ Phù hợp cho cross-cutting concerns không liên quan đến MVC

Khi nào dùng?

// Middleware - Cho toàn bộ app
app.Use(async (context, next) =>
{
    // Log mọi request
    await next();
});

// Filter - Cho MVC actions cụ thể
[ServiceFilter(typeof(MyActionFilter))]
public class ProductsController : ControllerBase { }

Đăng ký Filters

1. Global

builder.Services.AddControllersWithViews()
    .AddMvcOptions(options =>
    {
        options.Filters.Add(new GlobalExceptionFilter());
    });

2. Controller/Action Level

[ControllerLevelFilter]
public class ProductsController : ControllerBase
{
    [ActionLevelFilter]
    public IActionResult Get() { }
}

3. Service Filter

// Đăng ký trong DI
builder.Services.AddScoped<MyFilter>();

// Sử dụng
[ServiceFilter(typeof(MyFilter))]
public IActionResult Get() { }

4. Type Filter

// Không cần đăng ký trong DI
[TypeFilter(typeof(MyFilter))]
public IActionResult Get() { }

Cấu hình & Logging

appsettings.json

Cấu trúc cơ bản

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MyDb;Trusted_Connection=true;"
  },
  "AppSettings": {
    "MaxItemsPerPage": 50,
    "EnableCache": true
  }
}

Environment-specific Configuration

appsettings.Development.json  // Development
appsettings.Staging.json      // Staging  
appsettings.Production.json   // Production
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Tự động load appsettings.{Environment}.json
// Development: appsettings.json + appsettings.Development.json

IConfiguration

Truy cập Configuration

public class Startup
{
    public IConfiguration Configuration { get; }
    
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    
    public void ConfigureServices(IServiceCollection services)
    {
        // Cách 1: GetValue
        var maxItems = Configuration.GetValue<int>("AppSettings:MaxItemsPerPage", 10);
        
        // Cách 2: GetSection
        var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
        
        // Cách 3: Bind to object
        services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
    }
}

Options Pattern (IOptions)

// Model class
public class AppSettings
{
    public int MaxItemsPerPage { get; set; } = 10;
    public bool EnableCache { get; set; }
    public EmailSettings Email { get; set; }
}

public class EmailSettings
{
    public string SmtpHost { get; set; }
    public int SmtpPort { get; set; }
}

// Đăng ký
builder.Services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));

// Sử dụng
public class ProductService
{
    private readonly AppSettings _settings;
    
    public ProductService(IOptions<AppSettings> options)
    {
        _settings = options.Value;
    }
    
    public int GetPageSize() => _settings.MaxItemsPerPage;
}

IOptions vs IOptionsSnapshot vs IOptionsMonitor

// IOptions - Đọc config một lần khi app khởi động
// Singleton services nên dùng cái này

// IOptionsSnapshot - Đọc lại config mỗi request
// Scoped services nên dùng cái này để có config mới nhất
builder.Services.AddScoped<IOptionsSnapshot<AppSettings>>();

// IOptionsMonitor - Theo dõi thay đổi config real-time
// Dùng cho hot-reload configuration
builder.Services.AddSingleton<IOptionsMonitor<AppSettings>>();

Serilog Logging

Cài đặt

dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Console
dotnet add package Serilog.Sinks.File

Cấu hình cơ bản

// Program.cs
using Serilog;

var builder = WebApplication.CreateBuilder(args);

// Configure Serilog
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
    .MinimumLevel.Override("System", LogEventLevel.Warning)
    .Enrich.FromLogContext()
    .Enrich.WithProperty("Application", "MyApp")
    .WriteTo.Console(outputTemplate: 
        "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
    .WriteTo.File(
        "logs/log-.txt",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 30,
        outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
    .CreateLogger();

builder.Host.UseSerilog();

var app = builder.Build();

// Sử dụng
Log.Information("Application starting");

app.Run();

// Flush log trước khi exit
Log.CloseAndFlush();

Structured Logging

public class ProductService
{
    private readonly ILogger<ProductService> _logger;
    
    public ProductService(ILogger<ProductService> logger)
    {
        _logger = logger;
    }
    
    public void GetProduct(int id)
    {
        // ✅ Structured logging - dễ query và filter
        _logger.LogInformation("Fetching product {ProductId} for user {UserId}", 
            id, 
            _userId);
        
        // ❌ String interpolation - tránh dùng
        _logger.LogInformation($"Fetching product {id}");
    }
}

Log Levels

LevelUsage
VerboseDetailed tracing
DebugDebugging information
InformationGeneral information
WarningSomething unexpected happened
ErrorFunctionality issue
FatalCritical error causing shutdown

Best Practices

1. Sử dụng correct log levels

// ✅ Correct
Log.Debug("Processing request {RequestId}", requestId);
Log.Information("Product {ProductId} created successfully", productId);
Log.Warning("Cache miss for key {CacheKey}", key);
Log.Error(ex, "Failed to process order {OrderId}", orderId);
Log.Fatal(ex, "Application terminating due to unhandled exception");

2. Include context

public async Task<IActionResult> UpdateProduct(int id, [FromBody] Product product)
{
    Log.LogInformation(
        "Updating product {ProductId} by user {UserId}", 
        id, 
        User.Identity.Name);
        
    // ... code
}

3. Tránh logging sensitive data

// ❌ Bad - Log sensitive data
Log.Information("User login: {Email}, Password: {Password}", email, password);

// ✅ Good - Log masked data
Log.Information("User login attempt for {Email}", email);

Xử lý lỗi (Error Handling)

Global Error Handling

UseExceptionHandler

// Program.cs
var app = builder.Build();

// Development - hiển thị chi tiết lỗi
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    // Production - redirect đến error page
    app.UseExceptionHandler("/Error");
    
    // Hoặc custom error handler
    app.UseExceptionHandler(errorApp =>
    {
        errorApp.Run(async context =>
        {
            context.Response.StatusCode = 500;
            context.Response.ContentType = "application/json";
            
            var exception = context.Features.Get<IExceptionHandlerFeature>();
            var error = new
            {
                statusCode = 500,
                message = "An unexpected error occurred",
                traceId = context.TraceIdentifier
            };
            
            await context.Response.WriteAsJsonAsync(error);
        });
    });
}

UseStatusCodePages

// Xử lý các status code 4xx, 5xx
app.UseStatusCodePages();

// Hoặc redirect
app.UseStatusCodePagesWithRedirects("/Error/{0}");

// Hoặc re-execute
app.UseStatusCodePagesWithReExecute("/Error", "?statusCode={0}");

ProblemDetails (RFC 7807)

Cấu trúc ProblemDetails

┌─────────────────────────────────────────────────────────────────┐
│                    PROBLEMDETAILS STRUCTURE                      │
├─────────────────────────────────────────────────────────────────┤
│ {                                                               │
│   "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", │
│   "title": "Bad Request",                                       │
│   "status": 400,                                                │
│   "detail": "The input was invalid",                           │
│   "instance": "/api/products",                                  │
│   "traceId": "00-abc123-xyz789-00",                            │
│   "errors": {                                                   │
│     "Name": ["The Name field is required"],                     │
│     "Price": ["Price must be greater than 0"]                   │
│   }                                                             │
│ }                                                               │
└─────────────────────────────────────────────────────────────────┘

Cấu hình ProblemDetails

// Program.cs
builder.Services.AddProblemDetails(options =>
{
    options.CustomizeProblemDetails = context =>
    {
        context.ProblemDetails.Extensions["traceId"] = 
            context.HttpContext.TraceIdentifier;
        context.ProblemDetails.Extensions["requestId"] = 
            context.HttpContext.Request.Headers["X-Request-Id"].FirstOrDefault();
    };
});

// Sử dụng trong controller
[HttpPost]
public IActionResult CreateProduct(Product product)
{
    if (product.Price <= 0)
    {
        return Problem(
            title: "Invalid Price",
            detail: "Price must be greater than 0",
            statusCode: 400,
            type: "https://tools.ietf.org/html/rfc7231#section-6.5.1",
            instance: "/api/products");
    }
    
    return Ok(product);
}

Custom Exception Filter

Global Exception Handler

public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

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

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "Unhandled exception occurred");

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "Server Error",
            Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1",
            Instance = httpContext.Request.Path
        };

        httpContext.Response.StatusCode = problemDetails.Status.Value;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true; // Exception handled
    }
}

// Đăng ký
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

Exception Type Handler

public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}

public class ValidationException : Exception
{
    public IDictionary<string, string[]> Errors { get; }

    public ValidationException(IDictionary<string, string[]> errors)
        : base("Validation failed")
    {
        Errors = errors;
    }
}

// Handler cho từng loại exception
public class NotFoundExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        if (exception is not NotFoundException notFoundEx)
        {
            return false; // Không handle, để handler khác xử lý
        }

        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status404NotFound,
            Title = "Not Found",
            Detail = notFoundEx.Message,
            Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4"
        };

        httpContext.Response.StatusCode = 404;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
        return true;
    }
}

// Đăng ký theo thứ tự ưu tiên
builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

Error Handling trong Minimal APIs

var app = builder.Build();

// Global error handler
app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>();
        var problemDetails = new ProblemDetails
        {
            Status = 500,
            Title = "Internal Server Error",
            Instance = context.Request.Path
        };

        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(problemDetails);
    });
});

// Typed Results với error handling
app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
    try
    {
        db.Products.Add(product);
        await db.SaveChangesAsync();
        return Results.Created($"/api/products/{product.Id}", product);
    }
    catch (DbUpdateException ex)
    {
        return Results.Problem(
            title: "Database Error",
            detail: ex.Message,
            statusCode: 500);
    }
});

Best Practices

1. Không expose sensitive information

// ❌ Bad - expose internal details
return Problem(
    detail: $"SQL Error: {ex.Message} Connection: {connectionString}",
    statusCode: 500);

// ✅ Good - generic message
return Problem(
    detail: "An unexpected error occurred. Please try again later.",
    statusCode: 500);

2. Log errors properly

// ✅ Log với context
_logger.LogError(ex, 
    "Failed to process order {OrderId} for user {UserId}", 
    orderId, userId);

// ✅ Include request details
_logger.LogWarning(
    "Rate limit exceeded for IP {IP} on endpoint {Endpoint}",
    context.Connection.RemoteIpAddress,
    context.Request.Path);

3. Use appropriate status codes

Status CodeWhen to Use
400 Bad RequestInvalid input, validation failed
401 UnauthorizedMissing or invalid authentication
403 ForbiddenAuthenticated but no permission
404 Not FoundResource doesn’t exist
409 ConflictBusiness rule violation, duplicate
422 UnprocessableValid syntax but semantic errors
429 Too Many RequestsRate limit exceeded
500 Internal Server ErrorUnexpected server error
503 Service UnavailableMaintenance or overload

Model Binding

Khái niệm

Model Binding là quá trình ASP.NET Core chuyển đổi dữ liệu từ HTTP request thành các tham số của action method.

┌─────────────────────────────────────────────────────────────────┐
│                    MODEL BINDING SOURCES                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │   Form   │  │   Route  │  │  Query   │  │   Body   │       │
│  │  Values  │  │  Values  │  │  String  │  │  (JSON)  │       │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘       │
│       │             │             │             │              │
│       └─────────────┴─────────────┴─────────────┘              │
│                         │                                      │
│                         ▼                                      │
│              ┌─────────────────────┐                           │
│              │   Model Binder      │                           │
│              │  (Type Conversion)  │                           │
│              └─────────┬───────────┘                           │
│                        │                                       │
│                        ▼                                       │
│              ┌─────────────────────┐                           │
│              │   Action Method     │                           │
│              │   Parameters        │                           │
│              └─────────────────────┘                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Binding Sources

[FromBody]

// JSON request body
[HttpPost]
public IActionResult Create([FromBody] Product product)
{
    // product được deserialize từ JSON body
    return Ok(product);
}

// Request:
// POST /api/products
// Content-Type: application/json
// {"name": "Laptop", "price": 999.99}

[FromQuery]

// Query string parameters
[HttpGet]
public IActionResult GetProducts(
    [FromQuery] int page = 1,
    [FromQuery] int pageSize = 10,
    [FromQuery] string? search = null)
{
    // /api/products?page=2&pageSize=20&search=laptop
    return Ok(new { page, pageSize, search });
}

// Hoặc binding vào object
[HttpGet]
public IActionResult GetProducts([FromQuery] PagingRequest request)
{
    return Ok(request);
}

public class PagingRequest
{
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 10;
    public string? Search { get; set; }
}

[FromRoute]

// Route data
[HttpGet("{id}")]
public IActionResult GetById([FromRoute] int id)
{
    // /api/products/123 -> id = 123
    return Ok(id);
}

// Multiple route parameters
[HttpGet("{category}/{id}")]
public IActionResult GetByCategoryAndId(
    [FromRoute] string category,
    [FromRoute] int id)
{
    // /api/products/electronics/123
    return Ok(new { category, id });
}

[FromHeader]

[HttpGet]
public IActionResult GetWithHeader([FromHeader] string apiKey)
{
    // Lấy từ header: X-API-Key: abc123
    return Ok(apiKey);
}

// Hoặc lấy tất cả headers
[HttpGet]
public IActionResult GetHeaders([FromHeader] IHeaderDictionary headers)
{
    return Ok(headers);
}

[FromForm]

// Form data (multipart/form-data)
[HttpPost]
public IActionResult CreateForm([FromForm] ProductForm form)
{
    return Ok(form);
}

public class ProductForm
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public IFormFile Image { get; set; }
}

Binding Order

ASP.NET Core tìm kiếm giá trị theo thứ tự:

  1. Form values - POST form data
  2. Route values - URL route parameters
  3. Query string - URL query parameters
  4. Request body - JSON/XML (cho complex types)
// Không cần attribute cho complex types (mặc định là [FromBody])
[HttpPost]
public IActionResult Create(Product product) // Tự động [FromBody]
{
    return Ok(product);
}

// Cần attribute cho simple types
[HttpGet]
public IActionResult Get(int id) // Tự động [FromQuery]
{
    return Ok(id);
}

Custom Model Binding

Custom Model Binder

public class CommaSeparatedListBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue;
        
        if (string.IsNullOrEmpty(value))
        {
            bindingContext.Result = ModelBindingResult.Success(new List<string>());
            return Task.CompletedTask;
        }

        var list = value.Split(',', StringSplitOptions.RemoveEmptyEntries)
                       .Select(x => x.Trim())
                       .ToList();
        
        bindingContext.Result = ModelBindingResult.Success(list);
        return Task.CompletedTask;
    }
}

// Sử dụng
[HttpGet]
public IActionResult GetTags([ModelBinder(typeof(CommaSeparatedListBinder))] List<string> tags)
{
    // /api/products?tags=csharp,dotnet,web -> ["csharp", "dotnet", "web"]
    return Ok(tags);
}

Model Binder Provider

public class CommaSeparatedListBinderProvider : IModelBinderProvider
{
    public IModelBinder? GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(List<string>))
        {
            return new BinderTypeModelBinder(typeof(CommaSeparatedListBinder));
        }
        return null;
    }
}

// Đăng ký
builder.Services.AddControllers(options =>
{
    options.ModelBinderProviders.Insert(0, new CommaSeparatedListBinderProvider());
});

Binding Complex Types

Nested Objects

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

public class Customer
{
    public string Name { get; set; }
    public Address Address { get; set; }
    public List<string> Tags { get; set; }
}

// JSON binding
// {
//   "name": "John",
//   "address": {
//     "street": "123 Main St",
//     "city": "NYC",
//     "country": "USA"
//   },
//   "tags": ["vip", "active"]
// }

Collections

[HttpPost]
public IActionResult CreateMany([FromBody] List<Product> products)
{
    // [
    //   {"name": "Product 1", "price": 100},
    //   {"name": "Product 2", "price": 200}
    // ]
    return Ok(products.Count);
}

Binding với Minimal APIs

// Tự động binding
app.MapPost("/api/products", (Product product) => Results.Ok(product));

// Binding từ query string
app.MapGet("/api/products", (int page, int pageSize) => 
    Results.Ok(new { page, pageSize }));

// Binding từ route
app.MapGet("/api/products/{id}", (int id) => Results.Ok(id));

// Custom binding
app.MapGet("/api/products", ([FromQuery] PagingRequest request) => 
    Results.Ok(request));

// Binding từ header
app.MapGet("/api/secure", ([FromHeader(Name = "X-API-Key")] string apiKey) =>
{
    if (apiKey != "secret") return Results.Unauthorized();
    return Results.Ok("Authorized");
});

Best Practices

1. Sử dụng đúng binding source

// ✅ Rõ ràng
public IActionResult Get(
    [FromRoute] int id,
    [FromQuery] string? search,
    [FromBody] Product product)

// ❌ Không rõ ràng
public IActionResult Get(int id, string search, Product product)

2. Sử dụng nullable cho optional parameters

// ✅ Good
public IActionResult Get([FromQuery] string? search = null)

// ❌ Bad - sẽ throw nếu không có query param
public IActionResult Get([FromQuery] string search)

3. Validate binding

[HttpPost]
public IActionResult Create([FromBody] Product product)
{
    if (product == null)
    {
        return BadRequest("Invalid request body");
    }
    // ...
}

Model Validation

Overview Questions

  • Làm thế nào để đảm bảo dữ liệu nhận từ client là hợp lệ?
  • Data Annotations là gì và sử dụng như thế nào?
  • FluentValidation khác Data Annotations ra sao?
  • Làm sao để tạo custom validation attribute?
  • Validation response format chuẩn là gì?
  • [ApiController] tự động validation như thế nào?

Data Annotations Validation

Built-in Validation Attributes

public class Product
{
    public int Id { get; set; }
    
    [Required(ErrorMessage = "Name is required")]
    [StringLength(100, MinimumLength = 2, ErrorMessage = "Name must be 2-100 characters")]
    public string Name { get; set; } = string.Empty;
    
    [Range(0.01, 9999.99, ErrorMessage = "Price must be between 0.01 and 9999.99")]
    public decimal Price { get; set; }
    
    [EmailAddress(ErrorMessage = "Invalid email format")]
    public string? Email { get; set; }
    
    [Url(ErrorMessage = "Invalid URL format")]
    public string? Website { get; set; }
    
    [Phone(ErrorMessage = "Invalid phone number")]
    public string? Phone { get; set; }
    
    [RegularExpression(@"^[A-Z]{3}$", ErrorMessage = "Code must be 3 uppercase letters")]
    public string? Code { get; set; }
    
    [Compare("Password", ErrorMessage = "Passwords do not match")]
    public string? ConfirmPassword { get; set; }
}

Validation trong Controller

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public IActionResult Create(Product product)
    {
        // [ApiController] tự động check ModelState.IsValid
        // Nếu invalid, tự động return 400 Bad Request
        
        // Xử lý khi valid
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
    
    [HttpGet("{id}")]
    public IActionResult GetById(int id)
    {
        return Ok(new { id });
    }
}

Validation Error Response

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": ["Name is required"],
    "Price": ["Price must be between 0.01 and 9999.99"]
  }
}

Custom Validation Attribute

Simple Custom Attribute

public class MinimumAgeAttribute : ValidationAttribute
{
    private readonly int _minimumAge;

    public MinimumAgeAttribute(int minimumAge)
    {
        _minimumAge = minimumAge;
    }

    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        if (value is DateTime birthDate)
        {
            var age = DateTime.Today.Year - birthDate.Year;
            if (birthDate > DateTime.Today.AddYears(-age)) age--;
            
            if (age < _minimumAge)
            {
                return new ValidationResult($"Must be at least {_minimumAge} years old");
            }
        }
        
        return ValidationResult.Success;
    }
}

// Sử dụng
public class UserRegistration
{
    [Required]
    public string Name { get; set; } = string.Empty;
    
    [MinimumAge(18, ErrorMessage = "You must be at least 18 years old")]
    public DateTime DateOfBirth { get; set; }
}

Property Comparison Validation

public class DateRangeAttribute : ValidationAttribute
{
    private readonly string _startDateProperty;
    private readonly string _endDateProperty;

    public DateRangeAttribute(string startDateProperty, string endDateProperty)
    {
        _startDateProperty = startDateProperty;
        _endDateProperty = endDateProperty;
    }

    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        var startDateProperty = validationContext.ObjectType.GetProperty(_startDateProperty);
        var endDateProperty = validationContext.ObjectType.GetProperty(_endDateProperty);

        if (startDateProperty?.GetValue(validationContext.ObjectInstance) is DateTime start &&
            endDateProperty?.GetValue(validationContext.ObjectInstance) is DateTime end)
        {
            if (start >= end)
            {
                return new ValidationResult("Start date must be before end date");
            }
        }

        return ValidationResult.Success;
    }
}

// Sử dụng
[DateRange("StartDate", "EndDate", ErrorMessage = "Invalid date range")]
public class Event
{
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
}

IValidatableObject

Self-Validating Model

public class Order : IValidatableObject
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
    public DateTime? ShipDate { get; set; }
    public List<OrderItem> Items { get; set; } = new();
    public string? CouponCode { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // Cross-property validation
        if (ShipDate.HasValue && ShipDate <= OrderDate)
        {
            yield return new ValidationResult(
                "Ship date must be after order date",
                new[] { nameof(ShipDate) });
        }

        // Collection validation
        if (!Items.Any())
        {
            yield return new ValidationResult(
                "Order must have at least one item",
                new[] { nameof(Items) });
        }

        // Business rule validation
        if (Items.Count > 10 && string.IsNullOrEmpty(CouponCode))
        {
            yield return new ValidationResult(
                "Orders with more than 10 items require a coupon code",
                new[] { nameof(CouponCode) });
        }
    }
}

public class OrderItem
{
    public int ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

FluentValidation

Cài đặt

dotnet add package FluentValidation.AspNetCore

Cấu hình

// Program.cs
builder.Services.AddControllers()
    .AddFluentValidation(fv =>
    {
        fv.RegisterValidatorsFromAssemblyContaining<Program>();
        fv.DisableDataAnnotationsValidation = true; // Optional: disable data annotations
    });

Tạo Validator

public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Name is required")
            .MaximumLength(100).WithMessage("Name cannot exceed 100 characters");
            
        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Price must be greater than 0")
            .LessThan(10000).WithMessage("Price cannot exceed 10,000");
            
        RuleFor(x => x.Email)
            .EmailAddress().WithMessage("Invalid email format")
            .When(x => !string.IsNullOrEmpty(x.Email));
            
        RuleFor(x => x.Category)
            .Must(BeAValidCategory).WithMessage("Invalid category")
            .When(x => !string.IsNullOrEmpty(x.Category));
            
        // Conditional validation
        RuleFor(x => x.DiscountCode)
            .NotEmpty().WithMessage("Discount code is required for orders over $100")
            .When(x => x.Price > 100);
    }
    
    private bool BeAValidCategory(string category)
    {
        var validCategories = new[] { "Electronics", "Books", "Clothing", "Food" };
        return validCategories.Contains(category);
    }
}

Custom Validators với FluentValidation

public static class CustomValidators
{
    public static IRuleBuilderOptions<T, string> MustBeValidPhoneNumber<T>(
        this IRuleBuilder<T, string> ruleBuilder)
    {
        return ruleBuilder.Must(phone => 
            phone != null && phone.All(char.IsDigit) && phone.Length >= 10 && phone.Length <= 15)
            .WithMessage("Phone number must be 10-15 digits");
    }
    
    public static IRuleBuilderOptions<T, string> MustBeStrongPassword<T>(
        this IRuleBuilder<T, string> ruleBuilder)
    {
        return ruleBuilder
            .MinimumLength(8).WithMessage("Password must be at least 8 characters")
            .Matches("[A-Z]").WithMessage("Password must contain uppercase letter")
            .Matches("[a-z]").WithMessage("Password must contain lowercase letter")
            .Matches("[0-9]").WithMessage("Password must contain digit")
            .Matches("[^a-zA-Z0-9]").WithMessage("Password must contain special character");
    }
}

// Sử dụng
public class UserRegistrationValidator : AbstractValidator<UserRegistration>
{
    public UserRegistrationValidator()
    {
        RuleFor(x => x.Phone).MustBeValidPhoneNumber();
        RuleFor(x => x.Password).MustBeStrongPassword();
    }
}

Validation Pipeline

Custom Validation Filter

public class ValidateModelAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.ModelState.IsValid)
        {
            var errors = context.ModelState
                .Where(e => e.Value?.Errors.Count > 0)
                .ToDictionary(
                    kvp => kvp.Key,
                    kvp => kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray()
                );

            var response = new
            {
                type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
                title = "Validation Error",
                status = 400,
                errors = errors
            };

            context.Result = new BadRequestObjectResult(response);
        }
    }
}

// Đăng ký global
builder.Services.AddControllers(options =>
{
    options.Filters.Add<ValidateModelAttribute>();
});

Validation trong Minimal APIs

app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
    var validationContext = new ValidationContext<Product>(product);
    var validator = new ProductValidator();
    var validationResult = await validator.ValidateAsync(validationContext);
    
    if (!validationResult.IsValid)
    {
        var errors = validationResult.Errors
            .GroupBy(e => e.PropertyName)
            .ToDictionary(
                g => g.Key,
                g => g.Select(e => e.ErrorMessage).ToArray()
            );
        
        return Results.ValidationProblem(errors);
    }
    
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Results.Created($"/api/products/{product.Id}", product);
});

Best Practices

1. Kết hợp Data Annotations và FluentValidation

// Data Annotations cho simple validation
public class Product
{
    [Required]
    public string Name { get; set; } = string.Empty;
    
    [Range(0.01, 9999.99)]
    public decimal Price { get; set; }
}

// FluentValidation cho complex validation
public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(x => x.Name)
            .MustAsync(BeUniqueName).WithMessage("Product name must be unique");
            
        RuleFor(x => x.Price)
            .MustAsync(NotExceedBudget).WithMessage("Price exceeds budget limit");
    }
}

2. Validation ở nhiều layers

┌─────────────────────────────────────────────────────────────────┐
│                    VALIDATION LAYERS                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Layer 1: API Level (Data Annotations / FluentValidation)       │
│  - Input format validation                                      │
│  - Required fields                                              │
│  - Range/length constraints                                     │
│                                                                 │
│  Layer 2: Service Level (Business Rules)                        │
│  - Business logic validation                                    │
│  - Cross-entity validation                                      │
│  - Database-dependent validation                                │
│                                                                 │
│  Layer 3: Domain Level (Invariants)                             │
│  - Domain invariants                                            │
│  - Entity consistency                                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3. Fail Fast

// ✅ Validate sớm
public async Task<IActionResult> Create(Product product)
{
    // Validation xảy ra trước khi xử lý business logic
    if (!ModelState.IsValid)
        return BadRequest(ModelState);
    
    // Chỉ chạy khi valid
    await _service.ProcessAsync(product);
    return Ok();
}

ActionResult Types

Overview Questions

  • IActionResultActionResult<T> khác nhau như thế nào?
  • Khi nào nên dùng IActionResult vs ActionResult<T>?
  • Các helper methods nào có sẵn trong ControllerBase?
  • TypedResults trong Minimal APIs là gì?
  • Làm sao để return custom response format?

IActionResult

Interface cơ bản

// IActionResult là interface base cho tất cả action results
public interface IActionResult
{
    Task ExecuteResultAsync(ActionContext context);
}

Common Action Results

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // 200 OK
    [HttpGet]
    public IActionResult GetAll()
    {
        return Ok(products);
        // Equivalent: return new OkObjectResult(products);
    }
    
    // 200 OK với object
    [HttpGet("{id}")]
    public IActionResult GetById(int id)
    {
        return Ok(new { id, name = "Product" });
    }
    
    // 201 Created
    [HttpPost]
    public IActionResult Create(Product product)
    {
        var createdProduct = _service.Create(product);
        return CreatedAtAction(
            nameof(GetById), 
            new { id = createdProduct.Id }, 
            createdProduct);
    }
    
    // 204 No Content
    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        _service.Delete(id);
        return NoContent();
    }
    
    // 400 Bad Request
    [HttpPost("validate")]
    public IActionResult Validate(Product product)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        return Ok();
    }
    
    // 404 Not Found
    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        var product = _service.GetById(id);
        if (product == null)
        {
            return NotFound();
        }
        return Ok(product);
    }
    
    // 401 Unauthorized
    [HttpGet("secret")]
    public IActionResult GetSecret()
    {
        return Unauthorized();
    }
    
    // 403 Forbidden
    [HttpGet("admin")]
    public IActionResult GetAdmin()
    {
        return Forbid();
    }
    
    // 409 Conflict
    [HttpPost("check-name")]
    public IActionResult CheckName(string name)
    {
        if (_service.NameExists(name))
        {
            return Conflict(new { error = "Name already exists" });
        }
        return Ok();
    }
    
    // 422 Unprocessable Entity
    [HttpPost("process")]
    public IActionResult Process(Order order)
    {
        if (!CanProcess(order))
        {
            return UnprocessableEntity(new { error = "Cannot process order" });
        }
        return Ok();
    }
    
    // 500 Internal Server Error
    [HttpGet("error")]
    public IActionResult TriggerError()
    {
        return StatusCode(500, new { error = "Internal server error" });
    }
}

ActionResult

Generic ActionResult

// ActionResult<T> cho phép return cả IActionResult và T
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // Return T - tự động wrap thành OkObjectResult
    [HttpGet]
    public ActionResult<List<Product>> GetAll()
    {
        return _service.GetAll();
    }
    
    // Return IActionResult khi cần status code khác
    [HttpGet("{id}")]
    public ActionResult<Product> GetById(int id)
    {
        var product = _service.GetById(id);
        if (product == null)
        {
            return NotFound(); // IActionResult
        }
        return product; // T
    }
    
    // Mixed returns
    [HttpPost]
    public ActionResult<Product> Create(Product product)
    {
        var created = _service.Create(product);
        return CreatedAtAction(
            nameof(GetById), 
            new { id = created.Id }, 
            created);
    }
}

So sánh IActionResult vs ActionResult

AspectIActionResultActionResult
Return typeChỉ IActionResultCả IActionResult và T
Swagger/OpenAPIKhông rõ response typeTự động generate schema
Type safetyKhông
Use caseKhi cần linh hoạtKhi có response type rõ ràng

Content Results

Different Content Types

[HttpGet("json")]
public IActionResult GetJson()
{
    return Json(new { message = "Hello" });
}

[HttpGet("text")]
public IActionResult GetText()
{
    return Content("Hello World", "text/plain");
}

[HttpGet("html")]
public IActionResult GetHtml()
{
    return Content("<h1>Hello</h1>", "text/html");
}

[HttpGet("xml")]
public IActionResult GetXml()
{
    return Content("<message>Hello</message>", "application/xml");
}

File Results

File Downloads

[HttpGet("download")]
public IActionResult DownloadFile()
{
    var bytes = File.ReadAllBytes("path/to/file.pdf");
    return File(bytes, "application/pdf", "document.pdf");
}

[HttpGet("download-stream")]
public IActionResult DownloadStream()
{
    var stream = new FileStream("path/to/file.pdf", FileMode.Open);
    return File(stream, "application/pdf", "document.pdf");
}

[HttpGet("download-virtual")]
public IActionResult DownloadVirtualFile()
{
    return PhysicalFile(
        @"C:\files\document.pdf", 
        "application/pdf", 
        "document.pdf");
}

Redirect Results

Redirects

[HttpGet("redirect")]
public IActionResult RedirectExample()
{
    // 302 Found
    return Redirect("https://example.com");
    
    // 301 Moved Permanently
    return RedirectPermanent("https://example.com");
    
    // Redirect to action
    return RedirectToAction("GetAll");
    
    // Redirect to route
    return RedirectToRoute("default", new { controller = "Home", action = "Index" });
}

TypedResults (Minimal APIs)

.NET 7+ TypedResults

var app = builder.Build();

// TypedResults cung cấp type-safe results
app.MapGet("/api/products/{id}", async (int id, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    if (product == null)
    {
        return TypedResults.NotFound();
    }
    return TypedResults.Ok(product);
});

app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return TypedResults.Created($"/api/products/{product.Id}", product);
});

app.MapDelete("/api/products/{id}", async (int id, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    if (product == null)
    {
        return TypedResults.NotFound();
    }
    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return TypedResults.NoContent();
});

TypedResults vs Results

// Results - trả về IActionResult
app.MapGet("/old", () => Results.Ok("Hello"));

// TypedResults - trả về specific type (better for OpenAPI)
app.MapGet("/new", () => TypedResults.Ok("Hello"));

// TypedResults tốt hơn cho OpenAPI generation
app.MapGet("/api/products", () => TypedResults.Ok(new List<Product>()))
    .Produces<List<Product>>(200)
    .ProducesProblem(500);

Custom ActionResult

Custom Result Class

public class PagedResult<T> : IActionResult
{
    private readonly IEnumerable<T> _items;
    private readonly int _total;
    private readonly int _page;
    private readonly int _pageSize;

    public PagedResult(IEnumerable<T> items, int total, int page, int pageSize)
    {
        _items = items;
        _total = total;
        _page = page;
        _pageSize = pageSize;
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        var response = context.HttpContext.Response;
        response.ContentType = "application/json";
        
        var result = new
        {
            items = _items,
            total = _total,
            page = _page,
            pageSize = _pageSize,
            totalPages = (int)Math.Ceiling((double)_total / _pageSize)
        };
        
        await JsonSerializer.SerializeAsync(response.Body, result);
    }
}

// Sử dụng
[HttpGet]
public IActionResult GetProducts([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
    var (items, total) = _service.GetPaged(page, pageSize);
    return new PagedResult<Product>(items, total, page, pageSize);
}

Best Practices

1. Sử dụng ActionResult cho API rõ ràng

// ✅ Tốt - rõ ràng response type
[HttpGet]
public ActionResult<List<Product>> GetAll() => _service.GetAll();

// ❌ Tránh - không rõ response type
[HttpGet]
public IActionResult GetAll() => Ok(_service.GetAll());

2. Sử dụng helper methods

// ✅ Tốt
return Ok(data);
return NotFound();
return BadRequest(ModelState);

// ❌ Dài dòng
return new ObjectResult(data) { StatusCode = 200 };
return new ObjectResult(null) { StatusCode = 404 };

3. Consistent error responses

// ✅ Sử dụng ProblemDetails
return Problem(
    title: "Not Found",
    detail: $"Product with id {id} not found",
    statusCode: 404);

// ❌ Inconsistent
return NotFound(new { error = "Not found", message = "Product not found" });

Static Files

Overview Questions

  • ASP.NET Core phục vụ static files như thế nào?
  • wwwroot folder là gì và cấu hình ra sao?
  • Làm sao để enable directory browsing?
  • File providers là gì và khi nào cần custom file provider?
  • Cache headers cho static files được cấu hình ra sao?

Serving Static Files

Default Configuration

// Program.cs
var app = builder.Build();

// Enable static files middleware
app.UseStaticFiles();

// Static files được phục vụ từ wwwroot folder
// http://localhost/images/logo.png -> wwwroot/images/logo.png

Custom Static File Location

// Serve từ folder khác
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
    RequestPath = "/static"
});

// http://localhost/static/logo.png -> MyStaticFiles/logo.png

wwwroot Structure

Project/
├── wwwroot/
│   ├── css/
│   │   └── site.css
│   ├── js/
│   │   └── site.js
│   ├── images/
│   │   └── logo.png
│   └── lib/
│       └── bootstrap/
├── Program.cs
└── appsettings.json

HTML Reference

<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" href="/css/site.css" />
</head>
<body>
    <img src="/images/logo.png" alt="Logo" />
    <script src="/js/site.js"></script>
</body>
</html>

Directory Browsing

Enable Directory Browsing

var app = builder.Build();

// Enable directory browsing
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "wwwroot")),
    RequestPath = "/files"
});

// Hoặc cho toàn bộ static files
app.UseStaticFiles();
app.UseDirectoryBrowser();

Security Warning

⚠️ Chỉ enable directory browsing cho development hoặc khi cần thiết. Production nên disable để tránh expose file structure.


File Providers

Physical File Provider

// Default - sử dụng PhysicalFileProvider
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "wwwroot"))
});

Embedded File Provider

// Serve files từ embedded resources
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new EmbeddedFileProvider(
        typeof(Program).Assembly,
        "MyApp.EmbeddedFiles")
});

Composite File Provider

// Kết hợp nhiều file providers
var compositeProvider = new CompositeFileProvider(
    new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "wwwroot")),
    new EmbeddedFileProvider(typeof(Program).Assembly, "MyApp.EmbeddedFiles")
);

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = compositeProvider
});

Content Types

Default Content Types

// ASP.NET Core tự động detect content type từ file extension
// .css -> text/css
// .js  -> application/javascript
// .png -> image/png
// .html -> text/html

Custom Content Types

var provider = new FileExtensionContentTypeProvider();

// Add custom mapping
provider.Mappings[".webp"] = "image/webp";
provider.Mappings[".custom"] = "application/x-custom";

// Remove mapping
provider.Mappings.Remove(".rtf");

app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = provider
});

Cache Headers

Default Behavior

// Static files middleware tự động thêm Last-Modified header
// Browser sẽ cache và send If-Modified-Since request
// Server trả về 304 Not Modified nếu file không thay đổi

Custom Cache Headers

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        // Cache 1 năm cho versioned files
        const int durationInSeconds = 60 * 60 * 24 * 365;
        ctx.Context.Response.Headers["Cache-Control"] = 
            $"public, max-age={durationInSeconds}";
    }
});

Response Caching Middleware

// Add response caching
app.UseResponseCaching();

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        ctx.Context.Response.Headers["Cache-Control"] = "public, max-age=3600";
        ctx.Context.Response.Headers["Vary"] = "Accept-Encoding";
    }
});

Security Considerations

Block Sensitive Files

// Block access to sensitive file types
app.UseStaticFiles(new StaticFileOptions
{
    ServeUnknownFileTypes = false, // Default: false
    OnPrepareResponse = ctx =>
    {
        var path = ctx.Context.Request.Path.Value;
        if (path.EndsWith(".json") || path.EndsWith(".config"))
        {
            ctx.Context.Response.StatusCode = 403;
        }
    }
});

Best Practices

┌─────────────────────────────────────────────────────────────────┐
│                    STATIC FILE SECURITY                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ✅ DO:                                                         │
│  - Chỉ serve từ wwwroot hoặc folder chỉ định                    │
│  - Disable directory browsing trong production                  │
│  - Set appropriate cache headers                                │
│  - Use CDN for production static files                         │
│                                                                 │
│  ❌ DON'T:                                                      │
│  - Serve từ folders chứa sensitive data                        │
│  - Enable directory browsing không cần thiết                    │
│  - Serve .config, .json, .env files                            │
│  - Forget to validate file uploads                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Default Files

Serve Default File

// Serve default.html khi access root
app.UseDefaultFiles(new DefaultFilesOptions
{
    DefaultFileNames = new List<string> { "index.html", "default.html" }
});

// Phải đặt TRƯỚC UseStaticFiles
app.UseStaticFiles();

// http://localhost/ -> wwwroot/index.html

UseFileServer (Combined)

// Kết hợp UseDefaultFiles + UseStaticFiles + UseDirectoryBrowser
app.UseFileServer();

// Hoặc với custom options
app.UseFileServer(new FileServerOptions
{
    FileProvider = new PhysicalFileProvider(
        Path.Combine(builder.Environment.ContentRootPath, "wwwroot")),
    EnableDirectoryBrowsing = false
});

File Upload & Download

Overview Questions

  • Làm sao để upload file trong ASP.NET Core?
  • IFormFile là gì và sử dụng như thế nào?
  • Streaming upload khác buffering như thế nào?
  • Làm sao để handle large file uploads?
  • Download file được implement ra sao?

File Upload với IFormFile

Single File Upload

[HttpPost("upload")]
public async Task<IActionResult> UploadFile(IFormFile file)
{
    if (file == null || file.Length == 0)
    {
        return BadRequest("No file uploaded");
    }

    // Validate file size (max 10MB)
    if (file.Length > 10 * 1024 * 1024)
    {
        return BadRequest("File too large");
    }

    // Validate file type
    var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" };
    var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
    if (!allowedExtensions.Contains(extension))
    {
        return BadRequest("Invalid file type");
    }

    // Save file
    var fileName = $"{Guid.NewGuid()}{extension}";
    var filePath = Path.Combine("uploads", fileName);

    using var stream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(stream);

    return Ok(new { fileName, filePath });
}

Multiple File Upload

[HttpPost("upload-multiple")]
public async Task<IActionResult> UploadFiles(List<IFormFile> files)
{
    if (files == null || files.Count == 0)
    {
        return BadRequest("No files uploaded");
    }

    var uploadedFiles = new List<string>();

    foreach (var file in files)
    {
        var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
        var filePath = Path.Combine("uploads", fileName);

        using var stream = new FileStream(filePath, FileMode.Create);
        await file.CopyToAsync(stream);

        uploadedFiles.Add(fileName);
    }

    return Ok(uploadedFiles);
}

Upload với Form Data

public class ProductForm
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public IFormFile? Image { get; set; }
    public List<IFormFile>? Documents { get; set; }
}

[HttpPost("products")]
public async Task<IActionResult> CreateProduct([FromForm] ProductForm form)
{
    // Process form data
    var product = new Product
    {
        Name = form.Name,
        Price = form.Price
    };

    // Process image
    if (form.Image != null && form.Image.Length > 0)
    {
        var fileName = $"{Guid.NewGuid()}{Path.GetExtension(form.Image.FileName)}";
        var filePath = Path.Combine("uploads", "images", fileName);
        
        using var stream = new FileStream(filePath, FileMode.Create);
        await form.Image.CopyToAsync(stream);
        
        product.ImageUrl = $"/uploads/images/{fileName}";
    }

    // Process documents
    if (form.Documents != null)
    {
        foreach (var doc in form.Documents)
        {
            // Save documents
        }
    }

    return Ok(product);
}

Streaming Upload (Large Files)

Streaming vs Buffering

┌─────────────────────────────────────────────────────────────────┐
│                    UPLOAD METHODS                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Buffering (Default):                                           │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │───▶│  Memory  │───▶│   Disk   │                  │
│  │  Upload  │    │  (Form)  │    │  (Save)  │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│  - Toàn bộ file load vào memory trước                          │
│  - Phù hợp cho files nhỏ (< 64KB)                              │
│                                                                 │
│  Streaming:                                                     │
│  ┌──────────┐    ┌──────────┐                                  │
│  │  Client  │───▶│   Disk   │                                  │
│  │  Upload  │    │  (Save)  │                                  │
│  └──────────┘    └──────────┘                                  │
│  - Stream trực tiếp vào disk                                   │
│  - Phù hợp cho files lớn                                       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Streaming Implementation

[HttpPost("upload-stream")]
[RequestSizeLimit(100 * 1024 * 1024)] // 100MB
public async Task<IActionResult> UploadStream()
{
    var fileName = Request.Headers["X-File-Name"].FirstOrDefault();
    if (string.IsNullOrEmpty(fileName))
    {
        return BadRequest("Missing file name header");
    }

    var filePath = Path.Combine("uploads", fileName);

    // Stream trực tiếp vào file
    using var stream = new FileStream(filePath, FileMode.Create);
    await Request.Body.CopyToAsync(stream);

    return Ok(new { fileName, size = stream.Length });
}

Multipart Streaming

[HttpPost("upload-multipart-stream")]
[DisableFormValueModelBinding] // Custom attribute để disable automatic binding
public async Task<IActionResult> UploadMultipart()
{
    if (!Request.HasFormContentType)
    {
        return BadRequest("Invalid content type");
    }

    var form = await Request.ReadFormAsync();
    var file = form.Files["file"];
    
    if (file == null || file.Length == 0)
    {
        return BadRequest("No file");
    }

    var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
    var filePath = Path.Combine("uploads", fileName);

    using var stream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(stream);

    return Ok(new { fileName });
}

// Attribute để disable form binding
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<FormFileValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context) { }
}

File Download

Download từ File

[HttpGet("download/{fileName}")]
public IActionResult Download(string fileName)
{
    var filePath = Path.Combine("uploads", fileName);
    
    if (!System.IO.File.Exists(filePath))
    {
        return NotFound();
    }

    var contentType = GetContentType(fileName);
    return PhysicalFile(filePath, contentType, fileName);
}

private string GetContentType(string fileName)
{
    var extension = Path.GetExtension(fileName).ToLowerInvariant();
    return extension switch
    {
        ".jpg" or ".jpeg" => "image/jpeg",
        ".png" => "image/png",
        ".pdf" => "application/pdf",
        ".txt" => "text/plain",
        _ => "application/octet-stream"
    };
}

Download từ Stream

[HttpGet("download-stream/{fileName}")]
public IActionResult DownloadStream(string fileName)
{
    var filePath = Path.Combine("uploads", fileName);
    
    if (!System.IO.File.Exists(filePath))
    {
        return NotFound();
    }

    var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
    return File(stream, "application/octet-stream", fileName);
}

Download từ Byte Array

[HttpGet("download-bytes")]
public IActionResult DownloadBytes()
{
    var bytes = GeneratePdf(); // Generate file in memory
    return File(bytes, "application/pdf", "report.pdf");
}

Configuration

File Size Limits

// Program.cs
builder.Services.Configure<FormOptions>(options =>
{
    options.MultipartBodyLengthLimit = 100 * 1024 * 1024; // 100MB
    options.ValueLengthLimit = int.MaxValue;
    options.MultipartHeadersLengthLimit = int.MaxValue;
});

// Hoặc trong appsettings.json
{
  "Kestrel": {
    "Limits": {
      "MaxRequestBodySize": 104857600 // 100MB
    }
  }
}

Request Size Limit Attribute

// Per-action limit
[HttpPost("upload")]
[RequestSizeLimit(50 * 1024 * 1024)] // 50MB
public async Task<IActionResult> Upload(IFormFile file)
{
    // ...
}

// No limit
[HttpPost("upload-large")]
[DisableRequestSizeLimit]
public async Task<IActionResult> UploadLarge(IFormFile file)
{
    // ...
}

Best Practices

1. Validate File Uploads

public class FileUploadValidator
{
    private static readonly Dictionary<string, string[]> AllowedExtensions = new()
    {
        ["image"] = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" },
        ["document"] = new[] { ".pdf", ".doc", ".docx" },
        ["video"] = new[] { ".mp4", ".avi", ".mov" }
    };

    public static (bool IsValid, string? Error) Validate(IFormFile file, string category)
    {
        if (file == null || file.Length == 0)
            return (false, "No file uploaded");

        if (file.Length > 100 * 1024 * 1024)
            return (false, "File too large (max 100MB)");

        var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
        if (!AllowedExtensions.TryGetValue(category, out var allowed) || 
            !allowed.Contains(extension))
        {
            return (false, $"Invalid file type. Allowed: {string.Join(", ", allowed)}");
        }

        return (true, null);
    }
}

2. Use Safe File Names

// ✅ Good - generate safe file name
var safeFileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";

// ❌ Bad - use original file name (security risk)
var fileName = file.FileName; // Could contain path traversal

3. Scan for Malware

// Integrate with antivirus scanning
public async Task<bool> ScanForMalwareAsync(Stream fileStream)
{
    // Use antivirus API or external service
    // Return true if clean
    return true;
}

Hosted Services & Background Tasks

Overview Questions

  • Hosted Service là gì và khi nào cần sử dụng?
  • IHostedServiceBackgroundService khác nhau như thế nào?
  • Làm sao để thực thi task theo schedule?
  • Quản lý cancellation token trong background tasks ra sao?
  • Worker Service là gì và cách tạo?

IHostedService

Interface

public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}

Implementation

public class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger<TimedHostedService> _logger;
    private Timer? _timer;

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

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");
        
        // Chạy mỗi 1 phút
        _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
        
        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        _logger.LogInformation("Timed Hosted Service is working.");
        // Thực thi công việc background
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");
        
        _timer?.Change(Timeout.Infinite, 0);
        
        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

// Đăng ký
builder.Services.AddHostedService<TimedHostedService>();

BackgroundService

Abstract Class

// BackgroundService là implementation base của IHostedService
// Cung cấp ExecuteAsync method dễ sử dụng hơn
public abstract class BackgroundService : IHostedService, IDisposable
{
    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
    
    public virtual Task StartAsync(CancellationToken cancellationToken);
    public virtual Task StopAsync(CancellationToken cancellationToken);
    public virtual void Dispose();
}

Implementation

public class QueueProcessorService : BackgroundService
{
    private readonly ILogger<QueueProcessorService> _logger;
    private readonly Channel<string> _channel;

    public QueueProcessorService(
        ILogger<QueueProcessorService> logger,
        Channel<string> channel)
    {
        _logger = logger;
        _channel = channel;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queue Processor Service starting");

        await foreach (var item in _channel.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                _logger.LogInformation("Processing: {Item}", item);
                await ProcessItemAsync(item, stoppingToken);
            }
            catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogError(ex, "Error processing item: {Item}", item);
            }
        }

        _logger.LogInformation("Queue Processor Service stopping");
    }

    private async Task ProcessItemAsync(string item, CancellationToken token)
    {
        // Simulate processing
        await Task.Delay(100, token);
    }
}

// Đăng ký
builder.Services.AddSingleton(Channel.CreateUnbounded<string>());
builder.Services.AddHostedService<QueueProcessorService>();

Periodic Background Service

Pattern với Timer

public class PeriodicService : BackgroundService
{
    private readonly ILogger<PeriodicService> _logger;
    private readonly TimeSpan _period = TimeSpan.FromSeconds(30);

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                _logger.LogInformation("Doing periodic work at {Time}", DateTimeOffset.UtcNow);
                await DoWorkAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error in periodic work");
            }

            await Task.Delay(_period, stoppingToken);
        }
    }

    private async Task DoWorkAsync(CancellationToken token)
    {
        // Thực thi công việc
        await Task.CompletedTask;
    }
}

Scheduled Tasks

Cron-based Scheduler

dotnet add package Coravel
// Program.cs
builder.Services.AddScheduler();

// Tạo scheduled task
public class CleanupTask : IInvocable
{
    private readonly ILogger<CleanupTask> _logger;

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

    public async Task Invoke()
    {
        _logger.LogInformation("Running cleanup task");
        // Cleanup logic
    }
}

// Đăng ký schedule
builder.Services.AddTransient<CleanupTask>();

// Trong Program.cs
var app = builder.Build();
app.Services.UseScheduler(scheduler =>
{
    scheduler.Schedule<CleanupTask>()
        .EveryFiveMinutes()
        .PreventOverlapping();
});

app.Run();

Hangfire

dotnet add package Hangfire
// Program.cs
builder.Services.AddHangfire(config =>
    config.UseSqlServerStorage(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddHangfireServer();

var app = builder.Build();
app.UseHangfireDashboard();

// Recurring job
RecurringJob.AddOrUpdate<ICleanupService>(
    "cleanup",
    service => service.Cleanup(),
    Cron.Daily);

app.Run();

Worker Service Template

Tạo Worker Service

dotnet new worker -n MyWorker

Structure

// Program.cs
using MyWorker;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

var host = builder.Build();
host.Run();

// Worker.cs
namespace MyWorker;

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

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

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1000, stoppingToken);
        }
    }
}

Run as Windows Service

dotnet add package Microsoft.Extensions.Hosting.WindowsServices
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

// Add Windows Service support
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = "My Worker Service";
});

var host = builder.Build();
host.Run();

Best Practices

1. Handle Cancellation Properly

// ✅ Good
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        await DoWorkAsync(stoppingToken);
        await Task.Delay(1000, stoppingToken);
    }
}

// ❌ Bad - ignore cancellation
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (true) // Infinite loop, no cancellation check
    {
        await DoWorkAsync(CancellationToken.None);
    }
}

2. Handle Exceptions

// ✅ Good - catch và log exceptions
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        try
        {
            await DoWorkAsync(stoppingToken);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in background service");
        }
        await Task.Delay(1000, stoppingToken);
    }
}

3. Use Scoped Services Correctly

// ✅ Good - tạo scope cho scoped services
public class ScopedWorker : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<ScopedWorker> _logger;

    public ScopedWorker(IServiceProvider serviceProvider, ILogger<ScopedWorker> logger)
    {
        _serviceProvider = serviceProvider;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Tạo scope mới cho mỗi iteration
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            
            await ProcessAsync(dbContext, stoppingToken);
            
            await Task.Delay(5000, stoppingToken);
        }
    }
}

Health Checks

Overview Questions

  • Health Checks là gì và tại sao cần thiết?
  • Làm sao để cấu hình health check endpoints?
  • Custom health check được viết như thế nào?
  • Health check UI là gì và cách tích hợp?
  • Health check response format ra sao?

Basic Health Checks

Configuration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add health checks
builder.Services.AddHealthChecks();

var app = builder.Build();

// Map health check endpoint
app.MapHealthChecks("/health");

// Basic health check
app.MapGet("/health/basic", () => Results.Ok("Healthy"));

app.Run();

Response

// Healthy
{
  "status": "Healthy"
}

// Unhealthy
{
  "status": "Unhealthy",
  "results": {
    "database": {
      "status": "Unhealthy",
      "description": "Connection failed"
    }
  }
}

Built-in Health Checks

Database Health Check

dotnet add package AspNetCore.HealthChecks.SqlServer
builder.Services.AddHealthChecks()
    .AddSqlServer(
        builder.Configuration.GetConnectionString("Default"),
        name: "sqlserver",
        failureStatus: HealthStatus.Unhealthy,
        tags: new[] { "db", "sql" });

Redis Health Check

dotnet add package AspNetCore.HealthChecks.Redis
builder.Services.AddHealthChecks()
    .AddRedis(
        "localhost:6379",
        name: "redis",
        failureStatus: HealthStatus.Degraded);

HTTP Endpoint Health Check

builder.Services.AddHealthChecks()
    .AddUrlGroup(
        new Uri("https://api.external.com/health"),
        name: "external-api",
        failureStatus: HealthStatus.Degraded);

Multiple Health Checks

builder.Services.AddHealthChecks()
    .AddSqlServer(
        builder.Configuration.GetConnectionString("Default"),
        name: "database")
    .AddRedis("localhost:6379", name: "cache")
    .AddUrlGroup(new Uri("https://api.external.com"), name: "external");

Custom Health Checks

IHealthCheck Implementation

public class DiskSpaceHealthCheck : IHealthCheck
{
    private readonly ILogger<DiskSpaceHealthCheck> _logger;
    private readonly long _thresholdBytes = 1024 * 1024 * 100; // 100MB

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

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var drive = new DriveInfo(Path.GetPathRoot(Directory.GetCurrentDirectory())!);
        var freeSpace = drive.AvailableFreeSpace;

        if (freeSpace > _thresholdBytes)
        {
            return HealthCheckResult.Healthy(
                $"Free disk space: {freeSpace / 1024 / 1024}MB");
        }

        return HealthCheckResult.Unhealthy(
            $"Low disk space: {freeSpace / 1024 / 1024}MB remaining");
    }
}

// Đăng ký
builder.Services.AddHealthChecks()
    .AddCheck<DiskSpaceHealthCheck>("disk-space");

Delegate-based Health Check

builder.Services.AddHealthChecks()
    .AddCheck("memory", () =>
    {
        var memoryUsed = GC.GetGCMemoryInfo().HeapSizeBytes;
        var memoryLimit = 512L * 1024 * 1024; // 512MB

        return memoryUsed < memoryLimit
            ? HealthCheckResult.Healthy($"Memory: {memoryUsed / 1024 / 1024}MB")
            : HealthCheckResult.Unhealthy($"High memory: {memoryUsed / 1024 / 1024}MB");
    });

Health Check Options

Response Writer

app.MapHealthChecks("/health", new HealthCheckOptions
{
    // Custom response writer
    ResponseWriter = async (context, report) =>
    {
        var result = new
        {
            status = report.Status.ToString(),
            duration = report.TotalDuration,
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                description = e.Value.Description,
                data = e.Value.Data
            }),
            timestamp = DateTime.UtcNow
        };

        context.Response.ContentType = "application/json";
        await context.Response.WriteAsJsonAsync(result);
    }
});

Predicate & Tags

// Chỉ check database
app.MapHealthChecks("/health/db", new HealthCheckOptions
{
    Predicate = (check) => check.Tags.Contains("db")
});

// Chỉ check cache
app.MapHealthChecks("/health/cache", new HealthCheckOptions
{
    Predicate = (check) => check.Tags.Contains("cache")
});

Status Code Mapping

app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResultStatusCodes = new Dictionary<HealthStatus, int>
    {
        [HealthStatus.Healthy] = 200,
        [HealthStatus.Degraded] = 200,
        [HealthStatus.Unhealthy] = 503
    }
});

Health Check UI

Setup

dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage
// Program.cs
builder.Services.AddHealthChecksUI()
    .AddInMemoryStorage();

var app = builder.Build();

app.MapHealthChecks("/health");
app.MapHealthChecksUI("/health-ui");

app.Run();

Configuration

{
  "HealthChecksUI": {
    "HealthChecks": [
      {
        "Name": "API Health",
        "Uri": "https://localhost:5001/health"
      }
    ],
    "EvaluationTimeInSeconds": 30,
    "MinimumSecondsBetweenFailureNotifications": 60
  }
}

Kubernetes Probes

Liveness & Readiness

// Liveness - App is running
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
    Predicate = _ => false // No checks, just return 200
});

// Readiness - App is ready to receive traffic
app.MapHealthChecks("/ready", new HealthCheckOptions
{
    Predicate = check => true // Run all checks
});

// Startup - App has started
app.MapHealthChecks("/started", new HealthCheckOptions
{
    Predicate = _ => false
});

Kubernetes Config

livenessProbe:
  httpGet:
    path: /healthz
    port: 80
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: 80
  initialDelaySeconds: 10
  periodSeconds: 5

Best Practices

1. Use Appropriate Checks

// ✅ Good - lightweight checks
builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString, name: "database")
    .AddRedis(redisConnection, name: "cache");

// ❌ Bad - expensive checks
builder.Services.AddHealthChecks()
    .AddCheck("full-database-scan", async () => 
    {
        // Expensive query - avoid in health check
        var count = await db.Products.CountAsync();
        return count > 0 ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy();
    });

2. Separate Liveness and Readiness

┌─────────────────────────────────────────────────────────────────┐
│                    HEALTH CHECK TYPES                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Liveness (/healthz):                                           │
│  - App is running                                               │
│  - No deadlock or infinite loop                                 │
│  - Kubernetes restarts if failed                                │
│                                                                 │
│  Readiness (/ready):                                            │
│  - App can handle requests                                      │
│  - Dependencies are available                                   │
│  - Kubernetes removes from service if failed                    │
│                                                                 │
│  Startup (/started):                                            │
│  - App has finished initialization                              │
│  - Used during startup probe                                    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3. Don’t Overload Health Checks

// ✅ Good - simple checks
builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString, name: "db")
    .AddRedis(redisConnection, name: "cache");

// ❌ Bad - too many external dependencies
builder.Services.AddHealthChecks()
    .AddSqlServer(connectionString)
    .AddRedis(redisConnection)
    .AddUrlGroup(externalApi1)
    .AddUrlGroup(externalApi2)
    .AddUrlGroup(externalApi3); // External APIs có thể down

Minimal APIs Advanced

Overview Questions

  • Route Groups là gì và khi nào sử dụng?
  • Endpoint Filters hoạt động như thế nào?
  • Làm sao để organize large Minimal APIs?
  • Parameter binding trong Minimal APIs có gì đặc biệt?
  • So sánh Minimal APIs vs Controllers khi nào dùng cái nào?

Route Groups (.NET 7+)

Basic Route Groups

var app = builder.Build();

// Group với common prefix
var products = app.MapGroup("/api/products");

products.MapGet("/", () => new[] { "Product 1", "Product 2" });
products.MapGet("/{id}", (int id) => new { Id = id, Name = "Product" });
products.MapPost("/", (Product product) => Results.Created($"/api/products/{product.Id}", product));
products.MapPut("/{id}", (int id, Product product) => Results.NoContent());
products.MapDelete("/{id}", (int id) => Results.NoContent());

app.Run();

Route Groups với Common Configuration

var app = builder.Build();

// Group với common middleware
var products = app.MapGroup("/api/products")
    .RequireAuthorization()
    .WithTags("Products")
    .WithOpenApi();

products.MapGet("/", () => new[] { "Product 1" });
products.MapGet("/{id}", (int id) => new { Id = id });
products.MapPost("/", (Product product) => Results.Created($"/api/products/{product.Id}", product));

app.Run();

Nested Route Groups

var app = builder.Build();

var api = app.MapGroup("/api")
    .RequireAuthorization();

var products = api.MapGroup("/products")
    .WithTags("Products");

products.MapGet("/", () => new[] { "Product 1" });
products.MapGet("/{id}", (int id) => new { Id = id });

var orders = api.MapGroup("/orders")
    .WithTags("Orders");

orders.MapGet("/", () => new[] { "Order 1" });
orders.MapGet("/{id}", (int id) => new { Id = id });

app.Run();

Endpoint Filters (.NET 7+)

Basic Filter

var app = builder.Build();

app.MapGet("/api/products/{id}", (int id, AppDbContext db) =>
{
    return db.Products.Find(id);
})
.AddEndpointFilter(async (context, next) =>
{
    var id = context.GetArgument<int>(0);
    
    if (id <= 0)
    {
        return Results.BadRequest("Invalid ID");
    }
    
    return await next(context);
});

app.Run();

Logging Filter

public class LoggingFilter : IEndpointFilter
{
    private readonly ILogger<LoggingFilter> _logger;

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

    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var startTime = DateTime.UtcNow;
        _logger.LogInformation("Starting {Method} {Path}", 
            context.HttpContext.Request.Method,
            context.HttpContext.Request.Path);

        var result = await next(context);

        _logger.LogInformation("Completed in {Elapsed}ms", 
            (DateTime.UtcNow - startTime).TotalMilliseconds);

        return result;
    }
}

// Đăng ký
app.MapGet("/api/products", async (AppDbContext db) =>
{
    return await db.Products.ToListAsync();
})
.AddEndpointFilter<LoggingFilter>();

Validation Filter

public class ValidationFilter<T> : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        // Find the argument to validate
        for (var i = 0; i < context.Arguments.Count; i++)
        {
            if (context.Arguments[i] is T item)
            {
                var validationContext = new ValidationContext<T>(item);
                var validator = context.HttpContext.RequestServices
                    .GetRequiredService<IValidator<T>>();
                var result = await validator.ValidateAsync(validationContext);

                if (!result.IsValid)
                {
                    var errors = result.Errors
                        .GroupBy(e => e.PropertyName)
                        .ToDictionary(
                            g => g.Key,
                            g => g.Select(e => e.ErrorMessage).ToArray());

                    return Results.ValidationProblem(errors);
                }
            }
        }

        return await next(context);
    }
}

// Sử dụng
app.MapPost("/api/products", (Product product, AppDbContext db) =>
{
    db.Products.Add(product);
    db.SaveChanges();
    return Results.Created($"/api/products/{product.Id}", product);
})
.AddEndpointFilter<ValidationFilter<Product>>();

Parameter Binding

Built-in Binding

// FromRoute
app.MapGet("/api/products/{id}", (int id) => id);

// FromQuery
app.MapGet("/api/products", (int page, int pageSize) => new { page, pageSize });

// FromBody
app.MapPost("/api/products", (Product product) => product);

// FromServices
app.MapGet("/api/products", (AppDbContext db) => db.Products.ToList());

// FromHeader
app.MapGet("/api/secure", ([FromHeader(Name = "X-API-Key")] string apiKey) => apiKey);

// HttpContext
app.MapGet("/api/context", (HttpContext context) => context.Request.Path);

// HttpRequest/HttpResponse
app.MapGet("/api/request", (HttpRequest request) => request.Headers);

Custom Binding

// Bind từ query string vào complex type
app.MapGet("/api/products", ([AsParameters] PagingParams p) => 
{
    return new { p.Page, p.PageSize, p.Search };
});

public record PagingParams(
    int Page = 1,
    int PageSize = 10,
    string? Search = null);

TryParse Binding

// Custom type với TryParse
public class ProductId
{
    public int Value { get; }

    public ProductId(int value) => Value = value;

    public static bool TryParse(string? value, out ProductId? result)
    {
        if (int.TryParse(value, out var id))
        {
            result = new ProductId(id);
            return true;
        }
        result = null;
        return false;
    }
}

// Binding tự động qua TryParse
app.MapGet("/api/products/{id}", (ProductId id) => new { id.Value });

Organizing Large Minimal APIs

Extension Methods Pattern

// Program.cs
var app = builder.Build();

app.MapProductEndpoints();
app.MapOrderEndpoints();
app.MapUserEndpoints();

app.Run();

// ProductEndpoints.cs
public static class ProductEndpoints
{
    public static void MapProductEndpoints(this IEndpointRouteBuilder routes)
    {
        var group = routes.MapGroup("/api/products")
            .WithTags("Products")
            .WithOpenApi();

        group.MapGet("/", async (AppDbContext db) => 
            await db.Products.ToListAsync());

        group.MapGet("/{id}", async (int id, AppDbContext db) =>
            await db.Products.FindAsync(id) is Product product
                ? Results.Ok(product)
                : Results.NotFound());

        group.MapPost("/", async (Product product, AppDbContext db) =>
        {
            db.Products.Add(product);
            await db.SaveChangesAsync();
            return Results.Created($"/api/products/{product.Id}", product);
        });

        group.MapPut("/{id}", async (int id, Product product, AppDbContext db) =>
        {
            var existing = await db.Products.FindAsync(id);
            if (existing == null) return Results.NotFound();
            
            existing.Name = product.Name;
            existing.Price = product.Price;
            await db.SaveChangesAsync();
            return Results.NoContent();
        });

        group.MapDelete("/{id}", async (int id, AppDbContext db) =>
        {
            var product = await db.Products.FindAsync(id);
            if (product == null) return Results.NotFound();
            
            db.Products.Remove(product);
            await db.SaveChangesAsync();
            return Results.NoContent();
        });
    }
}

Interface-based Pattern

public interface IEndpoint
{
    void MapEndpoint(IEndpointRouteBuilder app);
}

public class ProductEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/products");
        group.MapGet("/", () => new[] { "Product 1" });
        group.MapGet("/{id}", (int id) => new { Id = id });
    }
}

// Program.cs
var app = builder.Build();

var scope = app.Services.CreateScope();
var endpoints = scope.ServiceProvider.GetServices<IEndpoint>();

foreach (var endpoint in endpoints)
{
    endpoint.MapEndpoint(app);
}

app.Run();

Minimal APIs vs Controllers

Comparison

AspectMinimal APIsControllers
Code sizeÍt hơnNhiều hơn
ComplexityĐơn giảnPhức tạp hơn
FeaturesĐầy đủ cho most casesFull MVC features
FiltersEndpoint FiltersAction/Result Filters
Model BindingTự độngAttribute-based
TestingKhó hơnDễ hơn
OrganizationExtension methodsControllers folder
Use caseSmall APIs, microservicesLarge applications

When to Use

┌─────────────────────────────────────────────────────────────────┐
│                    WHEN TO USE                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Minimal APIs:                                                  │
│  - Microservices                                                │
│  - Small to medium APIs                                         │
│  - Quick prototypes                                             │
│  - Serverless functions                                         │
│  - Simple CRUD operations                                       │
│                                                                 │
│  Controllers:                                                   │
│  - Large enterprise applications                                │
│  - Complex business logic                                       │
│  - Need MVC features (Views, Razor Pages)                       │
│  - Team với nhiều developers                                    │
│  - Need extensive filter pipeline                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Content Negotiation

Overview Questions

  • Content Negotiation là gì và tại sao cần thiết?
  • ASP.NET Core xử lý input/output formatting như thế nào?
  • Làm sao để thêm XML support?
  • Custom formatter được viết ra sao?
  • Response format negotiation hoạt động như thế nào?

Content Negotiation Basics

What is Content Negotiation?

┌─────────────────────────────────────────────────────────────────┐
│                CONTENT NEGOTIATION FLOW                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Client Request:                                                │
│  Accept: application/json                                       │
│  Content-Type: application/json                                 │
│         │                                                       │
│         ▼                                                       │
│  ┌─────────────────────────────┐                               │
│  │    Input Formatter          │                               │
│  │  (JSON, XML, etc.)          │                               │
│  └─────────────┬───────────────┘                               │
│                │                                               │
│                ▼                                               │
│  ┌─────────────────────────────┐                               │
│  │       Controller            │                               │
│  └─────────────┬───────────────┘                               │
│                │                                               │
│                ▼                                               │
│  ┌─────────────────────────────┐                               │
│  │    Output Formatter         │                               │
│  │  (JSON, XML, etc.)          │                               │
│  └─────────────┬───────────────┘                               │
│                │                                               │
│                ▼                                               │
│  Client Response:                                               │
│  Content-Type: application/json                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Default Configuration

// ASP.NET Core mặc định chỉ support JSON
builder.Services.AddControllers();

// JSON là default formatter
// System.Text.Json được sử dụng

Input Formatters

JSON Input (Default)

// Request
// POST /api/products
// Content-Type: application/json
// {"name": "Laptop", "price": 999.99}

[HttpPost]
public IActionResult Create(Product product)
{
    // product được deserialize từ JSON
    return Ok(product);
}

XML Input

dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson
builder.Services.AddControllers()
    .AddXmlSerializerFormatters(); // Add XML support

// Request
// POST /api/products
// Content-Type: application/xml
// <Product><Name>Laptop</Name><Price>999.99</Price></Product>

[HttpPost]
public IActionResult Create(Product product)
{
    return Ok(product);
}

Custom Input Formatter

public class CsvInputFormatter : InputFormatter
{
    public CsvInputFormatter()
    {
        SupportedMediaTypes.Add("text/csv");
    }

    protected override bool CanReadType(Type type)
    {
        return type == typeof(List<Product>);
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(
        InputFormatterContext context)
    {
        var request = context.HttpContext.Request;
        using var reader = new StreamReader(request.Body);
        var csv = await reader.ReadToEndAsync();
        
        var products = ParseCsv(csv);
        
        return await InputFormatterResult.SuccessAsync(products);
    }

    private List<Product> ParseCsv(string csv)
    {
        // Parse CSV logic
        return new List<Product>();
    }
}

// Đăng ký
builder.Services.AddControllers(options =>
{
    options.InputFormatters.Add(new CsvInputFormatter());
});

Output Formatters

JSON Output (Default)

[HttpGet]
public IActionResult Get()
{
    return Ok(new { name = "Product", price = 99.99 });
}

// Response
// Content-Type: application/json
// {"name":"Product","price":99.99}

XML Output

builder.Services.AddControllers()
    .AddXmlSerializerFormatters();

[HttpGet]
[Produces("application/xml")]
public IActionResult Get()
{
    return Ok(new Product { Name = "Product", Price = 99.99 });
}

// Response
// Content-Type: application/xml
// <Product><Name>Product</Name><Price>99.99</Price></Product>

Content Negotiation

[HttpGet]
public IActionResult Get()
{
    var product = new Product { Name = "Product", Price = 99.99 };
    return Ok(product);
}

// Request với Accept: application/json
// Response: JSON

// Request với Accept: application/xml
// Response: XML

JSON Configuration

System.Text.Json Options

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        // CamelCase property names
        options.JsonSerializerOptions.PropertyNamingPolicy = 
            JsonNamingPolicy.CamelCase;
        
        // Ignore null values
        options.JsonSerializerOptions.DefaultIgnoreCondition = 
            JsonIgnoreCondition.WhenWritingNull;
        
        // Write numbers as strings
        options.JsonSerializerOptions.NumberHandling = 
            JsonNumberHandling.WriteAsString;
        
        // Allow trailing commas
        options.JsonSerializerOptions.AllowTrailingCommas = true;
        
        // Custom converters
        options.JsonSerializerOptions.Converters.Add(
            new JsonStringEnumConverter());
    });

Newtonsoft.Json Options

builder.Services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.ContractResolver = 
            new CamelCasePropertyNamesContractResolver();
        
        options.SerializerSettings.NullValueHandling = 
            NullValueHandling.Ignore;
        
        options.SerializerSettings.DateFormatString = 
            "yyyy-MM-dd HH:mm:ss";
    });

Custom Output Formatter

public class CsvOutputFormatter : TextOutputFormatter
{
    public CsvOutputFormatter()
    {
        SupportedMediaTypes.Add("text/csv");
        SupportedEncodings.Add(Encoding.UTF8);
    }

    protected override bool CanWriteType(Type type)
    {
        return typeof(IEnumerable<Product>).IsAssignableFrom(type);
    }

    public override async Task WriteResponseBodyAsync(
        OutputFormatterWriteContext context,
        Encoding selectedEncoding)
    {
        var response = context.HttpContext.Response;
        var products = context.Object as IEnumerable<Product>;
        
        using var writer = new StreamWriter(response.Body, selectedEncoding);
        
        // Write header
        writer.WriteLine("Id,Name,Price");
        
        // Write data
        foreach (var product in products ?? Enumerable.Empty<Product>())
        {
            writer.WriteLine($"{product.Id},{product.Name},{product.Price}");
        }
    }
}

// Đăng ký
builder.Services.AddControllers(options =>
{
    options.OutputFormatters.Add(new CsvOutputFormatter());
});

Best Practices

1. Use JSON by Default

// ✅ JSON là default và được support tốt nhất
builder.Services.AddControllers();

// ❌ Tránh thêm quá nhiều formatters không cần thiết
builder.Services.AddControllers()
    .AddXmlSerializerFormatters()
    .AddXmlDataContractSerializerFormatters();

2. Configure Consistent JSON Settings

// ✅ Configure JSON settings globally
builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.PropertyNamingPolicy = 
            JsonNamingPolicy.CamelCase;
        options.JsonSerializerOptions.WriteIndented = true;
    });

3. Use [Produces] Attribute

// ✅ Explicit về response format
[HttpGet]
[Produces("application/json")]
public IActionResult Get() => Ok(products);

// ❌ Không rõ response format
[HttpGet]
public IActionResult Get() => Ok(products);

Output Caching

Overview Questions

  • Output Caching là gì và khác Response Caching ra sao?
  • Làm sao để cấu hình output caching trong .NET 7+?
  • Cache policies hoạt động như thế nào?
  • Khi nào nên dùng output caching?
  • Cached tagged responses là gì?

Output Caching vs Response Caching

Comparison

┌─────────────────────────────────────────────────────────────────┐
│                    CACHING COMPARISON                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Output Caching (.NET 7+):                                      │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │───▶│  Server  │───▶│  Cache   │                  │
│  │  Request │    │  (App)   │    │  (Mem)   │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│  - Cache trên server                                            │
│  - Không gửi cache headers cho client                          │
│  - Phù hợp cho server-side caching                             │
│                                                                 │
│  Response Caching:                                              │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │───▶│  Server  │───▶│  Client  │                  │
│  │  Request │    │  (App)   │    │  Cache   │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│  - Cache trên client/proxy                                      │
│  - Gửi cache headers (Cache-Control)                           │
│  - Phù hợp cho CDN/browser caching                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Output Caching Configuration

Basic Setup

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add output caching
builder.Services.AddOutputCache();

var app = builder.Build();

// Enable caching for endpoints
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput();

app.Run();

Cache Policies

builder.Services.AddOutputCache(options =>
{
    // Default policy - cache 60 seconds
    options.AddBasePolicy(builder => builder.Cache());
    
    // Custom policy - cache 5 minutes
    options.AddPolicy("ShortCache", builder => 
        builder.Cache().Expire(TimeSpan.FromMinutes(5)));
    
    // Custom policy - no cache
    options.AddPolicy("NoCache", builder => builder.NoCache());
    
    // Custom policy - vary by header
    options.AddPolicy("VaryByHeader", builder => 
        builder.Cache().VaryByHeader("Accept-Language"));
});

Endpoint Caching

Basic Caching

// Cache với default policy
app.MapGet("/api/products", async (AppDbContext db) =>
{
    return await db.Products.ToListAsync();
})
.CacheOutput();

// Cache với custom policy
app.MapGet("/api/products", async (AppDbContext db) =>
{
    return await db.Products.ToListAsync();
})
.CacheOutput(p => p.WithPolicyName("ShortCache"));

Vary By Query

// Cache khác nhau cho mỗi query param
app.MapGet("/api/products", async (int page, AppDbContext db) =>
{
    return await db.Products
        .Skip((page - 1) * 10)
        .Take(10)
        .ToListAsync();
})
.CacheOutput(b => b.VaryByQuery("page"));

// Vary by multiple query params
app.MapGet("/api/products", async (int page, string? category, AppDbContext db) =>
{
    var query = db.Products.AsQueryable();
    if (!string.IsNullOrEmpty(category))
        query = query.Where(p => p.Category == category);
    
    return await query.Skip((page - 1) * 10).Take(10).ToListAsync();
})
.CacheOutput(b => b.VaryByQuery("page", "category"));

Vary By Header

// Cache khác nhau cho mỗi Accept-Language
app.MapGet("/api/products", async (AppDbContext db) =>
{
    return await db.Products.ToListAsync();
})
.CacheOutput(b => b.VaryByHeader("Accept-Language"));

Vary By Route

// Cache khác nhau cho mỗi route param
app.MapGet("/api/products/{category}", async (string category, AppDbContext db) =>
{
    return await db.Products.Where(p => p.Category == category).ToListAsync();
})
.CacheOutput(b => b.VaryByRouteValue("category"));

Cache Expiration

Time-based Expiration

// Expire after 1 minute
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput(b => b.Expire(TimeSpan.FromMinutes(1)));

// Expire after 1 hour
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput(b => b.Expire(TimeSpan.FromHours(1)));

// Expire after 1 day
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput(b => b.Expire(TimeSpan.FromDays(1)));

No Caching

// Disable caching for this endpoint
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput(b => b.NoCache());

// No store - don't cache at all
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput(b => b.NoStore());

Cache Tags

Tag-based Invalidation

// Cache với tags
app.MapGet("/api/products", async (AppDbContext db) =>
{
    return await db.Products.ToListAsync();
})
.CacheOutput(b => b.Tag("products"));

app.MapGet("/api/products/{id}", async (int id, AppDbContext db) =>
{
    return await db.Products.FindAsync(id);
})
.CacheOutput(b => b.Tag($"product-{id}"));

// Invalidate cache khi update
app.MapPut("/api/products/{id}", async (int id, Product product, AppDbContext db, IOutputCacheStore cache) =>
{
    var existing = await db.Products.FindAsync(id);
    if (existing == null) return Results.NotFound();
    
    existing.Name = product.Name;
    existing.Price = product.Price;
    await db.SaveChangesAsync();
    
    // Evict cache
    await cache.EvictByTagAsync($"product-{id}", CancellationToken.None);
    
    return Results.NoContent();
});

Response Caching (Client-side)

Configuration

// Add response caching middleware
builder.Services.AddResponseCaching();

var app = builder.Build();

app.UseResponseCaching();

app.MapGet("/api/products", () => GetProducts());

Cache Headers

app.MapGet("/api/products", () =>
{
    var response = Results.Ok(GetProducts());
    return response;
})
.AddEndpointFilter(async (context, next) =>
{
    var response = await next(context);
    
    context.HttpContext.Response.Headers["Cache-Control"] = 
        "public, max-age=60"; // Cache 60 seconds
    
    return response;
});

Best Practices

1. Cache Appropriately

// ✅ Cache read-heavy endpoints
app.MapGet("/api/products", () => GetProducts())
    .CacheOutput(b => b.Expire(TimeSpan.FromMinutes(5)));

// ❌ Don't cache write endpoints
app.MapPost("/api/products", (Product p) => CreateProduct(p));

2. Use Tags for Invalidation

// ✅ Use tags for granular invalidation
app.MapGet("/api/products/{id}", (int id) => GetProduct(id))
    .CacheOutput(b => b.Tag($"product-{id}"));

// Invalidate when updated
await cache.EvictByTagAsync($"product-{id}", CancellationToken.None);

3. Vary by Appropriate Keys

// ✅ Vary by relevant parameters
app.MapGet("/api/products", (string? category, int page) => GetProducts(category, page))
    .CacheOutput(b => b.VaryByQuery("category", "page"));

// ❌ Don't vary by unnecessary parameters
app.MapGet("/api/products", (string? trackingId) => GetProducts())
    .CacheOutput(b => b.VaryByQuery("trackingId")); // Tracking ID không nên vary

Kestrel Configuration

Overview Questions

  • Kestrel là gì và tại sao nó quan trọng?
  • Làm sao để cấu hình HTTPS cho Kestrel?
  • Endpoint configuration hoạt động như thế nào?
  • Request limits được cấu hình ra sao?
  • Kestrel vs IIS/Nginx - khi nào dùng reverse proxy?

Kestrel Basics

What is Kestrel?

┌─────────────────────────────────────────────────────────────────┐
│                    KESTREL ARCHITECTURE                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │───▶│  Kestrel │───▶│  ASP.NET │                  │
│  │  (HTTP)  │    │  Server  │    │  Core    │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│                                                                 │
│  - Cross-platform web server                                    │
│  - Default server cho ASP.NET Core                              │
│  - High performance                                             │
│  - Có thể dùng standalone hoặc với reverse proxy                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Basic Configuration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    // Configure Kestrel here
});

var app = builder.Build();
app.Run();

Endpoint Configuration

appsettings.json

{
  "Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000"
      },
      "Https": {
        "Url": "https://localhost:5001"
      }
    }
  }
}

Code Configuration

builder.WebHost.ConfigureKestrel(options =>
{
    // HTTP endpoint
    options.ListenLocalhost(5000);
    
    // HTTPS endpoint
    options.ListenLocalhost(5001, listenOptions =>
    {
        listenOptions.UseHttps();
    });
    
    // Custom IP and port
    options.Listen(IPAddress.Parse("127.0.0.1"), 5002);
    
    // Unix socket (Linux)
    options.ListenUnixSocket("/tmp/kestrel.sock");
});

HTTPS Configuration

Development Certificate

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, listenOptions =>
    {
        listenOptions.UseHttps(); // Uses development certificate
    });
});

Production Certificate

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, listenOptions =>
    {
        listenOptions.UseHttps("certificate.pfx", "password");
    });
});

Certificate from Store

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, listenOptions =>
    {
        using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
        store.Open(OpenFlags.ReadOnly);
        
        var cert = store.Certificates
            .Find(X509FindType.FindByThumbprint, "CERT_THUMBPRINT", validOnly: false)
            .OfType<X509Certificate2>()
            .FirstOrDefault();
        
        if (cert != null)
        {
            listenOptions.UseHttps(cert);
        }
    });
});

HTTPS Redirection

// Program.cs
var app = builder.Build();

// Redirect HTTP to HTTPS
app.UseHttpsRedirection();

app.MapGet("/", () => "Hello HTTPS!");

app.Run();

Request Limits

Configure Limits

builder.WebHost.ConfigureKestrel(options =>
{
    // Max request body size (100MB)
    options.Limits.MaxRequestBodySize = 100 * 1024 * 1024;
    
    // Max request header size (32KB)
    options.Limits.MaxRequestHeadersTotalSize = 32 * 1024;
    
    // Max request header count
    options.Limits.MaxRequestHeaderCount = 100;
    
    // Keep-alive timeout
    options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
    
    // Request headers timeout
    options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
    
    // Min response data rate
    options.Limits.MinResponseDataRate = new MinDataRate(
        bytesPerSecond: 100, 
        gracePeriod: TimeSpan.FromSeconds(10));
});

Per-Request Limits

app.MapPost("/upload", async (HttpContext context) =>
{
    // Override limit for this request
    context.Features.Get<IHttpMaxRequestBodySizeFeature>()
        ?.MaxRequestBodySize = 200 * 1024 * 1024; // 200MB
    
    // Process upload
    await context.Request.Body.CopyToAsync(Stream.Null);
    
    return Results.Ok();
});

HTTP/2 and HTTP/3

HTTP/2 Configuration

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
        listenOptions.UseHttps();
    });
});

HTTP/3 Configuration

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5002, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http3;
    });
});

Protocol Selection

// Check protocol
app.MapGet("/protocol", (HttpContext context) =>
{
    var protocol = context.Request.Protocol;
    return new { Protocol = protocol };
});

Reverse Proxy

When to Use Reverse Proxy

┌─────────────────────────────────────────────────────────────────┐
│                    REVERSE PROXY SETUP                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │───▶│  Nginx/  │───▶│  Kestrel │                  │
│  │          │    │  IIS     │    │  (App)   │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│                                                                 │
│  Use reverse proxy for:                                         │
│  - Multiple apps trên cùng port 80/443                         │
│  - Static file serving                                          │
│  - Response compression                                         │
│  - Additional security layer                                    │
│  - Load balancing                                               │
│                                                                 │
│  Kestrel standalone OK cho:                                     │
│  - Internal services                                            │
│  - Container deployments                                        │
│  - Development                                                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Forwarded Headers

// Khi dùng reverse proxy
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | 
                       ForwardedHeaders.XForwardedProto
});

Best Practices

1. Use HTTPS in Production

// ✅ Always use HTTPS in production
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5001, o => o.UseHttps("cert.pfx", "password"));
});

// ❌ Never use HTTP only in production
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5000); // No HTTPS!
});

2. Set Appropriate Limits

// ✅ Set reasonable limits
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; // 50MB

// ❌ Don't remove limits entirely
options.Limits.MaxRequestBodySize = null; // Unlimited!

3. Configure for Environment

if (builder.Environment.IsDevelopment())
{
    builder.WebHost.ConfigureKestrel(options =>
    {
        options.ListenLocalhost(5000);
        options.ListenLocalhost(5001, o => o.UseHttps());
    });
}
else
{
    builder.WebHost.ConfigureKestrel(options =>
    {
        options.ListenLocalhost(5001, o => 
            o.UseHttps("prod-cert.pfx", config["CertPassword"]));
    });
}

Localization (i18n)

Overview Questions

  • Localization trong ASP.NET Core hoạt động như thế nào?
  • Resource files là gì và cách sử dụng?
  • Làm sao để localize controllers và views?
  • Culture providers hoạt động ra sao?
  • Localize data annotations và validation messages?

Localization Setup

Configuration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddLocalization(options =>
{
    options.ResourcesPath = "Resources";
});

builder.Services.AddControllersWithViews()
    .AddViewLocalization(LanguageViewLocationFormatSuffixFormat.Suffix)
    .AddDataAnnotationsLocalization();

var app = builder.Build();

// Supported cultures
var supportedCultures = new[] { "en-US", "vi-VN", "fr-FR" };
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture(supportedCultures[0])
    .AddSupportedCultures(supportedCultures)
    .AddSupportedUICultures(supportedCultures);

app.UseRequestLocalization(localizationOptions);

app.Run();

Resource Files

File Structure

Project/
├── Resources/
│   ├── Controllers/
│   │   ├── HomeController.en-US.resx
│   │   ├── HomeController.vi-VN.resx
│   │   └── HomeController.fr-FR.resx
│   ├── Views/
│   │   ├── Home/
│   │   │   ├── Index.en-US.resx
│   │   │   └── Index.vi-VN.resx
│   │   └── Shared/
│   │       └── _Layout.en-US.resx
│   ├── SharedResource.en-US.resx
│   └── SharedResource.vi-VN.resx
└── Program.cs

Resource File Content

<!-- SharedResource.en-US.resx -->
<data name="Hello" xml:space="preserve">
  <value>Hello</value>
</data>
<data name="Welcome" xml:space="preserve">
  <value>Welcome to our application</value>
</data>

<!-- SharedResource.vi-VN.resx -->
<data name="Hello" xml:space="preserve">
  <value>Xin chào</value>
</data>
<data name="Welcome" xml:space="preserve">
  <value>Chào mừng bạn đến với ứng dụng</value>
</data>

Using IStringLocalizer

Basic Usage

public class HomeController : Controller
{
    private readonly IStringLocalizer<SharedResource> _localizer;

    public HomeController(IStringLocalizer<SharedResource> localizer)
    {
        _localizer = localizer;
    }

    public IActionResult Index()
    {
        ViewData["Message"] = _localizer["Hello"];
        ViewData["Welcome"] = _localizer["Welcome"];
        return View();
    }
}

With Parameters

// Resource file
// "Greeting" = "Hello {0}, welcome to {1}"

public IActionResult Index(string name, string city)
{
    var message = _localizer["Greeting", name, city];
    // Output: "Hello John, welcome to New York"
    return View();
}

IStringLocalizer vs IHtmlLocalizer

// IStringLocalizer - plain text
public class MyController : Controller
{
    private readonly IStringLocalizer<MyController> _localizer;

    public MyController(IStringLocalizer<MyController> localizer)
    {
        _localizer = localizer;
    }
}

// IHtmlLocalizer - HTML content (không encode HTML)
public class MyViewComponent : ViewComponent
{
    private readonly IHtmlLocalizer<SharedResource> _localizer;

    public MyViewComponent(IHtmlLocalizer<SharedResource> localizer)
    {
        _localizer = localizer;
    }

    public IViewComponentResult Invoke()
    {
        // Resource: "Bold" = "<strong>Bold text</strong>"
        var html = _localizer["Bold"]; // Không encode HTML
        return View(html);
    }
}

View Localization

In Razor Views

@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer

<h1>@Localizer["Welcome"]</h1>
<p>@Localizer["Hello"]</p>

<!-- Hoặc dùng IHtmlLocalizer -->
@inject IHtmlLocalizer<SharedResource> HtmlLocalizer

<p>@HtmlLocalizer["Description"]</p>

View with Model

@model Product
@inject IViewLocalizer Localizer

<h1>@Localizer["ProductDetails"]</h1>
<div>
    <label>@Localizer["Name"]</label>
    <span>@Model.Name</span>
</div>
<div>
    <label>@Localizer["Price"]</label>
    <span>@Model.Price</span>
</div>

Data Annotations Localization

Localized Validation Messages

public class Product
{
    [Required(ErrorMessage = "NameRequired")]
    [Display(Name = "ProductName")]
    public string Name { get; set; } = string.Empty;

    [Range(0.01, 9999.99, ErrorMessage = "PriceRange")]
    [Display(Name = "ProductPrice")]
    public decimal Price { get; set; }
}

// Resource file
// "NameRequired" = "Tên sản phẩm là bắt buộc"
// "ProductName" = "Tên sản phẩm"
// "PriceRange" = "Giá phải từ 0.01 đến 9999.99"
// "ProductPrice" = "Giá sản phẩm"

Configuration

builder.Services.AddControllersWithViews()
    .AddViewLocalization()
    .AddDataAnnotationsLocalization(options =>
    {
        options.DataAnnotationLocalizerProvider = 
            (type, factory) => factory.Create(typeof(SharedResource));
    });

Culture Providers

Request Culture Providers

┌─────────────────────────────────────────────────────────────────┐
│                  CULTURE PROVIDERS                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Thứ tự kiểm tra (theo mặc định):                               │
│  1. QueryStringRequestCultureProvider                           │
│     ?culture=vi-VN&ui-culture=vi-VN                            │
│                                                                 │
│  2. CookieRequestCultureProvider                                │
│     Cookie: .AspNetCore.Culture=c=vi-VN\|uic=vi-VN             │
│                                                                 │
│  3. AcceptLanguageHeaderRequestCultureProvider                  │
│     Accept-Language: vi-VN, en-US;q=0.9                        │
│                                                                 │
│  Custom Provider:                                               │
│  - Route data (/vi/products, /en/products)                      │
│  - Subdomain (vi.example.com, en.example.com)                   │
│  - Custom header (X-Culture: vi-VN)                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Custom Culture Provider

public class RouteDataRequestCultureProvider : RequestCultureProvider
{
    public int RouteDataIndex = 0;

    public override Task<ProviderCultureResult> DetermineProviderCultureResult(
        HttpContext httpContext)
    {
        var culture = httpContext.Request.RouteValues["culture"]?.ToString();
        
        if (string.IsNullOrEmpty(culture))
        {
            return NullProviderCultureResult;
        }

        var result = new ProviderCultureResult(culture);
        return Task.FromResult(result);
    }
}

// Đăng ký
var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture("en-US")
    .AddSupportedCultures("en-US", "vi-VN");

// Thêm custom provider vào đầu danh sách
localizationOptions.RequestCultureProviders.Insert(
    0, new RouteDataRequestCultureProvider());

app.UseRequestLocalization(localizationOptions);
// Set culture cookie
app.MapGet("/set-culture/{culture}", (string culture, HttpContext context) =>
{
    context.Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(
            new RequestCulture(culture)));
    
    return Results.Redirect("/");
});

Best Practices

1. Use Shared Resources

// ✅ Good - centralized resources
public class SharedResource { } // Empty class

// Inject shared localizer
public class HomeController : Controller
{
    private readonly IStringLocalizer<SharedResource> _localizer;
    
    public HomeController(IStringLocalizer<SharedResource> localizer)
    {
        _localizer = localizer;
    }
}

2. Fallback Culture

var localizationOptions = new RequestLocalizationOptions()
    .SetDefaultCulture("en-US")
    .AddSupportedCultures("en-US", "vi-VN")
    .AddSupportedUICultures("en-US", "vi-VN")
    .SetFallbackCulture("en-US"); // Fallback nếu không tìm thấy culture

3. Culture in URLs

// Route với culture
app.MapControllerRoute(
    name: "localized",
    pattern: "{culture=en-US}/{controller=Home}/{action=Index}");

// URLs:
// /en-US/Home/Index
// /vi-VN/Home/Index

SignalR

Overview Questions

  • SignalR là gì và khi nào cần sử dụng?
  • Hub là gì và cách tạo Hub?
  • Client-server communication hoạt động ra sao?
  • Groups và Users trong SignalR khác nhau như thế nào?
  • Scale SignalR với Redis backplane ra sao?

SignalR Basics

What is SignalR?

┌─────────────────────────────────────────────────────────────────┐
│                    SIGNALR ARCHITECTURE                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │◀──▶│   Hub    │◀──▶│  Client  │                  │
│  │  (Web)   │    │  (Server)│    │  (Web)   │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│       │                                │                        │
│       │         ┌──────────┐           │                        │
│       └────────▶│  Client  │◀──────────┘                        │
│                 │  (Mobile)│                                     │
│                 └──────────┘                                     │
│                                                                 │
│  - Real-time communication                                      │
│  - Server push to clients                                       │
│  - WebSocket, Server-Sent Events, Long Polling                  │
│  - Chat, notifications, live updates                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Setup

dotnet add package Microsoft.AspNetCore.SignalR
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR();

var app = builder.Build();

app.MapHub<ChatHub>("/chat");

app.Run();

Hub

Basic Hub

public class ChatHub : Hub
{
    // Client gọi method này
    public async Task SendMessage(string user, string message)
    {
        // Server gọi method trên tất cả clients
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    // Gửi cho người gọi
    public async Task SendMessageToCaller(string user, string message)
    {
        await Clients.Caller.SendAsync("ReceiveMessage", user, message);
    }

    // Gửi cho người khác (trừ người gọi)
    public async Task SendMessageToOthers(string user, string message)
    {
        await Clients.Others.SendAsync("ReceiveMessage", user, message);
    }
}

Hub with Dependency Injection

public class ChatHub : Hub
{
    private readonly ILogger<ChatHub> _logger;
    private readonly IChatService _chatService;

    public ChatHub(ILogger<ChatHub> logger, IChatService chatService)
    {
        _logger = logger;
        _chatService = chatService;
    }

    public async Task SendMessage(string user, string message)
    {
        _logger.LogInformation("Message from {User}: {Message}", user, message);
        
        // Save to database
        await _chatService.SaveMessage(user, message);
        
        // Broadcast
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

Client Communication

Server to Client

public class NotificationHub : Hub
{
    // Gửi cho tất cả
    public async Task Broadcast(string message)
    {
        await Clients.All.SendAsync("OnBroadcast", message);
    }

    // Gửi cho client cụ thể (theo ConnectionId)
    public async Task SendToClient(string connectionId, string message)
    {
        await Clients.Client(connectionId).SendAsync("OnMessage", message);
    }

    // Gửi cho nhóm
    public async Task SendToGroup(string group, string message)
    {
        await Clients.Group(group).SendAsync("OnMessage", message);
    }

    // Gửi cho user cụ thể (theo UserId)
    public async Task SendToUser(string userId, string message)
    {
        await Clients.User(userId).SendAsync("OnMessage", message);
    }
}

Client to Server

// JavaScript Client
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chat")
    .build();

// Receive message from server
connection.on("ReceiveMessage", (user, message) => {
    console.log(`${user}: ${message}`);
});

// Send message to server
connection.invoke("SendMessage", "John", "Hello!").catch(err => console.error(err));

// Start connection
connection.start().catch(err => console.error(err));

Groups

Group Management

public class ChatHub : Hub
{
    // Join group
    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        
        await Clients.Group(groupName).SendAsync(
            "OnUserJoined", 
            $"{Context.ConnectionId} joined {groupName}");
    }

    // Leave group
    public async Task LeaveGroup(string groupName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
        
        await Clients.Group(groupName).SendAsync(
            "OnUserLeft", 
            $"{Context.ConnectionId} left {groupName}");
    }

    // Send to group
    public async Task SendToGroup(string groupName, string message)
    {
        await Clients.Group(groupName).SendAsync("OnMessage", message);
    }
}

Group Lifecycle

public class ChatHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, "General");
        await Clients.Caller.SendAsync("OnConnected", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, "General");
        await Clients.Others.SendAsync(
            "OnDisconnected", 
            Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }
}

Strongly-typed Hubs

Interface-based Hub

// Define client methods interface
public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
    Task UserJoined(string user);
    Task UserLeft(string user);
}

// Strongly-typed Hub
public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
    {
        // Type-safe client calls
        await Clients.All.ReceiveMessage(user, message);
    }

    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).UserJoined(Context.ConnectionId);
    }
}

Authentication & Authorization

Hub Authorization

// Program.cs
app.MapHub<ChatHub>("/chat", options =>
{
    options.Transports = HttpTransportType.WebSockets | 
                         HttpTransportType.ServerSentEvents;
});

// Hub với authorization
[Authorize]
public class ChatHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        var userId = Context.UserIdentifier; // From JWT claim
        var userName = Context.User?.Identity?.Name;
        
        await base.OnConnectedAsync();
    }
}

Scaling

Redis Backplane

dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis
builder.Services.AddSignalR()
    .AddStackExchangeRedis("localhost:6379");

Azure SignalR Service

dotnet add package Microsoft.Azure.SignalR
builder.Services.AddSignalR()
    .AddAzureSignalR(options =>
    {
        options.ConnectionString = 
            builder.Configuration["Azure:SignalR:ConnectionString"];
    });

// Map hub
app.MapHub<ChatHub>("/chat");

Best Practices

1. Handle Connection Lifecycle

// ✅ Good - handle connection events
public class ChatHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        _logger.LogInformation("Client connected: {ConnectionId}", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        _logger.LogInformation("Client disconnected: {ConnectionId}", Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }
}

2. Use Groups for Targeted Messages

// ✅ Good - use groups for targeted messages
public async Task SendToRoom(string roomId, string message)
{
    await Clients.Group($"room-{roomId}").SendAsync("OnMessage", message);
}

// ❌ Bad - send to all when only room needs it
public async Task SendToRoom(string roomId, string message)
{
    await Clients.All.SendAsync("OnMessage", message); // All clients receive!
}

3. Handle Reconnection

// JavaScript Client - handle reconnection
connection.onclose(async () => {
    await start();
});

async function start() {
    try {
        await connection.start();
        console.log("SignalR connected");
    } catch (err) {
        console.log("SignalR connection failed");
        setTimeout(() => start(), 5000);
    }
}

gRPC in ASP.NET Core

Overview Questions

  • gRPC là gì và khác REST API như thế nào?
  • Protocol Buffers (protobuf) là gì?
  • Làm sao để tạo gRPC service trong ASP.NET Core?
  • Unary, Server Streaming, Client Streaming, Bidirectional Streaming là gì?
  • Khi nào nên dùng gRPC thay vì REST?

gRPC Basics

What is gRPC?

┌─────────────────────────────────────────────────────────────────┐
│                    gRPC vs REST                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  REST:                                                          │
│  ┌──────────┐    HTTP/JSON    ┌──────────┐                     │
│  │  Client  │◀───────────────▶│  Server  │                     │
│  └──────────┘                 └──────────┘                     │
│  - Text-based (JSON)                                              │
│  - Human readable                                                 │
│  - Larger payload size                                            │
│  - Request/Response only                                          │
│                                                                 │
│  gRPC:                                                          │
│  ┌──────────┐   HTTP/2/Proto  ┌──────────┐                     │
│  │  Client  │◀───────────────▶│  Server  │                     │
│  └──────────┘   buf           └──────────┘                     │
│  - Binary (Protocol Buffers)                                    │
│  - Not human readable                                           │
│  - Smaller payload size                                         │
│  - Streaming support                                            │
│  - Strongly typed contracts                                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

When to Use gRPC

┌─────────────────────────────────────────────────────────────────┐
│                    WHEN TO USE gRPC                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Use gRPC for:                                                  │
│  - Microservices communication                                  │
│  - Real-time streaming                                          │
│  - Low latency requirements                                     │
│  - Polyglot environments (multiple languages)                   │
│  - Internal service-to-service calls                            │
│                                                                 │
│  Use REST for:                                                  │
│  - Public APIs                                                  │
│  - Browser clients                                              │
│  - Simple CRUD operations                                       │
│  - When human readability matters                               │
│  - When caching is important                                    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Protocol Buffers

.proto File

// Protos/greet.proto
syntax = "proto3";

option csharp_namespace = "MyGrpcService";

package greet;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
  
  // Streaming: server sends multiple responses
  rpc SayHelloStream (HelloRequest) returns (stream HelloReply);
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greeting.
message HelloReply {
  string message = 1;
}

Project Configuration

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.57.0" />
  </ItemGroup>
</Project>

gRPC Service

Basic Service

public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;

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

    public override Task<HelloReply> SayHello(
        HelloRequest request, 
        ServerCallContext context)
    {
        _logger.LogInformation("Saying hello to {Name}", request.Name);
        
        return Task.FromResult(new HelloReply
        {
            Message = $"Hello {request.Name}"
        });
    }
}

Server Configuration

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();

var app = builder.Build();

app.MapGrpcService<GreeterService>();

app.Run();

Streaming

Server Streaming

service ChatService {
  // Server sends multiple messages
  rpc Subscribe (SubscribeRequest) returns (stream ChatMessage);
}

message SubscribeRequest {
  string channel = 1;
}

message ChatMessage {
  string user = 1;
  string message = 2;
  int64 timestamp = 3;
}
public class ChatService : ChatService.ChatServiceBase
{
    private readonly IChannelReader<ChatMessage> _channel;

    public ChatService(IChannelReader<ChatMessage> channel)
    {
        _channel = channel;
    }

    public override async Task Subscribe(
        SubscribeRequest request,
        IServerStreamWriter<ChatMessage> responseStream,
        ServerCallContext context)
    {
        await foreach (var message in _channel.ReadAllAsync(context.CancellationToken))
        {
            if (message.Channel == request.Channel)
            {
                await responseStream.WriteAsync(message);
            }
        }
    }
}

Client Streaming

service UploadService {
  // Client sends multiple messages
  rpc Upload (stream FileChunk) returns (UploadResponse);
}

message FileChunk {
  bytes data = 1;
  int32 chunk_number = 2;
}

message UploadResponse {
  string file_id = 1;
  int64 total_size = 2;
}
public class UploadService : UploadService.UploadServiceBase
{
    public override async Task<UploadResponse> Upload(
        IAsyncStreamReader<FileChunk> requestStream,
        ServerCallContext context)
    {
        var totalSize = 0L;
        var fileId = Guid.NewGuid().ToString();
        
        await foreach (var chunk in requestStream.ReadAllAsync())
        {
            totalSize += chunk.Data.Length;
            // Process chunk
        }
        
        return new UploadResponse
        {
            FileId = fileId,
            TotalSize = totalSize
        };
    }
}

Bidirectional Streaming

service ChatService {
  // Both client and server stream
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
public class ChatService : ChatService.ChatServiceBase
{
    public override async Task Chat(
        IAsyncStreamReader<ChatMessage> requestStream,
        IServerStreamWriter<ChatMessage> responseStream,
        ServerCallContext context)
    {
        // Read from client and broadcast to all
        await foreach (var message in requestStream.ReadAllAsync())
        {
            // Process and broadcast
            await responseStream.WriteAsync(new ChatMessage
            {
                User = "Server",
                Message = $"Echo: {message.Message}"
            });
        }
    }
}

gRPC Client

Setup Client

dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

Client Code

// Program.cs
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);

// Unary call
var response = await client.SayHelloAsync(
    new HelloRequest { Name = "World" });

Console.WriteLine(response.Message);

gRPC Client Factory

builder.Services.AddGrpcClient<Greeter.GreeterClient>(options =>
{
    options.Address = new Uri("https://localhost:5001");
});

// Usage
public class MyService
{
    private readonly Greeter.GreeterClient _client;

    public MyService(Greeter.GreeterClient client)
    {
        _client = client;
    }

    public async Task<string> Greet(string name)
    {
        var response = await _client.SayHelloAsync(
            new HelloRequest { Name = name });
        return response.Message;
    }
}

Interceptors

Server Interceptor

public class LoggingInterceptor : Interceptor
{
    private readonly ILogger<LoggingInterceptor> _logger;

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

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        _logger.LogInformation("Starting call {Method}", context.Method);
        
        try
        {
            return await continuation(request, context);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error in call {Method}", context.Method);
            throw;
        }
    }
}

// Register
builder.Services.AddGrpc(options =>
{
    options.Interceptors.Add<LoggingInterceptor>();
});

Best Practices

1. Use Appropriate Call Types

// ✅ Unary for simple request/response
var response = await client.SayHelloAsync(request);

// ✅ Server streaming for real-time updates
await foreach (var message in client.SubscribeAsync(request))
{
    Console.WriteLine(message.Message);
}

// ✅ Client streaming for uploads
var uploadResponse = await client.UploadAsync(requestStream);

// ✅ Bidirectional for chat
await foreach (var msg in client.ChatAsync(requestStream))
{
    // Process
}

2. Handle Errors Properly

try
{
    var response = await client.SayHelloAsync(request);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
    Console.WriteLine("Resource not found");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable)
{
    Console.WriteLine("Service unavailable");
}
catch (RpcException ex)
{
    Console.WriteLine($"gRPC error: {ex.StatusCode}");
}

3. Use Deadlines

// Set deadline for call
var deadline = DateTime.UtcNow.AddSeconds(5);
var response = await client.SayHelloAsync(
    request,
    deadline: deadline);

3. Xây dựng Web API

Giới thiệu

Phần này trình bày cách xây dựng RESTful API với ASP.NET Core.

Nội dung chính

RESTful API

Bảo mật

Phiên bản & Tài liệu

RESTful API

Overview Questions

  • REST là gì và tại sao nó quan trọng?
  • Các HTTP methods nào được sử dụng và khi nào dùng?
  • Status codes nào nên dùng cho từng trường hợp?
  • URL naming conventions như thế nào cho đúng chuẩn?
  • Paging, filtering, sorting được implement ra sao?

Thiết kế API đúng chuẩn REST

REST Principles

  1. Client-Server - Tách biệt client và server
  2. Stateless - Mỗi request chứa đủ thông tin
  3. Cacheable - Response có thể được cache
  4. Uniform Interface - Sử dụng HTTP methods và status codes đúng cách
  5. Layered System - Có thể có nhiều layers

HTTP Methods

MethodPurposeIdempotent
GETLấy resource✅ Yes
POSTTạo resource mới❌ No
PUTThay thế toàn bộ resource✅ Yes
PATCHCập nhật một phần resource❌ No
DELETEXóa resource✅ Yes

Status Codes

CodeMeaningUsage
200OKGET, PUT, PATCH thành công
201CreatedPOST tạo mới thành công
204No ContentDELETE thành công
400Bad RequestValidation failed
401UnauthorizedChưa authenticate
403ForbiddenKhông có permission
404Not FoundResource không tồn tại
500Internal Server ErrorServer error

URL Naming Conventions

// ✅ Tốt
GET    /api/products              // Lấy danh sách products
GET    /api/products/1          // Lấy product có id = 1
POST   /api/products             // Tạo product mới
PUT    /api/products/1          // Cập nhật product 1
DELETE /api/products/1          // Xóa product 1

// ❌ Tránh
GET    /api/getProducts
GET    /api/ProductController/GetAll
POST   /api/createProduct

Model Binding & Validation

Note: Model Binding và Model Validation đã được chuyển sang các file riêng để dễ học hơn:

Quick Reference

[ApiController]
public class ProductsController : ControllerBase
{
    // FromRoute - Từ URL path
    [HttpGet("{id}")]
    public IActionResult GetById([FromRoute] int id)
    {
        return Ok(id);
    }
    
    // FromQuery - Từ query string
    [HttpGet]
    public IActionResult Search([FromQuery] string name, [FromQuery] int page = 1)
    {
        return Ok(new { name, page });
    }
    
    // FromBody - Từ request body (JSON)
    [HttpPost]
    public IActionResult Create([FromBody] Product product)
    {
        // [ApiController] tự động validate ModelState
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
}

Paging, Filtering, Sorting

[HttpGet]
public IActionResult GetProducts(
    [FromQuery] PagingParameters paging,
    [FromQuery] ProductFilter filter,
    [FromQuery] string sortBy = "Name")
{
    var query = _context.Products.AsQueryable();
    
    // Filtering
    if (!string.IsNullOrEmpty(filter.Category))
        query = query.Where(p => p.Category == filter.Category);
    
    if (filter.MinPrice.HasValue)
        query = query.Where(p => p.Price >= filter.MinPrice.Value);
    
    // Sorting
    query = sortBy?.ToLower() switch
    {
        "price" => query.OrderBy(p => p.Price),
        "pricedesc" => query.OrderByDescending(p => p.Price),
        _ => query.OrderBy(p => p.Name)
    };
    
    // Paging
    var total = query.Count();
    var items = query
        .Skip((paging.Page - 1) * paging.PageSize)
        .Take(paging.PageSize)
        .ToList();
    
    return Ok(new PagedResult(items, total, paging.Page, paging.PageSize));
}

public class PagingParameters
{
    [FromQuery(Name = "page")]
    public int Page { get; set; } = 1;
    
    [FromQuery(Name = "pageSize")]
    public int PageSize { get; set; } = 10;
}

public class ProductFilter
{
    [FromQuery(Name = "category")]
    public string Category { get; set; }
    
    [FromQuery(Name = "minPrice")]
    public decimal? MinPrice { get; set; }
    
    [FromQuery(Name = "maxPrice")]
    public decimal? MaxPrice { get; set; }
}

Bảo mật

Authentication (Xác thực)

JWT (JSON Web Token)

JWT là một chuẩn token để truyền thông tin an toàn giữa các parties dưới dạng JSON.

Cấu trúc JWT

┌─────────────────────────────────────────────────────────────────────┐
│                        JWT STRUCTURE                                │
├─────────────────┬─────────────────┬───────────────────────────────┤
│    HEADER       │     PAYLOAD     │         SIGNATURE             │
│  (Base64URL)    │   (Base64URL)   │       (Base64URL)             │
├─────────────────┼─────────────────┼───────────────────────────────┤
│ {               │ {               │ HmacSHA256(                   │
│   "alg":        │   "sub":        │   header + "." + payload,     │
│     "HS256",    │     "1234567890",│   secret_key                  │
│   "typ":        │   "name":       │ )                             │
│     "JWT"       │     "John Doe", │                               │
│ }               │   "iat":         │                               │
│                 │     1516239022,  │                               │
│                 │   "exp":         │                               │
│                 │     1516242622   │                               │
│                 │ }               │                               │
└─────────────────┴─────────────────┴───────────────────────────────┘

Cấu hình JwtBearer trong .NET Core

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Add Authentication
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = 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:SecretKey"]))
    };
});

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

Tạo JWT Token

public class JwtService
{
    private readonly JwtSettings _settings;
    
    public string GenerateToken(User user)
    {
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new Claim(JwtRegisteredClaimNames.Email, user.Email),
            new Claim(ClaimTypes.Role, user.Role),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };
        
        var key = new SymmetricSecurityKey(
            Encoding.UTF8.GetBytes(_settings.SecretKey));
        var credentials = new SigningCredentials(
            key, SecurityAlgorithms.HmacSha256);
        
        var token = new JwtSecurityToken(
            issuer: _settings.Issuer,
            audience: _settings.Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(_settings.ExpiryMinutes),
            signingCredentials: credentials);
        
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Authorization (Phân quyền)

[Authorize] Attribute

// Yêu cầu authenticate
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile() { }

// Yêu cầu role cụ thể
[Authorize(Roles = "Admin")]
[HttpGet("admin")]
public IActionResult GetAdminData() { }

// Yêu cầu nhiều roles (AND logic)
[Authorize(Roles = "Admin,Manager")]
[HttpGet("manage")]
public IActionResult Manage() { }

// Yếu tố OR - dùng Policy
[Authorize(Policy = "AdminOrManager")]
[HttpGet("manage")]
public IActionResult Manage() { }

Role-based Authorization

[Authorize(Roles = "Admin")]
public class AdminController : ControllerBase
{
    [HttpGet("users")]
    public IActionResult GetAllUsers()
    {
        // Only admins can access
        return Ok();
    }
}

Policy-based Authorization

// Đăng ký policy trong Program.cs
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdultOnly", policy =>
        policy.RequireClaim("Age", "18", "19", "20", "21", "22", "23", "24", "25"));
    
    options.AddPolicy("PremiumUser", policy =>
        policy.RequireAssertion(context =>
            context.User.HasClaim(c => c.Type == "Subscription" && 
                                      c.Value == "Premium")));
    
    options.AddPolicy("CanDeleteProduct", policy =>
        policy.RequireAssertion(context =>
            context.User.IsInRole("Admin") ||
            (context.User.IsInRole("Manager") && 
             context.User.HasClaim(c => c.Type == "CanDelete"))));
});

// Sử dụng
[Authorize(Policy = "PremiumUser")]
[HttpGet("premium-content")]
public IActionResult GetPremiumContent() { }

CORS (Cross-Origin Resource Sharing)

Vấn đề

Browser chặn requests từ một domain khác với server (cross-origin requests) vì lý do bảo mật. CORS cho phép server chỉ định origins nào được phép truy cập.

Cấu hình CORS

// Program.cs
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
    {
        policy.WithOrigins("http://localhost:3000", "https://myapp.com")
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials(); // Chỉ dùng với specific origins
    });
    
    options.AddPolicy("AllowAll", policy =>
    {
        policy.AllowAnyOrigin()
              .AllowAnyHeader()
              .AllowAnyMethod();
    });
});

var app = builder.Build();

app.UseCors("AllowFrontend");

CORS với named policy

[ApiController]
[Route("api/[controller]")]
[EnableCors("AllowFrontend")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Ok();
    
    [HttpGet]
    [DisableCors] // Disable CORS cho action cụ thể
    public IActionResult GetSecret() => Ok();
}

Preflight Request

┌─────────────────────────────────────────────────────────────────────┐
│                     CORS REQUEST FLOW                              │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Browser                                                        │
│      │                                                              │
│      │ 1. OPTIONS /api/products                                  │
│      │    Access-Control-Request-Method: GET                     │
│      │    Access-Control-Request-Headers: Content-Type          │
│      ├────────────────────────────────────────────────────────►   │
│      │◄────────────────────────────────────────────────────────┤  │
│      │ 2. 200 OK                                                 │
│      │    Access-Control-Allow-Origin: http://localhost:3000   │
│      │    Access-Control-Allow-Methods: GET, POST, PUT, DELETE │
│      │    Access-Control-Allow-Headers: Content-Type            │
│      │                                                              │
│      │ 3. GET /api/products                                      │
│      ├────────────────────────────────────────────────────────►   │
│      │◄────────────────────────────────────────────────────────┤  │
│      │ 4. 200 OK                                                 │
│      │    Access-Control-Allow-Origin: http://localhost:3000   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Phiên bản & Tài liệu

API Versioning

Cài đặt

dotnet add package Microsoft.AspNetCore.Mvc.Versioning

Cấu hình

// Program.cs
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new HeaderApiVersionReader("X-Api-Version"),
        new QueryStringApiVersionReader("v"));
});

Versioning Strategies

1. URL Path (phổ biến nhất)

[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{v:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public IActionResult GetV1() => Ok(new { version = "1.0" });
    
    [HttpGet]
    [MapToApiVersion("2.0")]
    public IActionResult GetV2() => Ok(new { version = "2.0", extra = "data" });
}

2. Query String

GET /api/products?api-version=1.0
GET /api/products?api-version=2.0

3. Header

GET /api/products
X-Api-Version: 1.0

Deprecation

[ApiVersion("1.0")]
[ApiVersion("2.0", Deprecated = true)] // Mark as deprecated
[Route("api/v{v:apiVersion}/[controller]")]
public class ProductsController : ControllerBase { }

Swagger/OpenAPI

Cài đặt

dotnet add package Swashbuckle.AspNetCore

Cấu hình

// Program.cs
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "My API",
        Version = "v1",
        Description = "API Description",
        Contact = new OpenApiContact
        {
            Name = "Support",
            Email = "support@example.com"
        }
    });
    
    // Add JWT Authentication to Swagger
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT Authorization header using the Bearer scheme",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer"
    });
    
    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            Array.Empty<string>()
        }
    });
});

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1");
    c.RoutePrefix = "swagger"; // Access at /swagger
});

XML Documentation

// Program.cs
builder.Services.AddSwaggerGen(c =>
{
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    c.IncludeXmlComments(xmlPath);
});

/// <summary>
/// Get all products
/// </summary>
/// <param name="category">Filter by category</param>
/// <returns>List of products</returns>
[HttpGet]
[ProducesResponseType(typeof(List<Product>), 200)]
[ProducesResponseType(400)]
public IActionResult GetProducts([FromQuery] string category) { }

Swagger Response Attributes

[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult Create([FromBody] Product product)
{
    // ...
}

Best Practices cho API Versioning

1. Chọn chiến lược version phù hợp

StrategyProsCons
URL PathRõ ràng, dễ debugPhải update client URLs
Query StringKhông thay đổi URLÍt visible
HeaderLinh hoạtKhó debug

2. Support backwards compatibility

// ✅ Tốt - Add new fields, không break old
public class ProductResponse
{
    public int Id { get; set; }
    public string Name { get; set; }
    // New in v2
    public string Description { get; set; } 
}

// ❌ Tránh - Breaking changes
// - Remove fields
// - Change field types
// - Change validation rules

3. Document changes

# OpenAPI spec
components:
  schemas:
    ProductV1:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
    ProductV2:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        description:  # New field
          type: string

4. Deprecation policy

  • Thông báo deprecation trước khi remove
  • Sử dụng HTTP headers để warning
  • Cung cấp migration guide

4. Truy cập Dữ liệu với EF Core

Giới thiệu

Entity Framework Core là ORM (Object-Relational Mapper) của Microsoft, cho phép làm việc với database bằng cách sử dụng đối tượng C# thay vì SQL queries trực tiếp.

Nội dung chính

Cơ bản & Thiết kế

Tối ưu hiệu suất

Giao dịch & Đồng thời

EF Core - Cơ bản & Thiết kế

Code First vs Database First

Code First

Tạo database từ C# classes:

// Models
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }
}

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Product> Products { get; set; }
}

// DbContext
public class AppDbContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer("ConnectionString");
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>(entity =>
        {
            entity.HasKey(p => p.Id);
            entity.Property(p => p.Name).IsRequired().HasMaxLength(100);
            entity.Property(p => p.Price).HasColumnType("decimal(18,2)");
        });
    }
}

Database First

Reverse engineer từ existing database:

# Scaffold từ database
dotnet ef dbcontext scaffold "ConnectionString" Microsoft.EntityFrameworkCore.SqlServer

# Với options
dotnet ef dbcontext scaffold "ConnectionString" Microsoft.EntityFrameworkCore.SqlServer `
    --table Products,Categories `
    --context AppDbContext `
    --output-dir Models

Migrations

Cài đặt

dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design

Commands

# Tạo migration
dotnet ef migrations add InitialCreate

# Apply migrations
dotnet ef database update

# Update với migration cụ thể
dotnet ef database update 20240101000000_InitialCreate

# Rollback
dotnet ef database update PreviousMigrationName

# Remove last migration
dotnet ef migrations remove

# List migrations
dotnet ef migrations list

# Generate SQL script
dotnet ef migrations script

Migration Structure

public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Categories",
            columns: table => new
            {
                Id = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Categories", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(name: "Categories");
    }
}

DbContext và Change Tracker

Change Tracker

EF Core theo dõi các thay đổi trên entities:

var context = new AppDbContext();

// Read
var product = context.Products.First(); // Tracked

// Update
product.Price = 99.99m;
// EF tự động mark là Modified

// Delete
context.Products.Remove(product);
// EF mark là Deleted

// SaveChanges - Tạo SQL UPDATE/DELETE
context.SaveChanges();

Entity States

StateDescription
DetachedKhông được track
AddedMới, chưa có trong database
UnchangedKhông thay đổi
ModifiedĐã thay đổi
DeletedĐánh dấu xóa

Tracking Behavior

// Tracked (default)
var product = context.Products.First();

// No tracking - tốt cho read-only
var product = context.Products.AsNoTracking().First();

// Chỉ định rõ ràng tracking
var product = context.Products.AsTracking().First();

// Kiểm tra state
var entry = context.Entry(product);
Console.WriteLine(entry.State); // Modified

DbContext Lifetime

// ✅ Tốt - Scoped cho mỗi request
builder.Services.AddScoped<AppDbContext>();

// ❌ Tránh - Singleton
builder.Services.AddSingleton<AppDbContext>(); // Bad practice!

Relationships Configuration

One-to-Many

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<Book> Books { get; set; }
}

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public int AuthorId { get; set; }
    public Author Author { get; set; }
}

// Configuration
modelBuilder.Entity<Book>()
    .HasOne(b => b.Author)
    .WithMany(a => a.Books)
    .HasForeignKey(b => b.AuthorId);

One-to-One

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public StudentProfile Profile { get; set; }
}

public class StudentProfile
{
    public int Id { get; set; }
    public string Bio { get; set; }
    public int StudentId { get; set; }
    public Student Student { get; set; }
}

// Configuration
modelBuilder.Entity<Student>()
    .HasOne(s => s.Profile)
    .WithOne(p => p.Student)
    .HasForeignKey<StudentProfile>(p => p.StudentId);

Many-to-Many

public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<StudentCourse> StudentCourses { get; set; }
}

public class Course
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<StudentCourse> StudentCourses { get; set; }
}

// Junction table
public class StudentCourse
{
    public int StudentId { get; set; }
    public Student Student { get; set; }
    public int CourseId { get; set; }
    public Course Course { get; set; }
}

// Cấu hình composite key
modelBuilder.Entity<StudentCourse>()
    .HasKey(sc => new { sc.StudentId, sc.CourseId });

EF Core - Tối ưu Hiệu suất

N+1 Query Problem

Vấn đề

N+1 xảy ra khi bạn load một list objects, sau đó access navigation property của từng object (lazy loading):

// ❌ BAD - N+1 Query
var products = context.Products.ToList();  // 1 query

foreach (var product in products)
{
    Console.WriteLine(product.Category.Name); // N queries!
}

// Generated SQL:
// SELECT * FROM Products        -- 1 query
// SELECT * FROM Categories WHERE Id = 1  -- N queries!

Giải pháp

1. Eager Loading với Include

// ✅ Sử dụng Include
var products = context.Products
    .Include(p => p.Category)
    .ToList();

// Generated SQL:
// SELECT p.*, c.* FROM Products p
// LEFT JOIN Categories c ON p.CategoryId = c.Id

2. ThenInclude cho nested relationships

var orders = context.Orders
    .Include(o => o.Customer)
        .ThenInclude(c => c.Address)
    .Include(o => o.OrderItems)
        .ThenInclude(oi => oi.Product)
            .ThenInclude(p => p.Category)
    .ToList();

3. Explicit Loading

var product = context.Products.First();

// Load Category explicitly
await context.Entry(product)
    .Reference(p => p.Category)
    .LoadAsync();

// Load Collection explicitly  
await context.Entry(product)
    .Collection(p => p.Reviews)
    .LoadAsync();

4. Projections (Select)

// ✅ Tốt - Chỉ lấy những gì cần
var productDtos = context.Products
    .Select(p => new ProductDto
    {
        Id = p.Id,
        Name = p.Name,
        CategoryName = p.Category.Name
    })
    .ToList();

// Generated SQL: Chỉ select Id, Name, CategoryName

AsNoTracking

Khi nào sử dụng

// ✅ Sử dụng AsNoTracking() cho read-only queries
var products = context.Products
    .AsNoTracking()
    .Where(p => p.Price > 100)
    .ToList();

// ✅ Hoặc với AsNoTrackingWithIdentityResolution
var products = context.Products
    .AsNoTrackingWithIdentityResolution()
    .Include(p => p.Category)
    .ToList();

So sánh

MethodTrackingIdentity ResolutionPerformance
AsNoTracking()❌ No❌ NoFastest
AsTracking()✅ Yes✅ YesDefault
AsNoTrackingWithIdentityResolution()❌ No✅ YesFast

Lưu ý

// ❌ AsNoTracking - Không thể save changes
var product = context.Products.AsNoTracking().First();
product.Name = "New Name";
context.SaveChanges(); // Không có gì thay đổi!

// ✅ AsTracking - Có thể save changes  
var product = context.Products.First();
product.Name = "New Name";
context.SaveChanges(); // Update thành công

Compiled Queries

Cache query plan

// Đăng ký compiled query
private static readonly Func<AppDbContext, IQueryable<Product>> 
    GetAllProducts = EF.CompileQuery((AppDbContext ctx) => 
        ctx.Products.Include(p => p.Category));

// Sử dụng
using (var context = new AppDbContext())
{
    var products = GetAllProducts(context).ToList();
}

Pagination

Skip/Take

var page = 1;
var pageSize = 10;

var products = context.Products
    .OrderBy(p => p.Name)
    .Skip((page - 1) * pageSize)
    .Take(pageSize)
    .ToList();

Batch Size

// Cấu hình batch size
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
    options.UseSqlServer(
        "ConnectionString",
        options => options.MaxBatchSize(100));
}

Split Queries

Giải quyết vấn đề cartesian product

// ✅ Sử dụng SplitQuery khi có nhiều collections
var orders = context.Orders
    .Include(o => o.OrderItems)
    .AsSplitQuery()
    .ToList();

// Generated SQL: 2 queries thay vì 1 với cartesian product

Raw SQL Queries

FormattedRawSql

// Raw SQL với parameters
var products = context.Products
    .FromSqlRaw("SELECT * FROM Products WHERE Price > {0}", minPrice)
    .ToList();

// Stored Procedure
var products = context.Products
    .FromSqlRaw("EXEC GetProducts @CategoryId = {0}", categoryId)
    .ToList();

Bulk Operations

Install package

dotnet add package EFCore.BulkExtensions

Bulk Insert

var entities = Enumerable.Range(0, 1000)
    .Select(i => new Product { Name = $"Product {i}", Price = i })
    .ToList();

context.BulkInsert(entities);

Bulk Update

context.BulkUpdate(entities);

Bulk Delete

context.BulkDelete(entitiesToDelete);

EF Core - Giao dịch & Đồng thời

Transactions

Basic Transaction

using var transaction = await context.Database.BeginTransactionAsync();

try
{
    var order = new Order { CustomerId = 1 };
    context.Orders.Add(order);
    await context.SaveChangesAsync();
    
    var orderItem = new OrderItem { OrderId = order.Id, ProductId = 1 };
    context.OrderItems.Add(orderItem);
    await context.SaveChangesAsync();
    
    await transaction.CommitAsync();
}
catch (Exception ex)
{
    await transaction.RollbackAsync();
    throw;
}

Transaction với Isolation Level

var transaction = await context.Database.BeginTransactionAsync(
    System.Data.IsolationLevel.Serializable);

try
{
    // Operations với Serializable isolation
    await transaction.CommitAsync();
}
finally
{
    await transaction.DisposeAsync();
}

Transaction với Database Transaction

await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();

await using var transaction = await connection.BeginTransactionAsync();
try
{
    var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = "INSERT INTO Products VALUES (@name, @price)";
    
    var param1 = command.CreateParameter();
    param1.ParameterName = "@name";
    param1.Value = "Product";
    
    var param2 = command.CreateParameter();
    param2.ParameterName = "@price";
    param2.Value = 9.99m;
    
    command.Parameters.Add(param1);
    command.Parameters.Add(param2);
    
    await command.ExecuteNonQueryAsync();
    
    await transaction.CommitAsync();
}
catch
{
    await transaction.RollbackAsync();
    throw;
}

Concurrency (Xung đột dữ liệu)

Vấn đề Concurrency

User A reads product (Version = 1)
User B reads product (Version = 1)
User A updates price to 50, saves (Version = 1 → 2)
User B updates price to 60, saves (Version = 1 → ?)
                              ↓
                    CONFLICT - User B should fail!

Giải pháp: RowVersion/Timestamp

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    [Timestamp]
    public byte[] RowVersion { get; set; }
}

Cấu hình Fluent API

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .Property(p => p.RowVersion)
        .IsRowVersion(); // SQL Server会自动使用 rowversion
    
    // Hoặc cho các database khác
    modelBuilder.Entity<Product>()
        .Property(p => p.RowVersion)
        .IsConcurrencyToken();
}

Xử lý DbUpdateConcurrencyException

public async Task<bool> UpdateProduct(Product product)
{
    try
    {
        context.Products.Update(product);
        await context.SaveChangesAsync();
        return true;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        // Get entry để xử lý
        var entry = ex.Entries.Single();
        
        // Lấy database values
        var databaseValues = await entry.GetDatabaseValuesAsync();
        
        // Option 1: Reload từ database
        await entry.ReloadAsync();
        
        // Option 2: Merge với client values
        var clientValues = entry.CurrentValues;
        var databaseValues = await entry.GetDatabaseValuesAsync();
        
        // Log hoặc thông báo cho user
        Console.WriteLine("Concurrency conflict detected!");
        
        return false;
    }
}

Retry on Concurrency

public async Task<bool> UpdateProductWithRetry(Product product)
{
    var retryCount = 0;
    const int maxRetries = 3;
    
    while (retryCount < maxRetries)
    {
        try
        {
            context.Products.Update(product);
            await context.SaveChangesAsync();
            return true;
        }
        catch (DbUpdateConcurrencyException)
        {
            retryCount++;
            if (retryCount >= maxRetries) throw;
            
            // Reload và retry
            var entry = context.Entry(product);
            await entry.ReloadAsync();
        }
    }
    
    return false;
}

Client-Side Concurrency Token

Custom concurrency token

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    // Sử dụng property khác làm concurrency token
    public DateTime UpdatedAt { get; set; }
}

// Cấu hình
modelBuilder.Entity<Product>()
    .Property(p => p.UpdatedAt)
    .IsConcurrencyToken();

Optimistic vs Pessimistic Concurrency

Optimistic (EF Core default)

  • Không lock records
  • Kiểm tra version khi save
  • Nếu conflict → exception
  • Phù hợp cho low-contention scenarios

Pessimistic

  • Lock records trước khi update
  • Sử dụng explicit transactions
  • Phù hợp cho high-contention scenarios
// Pessimistic lock example
await using var transaction = await context.Database.BeginTransactionAsync();

var product = await context.Products
    .FromSqlRaw("SELECT * FROM Products WITH (UPDLOCK) WHERE Id = {0}", id)
    .FirstAsync();

// Update product
product.Price = newPrice;
await context.SaveChangesAsync();

await transaction.CommitAsync();

5. Kiến trúc Phần mềm

Giới thiệu

Phần này trình bày các nguyên tắc và pattern kiến trúc phần mềm phổ biến.

Nội dung chính

Nguyên tắc & Pattern

  • SOLID - Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion
  • Design Patterns - Repository, Singleton, Factory, Strategy, Observer

Kiến trúc Ứng dụng

  • Clean Architecture - Layer separation
  • DDD - Domain-Driven Design
  • CQRS - Command Query Responsibility Segregation với MediatR

Microservices

Nguyên tắc & Pattern

SOLID

1. Single Responsibility Principle (SRP)

Mỗi class chỉ có một lý do để thay đổi.

// ❌ BAD - Nhiều responsibilities
public class User
{
    public void Save() { /* Save to database */ }
    public void SendEmail() { /* Send email */ }
    public void GenerateReport() { /* Generate report */ }
}

// ✅ GOOD - Tách thành các classes riêng biệt
public class UserRepository
{
    public void Save(User user) { /* Save to database */ }
}

public class EmailService
{
    public void Send(User user) { /* Send email */ }
}

public class ReportGenerator
{
    public void Generate(User user) { /* Generate report */ }
}

2. Open/Closed Principle (OCP)

Class open cho việc mở rộng, closed cho việc sửa đổi.

// ❌ BAD - Phải sửa class khi thêm payment method mới
public class PaymentProcessor
{
    public void Process(Payment payment)
    {
        if (payment.Type == PaymentType.CreditCard)
        {
            // Process credit card
        }
        else if (payment.Type == PaymentType.PayPal)
        {
            // Process PayPal
        }
        // Thêm method mới phải sửa code ở đây!
    }
}

// ✅ GOOD - Sử dụng polymorphism
public interface IPaymentMethod
{
    void Process(decimal amount);
}

public class CreditCardPayment : IPaymentMethod
{
    public void Process(decimal amount) { /* Credit card logic */ }
}

public class PayPalPayment : IPaymentMethod
{
    public void Process(decimal amount) { /* PayPal logic */ }
}

public class PaymentProcessor
{
    public void Process(IPaymentMethod paymentMethod, decimal amount)
    {
        paymentMethod.Process(amount); // Không cần sửa class này!
    }
}

3. Liskov Substitution Principle (LSP)

Objects của subclass có thể thay thế objects của parent class.

public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
    public int Area => Width * Height;
}

public class Square : Rectangle
{
    private int _side;
    
    public override int Width
    {
        get => _side;
        set { _side = value; Height = value; }
    }
    
    public override int Height
    {
        get => _side;
        set { _side = value; Width = value; }
    }
}

// ❌ BAD - Violates LSP
void CalculateArea(Rectangle rect)
{
    rect.Width = 5;
    rect.Height = 4;
    Console.WriteLine(rect.Area); // Expect 20, Square returns 16!
}

4. Interface Segregation Principle (ISP)

Nhiều interfaces nhỏ tốt hơn một interface lớn.

// ❌ BAD - Fat interface
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
}

public class Robot : IWorker
{
    public void Work() { /* Work */ }
    public void Eat() { /* Robot doesn't eat! */ } // Violates ISP
    public void Sleep() { /* Robot doesn't sleep! */ }
}

// ✅ GOOD - Separate interfaces
public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public interface ISleepable
{
    void Sleep();
}

public class Human : IWorkable, IFeedable, ISleepable { }
public class Robot : IWorkable { }

5. Dependency Inversion Principle (DIP)

Depend on abstractions, not concretions.

// ❌ BAD - Direct dependency on concrete class
public class OrderService
{
    private readonly EmailSender _emailSender; // Concrete class
    
    public OrderService()
    {
        _emailSender = new EmailSender();
    }
}

// ✅ GOOD - Depend on abstraction
public interface IEmailSender
{
    void Send(string to, string subject, string body);
}

public class OrderService
{
    private readonly IEmailSender _emailSender; // Abstraction
    
    public OrderService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }
}

Design Patterns

Repository Pattern

public interface IRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task AddAsync(T entity);
    void Update(T entity);
    void Delete(T entity);
}

public class Repository<T> : IRepository<T> where T : class
{
    private readonly DbContext _context;
    private readonly DbSet<T> _dbSet;

    public Repository(DbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public async Task<T> GetByIdAsync(int id) =>
        await _dbSet.FindAsync(id);

    public async Task<IEnumerable<T>> GetAllAsync() =>
        await _dbSet.ToListAsync();

    public async Task AddAsync(T entity) =>
        await _dbSet.AddAsync(entity);

    public void Update(T entity) =>
        _context.Entry(entity).State = EntityState.Modified;

    public void Delete(T entity) =>
        _dbSet.Remove(entity);
}

Factory Pattern

public interface IPaymentFactory
{
    IPayment CreatePayment(PaymentType type);
}

public class PaymentFactory : IPaymentFactory
{
    public IPayment CreatePayment(PaymentType type)
    {
        return type switch
        {
            PaymentType.CreditCard => new CreditCardPayment(),
            PaymentType.PayPal => new PayPalPayment(),
            PaymentType.BankTransfer => new BankTransferPayment(),
            _ => throw new ArgumentException("Invalid payment type")
        };
    }
}

// Sử dụng
public class OrderService
{
    private readonly IPaymentFactory _factory;
    
    public OrderService(IPaymentFactory factory)
    {
        _factory = factory;
    }
    
    public void ProcessOrder(Order order)
    {
        var payment = _factory.CreatePayment(order.PaymentType);
        payment.Process(order.Amount);
    }
}

Strategy Pattern

public interface ISortingStrategy<T>
{
    IEnumerable<T> Sort(IEnumerable<T> items);
}

public class QuickSortStrategy<T> : ISortingStrategy<T>
{
    public IEnumerable<T> Sort(IEnumerable<T> items) { /* QuickSort */ }
}

public class BubbleSortStrategy<T> : ISortingStrategy<T>
{
    public IEnumerable<T> Sort(IEnumerable<T> items) { /* BubbleSort */ }
}

public class Sorter<T>
{
    private readonly ISortingStrategy<T> _strategy;

    public Sorter(ISortingStrategy<T> strategy)
    {
        _strategy = strategy;
    }

    public IEnumerable<T> Sort(IEnumerable<T> items) =>
        _strategy.Sort(items);
}

Singleton Pattern

// Singleton với Lazy<T>
public sealed class Singleton
{
    private static readonly Lazy<Singleton> _instance = 
        new(() => new Singleton());

    public static Singleton Instance => _instance.Value;

    private Singleton() { }
}

Kiến trúc Ứng dụng

Clean Architecture

Layer Structure

┌─────────────────────────────────────────────────────────────────┐
│                      PRESENTATION LAYER                         │
│   (Controllers, Views, DTOs, ViewModels)                       │
├─────────────────────────────────────────────────────────────────┤
│                      APPLICATION LAYER                          │
│   (Use Cases, Commands, Queries, Services, DTOs)                │
├─────────────────────────────────────────────────────────────────┤
│                        DOMAIN LAYER                             │
│   (Entities, Value Objects, Domain Services, Interfaces)       │
├─────────────────────────────────────────────────────────────────┤
│                     INFRASTRUCTURE LAYER                        │
│   (DbContext, Repositories, External Services, Logging)         │
└─────────────────────────────────────────────────────────────────┘

Project Structure

src/
├── Domain/
│   ├── Entities/
│   │   └── Product.cs
│   ├── ValueObjects/
│   │   └── Money.cs
│   ├── Interfaces/
│   │   ├── IProductRepository.cs
│   │   └── IEmailService.cs
│   └── Services/
│       └── DomainProductService.cs
│
├── Application/
│   ├── Commands/
│   │   ├── CreateProductCommand.cs
│   │   └── UpdateProductCommand.cs
│   ├── Queries/
│   │   └── GetProductsQuery.cs
│   ├── DTOs/
│   │   └── ProductDto.cs
│   └── Services/
│       └── ApplicationProductService.cs
│
├── Infrastructure/
│   ├── Data/
│   │   ├── AppDbContext.cs
│   │   └── Repositories/
│   │       └── ProductRepository.cs
│   └── Services/
│       └── EmailService.cs
│
└── Presentation/
    └── Controllers/
        └── ProductsController.cs

DDD (Domain-Driven Design)

Domain Entities

public class Order : Entity
{
    public Guid Id { get; private set; }
    public CustomerId CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public Money TotalAmount { get; private set; }
    
    private Order() { } // For EF Core
    
    public static Order Create(CustomerId customerId)
    {
        return new Order
        {
            Id = Guid.NewGuid(),
            CustomerId = customerId,
            Status = OrderStatus.Draft,
            Items = new List<OrderItem>(),
            TotalAmount = Money.Zero
        };
    }
    
    public void AddItem(Product product, int quantity)
    {
        // Domain logic
        if (Status != OrderStatus.Draft)
            throw new DomainException("Cannot add items to confirmed order");
            
        Items.Add(OrderItem.Create(product, quantity));
        RecalculateTotal();
    }
}

Value Objects

public record Money(decimal Amount, string Currency)
{
    public static Money Zero => new(0, "USD");
    
    public static Money operator +(Money a, Money b)
    {
        if (a.Currency != b.Currency)
            throw new DomainException("Cannot add different currencies");
        return new Money(a.Amount + b.Amount, a.Currency);
    }
}

public record Address(
    string Street,
    string City,
    string State,
    string ZipCode,
    string Country)
{
    public string FullAddress => $"{Street}, {City}, {State} {ZipCode}, {Country}";
}

Aggregates

public class OrderAggregate
{
    private readonly List<OrderItem> _items = new();
    
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
    
    public void AddItem(Product product, int quantity)
    {
        // Aggregate boundary - maintain invariants
        if (quantity <= 0)
            throw new DomainException("Quantity must be positive");
            
        var existing = _items.FirstOrDefault(i => i.ProductId == product.Id);
        if (existing != null)
            existing.IncreaseQuantity(quantity);
        else
            _items.Add(OrderItem.Create(product, quantity));
    }
}

CQRS (Command Query Responsibility Segregation)

Command Handler

public class CreateProductCommand : IRequest<ProductDto>
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
}

public class CreateProductCommandHandler 
    : IRequestHandler<CreateProductCommand, ProductDto>
{
    private readonly AppDbContext _context;
    
    public CreateProductCommandHandler(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<ProductDto> Handle(
        CreateProductCommand request, 
        CancellationToken cancellationToken)
    {
        var product = new Product
        {
            Name = request.Name,
            Price = request.Price,
            CategoryId = request.CategoryId
        };
        
        _context.Products.Add(product);
        await _context.SaveChangesAsync(cancellationToken);
        
        return new ProductDto
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price
        };
    }
}

Query Handler

public class GetProductsQuery : IRequest<List<ProductDto>>
{
    public int? CategoryId { get; set; }
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 10;
}

public class GetProductsQueryHandler 
    : IRequestHandler<GetProductsQuery, List<ProductDto>>
{
    private readonly AppDbContext _context;
    
    public GetProductsQueryHandler(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<List<ProductDto>> Handle(
        GetProductsQuery request, 
        CancellationToken cancellationToken)
    {
        var query = _context.Products
            .AsNoTracking();
            
        if (request.CategoryId.HasValue)
            query = query.Where(p => p.CategoryId == request.CategoryId);
            
        return await query
            .Select(p => new ProductDto
            {
                Id = p.Id,
                Name = p.Name,
                Price = p.Price
            })
            .Skip((request.Page - 1) * request.PageSize)
            .Take(request.PageSize)
            .ToListAsync(cancellationToken);
    }
}

MediatR Integration

// Program.cs
builder.Services.AddMediatR(cfg => 
    cfg.RegisterServicesFromAssembly(typeof(CreateProductCommand).Assembly));

// Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;
    
    public ProductsController(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateProductCommand command)
    {
        var result = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
    }
    
    [HttpGet]
    public async Task<IActionResult> GetAll([FromQuery] GetProductsQuery query)
    {
        var result = await _mediator.Send(query);
        return Ok(result);
    }
}

Separate Read/Write Models

// Write Model (Command)
public class CreateOrderCommand
{
    public List<CreateOrderItemCommand> Items { get; set; }
    public Guid CustomerId { get; set; }
}

// Read Model (Query)
public class OrderSummaryDto
{
    public Guid Id { get; set; }
    public string CustomerName { get; set; }
    public int ItemCount { get; set; }
    public decimal TotalAmount { get; set; }
    public string Status { get; set; }
}

Microservices

Nguyên tắc Microservice

Đặc điểm chính

CharacteristicDescription
Single ResponsibilityMỗi service chỉ làm một việc
Loose CouplingServices giao tiếp qua APIs
Independent DeployDeploy không ảnh hưởng services khác
Technology DiversityMỗi service có thể dùng công nghệ khác
OwnershipTeam sở hữu service từ dev đến production

Monolith vs Microservices

┌─────────────────────────────────┐     ┌───────┐  ┌───────┐  ┌───────┐
│          MONOLITH               │     │Order  │  │Product│  │Customer│
│  ┌───────────────────────────┐  │     │Service│  │Service│  │Service │
│  │ UI │ Business │ Data │    │  │     └───┬───┘  └───┬───┘  └───┬───┘
│  └───────────────────────────┘  │         │          │          │
└─────────────────────────────────┘         └──────────┴──────────┘
                                          ┌─────────────────────────┐
                                          │     API GATEWAY         │
                                          └─────────────────────────┘

API Gateway (Ocelot)

Cài đặt

dotnet add package Ocelot

Cấu hình ocelot.json

{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/products/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        { "Host": "product-service", "Port": 80 }
      ],
      "UpstreamPathTemplate": "/products/{everything}",
      "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"],
      "RateLimitOptions": {
        "ClientWhitelist": "",
        "EnableRateLimiting": true,
        "Period": "1s",
        "PeriodTimespan": 1,
        "Limit": 100
      },
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "Bearer",
        "AllowedScopes": []
      }
    },
    {
      "DownstreamPathTemplate": "/api/orders/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [
        { "Host": "order-service", "Port": 80 }
      ],
      "UpstreamPathTemplate": "/orders/{everything}",
      "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"]
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "http://localhost:5000"
  }
}

Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOcelot();

var app = builder.Build();

await app.UseOcelot();

app.Run();

Message Bus

RabbitMQ

Cài đặt

dotnet add package RabbitMQ.Client

Producer

public class OrderMessagePublisher
{
    private readonly IConnection _connection;
    private readonly IModel _channel;
    
    public OrderMessagePublisher()
    {
        var factory = new ConnectionFactory
        {
            HostName = "rabbitmq",
            UserName = "guest",
            Password = "guest"
        };
        
        _connection = factory.CreateConnection();
        _channel = _connection.CreateModel();
        
        _channel.ExchangeDeclare("orders", ExchangeType.Direct, durable: true);
    }
    
    public void PublishOrderCreated(Order order)
    {
        var message = JsonSerializer.Serialize(order);
        var body = Encoding.UTF8.GetBytes(message);
        
        var properties = _channel.CreateBasicProperties();
        properties.Persistent = true;
        properties.ContentType = "application/json";
        
        _channel.BasicPublish(
            exchange: "orders",
            routingKey: "order.created",
            basicProperties: properties,
            body: body);
    }
}

Consumer

public class OrderMessageConsumer : BackgroundService
{
    private readonly IConnection _connection;
    private readonly IModel _channel;
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var factory = new ConnectionFactory
        {
            HostName = "rabbitmq",
            UserName = "guest",
            Password = "guest"
        };
        
        _connection = factory.CreateConnection();
        _channel = _connection.CreateModel();
        
        _channel.QueueDeclare("order.created", durable: true);
        
        var consumer = new EventingBasicConsumer(_channel);
        consumer.Received += async (model, ea) =>
        {
            var body = ea.Body.ToArray();
            var message = Encoding.UTF8.GetString(body);
            
            var order = JsonSerializer.Deserialize<Order>(message);
            await ProcessOrderAsync(order);
            
            _channel.BasicAck(ea.DeliveryTag, false);
        };
        
        _channel.BasicConsume(
            queue: "order.created",
            autoAck: false,
            consumer: consumer);
        
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(1000, stoppingToken);
        }
    }
}

Kafka

Cài đặt

dotnet add package Confluent.Kafka

Producer

var config = new ProducerConfig
{
    BootstrapServers = "localhost:9092",
    ClientId = "order-producer"
};

using var producer = new ProducerBuilder<string, string>(config).Build();

var message = new Message<string, string>
{
    Key = order.Id.ToString(),
    Value = JsonSerializer.Serialize(order)
};

var result = await producer.ProduceAsync("orders", message);

Consumer

var config = new ConsumerConfig
{
    BootstrapServers = "localhost:9092",
    GroupId = "order-consumer",
    AutoOffsetReset = AutoOffsetReset.Earliest
};

using var consumer = new ConsumerBuilder<string, string>(config).Build();

consumer.Subscribe("orders");

while (true)
{
    var consumeResult = consumer.Consume();
    var order = JsonSerializer.Deserialize<Order>(consumeResult.Message.Value);
    await ProcessOrderAsync(order);
}

RabbitMQ vs Kafka

AspectRabbitMQKafka
ProtocolAMQPBinary (custom)
Use CaseTask queues, simple messagingEvent streaming, high throughput
Message RetentionPer-queue (short-term)Topic-based (long-term)
OrderingPer-queuePer-partition
ScalabilityHorizontalVery high
ComplexitySimplerMore complex
Delivery GuaranteeAt-least-once, Exactly-onceAt-least-once, Exactly-once

6. Hiệu suất và Xử lý Bất đồng bộ

Giới thiệu

Phần này trình bày các kỹ thuật tối ưu hiệu suất và xử lý bất đồng bộ trong .NET.

Nội dung chính

Caching

Xử lý tải

Bất đồng bộ

Caching

In-Memory Cache

IMemoryCache

// Đăng ký
builder.Services.AddMemoryCache();

// Sử dụng
public class ProductService
{
    private readonly IMemoryCache _cache;
    private readonly AppDbContext _context;
    
    public ProductService(IMemoryCache cache, AppDbContext context)
    {
        _cache = cache;
        _context = context;
    }
    
    public async Task<List<Product>> GetProductsAsync()
    {
        // TryGetValue - Kiểm tra cache
        if (!_cache.TryGetValue("products", out List<Product> products))
        {
            // Load từ database
            products = await _context.Products.ToListAsync();
            
            // Set cache với options
            var options = new MemoryCacheEntryOptions()
                .SetSlidingExpiration(TimeSpan.FromMinutes(10))
                .SetAbsoluteExpiration(TimeSpan.FromHours(1))
                .SetPriority(CacheItemPriority.Normal)
                .SetSize(products.Count);
            
            _cache.Set("products", products, options);
        }
        
        return products;
    }
    
    public async Task<Product> GetProductByIdAsync(int id)
    {
        var key = $"product_{id}";
        
        return await _cache.GetOrCreateAsync(key, async entry =>
        {
            entry.SlidingExpiration = TimeSpan.FromMinutes(5);
            
            return await _context.Products.FindAsync(id);
        });
    }
}

Cache Options

var options = new MemoryCacheEntryOptions()
    // Thời gian cache không được access trước khi expire
    .SetSlidingExpiration(TimeSpan.FromMinutes(10))
    // Thời gian cache tồn tại tuyệt đối
    .SetAbsoluteExpiration(TimeSpan.FromHours(1))
    // Kết hợp cả hai
    .SetAbsoluteExpirationRelativeToNow(TimeSpan.FromHours(1))
    // Callback khi cache bị remove
    .RegisterPostEvictionCallback((key, value, reason, state) =>
    {
        Console.WriteLine($"Cache '{key}' removed: {reason}");
    });

Distributed Cache (Redis)

Cài đặt

dotnet add package StackExchange.Redis

Cấu hình

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
    options.InstanceName = "MyApp:";
});

Sử dụng

public class RedisCacheService
{
    private readonly IDistributedCache _cache;
    
    public RedisCacheService(IDistributedCache cache)
    {
        _cache = cache;
    }
    
    // Set
    public async Task SetAsync<T>(string key, T value)
    {
        var json = JsonSerializer.Serialize(value);
        var bytes = Encoding.UTF8.GetBytes(json);
        
        await _cache.SetAsync(key, bytes, new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
            SlidingExpiration = TimeSpan.FromMinutes(10)
        });
    }
    
    // Get
    public async Task<T> GetAsync<T>(string key)
    {
        var bytes = await _cache.GetAsync(key);
        if (bytes == null) return default;
        
        var json = Encoding.UTF8.GetString(bytes);
        return JsonSerializer.Deserialize<T>(json);
    }
    
    // Remove
    public async Task RemoveAsync(string key)
    {
        await _cache.RemoveAsync(key);
    }
}

Response Caching

Cấu hình

// Program.cs
builder.Services.AddResponseCaching();

var app = builder.Build();
app.UseResponseCaching();

Sử dụng

[ApiController]
[Route("api/[controller]")]
[ResponseCache(Duration = 60, VaryByHeader = "Accept")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [ResponseCache(Duration = 120)]
    public IActionResult GetProducts()
    {
        return Ok(new { data = "cached" });
    }
}

Cache Headers

[HttpGet]
public IActionResult GetProducts()
{
    Response.Headers.CacheControl = "public, max-age=60";
    Response.Headers.Vary = "Accept-Encoding";
    
    return Ok();
}

Cache Strategies

Cache-Aside Pattern

┌─────────────────────────────────────────────────────────────────┐
│                      CACHE-ASIDE PATTERN                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   1. Request → Cache                                            │
│                         ↓                                       │
│   2. Cache hit? ──No──→ Database → Cache → Response            │
│        ↓                                                        │
│       Yes                                                        │
│        ↓                                                        │
│   3. Response (from cache)                                       │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘
public async Task<Product> GetProductAsync(int id)
{
    var key = $"product_{id}";
    
    // 1. Check cache
    var cached = await _cache.GetAsync<Product>(key);
    if (cached != null)
        return cached;
    
    // 2. Load from database
    var product = await _context.Products.FindAsync(id);
    if (product != null)
    {
        // 3. Store in cache
        await _cache.SetAsync(key, product, TimeSpan.FromMinutes(10));
    }
    
    return product;
}

Invalidate Cache

public async Task UpdateProductAsync(Product product)
{
    // Update database
    _context.Products.Update(product);
    await _context.SaveChangesAsync();
    
    // Invalidate cache
    await _cache.RemoveAsync($"product_{product.Id}");
}

Xử lý Tải

Rate Limiting

Cài đặt

dotnet add package AspNetCoreRateLimit

Cấu hình

// Program.cs
builder.Services.AddMemoryCache();
builder.Services.Configure<IpRateLimitOptions>(options =>
{
    options.EnableEndpointRateLimiting = true;
    options.StackBlockedRequests = false;
    options.GeneralRules = new List<RateLimitRule>
    {
        new RateLimitRule
        {
            Endpoint = "*",
            Period = "1m",
            Limit = 60
        },
        new RateLimitRule
        {
            Endpoint = "POST:/api/auth/login",
            Period = "1m",
            Limit = 5
        }
    };
});

builder.Services.AddInProcessMessageBus();
builder.Services.AddIpRateLimiting();

var app = builder.Build();
app.UseIpRateLimiting();

Controller

[HttpGet]
[EnableRateLimiting("PerUser")]
public IActionResult GetExpensiveData()
{
    return Ok();
}

Load Balancing

Round Robin

Request 1 → Instance 1
Request 2 → Instance 2
Request 3 → Instance 3
Request 4 → Instance 1 (loop)

Health Checks với Load Balancer

builder.Services.AddHealthChecks()
    .AddDbContextCheck<AppDbContext>("database")
    .AddRedis("localhost:6379", name: "redis");

var app = builder.Build();

// Health endpoint
app.MapHealthChecks("/health");

// Detailed health endpoint
app.MapHealthChecks("/health/detail", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
    {
        context.ContentType = "application/json";
        await context.Response.WriteAsJsonAsync(new
        {
            status = report.Status.ToString(),
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                description = e.Value.Description,
                duration = e.Value.Duration.TotalMilliseconds
            })
        });
    }
});

Health Checks

Basic Health Check

public class DatabaseHealthCheck : IHealthCheck
{
    private readonly AppDbContext _context;
    
    public DatabaseHealthCheck(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            await _context.Database.CanConnectAsync(cancellationToken);
            return HealthCheckResult.Healthy();
        }
        catch (Exception ex)
        {
            return HealthCheckResult.Unhealthy(
                "Database connection failed", 
                ex);
        }
    }
}

Đăng ký Health Check

builder.Services.AddHealthChecks()
    .AddCheck<DatabaseHealthCheck>("database")
    .AddCheck<ExternalApiHealthCheck>("external_api")
    .AddUrlGroup(new Uri("https://api.example.com/health"), "api");

Bất đồng bộ

IAsyncEnumerable

Giới thiệu

IAsyncEnumerable<T> cho phép stream dữ liệu bất đồng bộ, tốt cho việc xử lý large datasets.

Sử dụng

public async IAsyncEnumerable<Product> GetProductsStreamAsync()
{
    using var connection = new SqlConnection(connectionString);
    await connection.OpenAsync();
    
    using var command = new SqlCommand("SELECT * FROM Products", connection);
    using var reader = await command.ExecuteReaderAsync();
    
    while (await reader.ReadAsync())
    {
        yield return new Product
        {
            Id = reader.GetInt32(0),
            Name = reader.GetString(1),
            Price = reader.GetDecimal(2)
        };
    }
}

// Sử dụng
await foreach (var product in GetProductsStreamAsync())
{
    Console.WriteLine(product.Name);
}

So sánh với IEnumerable

// IEnumerable - Block thread, load all to memory
public IEnumerable<Product> GetProductsSync()
{
    var products = _context.Products.ToList(); // Load all
    foreach (var p in products)
        yield return p;
}

// IAsyncEnumerable - Non-blocking, stream data
public async IAsyncEnumerable<Product> GetProductsAsync()
{
    await foreach (var p in _context.Products.AsAsyncEnumerable())
        yield return p;
}

Kỹ thuật Stream dữ liệu lớn

Chunking

public async Task ProcessLargeDatasetAsync()
{
    const int batchSize = 1000;
    var skip = 0;
    
    while (true)
    {
        var batch = await _context.Products
            .AsNoTracking()
            .OrderBy(p => p.Id)
            .Skip(skip)
            .Take(batchSize)
            .ToListAsync();
        
        if (batch.Count == 0)
            break;
        
        // Process batch
        await ProcessBatchAsync(batch);
        
        skip += batchSize;
        Console.WriteLine($"Processed {skip} items");
    }
}

Parallel Processing

public async Task ProcessInParallelAsync()
{
    var products = await _context.Products
        .AsNoTracking()
        .Where(p => !p.Processed)
        .ToListAsync();
    
    var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };
    
    await Parallel.ForEachAsync(products, options, async (product, ct) =>
    {
        await ProcessProductAsync(product);
    });
}

Cancellation Token

public async Task<List<Product>> GetProductsAsync(
    CancellationToken cancellationToken = default)
{
    var result = new List<Product>();
    
    await foreach (var product in GetProductsStreamAsync())
    {
        cancellationToken.ThrowIfCancellationRequested();
        
        result.Add(product);
        
        if (result.Count >= 1000)
            break;
    }
    
    return result;
}

// Sử dụng với timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
    var products = await GetProductsAsync(cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operation timed out");
}

Channel (Producer-Consumer)

// Producer
public class DataProducer
{
    private readonly Channel<int> _channel;
    
    public DataProducer()
    {
        _channel = Channel.CreateBounded<int>(100);
    }
    
    public async Task ProduceAsync(CancellationToken ct)
    {
        for (var i = 0; i < 10000; i++)
        {
            await _channel.Writer.WriteAsync(i, ct);
        }
        _channel.Writer.Complete();
    }
    
    public ChannelReader<int> Reader => _channel.Reader;
}

// Consumer
public class DataConsumer
{
    private readonly ChannelReader<int> _reader;
    
    public DataConsumer(ChannelReader<int> reader)
    {
        _reader = reader;
    }
    
    public async Task ConsumeAsync(CancellationToken ct)
    {
        await foreach (var item in _reader.ReadAllAsync(ct))
        {
            await ProcessAsync(item);
        }
    }
}

ValueTask vs Task

// Task - Always allocate
public async Task<int> GetValueAsync()
{
    await Task.Delay(1);
    return 42;
}

// ValueTask - Avoid allocation for synchronous completion
public async ValueTask<int> GetValueAsync()
{
    if (_cache.TryGetValue(out int value))
        return value; // Synchronous - no allocation
    
    return new ValueTask<int>(42); // Async path
}
AspectTaskValueTask
AllocationAlways heapAvoided if sync
Use caseStandard asyncHot path
Synchronous returnNot allowedAllowed

7. Hệ thống Phân tán

Giới thiệu

Phần này trình bày các kiến thức về hệ thống phân tán, message queue, và container orchestration.

Nội dung chính

Message Queue

Container & Cloud

Message Queue

RabbitMQ

Exchange Types

┌─────────────────────────────────────────────────────────────────┐
│                    RABBITMQ TOPOLOGY                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Producer ──► Exchange ──► Queue ──► Consumer                  │
│                                                                  │
│  Exchange Types:                                               │
│  ┌──────────┬────────────────────────────────────────────────┐ │
│  │  Direct  │ Route to queue matching exact routing key     │ │
│  ├──────────┼────────────────────────────────────────────────┤ │
│  │  Topic   │ Route using wildcard matching                  │ │
│  ├──────────┼────────────────────────────────────────────────┤ │
│  │  Headers │ Route based on message headers                │ │
│  ├──────────┼────────────────────────────────────────────────┤ │
│  │  Fanout  │ Broadcast to all queues                       │ │
│  └──────────┴────────────────────────────────────────────────┘ │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Code Example

// Connection
var factory = new ConnectionFactory
{
    HostName = "localhost",
    UserName = "guest",
    Password = "guest"
};

using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();

// Declare exchange
channel.ExchangeDeclare("orders", ExchangeType.Topic, durable: true);

// Declare queue
channel.QueueDeclare("order.notifications", durable: true, exclusive: false);

// Bind
channel.QueueBind("order.notifications", "orders", "order.#");

// Publish
var message = JsonSerializer.Serialize(order);
var body = Encoding.UTF8.GetBytes(message);

var properties = channel.CreateBasicProperties();
properties.Persistent = true;
properties.MessageId = Guid.NewGuid().ToString();

channel.BasicPublish("orders", "order.created", properties, body);

Kafka

Concepts

  • Topic: Category of messages
  • Partition: Ordered, immutable sequence within topic
  • Producer: Publishes messages to topics
  • Consumer: Subscribes to topics
  • Consumer Group: Group of consumers sharing workload
  • Offset: Position in partition

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                       KAFKA ARCHITECTURE                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐        │
│  │  Producer  │───▶│  Producer  │───▶│  Producer  │        │
│  └──────┬──────┘    └──────┬──────┘    └──────┬──────┘        │
│         │                   │                   │               │
│         │          ┌────────▼────────┐         │               │
│         │          │     BROKER      │         │               │
│         │          │  ┌──────────┐   │         │               │
│         │          │  │Partition1│◀──┘         │               │
│         │          │  ├──────────┤             │               │
│         │          │  │Partition2│─────────────┘               │
│         │          │  └──────────┘                              │
│         │          └─────────────┬──────┘                        │
│         │                        │                              │
│  ┌──────▼──────┐    ┌────────────▼──────┐    ┌─────────────┐  │
│  │ Consumer G1 │◀───│ Consumer Group 1   │───▶│ Consumer G2 │  │
│  └─────────────┘    └───────────────────┘    └─────────────┘  │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Code Example

var config = new ProducerConfig
{
    BootstrapServers = "localhost:9092",
    ClientId = "my-producer",
    Acks = Acks.Leader,
    EnableIdempotence = true
};

using var producer = new ProducerBuilder<string, string>(config).Build();

var order = new Order { Id = Guid.NewGuid(), Items = new List<Item>() };
var message = new Message<string, string>
{
    Key = order.Id.ToString(),
    Value = JsonSerializer.Serialize(order),
    Timestamp = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow)
};

var deliveryResult = await producer.ProduceAsync("orders", message);
Console.WriteLine($"Delivered to: {deliveryResult.TopicPartitionOffset}");

Pub/Sub Pattern

Concept

┌─────────────────────────────────────────────────────────────────┐
│                      PUB/SUB PATTERN                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│   Publisher                                                     │
│       │                                                         │
│       ├──▶ [Topic: Orders] ──▶ Subscriber 1                   │
│       │                                                        │
│       ├──▶ [Topic: Orders] ──▶ Subscriber 2                   │
│       │                                                        │
│       └──▶ [Topic: Orders] ──▶ Subscriber 3                   │
│                                                                  │
│   All subscribers receive the same message                     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Implementation với MediatR

// Event
public record OrderCreatedEvent(Guid OrderId, decimal TotalAmount) : INotification;

// Handler
public class SendOrderNotificationHandler 
    : INotificationHandler<OrderCreatedEvent>
{
    public async Task Handle(OrderCreatedEvent notification, 
        CancellationToken cancellationToken)
    {
        await _emailService.SendOrderConfirmationAsync(
            notification.OrderId);
    }
}

// Publish
public class OrderService
{
    private readonly IMediator _mediator;
    
    public async Task CreateOrder(Order order)
    {
        await _mediator.Publish(new OrderCreatedEvent(
            order.Id, 
            order.TotalAmount));
    }
}

RabbitMQ vs Kafka

FeatureRabbitMQKafka
Delivery ModelQueue-basedLog-based
Message RetentionUntil consumed (default)Configurable time
OrderingPer-queuePer-partition
ThroughputModerateVery High
LatencyLowVery Low
Use CasesTask queues, RPCEvent streaming, audit log
ComplexityLowerHigher
ScalingHorizontalHorizontal + partitions

Container & Cloud

Docker

Dockerfile cho .NET

# Build Stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src

# Copy project files
COPY ["src/MyApp/MyApp.csproj", "MyApp/"]
RUN dotnet restore "MyApp/MyApp.csproj"

# Copy source and build
COPY src/MyApp/. MyApp/
WORKDIR "/src/MyApp"
RUN dotnet publish -c Release -o /app/publish

# Runtime Stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .

# Non-root user
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
RUN chown -R appuser:appgroup /app
USER appuser

EXPOSE 8080
ENV ASPNETCORE_URLS=http+:8080

ENTRYPOINT ["dotnet", "MyApp.dll"]

Docker Commands

# Build image
docker build -t myapp:latest .

# Run container
docker run -d -p 8080:8080 --name myapp myapp:latest

# Run với environment variables
docker run -d -p 8080:8080 \
  -e "ASPNETCORE_ENVIRONMENT=Production" \
  -e "ConnectionStrings__DefaultConnection=..." \
  myapp:latest

# Docker Compose
docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - db
      - redis
  db:
    image: mcr.microsoft.com/mssql/server
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=YourStrong!Passw0rd
  redis:
    image: redis:alpine

Kubernetes

Pod

apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: myapp
    image: myapp:latest
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8080
        env:
        - name: ASPNETCORE_ENVIRONMENT
          value: "Production"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

Service

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: LoadBalancer

Cloud Deployment

Azure

# Azure Container Apps
az containerapp create \
  --name myapp \
  --resource-group mygroup \
  --image myregistry.azurecr.io/myapp:latest \
  --cpu 0.25 --memory 0.5Gi \
  --ingress external \
  --target-port 8080

# Azure Kubernetes Service
az aks create \
  --resource-group mygroup \
  --name mycluster \
  --node-count 3 \
  --enable-addons monitoring

AWS

# Amazon ECS
aws ecs create-cluster --cluster-name mycluster

# Amazon EKS
aws eks create-cluster \
  --name mycluster \
  --role-arn arn:aws:iam::123456789:role/EKSRole \
  --resources-vpc-config subnetIds=subnet-12345

Best Practices

12-Factor App

FactorDescription
CodebaseOne codebase tracked in version control
DependenciesExplicitly declare dependencies
ConfigStore config in environment
Backing ServicesTreat backing services as attached resources
Build/Release/RunStrictly separate build and run stages
ProcessesExecute app as one or more stateless processes
Port BindingExport HTTP as a service by port binding
ConcurrencyScale out via process model
DisposabilityFast startup and graceful shutdown
Dev/Prod ParityKeep development, staging, production similar
LogsTreat logs as event streams
Admin ProcessesRun admin/maintenance tasks as one-off processes

8. Kiểm thử

Giới thiệu

Phần này trình bày các kỹ thuật kiểm thử trong .NET.

Nội dung chính

Đơn vị & Tích hợp

Unit Test & Integration Test

Unit Test với xUnit

Cấu trúc Test

public class ProductServiceTests
{
    [Fact]
    public void GetProductById_ReturnsProduct_WhenProductExists()
    {
        // Arrange
        var productId = 1;
        var expectedProduct = new Product { Id = 1, Name = "Test" };
        
        var mockRepo = new Mock<IProductRepository>();
        mockRepo.Setup(r => r.GetByIdAsync(productId))
            .ReturnsAsync(expectedProduct);
            
        var service = new ProductService(mockRepo.Object);
        
        // Act
        var result = service.GetProductByIdAsync(productId).Result;
        
        // Assert
        Assert.NotNull(result);
        Assert.Equal(expectedProduct.Name, result.Name);
    }
    
    [Theory]
    [InlineData(0, false)]
    [InlineData(1, true)]
    [InlineData(100, true)]
    public void IsValidPrice_ReturnsExpected(int price, bool expected)
    {
        var service = new ProductService(null);
        var result = service.IsValidPrice(price);
        Assert.Equal(expected, result);
    }
}

Mocking với Moq

Basic Mocking

// Mock interface
var mockRepo = new Mock<IProductRepository>();

// Setup method
mockRepo.Setup(r => r.GetByIdAsync(It.IsAny<int>()))
    .ReturnsAsync(new Product { Id = 1, Name = "Test" });

// Setup property
mockRepo.SetupGet(r => r.Count)
    .Returns(10);

// Verify calls
mockRepo.Verify(r => r.GetByIdAsync(It.IsAny<int>()), Times.Once);

Mocking Returns

// Sequential returns
var mockRepo = new Mock<IProductRepository>();
mockRepo.SetupSequence(r => r.GetNextAsync())
    .ReturnsAsync(new Product { Id = 1 })
    .ReturnsAsync(new Product { Id = 2 })
    .ReturnsAsync((Product)null);
    
// Callback
var products = new List<Product>();
var mockRepo = new Mock<IProductRepository>();
mockRepo.Setup(r => r.AddAsync(It.IsAny<Product>()))
    .Callback<Product>(p => products.Add(p))
    .ReturnsAsync((Product p) => p);

Integration Testing

WebApplicationFactory

public class CustomWebApplicationFactory<TStartup> 
    : WebApplicationFactory<TStartup> where TStartup : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove existing DbContext
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
            if (descriptor != null)
                services.Remove(descriptor);
                
            // Add test DbContext
            services.AddDbContext<AppDbContext>(options =>
                options.UseInMemoryDatabase("TestDatabase"));
                
            // Build service provider
            var sp = services.BuildServiceProvider();
            
            // Seed data
            using var scope = sp.CreateScope();
            var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            context.Products.Add(new Product { Name = "Test Product" });
            context.SaveChanges();
        });
    }
}

Test Class

public class ProductsControllerIntegrationTests 
    : IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program> _factory;
    
    public ProductsControllerIntegrationTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient();
    }
    
    [Fact]
    public async Task GetProducts_ReturnsProducts()
    {
        // Act
        var response = await _client.GetAsync("/api/products");
        
        // Assert
        response.EnsureSuccessStatusCode();
        var content = await response.Content.ReadAsStringAsync();
        var products = JsonSerializer.Deserialize<List<Product>>(content);
        
        Assert.NotNull(products);
        Assert.NotEmpty(products);
    }
}

Test Database

public class TestDatabaseFixture : IDisposable
{
    private readonly SqliteConnection _connection;
    public AppDbContext CreateContext() 
        => new(new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(_connection).Options);
    
    public TestDatabaseFixture()
    {
        _connection = new SqliteConnection("DataSource=:memory:");
        _connection.Open();
        
        using var context = CreateContext();
        context.Database.EnsureCreated();
    }
    
    public void Dispose() => _connection.Dispose();
}

Test Patterns

Arrange-Act-Assert

[Fact]
public void CalculateTotal_ReturnsCorrectTotal()
{
    // Arrange
    var order = new Order
    {
        Items = new List<OrderItem>
        {
            new() { Price = 10.00m, Quantity = 2 },
            new() { Price = 5.00m, Quantity = 3 }
        }
    };
    
    // Act
    var total = order.CalculateTotal();
    
    // Assert
    Assert.Equal(35.00m, total);
}

AAA with Helper Methods

public class OrderServiceTests
{
    private readonly Mock<IOrderRepository> _mockRepo;
    private readonly OrderService _service;
    
    public OrderServiceTests()
    {
        _mockRepo = new Mock<IOrderRepository>();
        _service = new OrderService(_mockRepo.Object);
    }
    
    [Fact]
    public async Task CreateOrder_ReturnsOrder_WithGeneratedId()
    {
        // Arrange
        var order = CreateValidOrder();
        _mockRepo.Setup(r => r.AddAsync(It.IsAny<Order>()))
            .Callback<Order>(o => o.Id = 1)
            .ReturnsAsync((Order o) => o);
            
        // Act
        var result = await _service.CreateOrderAsync(order);
        
        // Assert
        Assert.NotEqual(Guid.Empty, result.Id);
        _mockRepo.Verify(r => r.AddAsync(It.IsAny<Order>()), Times.Once);
    }
    
    private Order CreateValidOrder() => new()
    {
        CustomerId = Guid.NewGuid(),
        Items = new List<OrderItem>
        {
            new() { ProductId = 1, Quantity = 1, Price = 10 }
        }
    };
}

Câu hỏi Phân biệt

.NET Core vs .NET Framework

Aspect.NET Core.NET Framework
PlatformCross-platform (Windows, Linux, macOS)Windows only
SourceOpen sourceMostly closed source
PerformanceBetter, optimized runtimeLegacy
ModularityPackage-based (NuGet)Full framework
CLIFull CLI supportLimited
Side-by-sideMultiple versionsSingle version per machine
Use caseModern apps, microservicesLegacy Windows apps

MVC vs Web API vs Minimal API

MVC

  • Hỗ trợ Views (Razor pages)
  • Phù hợp cho web applications với UI
  • Convention-based routing
  • Model binding đầy đủ
  • ViewBag, ViewData
public class HomeController : Controller
{
    public IActionResult Index()
    {
        ViewData["Title"] = "Home";
        return View();
    }
}

Web API

  • RESTful services, JSON/XML
  • Không có Views
  • Attribute routing
  • Content negotiation
  • [ApiController] attribute
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Ok();
}

Minimal APIs

  • Rút gọn code
  • Không cần Controller
  • Top-level statements
  • Phù hợp cho microservices
  • Lambda expressions
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", () => Results.Ok(new { name = "Product" }));

app.Run();

So sánh

FeatureMVCWeb APIMinimal API
Views✅ Yes❌ No❌ No
Controllers✅ Yes✅ Yes❌ No
RoutingConvention + AttributeAttributeMapGet/MapPost
Model BindingFullFullLimited
TestabilityMediumMediumHigh
Use caseWeb appsAPIsMicroservices

Abstract class vs Interface

Abstract Class

public abstract class Animal
{
    public string Name { get; set; }
    
    // Abstract method - phải implement
    public abstract void MakeSound();
    
    // Virtual method - có thể override
    public virtual void Sleep()
    {
        Console.WriteLine("Sleeping...");
    }
    
    // Non-abstract method
    public void Eat()
    {
        Console.WriteLine("Eating...");
    }
}

public class Dog : Animal
{
    public override void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
}

Interface

public interface IAnimal
{
    string Name { get; set; }
    void MakeSound();
}

public interface IFlyable
{
    void Fly();
}

public interface IPlayable
{
    void Play();
}

So sánh

AspectAbstract ClassInterface
Multiple inheritance❌ No✅ Yes
Constructor✅ Yes❌ No
Fields✅ Yes❌ No (properties only)
Access modifiers✅ Yes❌ No (public implicit)
Default implementation✅ Yes✅ Yes (C# 8+)
State✅ Can have❌ No
InheritanceSingleMultiple
Use case“is-a” relationship“can-do” capability

IEnumerable vs IQueryable

IEnumerable

public IEnumerable<Product> GetProducts()
{
    return _context.Products; // Local collection
}

foreach (var product in GetProducts())
{
    // Process
}
  • Thực thi trên client
  • Toàn bộ data được load vào memory trước khi filter
  • Phù hợp cho small datasets hoặc in-memory data

IQueryable

public IQueryable<Product> GetProducts()
{
    return _context.Products; // Query provider
}

var results = GetProducts()
    .Where(p => p.Price > 100)
    .OrderBy(p => p.Name);
    
// Query được translate sang SQL và execute trên database
  • Thực thi trên database
  • Query được build và translate sang SQL
  • Phù hợp cho large datasets và remote data sources

So sánh

AspectIEnumerableIQueryable
LocationClient-sideServer-side
ExecutionIn-memoryDatabase query
SQL Translation❌ No✅ Yes
DeferredYesYes
Use caseIn-memory collectionsDatabase queries
PerformanceSlow with large dataOptimized

Khi nào dùng?

// ✅ IEnumerable - Khi đã có data trong memory
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evens = numbers.Where(n => n % 2 == 0);

// ✅ IQueryable - Khi query database
var products = _context.Products
    .Where(p => p.Price > 100)
    .OrderBy(p => p.Name);

// ❌ Tránh - IEnumerable từ database
var products = _context.Products.ToList()
    .Where(p => p.Price > 100); // Load all, then filter

Các câu hỏi so sánh khác

var vs dynamic

Aspectvardynamic
Type checkingCompile-timeRuntime
IntelliSense✅ Yes❌ No
PerformanceFastSlower
Use caseType known at compileLate binding

const vs readonly

Aspectconstreadonly
EvaluationCompile-timeRuntime
ValueMust be known at compileSet at runtime
StaticAlways staticCan be instance-level
Use caseCompile-time constantsRuntime constants

string vs StringBuilder

AspectstringStringBuilder
TypeImmutableMutable
MemoryNew allocation per changeDynamic buffer
Use caseSmall strings, no changesLarge strings, many concatenations

Task.WhenAll vs await in loop

// ❌ Sequential - Chậm
foreach (var url in urls)
{
    var response = await httpClient.GetAsync(url);
}

// ✅ Parallel - Nhanh hơn
var tasks = urls.Select(url => httpClient.GetAsync(url));
var responses = await Task.WhenAll(tasks);