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

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);

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

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

Binding Sources

[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)
    {
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
    
    // FromForm - Từ form data
    [HttpPost("upload")]
    public IActionResult Upload([FromForm] IFormFile file)
    {
        return Ok();
    }
    
    // FromHeader - Từ HTTP header
    [HttpGet]
    public IActionResult Get([FromHeader(Name = "X-Request-Id")] string requestId)
    {
        return Ok(requestId);
    }
}

Complex Binding

// Request URL: /api/products?category=electronics&price[min]=10&price[max]=100
public IActionResult Search(
    [FromQuery] string category,
    [FromQuery] PriceRange price)
{
    return Ok();
}

public class PriceRange
{
    public decimal Min { get; set; }
    public decimal Max { get; set; }
}

Model Validation

Data Annotations

public class Product
{
    public int Id { get; set; }
    
    [Required(ErrorMessage = "Name is required")]
    [StringLength(100, MinimumLength = 2)]
    public string Name { get; set; }
    
    [Range(0.01, 9999.99)]
    public decimal Price { get; set; }
    
    [EmailAddress]
    public string Email { get; set; }
    
    [Url]
    public string Website { get; set; }
    
    [Phone]
    public string Phone { get; set; }
    
    [CreditCard]
    public string CreditCard { get; set; }
    
    [RegularExpression(@"^[A-Z]{3}$", ErrorMessage = "Invalid code")]
    public string Code { get; set; }
}

Custom Validation

public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(x => x.Name)
            .NotEmpty()
            .WithMessage("Name is required");
            
        RuleFor(x => x.Price)
            .GreaterThan(0)
            .When(x => x.IsActive)
            .WithMessage("Price must be greater than 0");
            
        RuleFor(x => x.StartDate)
            .LessThan(x => x.EndDate)
            .WithMessage("Start date must be before end date");
    }
}

Validation trong Controller

[HttpPost]
public IActionResult Create([FromBody] Product product)
{
    // ✅ Tự động validate nếu dùng [ApiController]
    // ModelState.IsValid sẽ được check tự động
    
    // Hoặc validate thủ công
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    
    return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}

Validation Error Response

{
  "errors": {
    "Name": [
      "Name is required"
    ],
    "Price": [
      "Price must be greater than 0"
    ]
  }
}

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);