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)
- Enable “Include Actual Execution Plan” (Ctrl + M)
- Run your query
- View the Execution Plan tab
Using T-SQL
SET STATISTICS XML ON;
-- Your query here
SET STATISTICS XML OFF;
Common Operators
| Operator | Description |
|---|---|
| Table Scan | Reads all rows from a table |
| Index Scan | Reads all entries from an index |
| Index Seek | Uses index to find specific rows |
| Clustered Index Scan | Scans entire clustered index (entire table) |
| Nested Loops | Joins using nested iteration |
| Hash Join | Joins using hash table |
| Merge Join | Joins using sorted inputs |
| Compute Scalar | Calculates new values from existing data |
| Sort | Orders data |
| Filter | Filters 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:
| Metric | Value | Assessment |
|---|---|---|
| I/O Cost | 0 | No disk reads |
| CPU Cost | 0.001982 | Very small |
| % Cost | 2% | Not a bottleneck |
| Executions | 1 | No 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
| Parameter | Your Value | Meaning |
|---|---|---|
| Operator Cost | 0.001982 (2%) | ✅ No concern, only 2% of query |
| I/O Cost | 0 | ✅ No disk reads, CPU only |
| Estimated Rows | 19,820 | ⚠️ Need to compare with Actual Rows |
| Executions | 1 | ✅ No loop |
| Row Size | 56 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:
| Metric | Value | Percentage | Assessment |
|---|---|---|---|
| Operator Cost | 0.113973 | 97% | ❌ Very high |
| I/O Cost | 0.0920139 | 81% of operator | ❌ Reading from disk |
| CPU Cost | 0.021959 | 19% 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
| Parameter | Value | Meaning |
|---|---|---|
| Executions | 1 | Runs only once (not in a loop) ✅ |
| Rebinds | 0 | Not re-initializing parameters ❌ |
| Rewinds | 0 | No loop join rewind ✅ |
| Ordered | False | Result 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
| Operator | Cost | I/O Cost | CPU Cost | Rows |
|---|---|---|---|---|
| Clustered Index Scan | 0.113973 (97%) | 0.092014 | 0.021959 | 19,820 |
| Compute Scalar | 0.001982 (2%) | 0 | 0.001982 | 19,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:
| Metric | Value | Assessment |
|---|---|---|
| Actual vs Estimated Rows | 19,820 = 19,820 | Statistics accurate |
| Executions | 1 | No loop |
| Row Size | 47 B | Small data |
❌ Points to Improve:
| Metric | Value | Issue |
|---|---|---|
| Operator Cost | 97% | Takes almost entire cost |
| I/O Cost | 0.092 | Reading disk, not cache |
| Storage | RowStore | Appropriate but scanning all |
Questions to Determine If Optimization is Needed
-
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
-
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
-
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:
- View query text (F4 or hover over operator) to see WHERE clause
- Check total rows in the table
- If there’s a valid WHERE clause → create non-clustered index
- If query actually runs slow (> 500ms) → optimize now
- 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ức | Mô tả |
|---|---|---|
| 1 | Nền Tảng C# và .NET | Ngôn ngữ C#, CLR, Garbage Collection, Value Types vs Reference Types |
| 2 | ASP.NET Core Cốt lõi | Middleware, DI, Routing, Filters, Configuration |
| 3 | Xây dựng Web API | RESTful, Authentication, Authorization, Swagger |
| 4 | Truy cập Dữ liệu với EF Core | Code First, Migrations, N+1 Query, Transactions |
| 5 | Kiến trúc Phần mềm | SOLID, Design Patterns, Clean Architecture, DDD, CQRS |
| 6 | Hiệu suất và Xử lý Bất đồng bộ | Caching, Rate Limiting, Load Balancing |
| 7 | Hệ thống Phân tán | Message Queue, Docker, Kubernetes |
| 8 | Kiểm thử | Unit Test, Integration Test với xUnit |
| 9 | Câu hỏi Phân biệt | So sánh các công nghệ và concepts |
Hướng dẫn sử dụng
- Đọ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
- Thực hành: Mỗi chủ đề cần có code example đi kèm
- Ô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
- Delegates & Events - Func, Action, events, lambda expressions
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
- Async/Await - Cơ chế bất đồng bộ trong C#
- IEnumerable vs IAsyncEnumerable - So sánh synchronous và asynchronous enumeration
- LINQ - Language Integrated Query (IEnumerable vs IQueryable)
Exception Handling & IDisposable
- Exception Handling & IDisposable - Try-catch-finally, IDisposable pattern, tương tác giữa chúng
Pattern Matching, Records & Reflection
- Pattern Matching - Pattern Matching
- Records - C# 9+ Records
- Attributes & Reflection - Metadata và runtime type inspection
- Tính năng mới C# 12 - C# 12 New Features
CLR & Bộ nhớ
- Garbage Collection - Hoạt động của GC
- Thế hệ GC - Generations (Gen 0, 1, 2)
- Tối ưu GC - Cách giảm áp lực lên GC
- Value Types vs Reference Types - So sánh struct và class
- ref, out, in - Các từ khóa ref modifiers
Câu hỏi phỏng vấn (Sắp xếp từ dễ đến khó)
Mức độ: Dễ (Junior)
- Sự khác biệt giữa
value typesvàreference typestrong C#? constvàreadonlykhác nhau như thế nào?StringvsStringBuilder- khi nào nên dùng cái nào?- Sự khác biệt giữa
for,while, vàforeachloops? break,continue, vàreturnkhác nhau như thế nào?ArrayvàList<T>khác nhau gì?ref,out,inparameters khác nhau như thế nào?null-coalescing operator(??) vànull-conditional operator(?.) dùng để làm gì?
Mức độ: Trung bình (Mid-level)
abstract classvàinterfacekhác nhau như thế nào? Khi nào dùng cái nào?- Giải thích
inheritance,polymorphism, vàencapsulationtrong OOP? - Sự khác biệt giữa
delegate,event, vàlambda expression? - So sánh
List<T>,Dictionary<TKey, TValue>, vàHashSet<T>- khi nào dùng cái nào? - Khi nào nên sử dụng
generic typesvà cách áp dụng constraints? BoxingvàUnboxinglà gì? Tại sao nên tránh?Deferred Executiontrong LINQ là gì?- Sự khác biệt giữa
IEnumerablevàIQueryable- khi nào dùng cái nào? - Giải thích
try-catch-finallyhoạt động thế nào?throwvàthrow exkhác nhau gì? IDisposablelà gì? Tại sao cần implement nó?usingstatement hoạt động như thế nào với IDisposable?
Mức độ: Khó (Senior)
- Giải thích
async/awaithoạt động thế nào dưới the hood? - Sự khác biệt giữa
IEnumerablevàIAsyncEnumerable? - Cách tối ưu truy vấn LINQ trên tập dữ liệu lớn?
- Garbage Collection (GC) hoạt động như thế nào?
- Các thế hệ (Generations) trong GC là gì?
- Cách tối ưu để giảm áp lực lên GC?
- 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? Reflectionlà gì và khi nào nên sử dụng? Performance considerations?Attributestrong C# dùng để làm gì? Cho ví dụ về custom attribute.- Pattern Matching trong C# 9+ có gì mới?
RecordskhácClassesnhư thế nào? Khi nào nên dùng Records?- Tương tác giữa try-catch-finally và IDisposable -
usingstatement tương đương với try-finally như thế nào? - Tại sao không nên throw exception từ Dispose method?
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
| Feature | Abstract Class | Interface |
|---|---|---|
| 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ệm | Bản chất | Vai trò |
|---|---|---|
| Delegate | Kiểu dữ liệu (type-safe function pointer) | Định nghĩa “hợp đồng” cho phương thức |
| Lambda Expression | Cú pháp (syntax) | Cách viết ngắn gọn cho anonymous method |
| Event | Cơ chế bảo vệ (wrapper) | Giới hạn truy cập delegate, chỉ cho phép += và -= |
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ống | Sử dụng |
|---|---|
| Truyền method như parameter | Func<>, Action<>, hoặc custom delegate |
| LINQ queries | Lambda expression |
| Callback không cần expose | Lambda → Delegate |
| Publisher-Subscriber pattern | Event |
| Cần giới hạn truy cập (chỉ +=/-=) | Event |
| Cần Invoke từ bên ngoài | Delegate (không dùng event) |
Lưu ý quan trọng
- Event không thể Invoke từ bên ngoài class - Đây là sự khác biệt chính so với delegate
- Lambda expression không có kiểu riêng - Nó phải được gán cho một delegate type
- 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
| Interface | Mô tả | Ví dụ |
|---|---|---|
IEnumerable<T> | Chỉ hỗ trợ iteration | LINQ queries |
ICollection<T> | Thêm Count, Add, Remove, Clear | List<T>, HashSet<T> |
IList<T> | Thêm index-based access | List<T>, arrays |
IDictionary<TKey, TValue> | Key-value pairs | Dictionary<TKey, TValue> |
IReadOnlyCollection<T> | Chỉ đọc với Count | IEnumerable<T>.ToList() |
Span và Memory - High-Performance Memory Views
Lưu ý:
Span<T>vàMemory<T>không phải là collections theo nghĩa truyền thống. Chúng là cácref structcung 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ểm | Span<T> | Collections (List<T>, T[]) |
|---|---|---|
| Kiểu | ref struct (stack-only) | Reference type hoặc array |
| Allocation | Zero heap allocation | Heap allocation |
| Lưu trữ | Không thể là field của class | Có thể là field |
| Async | Không hỗ trợ (ref struct limitation) | Hỗ trợ đầy đủ |
| IEnumerable | Không implement | Có implement |
| Performance | Cao nhất | Thấp hơn do allocation |
| Use case | Parsing, slicing, buffer manipulation | General 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ống | Khuyến nghị |
|---|---|
| Parsing string/byte buffer | Span<T> |
| Slicing array mà không copy | Span<T> |
| High-performance buffer manipulation | Span<T> |
| Cần lưu trữ trong class | Memory<T> |
| Cần sử dụng với async/await | Memory<T> |
| General purpose data storage | List<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?
- Không implement
IEnumerable<T>- Không thể dùngforeachtrực tiếp (phải dùngforeach(ref var item in span)) - Không implement
ICollection<T>- Không cóAdd,Remove,Clear - 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 - Stack-only - Không thể boxing, không thể lưu trong heap
Tóm lại:
Span<T>vàMemory<T>là 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ý
- Khi gọi method
async, thread hiện tại không bị block - Task được tạo và chạy trên Thread Pool
- Khi async operation hoàn thành, continuation được schedule lại
- Kết quả được trả về cho caller
Lưu ý quan trọng
asyncmethod luôn trả vềTask,Task<T>, hoặcvoid(chỉ dùng cho event handlers)- Không nên dùng
.Resulthoặ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
| Feature | IEnumerable | IAsyncEnumerable |
|---|---|---|
| Execution | Synchronous | Asynchronous |
| Memory | Load all | Stream |
| Performance | Chậm với large data | Tốt với large data |
| Use case | Small datasets | Large 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
- Gọi
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
| Feature | IEnumerable | IQueryable |
|---|---|---|
| Execution | In-memory | Database/Provider |
| Deferred Execution | ✅ Yes | ✅ Yes |
| Query Composition | LINQ to Objects | LINQ to Entities/SQL |
| Performance | Load all data first | Filter at database |
| Use case | Small datasets, in-memory | Database 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
-
Sử dụng IQueryable thay vì IEnumerable cho database queries
public IQueryable<Product> GetProducts() => _context.Products; -
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 -
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ại Khi nào dùng Ưu điểm Nhược điểm Offset-based Admin 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-based Infinite scroll, mobile app, real-time feed (Facebook, Twitter) Hiệu suất cao (dùng index), không bị drift Không nhảy page được, chỉ “next/prev” -
Select only needed columns
var names = products.Select(p => p.Name).ToList(); // Not full entity -
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
| Aspect | throw ex | throw |
|---|---|---|
| Stack trace | Bị reset từ vị trí throw | Giữ nguyên stack trace gốc |
| Exception object | Tạo mới (về mặt stack trace) | Giữ nguyên object gốc |
| Debugging | Khó tìm root cause | Dễ dàng trace ngược lại |
| IL Code | throw instruction | rethrow 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
-
Luôn sử dụng
usingcho IDisposable resources// ✅ Good using var connection = new SqlConnection(connectionString); connection.Open(); // ❌ Bad var connection = new SqlConnection(connectionString); connection.Open(); // Forgot to dispose! -
Không throw exception từ Dispose
public void Dispose() { try { Cleanup(); } catch (Exception ex) { Log.Error(ex); // Log, don't throw } } -
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; } } } -
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
| Attribute | Mục đích | Ví 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
- Dependency Injection Frameworks - Tìm và register services
- ORM Frameworks - Map database columns to properties
- Serialization/Deserialization - Inspect object structure
- Testing Frameworks - Tìm và chạy test methods
- 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
- Allocation: Khi object mới được tạo, nó được allocate trên Heap
- Mark Phase: GC đánh dấu tất cả objects có reference (root objects)
- Sweep Phase: Xóa các objects không có reference
- 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 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| Generation | Mô tả | Tần suất GC |
|---|---|---|
| Gen 0 | Short-lived objects (local variables) | Thường xuyên nhất |
| Gen 1 | Intermediate objects | Ít hơn Gen 0 |
| Gen 2 | Long-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
| Operation | Value Type | Reference Type |
|---|---|---|
| Allocation | Stack (nhanh) | Heap (chậm hơn) |
| Copy | Copy toàn bộ | Copy reference |
| Passing | Copy toàn bộ | Copy reference (4/8 bytes) |
| GC Pressure | Không | Có |
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
| Modifier | Caller initialize | Can modify | Use case |
|---|---|---|---|
ref | ✅ Yes | ✅ Yes | In-place modification |
out | ❌ No | ✅ Yes | Return multiple values |
in | ✅ Yes | ❌ No | Read-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
- Program.cs và Minimal APIs - So sánh Minimal APIs với Startup.cs truyền thống
Middleware
- Middleware là gì? - Khái niệm và cách hoạt động
- Thứ tự Middleware - Pipeline execution order
- Custom Middleware - Cách viết Middleware tùy chỉnh
Dependency Injection
- 3 loại Lifecycle - Singleton, Scoped, Transient
- Ví dụ thực tế - Khi nào dùng loại nào?
- Scoped vào Singleton - Vì sao không nên?
Routing
- Conventional Routing - MVC routing
- Attribute Routing - Web API routing
Filters
- Các loại Filter - Authorization, Resource, Action, Exception, Result
- So sánh với Middleware - Khi nào dùng filter?
Cấu hình & Logging
- appsettings.json - Configuration
- IOptions Pattern - Options pattern
- Serilog - Logging tối ưu
Xử lý lỗi
- Global Error Handling - UseExceptionHandler, UseStatusCodePages
- ProblemDetails (RFC 7807) - Standard error response
- Custom Exception Filter - IExceptionHandler implementation
- Error Handling trong Minimal APIs - TypedResults
Model Binding
- Binding Sources - FromBody, FromQuery, FromRoute, FromHeader, FromForm
- Binding Order - Thứ tự ưu tiên
- Custom Model Binding - Custom binders và providers
- Binding trong Minimal APIs - Parameter binding
Model Validation
- Data Annotations - Built-in validation attributes
- Custom Validation Attribute - Tạo custom attributes
- IValidatableObject - Self-validating models
- FluentValidation - Fluent validation library
- Validation trong Minimal APIs - Validation pattern
ActionResult Types
- IActionResult - Common action results
- ActionResult
- Generic action results - TypedResults (Minimal APIs) - Type-safe results
- Custom ActionResult - Custom result classes
Static Files & File Handling
- Serving Static Files - wwwroot, UseStaticFiles
- File Providers - Physical, Embedded, Composite
- Cache Headers - Caching static files
- File Upload - IFormFile, streaming uploads
- File Download - File results, streaming downloads
Background Tasks
- IHostedService - Background service interface
- BackgroundService - Abstract base class
- Scheduled Tasks - Cron-based scheduling
- Worker Service Template - Creating worker services
Health Checks
- Basic Health Checks - Setup and configuration
- Custom Health Checks - IHealthCheck implementation
- Health Check UI - Visual health monitoring
- Kubernetes Probes - Liveness, Readiness, Startup
Minimal APIs Advanced
- Route Groups - Grouping routes (.NET 7+)
- Endpoint Filters - Filter pipeline
- Parameter Binding - Advanced binding
- Organizing Large APIs - Best practices
Content Negotiation
- Input Formatters - JSON, XML, custom formatters
- Output Formatters - Response formatting
- JSON Configuration - Serializer options
- Custom Formatters - CSV, etc.
Output Caching
- Output Caching vs Response Caching - Server vs client caching
- Cache Policies - Policy configuration
- Cache Tags - Tag-based invalidation
- Vary By - Query, Header, Route variation
Kestrel Configuration
- Endpoint Configuration - HTTP, HTTPS, Unix sockets
- HTTPS Configuration - Certificates, redirection
- Request Limits - Body size, headers, timeouts
- HTTP/2 & HTTP/3 - Protocol configuration
- Reverse Proxy - When to use
Localization (i18n)
- Resource Files - .resx files structure
- IStringLocalizer - Using localizers
- View Localization - Localizing Razor views
- Data Annotations Localization - Localized validation
- Culture Providers - Query, Cookie, Header, Route
Real-time Communication
- SignalR Basics - Hub, client-server communication
- Groups & Users - Targeted messaging
- Strongly-typed Hubs - Interface-based hubs
- Scaling with Redis - Backplane configuration
gRPC
- gRPC vs REST - When to use gRPC
- Protocol Buffers - .proto files
- Streaming - Server, Client, Bidirectional
- gRPC Client - Client setup and usage
- Interceptors - Logging, error handling
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
| Aspect | Minimal APIs | Startup.cs |
|---|---|---|
| Code lines | Ít hơn | Nhiều hơn |
| Complexity | Đơn giản | Phức tạp hơn |
| Flexibility | Hạn chế | Linh hoạt |
| Testability | Khó test hơn | Dễ test hơn |
| Use case | Microservices, small APIs | Large applications |
| DI Configuration | Trong Program.cs | Trong 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
- Exception Handling - Đầu tiên để bắt exceptions
- Security (CORS, Authentication, Authorization)
- Static Files - Nếu cần
- Routing - Xác định endpoint
- Endpoints - Controller/Action
- 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
-
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)
-
Register services với interface:
// ✅ Tốt builder.Services.AddScoped<IProductRepository, ProductRepository>(); // ❌ Tránh builder.Services.AddScoped<ProductRepository>(); -
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
| Aspect | Conventional Routing | Attribute Routing |
|---|---|---|
| Definition | Tập trung trong config | Trực tiếp trên method |
| Flexibility | Linh hoạt với patterns | Chi tiết, rõ ràng |
| RESTful | Khó tạo RESTful | Dễ dàng |
| Convention | Theo quy ước đặt tên | Tùy chỉnh |
| Use case | MVC với Views | Web 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
| Constraint | Example | Description |
|---|---|---|
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:
- Static segments (e.g.,
api/products) - Route parameters có constraints
- Route parameters không constraints
- 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 Type | Implements | Runs | Use Case |
|---|---|---|---|
| Authorization | IAuthorizationFilter | Đầu tiên | Check authentication/authorization |
| Resource | IResourceFilter | Trước & sau model binding | Caching, performance tracking |
| Action | IActionFilter | Trước & sau action | Logging, validation |
| Exception | IExceptionFilter | Khi exception xảy ra | Error handling |
| Result | IResultFilter | Trước & sau result execution | Output 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
| Level | Usage |
|---|---|
Verbose | Detailed tracing |
Debug | Debugging information |
Information | General information |
Warning | Something unexpected happened |
Error | Functionality issue |
Fatal | Critical error causing shutdown |
Best Practices
1. Sử dụng correct log levels
// ✅ Correct
Log.Debug("Processing request {RequestId}", requestId);
Log.Information("Product {ProductId} created successfully", productId);
Log.Warning("Cache miss for key {CacheKey}", key);
Log.Error(ex, "Failed to process order {OrderId}", orderId);
Log.Fatal(ex, "Application terminating due to unhandled exception");
2. Include context
public async Task<IActionResult> UpdateProduct(int id, [FromBody] Product product)
{
Log.LogInformation(
"Updating product {ProductId} by user {UserId}",
id,
User.Identity.Name);
// ... code
}
3. Tránh logging sensitive data
// ❌ Bad - Log sensitive data
Log.Information("User login: {Email}, Password: {Password}", email, password);
// ✅ Good - Log masked data
Log.Information("User login attempt for {Email}", email);
Xử lý lỗi (Error Handling)
Global Error Handling
UseExceptionHandler
// Program.cs
var app = builder.Build();
// Development - hiển thị chi tiết lỗi
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// Production - redirect đến error page
app.UseExceptionHandler("/Error");
// Hoặc custom error handler
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "application/json";
var exception = context.Features.Get<IExceptionHandlerFeature>();
var error = new
{
statusCode = 500,
message = "An unexpected error occurred",
traceId = context.TraceIdentifier
};
await context.Response.WriteAsJsonAsync(error);
});
});
}
UseStatusCodePages
// Xử lý các status code 4xx, 5xx
app.UseStatusCodePages();
// Hoặc redirect
app.UseStatusCodePagesWithRedirects("/Error/{0}");
// Hoặc re-execute
app.UseStatusCodePagesWithReExecute("/Error", "?statusCode={0}");
ProblemDetails (RFC 7807)
Cấu trúc ProblemDetails
┌─────────────────────────────────────────────────────────────────┐
│ PROBLEMDETAILS STRUCTURE │
├─────────────────────────────────────────────────────────────────┤
│ { │
│ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", │
│ "title": "Bad Request", │
│ "status": 400, │
│ "detail": "The input was invalid", │
│ "instance": "/api/products", │
│ "traceId": "00-abc123-xyz789-00", │
│ "errors": { │
│ "Name": ["The Name field is required"], │
│ "Price": ["Price must be greater than 0"] │
│ } │
│ } │
└─────────────────────────────────────────────────────────────────┘
Cấu hình ProblemDetails
// Program.cs
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Extensions["traceId"] =
context.HttpContext.TraceIdentifier;
context.ProblemDetails.Extensions["requestId"] =
context.HttpContext.Request.Headers["X-Request-Id"].FirstOrDefault();
};
});
// Sử dụng trong controller
[HttpPost]
public IActionResult CreateProduct(Product product)
{
if (product.Price <= 0)
{
return Problem(
title: "Invalid Price",
detail: "Price must be greater than 0",
statusCode: 400,
type: "https://tools.ietf.org/html/rfc7231#section-6.5.1",
instance: "/api/products");
}
return Ok(product);
}
Custom Exception Filter
Global Exception Handler
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
_logger.LogError(exception, "Unhandled exception occurred");
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "Server Error",
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1",
Instance = httpContext.Request.Path
};
httpContext.Response.StatusCode = problemDetails.Status.Value;
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
return true; // Exception handled
}
}
// Đăng ký
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
Exception Type Handler
public class NotFoundException : Exception
{
public NotFoundException(string message) : base(message) { }
}
public class ValidationException : Exception
{
public IDictionary<string, string[]> Errors { get; }
public ValidationException(IDictionary<string, string[]> errors)
: base("Validation failed")
{
Errors = errors;
}
}
// Handler cho từng loại exception
public class NotFoundExceptionHandler : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
if (exception is not NotFoundException notFoundEx)
{
return false; // Không handle, để handler khác xử lý
}
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status404NotFound,
Title = "Not Found",
Detail = notFoundEx.Message,
Type = "https://datatracker.ietf.org/doc/html/rfc7231#section-6.5.4"
};
httpContext.Response.StatusCode = 404;
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
return true;
}
}
// Đăng ký theo thứ tự ưu tiên
builder.Services.AddExceptionHandler<NotFoundExceptionHandler>();
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
Error Handling trong Minimal APIs
var app = builder.Build();
// Global error handler
app.UseExceptionHandler(exceptionHandlerApp =>
{
exceptionHandlerApp.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>();
var problemDetails = new ProblemDetails
{
Status = 500,
Title = "Internal Server Error",
Instance = context.Request.Path
};
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(problemDetails);
});
});
// Typed Results với error handling
app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
try
{
db.Products.Add(product);
await db.SaveChangesAsync();
return Results.Created($"/api/products/{product.Id}", product);
}
catch (DbUpdateException ex)
{
return Results.Problem(
title: "Database Error",
detail: ex.Message,
statusCode: 500);
}
});
Best Practices
1. Không expose sensitive information
// ❌ Bad - expose internal details
return Problem(
detail: $"SQL Error: {ex.Message} Connection: {connectionString}",
statusCode: 500);
// ✅ Good - generic message
return Problem(
detail: "An unexpected error occurred. Please try again later.",
statusCode: 500);
2. Log errors properly
// ✅ Log với context
_logger.LogError(ex,
"Failed to process order {OrderId} for user {UserId}",
orderId, userId);
// ✅ Include request details
_logger.LogWarning(
"Rate limit exceeded for IP {IP} on endpoint {Endpoint}",
context.Connection.RemoteIpAddress,
context.Request.Path);
3. Use appropriate status codes
| Status Code | When to Use |
|---|---|
| 400 Bad Request | Invalid input, validation failed |
| 401 Unauthorized | Missing or invalid authentication |
| 403 Forbidden | Authenticated but no permission |
| 404 Not Found | Resource doesn’t exist |
| 409 Conflict | Business rule violation, duplicate |
| 422 Unprocessable | Valid syntax but semantic errors |
| 429 Too Many Requests | Rate limit exceeded |
| 500 Internal Server Error | Unexpected server error |
| 503 Service Unavailable | Maintenance or overload |
Model Binding
Khái niệm
Model Binding là quá trình ASP.NET Core chuyển đổi dữ liệu từ HTTP request thành các tham số của action method.
┌─────────────────────────────────────────────────────────────────┐
│ MODEL BINDING SOURCES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Form │ │ Route │ │ Query │ │ Body │ │
│ │ Values │ │ Values │ │ String │ │ (JSON) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └─────────────┴─────────────┴─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Model Binder │ │
│ │ (Type Conversion) │ │
│ └─────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Action Method │ │
│ │ Parameters │ │
│ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Binding Sources
[FromBody]
// JSON request body
[HttpPost]
public IActionResult Create([FromBody] Product product)
{
// product được deserialize từ JSON body
return Ok(product);
}
// Request:
// POST /api/products
// Content-Type: application/json
// {"name": "Laptop", "price": 999.99}
[FromQuery]
// Query string parameters
[HttpGet]
public IActionResult GetProducts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? search = null)
{
// /api/products?page=2&pageSize=20&search=laptop
return Ok(new { page, pageSize, search });
}
// Hoặc binding vào object
[HttpGet]
public IActionResult GetProducts([FromQuery] PagingRequest request)
{
return Ok(request);
}
public class PagingRequest
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 10;
public string? Search { get; set; }
}
[FromRoute]
// Route data
[HttpGet("{id}")]
public IActionResult GetById([FromRoute] int id)
{
// /api/products/123 -> id = 123
return Ok(id);
}
// Multiple route parameters
[HttpGet("{category}/{id}")]
public IActionResult GetByCategoryAndId(
[FromRoute] string category,
[FromRoute] int id)
{
// /api/products/electronics/123
return Ok(new { category, id });
}
[FromHeader]
[HttpGet]
public IActionResult GetWithHeader([FromHeader] string apiKey)
{
// Lấy từ header: X-API-Key: abc123
return Ok(apiKey);
}
// Hoặc lấy tất cả headers
[HttpGet]
public IActionResult GetHeaders([FromHeader] IHeaderDictionary headers)
{
return Ok(headers);
}
[FromForm]
// Form data (multipart/form-data)
[HttpPost]
public IActionResult CreateForm([FromForm] ProductForm form)
{
return Ok(form);
}
public class ProductForm
{
public string Name { get; set; }
public decimal Price { get; set; }
public IFormFile Image { get; set; }
}
Binding Order
ASP.NET Core tìm kiếm giá trị theo thứ tự:
- Form values - POST form data
- Route values - URL route parameters
- Query string - URL query parameters
- Request body - JSON/XML (cho complex types)
// Không cần attribute cho complex types (mặc định là [FromBody])
[HttpPost]
public IActionResult Create(Product product) // Tự động [FromBody]
{
return Ok(product);
}
// Cần attribute cho simple types
[HttpGet]
public IActionResult Get(int id) // Tự động [FromQuery]
{
return Ok(id);
}
Custom Model Binding
Custom Model Binder
public class CommaSeparatedListBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue;
if (string.IsNullOrEmpty(value))
{
bindingContext.Result = ModelBindingResult.Success(new List<string>());
return Task.CompletedTask;
}
var list = value.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.ToList();
bindingContext.Result = ModelBindingResult.Success(list);
return Task.CompletedTask;
}
}
// Sử dụng
[HttpGet]
public IActionResult GetTags([ModelBinder(typeof(CommaSeparatedListBinder))] List<string> tags)
{
// /api/products?tags=csharp,dotnet,web -> ["csharp", "dotnet", "web"]
return Ok(tags);
}
Model Binder Provider
public class CommaSeparatedListBinderProvider : IModelBinderProvider
{
public IModelBinder? GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(List<string>))
{
return new BinderTypeModelBinder(typeof(CommaSeparatedListBinder));
}
return null;
}
}
// Đăng ký
builder.Services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new CommaSeparatedListBinderProvider());
});
Binding Complex Types
Nested Objects
public class Address
{
public string Street { get; set; }
public string City { get; set; }
public string Country { get; set; }
}
public class Customer
{
public string Name { get; set; }
public Address Address { get; set; }
public List<string> Tags { get; set; }
}
// JSON binding
// {
// "name": "John",
// "address": {
// "street": "123 Main St",
// "city": "NYC",
// "country": "USA"
// },
// "tags": ["vip", "active"]
// }
Collections
[HttpPost]
public IActionResult CreateMany([FromBody] List<Product> products)
{
// [
// {"name": "Product 1", "price": 100},
// {"name": "Product 2", "price": 200}
// ]
return Ok(products.Count);
}
Binding với Minimal APIs
// Tự động binding
app.MapPost("/api/products", (Product product) => Results.Ok(product));
// Binding từ query string
app.MapGet("/api/products", (int page, int pageSize) =>
Results.Ok(new { page, pageSize }));
// Binding từ route
app.MapGet("/api/products/{id}", (int id) => Results.Ok(id));
// Custom binding
app.MapGet("/api/products", ([FromQuery] PagingRequest request) =>
Results.Ok(request));
// Binding từ header
app.MapGet("/api/secure", ([FromHeader(Name = "X-API-Key")] string apiKey) =>
{
if (apiKey != "secret") return Results.Unauthorized();
return Results.Ok("Authorized");
});
Best Practices
1. Sử dụng đúng binding source
// ✅ Rõ ràng
public IActionResult Get(
[FromRoute] int id,
[FromQuery] string? search,
[FromBody] Product product)
// ❌ Không rõ ràng
public IActionResult Get(int id, string search, Product product)
2. Sử dụng nullable cho optional parameters
// ✅ Good
public IActionResult Get([FromQuery] string? search = null)
// ❌ Bad - sẽ throw nếu không có query param
public IActionResult Get([FromQuery] string search)
3. Validate binding
[HttpPost]
public IActionResult Create([FromBody] Product product)
{
if (product == null)
{
return BadRequest("Invalid request body");
}
// ...
}
Model Validation
Overview Questions
- Làm thế nào để đảm bảo dữ liệu nhận từ client là hợp lệ?
- Data Annotations là gì và sử dụng như thế nào?
- FluentValidation khác Data Annotations ra sao?
- Làm sao để tạo custom validation attribute?
- Validation response format chuẩn là gì?
[ApiController]tự động validation như thế nào?
Data Annotations Validation
Built-in Validation Attributes
public class Product
{
public int Id { get; set; }
[Required(ErrorMessage = "Name is required")]
[StringLength(100, MinimumLength = 2, ErrorMessage = "Name must be 2-100 characters")]
public string Name { get; set; } = string.Empty;
[Range(0.01, 9999.99, ErrorMessage = "Price must be between 0.01 and 9999.99")]
public decimal Price { get; set; }
[EmailAddress(ErrorMessage = "Invalid email format")]
public string? Email { get; set; }
[Url(ErrorMessage = "Invalid URL format")]
public string? Website { get; set; }
[Phone(ErrorMessage = "Invalid phone number")]
public string? Phone { get; set; }
[RegularExpression(@"^[A-Z]{3}$", ErrorMessage = "Code must be 3 uppercase letters")]
public string? Code { get; set; }
[Compare("Password", ErrorMessage = "Passwords do not match")]
public string? ConfirmPassword { get; set; }
}
Validation trong Controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpPost]
public IActionResult Create(Product product)
{
// [ApiController] tự động check ModelState.IsValid
// Nếu invalid, tự động return 400 Bad Request
// Xử lý khi valid
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
return Ok(new { id });
}
}
Validation Error Response
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Name": ["Name is required"],
"Price": ["Price must be between 0.01 and 9999.99"]
}
}
Custom Validation Attribute
Simple Custom Attribute
public class MinimumAgeAttribute : ValidationAttribute
{
private readonly int _minimumAge;
public MinimumAgeAttribute(int minimumAge)
{
_minimumAge = minimumAge;
}
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is DateTime birthDate)
{
var age = DateTime.Today.Year - birthDate.Year;
if (birthDate > DateTime.Today.AddYears(-age)) age--;
if (age < _minimumAge)
{
return new ValidationResult($"Must be at least {_minimumAge} years old");
}
}
return ValidationResult.Success;
}
}
// Sử dụng
public class UserRegistration
{
[Required]
public string Name { get; set; } = string.Empty;
[MinimumAge(18, ErrorMessage = "You must be at least 18 years old")]
public DateTime DateOfBirth { get; set; }
}
Property Comparison Validation
public class DateRangeAttribute : ValidationAttribute
{
private readonly string _startDateProperty;
private readonly string _endDateProperty;
public DateRangeAttribute(string startDateProperty, string endDateProperty)
{
_startDateProperty = startDateProperty;
_endDateProperty = endDateProperty;
}
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
var startDateProperty = validationContext.ObjectType.GetProperty(_startDateProperty);
var endDateProperty = validationContext.ObjectType.GetProperty(_endDateProperty);
if (startDateProperty?.GetValue(validationContext.ObjectInstance) is DateTime start &&
endDateProperty?.GetValue(validationContext.ObjectInstance) is DateTime end)
{
if (start >= end)
{
return new ValidationResult("Start date must be before end date");
}
}
return ValidationResult.Success;
}
}
// Sử dụng
[DateRange("StartDate", "EndDate", ErrorMessage = "Invalid date range")]
public class Event
{
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
IValidatableObject
Self-Validating Model
public class Order : IValidatableObject
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
public DateTime? ShipDate { get; set; }
public List<OrderItem> Items { get; set; } = new();
public string? CouponCode { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Cross-property validation
if (ShipDate.HasValue && ShipDate <= OrderDate)
{
yield return new ValidationResult(
"Ship date must be after order date",
new[] { nameof(ShipDate) });
}
// Collection validation
if (!Items.Any())
{
yield return new ValidationResult(
"Order must have at least one item",
new[] { nameof(Items) });
}
// Business rule validation
if (Items.Count > 10 && string.IsNullOrEmpty(CouponCode))
{
yield return new ValidationResult(
"Orders with more than 10 items require a coupon code",
new[] { nameof(CouponCode) });
}
}
}
public class OrderItem
{
public int ProductId { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
}
FluentValidation
Cài đặt
dotnet add package FluentValidation.AspNetCore
Cấu hình
// Program.cs
builder.Services.AddControllers()
.AddFluentValidation(fv =>
{
fv.RegisterValidatorsFromAssemblyContaining<Program>();
fv.DisableDataAnnotationsValidation = true; // Optional: disable data annotations
});
Tạo Validator
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.MaximumLength(100).WithMessage("Name cannot exceed 100 characters");
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("Price must be greater than 0")
.LessThan(10000).WithMessage("Price cannot exceed 10,000");
RuleFor(x => x.Email)
.EmailAddress().WithMessage("Invalid email format")
.When(x => !string.IsNullOrEmpty(x.Email));
RuleFor(x => x.Category)
.Must(BeAValidCategory).WithMessage("Invalid category")
.When(x => !string.IsNullOrEmpty(x.Category));
// Conditional validation
RuleFor(x => x.DiscountCode)
.NotEmpty().WithMessage("Discount code is required for orders over $100")
.When(x => x.Price > 100);
}
private bool BeAValidCategory(string category)
{
var validCategories = new[] { "Electronics", "Books", "Clothing", "Food" };
return validCategories.Contains(category);
}
}
Custom Validators với FluentValidation
public static class CustomValidators
{
public static IRuleBuilderOptions<T, string> MustBeValidPhoneNumber<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder.Must(phone =>
phone != null && phone.All(char.IsDigit) && phone.Length >= 10 && phone.Length <= 15)
.WithMessage("Phone number must be 10-15 digits");
}
public static IRuleBuilderOptions<T, string> MustBeStrongPassword<T>(
this IRuleBuilder<T, string> ruleBuilder)
{
return ruleBuilder
.MinimumLength(8).WithMessage("Password must be at least 8 characters")
.Matches("[A-Z]").WithMessage("Password must contain uppercase letter")
.Matches("[a-z]").WithMessage("Password must contain lowercase letter")
.Matches("[0-9]").WithMessage("Password must contain digit")
.Matches("[^a-zA-Z0-9]").WithMessage("Password must contain special character");
}
}
// Sử dụng
public class UserRegistrationValidator : AbstractValidator<UserRegistration>
{
public UserRegistrationValidator()
{
RuleFor(x => x.Phone).MustBeValidPhoneNumber();
RuleFor(x => x.Password).MustBeStrongPassword();
}
}
Validation Pipeline
Custom Validation Filter
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var errors = context.ModelState
.Where(e => e.Value?.Errors.Count > 0)
.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray()
);
var response = new
{
type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
title = "Validation Error",
status = 400,
errors = errors
};
context.Result = new BadRequestObjectResult(response);
}
}
}
// Đăng ký global
builder.Services.AddControllers(options =>
{
options.Filters.Add<ValidateModelAttribute>();
});
Validation trong Minimal APIs
app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
var validationContext = new ValidationContext<Product>(product);
var validator = new ProductValidator();
var validationResult = await validator.ValidateAsync(validationContext);
if (!validationResult.IsValid)
{
var errors = validationResult.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(
g => g.Key,
g => g.Select(e => e.ErrorMessage).ToArray()
);
return Results.ValidationProblem(errors);
}
db.Products.Add(product);
await db.SaveChangesAsync();
return Results.Created($"/api/products/{product.Id}", product);
});
Best Practices
1. Kết hợp Data Annotations và FluentValidation
// Data Annotations cho simple validation
public class Product
{
[Required]
public string Name { get; set; } = string.Empty;
[Range(0.01, 9999.99)]
public decimal Price { get; set; }
}
// FluentValidation cho complex validation
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
RuleFor(x => x.Name)
.MustAsync(BeUniqueName).WithMessage("Product name must be unique");
RuleFor(x => x.Price)
.MustAsync(NotExceedBudget).WithMessage("Price exceeds budget limit");
}
}
2. Validation ở nhiều layers
┌─────────────────────────────────────────────────────────────────┐
│ VALIDATION LAYERS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: API Level (Data Annotations / FluentValidation) │
│ - Input format validation │
│ - Required fields │
│ - Range/length constraints │
│ │
│ Layer 2: Service Level (Business Rules) │
│ - Business logic validation │
│ - Cross-entity validation │
│ - Database-dependent validation │
│ │
│ Layer 3: Domain Level (Invariants) │
│ - Domain invariants │
│ - Entity consistency │
│ │
└─────────────────────────────────────────────────────────────────┘
3. Fail Fast
// ✅ Validate sớm
public async Task<IActionResult> Create(Product product)
{
// Validation xảy ra trước khi xử lý business logic
if (!ModelState.IsValid)
return BadRequest(ModelState);
// Chỉ chạy khi valid
await _service.ProcessAsync(product);
return Ok();
}
ActionResult Types
Overview Questions
IActionResultvàActionResult<T>khác nhau như thế nào?- Khi nào nên dùng
IActionResultvsActionResult<T>? - Các helper methods nào có sẵn trong ControllerBase?
TypedResultstrong Minimal APIs là gì?- Làm sao để return custom response format?
IActionResult
Interface cơ bản
// IActionResult là interface base cho tất cả action results
public interface IActionResult
{
Task ExecuteResultAsync(ActionContext context);
}
Common Action Results
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// 200 OK
[HttpGet]
public IActionResult GetAll()
{
return Ok(products);
// Equivalent: return new OkObjectResult(products);
}
// 200 OK với object
[HttpGet("{id}")]
public IActionResult GetById(int id)
{
return Ok(new { id, name = "Product" });
}
// 201 Created
[HttpPost]
public IActionResult Create(Product product)
{
var createdProduct = _service.Create(product);
return CreatedAtAction(
nameof(GetById),
new { id = createdProduct.Id },
createdProduct);
}
// 204 No Content
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
_service.Delete(id);
return NoContent();
}
// 400 Bad Request
[HttpPost("validate")]
public IActionResult Validate(Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return Ok();
}
// 404 Not Found
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var product = _service.GetById(id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
// 401 Unauthorized
[HttpGet("secret")]
public IActionResult GetSecret()
{
return Unauthorized();
}
// 403 Forbidden
[HttpGet("admin")]
public IActionResult GetAdmin()
{
return Forbid();
}
// 409 Conflict
[HttpPost("check-name")]
public IActionResult CheckName(string name)
{
if (_service.NameExists(name))
{
return Conflict(new { error = "Name already exists" });
}
return Ok();
}
// 422 Unprocessable Entity
[HttpPost("process")]
public IActionResult Process(Order order)
{
if (!CanProcess(order))
{
return UnprocessableEntity(new { error = "Cannot process order" });
}
return Ok();
}
// 500 Internal Server Error
[HttpGet("error")]
public IActionResult TriggerError()
{
return StatusCode(500, new { error = "Internal server error" });
}
}
ActionResult
Generic ActionResult
// ActionResult<T> cho phép return cả IActionResult và T
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// Return T - tự động wrap thành OkObjectResult
[HttpGet]
public ActionResult<List<Product>> GetAll()
{
return _service.GetAll();
}
// Return IActionResult khi cần status code khác
[HttpGet("{id}")]
public ActionResult<Product> GetById(int id)
{
var product = _service.GetById(id);
if (product == null)
{
return NotFound(); // IActionResult
}
return product; // T
}
// Mixed returns
[HttpPost]
public ActionResult<Product> Create(Product product)
{
var created = _service.Create(product);
return CreatedAtAction(
nameof(GetById),
new { id = created.Id },
created);
}
}
So sánh IActionResult vs ActionResult
| Aspect | IActionResult | ActionResult |
|---|---|---|
| Return type | Chỉ IActionResult | Cả IActionResult và T |
| Swagger/OpenAPI | Không rõ response type | Tự động generate schema |
| Type safety | Không | Có |
| Use case | Khi cần linh hoạt | Khi có response type rõ ràng |
Content Results
Different Content Types
[HttpGet("json")]
public IActionResult GetJson()
{
return Json(new { message = "Hello" });
}
[HttpGet("text")]
public IActionResult GetText()
{
return Content("Hello World", "text/plain");
}
[HttpGet("html")]
public IActionResult GetHtml()
{
return Content("<h1>Hello</h1>", "text/html");
}
[HttpGet("xml")]
public IActionResult GetXml()
{
return Content("<message>Hello</message>", "application/xml");
}
File Results
File Downloads
[HttpGet("download")]
public IActionResult DownloadFile()
{
var bytes = File.ReadAllBytes("path/to/file.pdf");
return File(bytes, "application/pdf", "document.pdf");
}
[HttpGet("download-stream")]
public IActionResult DownloadStream()
{
var stream = new FileStream("path/to/file.pdf", FileMode.Open);
return File(stream, "application/pdf", "document.pdf");
}
[HttpGet("download-virtual")]
public IActionResult DownloadVirtualFile()
{
return PhysicalFile(
@"C:\files\document.pdf",
"application/pdf",
"document.pdf");
}
Redirect Results
Redirects
[HttpGet("redirect")]
public IActionResult RedirectExample()
{
// 302 Found
return Redirect("https://example.com");
// 301 Moved Permanently
return RedirectPermanent("https://example.com");
// Redirect to action
return RedirectToAction("GetAll");
// Redirect to route
return RedirectToRoute("default", new { controller = "Home", action = "Index" });
}
TypedResults (Minimal APIs)
.NET 7+ TypedResults
var app = builder.Build();
// TypedResults cung cấp type-safe results
app.MapGet("/api/products/{id}", async (int id, AppDbContext db) =>
{
var product = await db.Products.FindAsync(id);
if (product == null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(product);
});
app.MapPost("/api/products", async (Product product, AppDbContext db) =>
{
db.Products.Add(product);
await db.SaveChangesAsync();
return TypedResults.Created($"/api/products/{product.Id}", product);
});
app.MapDelete("/api/products/{id}", async (int id, AppDbContext db) =>
{
var product = await db.Products.FindAsync(id);
if (product == null)
{
return TypedResults.NotFound();
}
db.Products.Remove(product);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
TypedResults vs Results
// Results - trả về IActionResult
app.MapGet("/old", () => Results.Ok("Hello"));
// TypedResults - trả về specific type (better for OpenAPI)
app.MapGet("/new", () => TypedResults.Ok("Hello"));
// TypedResults tốt hơn cho OpenAPI generation
app.MapGet("/api/products", () => TypedResults.Ok(new List<Product>()))
.Produces<List<Product>>(200)
.ProducesProblem(500);
Custom ActionResult
Custom Result Class
public class PagedResult<T> : IActionResult
{
private readonly IEnumerable<T> _items;
private readonly int _total;
private readonly int _page;
private readonly int _pageSize;
public PagedResult(IEnumerable<T> items, int total, int page, int pageSize)
{
_items = items;
_total = total;
_page = page;
_pageSize = pageSize;
}
public async Task ExecuteResultAsync(ActionContext context)
{
var response = context.HttpContext.Response;
response.ContentType = "application/json";
var result = new
{
items = _items,
total = _total,
page = _page,
pageSize = _pageSize,
totalPages = (int)Math.Ceiling((double)_total / _pageSize)
};
await JsonSerializer.SerializeAsync(response.Body, result);
}
}
// Sử dụng
[HttpGet]
public IActionResult GetProducts([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
var (items, total) = _service.GetPaged(page, pageSize);
return new PagedResult<Product>(items, total, page, pageSize);
}
Best Practices
1. Sử dụng ActionResult cho API rõ ràng
// ✅ Tốt - rõ ràng response type
[HttpGet]
public ActionResult<List<Product>> GetAll() => _service.GetAll();
// ❌ Tránh - không rõ response type
[HttpGet]
public IActionResult GetAll() => Ok(_service.GetAll());
2. Sử dụng helper methods
// ✅ Tốt
return Ok(data);
return NotFound();
return BadRequest(ModelState);
// ❌ Dài dòng
return new ObjectResult(data) { StatusCode = 200 };
return new ObjectResult(null) { StatusCode = 404 };
3. Consistent error responses
// ✅ Sử dụng ProblemDetails
return Problem(
title: "Not Found",
detail: $"Product with id {id} not found",
statusCode: 404);
// ❌ Inconsistent
return NotFound(new { error = "Not found", message = "Product not found" });
Static Files
Overview Questions
- ASP.NET Core phục vụ static files như thế nào?
wwwrootfolder là gì và cấu hình ra sao?- Làm sao để enable directory browsing?
- File providers là gì và khi nào cần custom file provider?
- Cache headers cho static files được cấu hình ra sao?
Serving Static Files
Default Configuration
// Program.cs
var app = builder.Build();
// Enable static files middleware
app.UseStaticFiles();
// Static files được phục vụ từ wwwroot folder
// http://localhost/images/logo.png -> wwwroot/images/logo.png
Custom Static File Location
// Serve từ folder khác
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "MyStaticFiles")),
RequestPath = "/static"
});
// http://localhost/static/logo.png -> MyStaticFiles/logo.png
wwwroot Structure
Project/
├── wwwroot/
│ ├── css/
│ │ └── site.css
│ ├── js/
│ │ └── site.js
│ ├── images/
│ │ └── logo.png
│ └── lib/
│ └── bootstrap/
├── Program.cs
└── appsettings.json
HTML Reference
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/css/site.css" />
</head>
<body>
<img src="/images/logo.png" alt="Logo" />
<script src="/js/site.js"></script>
</body>
</html>
Directory Browsing
Enable Directory Browsing
var app = builder.Build();
// Enable directory browsing
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "wwwroot")),
RequestPath = "/files"
});
// Hoặc cho toàn bộ static files
app.UseStaticFiles();
app.UseDirectoryBrowser();
Security Warning
⚠️ Chỉ enable directory browsing cho development hoặc khi cần thiết. Production nên disable để tránh expose file structure.
File Providers
Physical File Provider
// Default - sử dụng PhysicalFileProvider
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "wwwroot"))
});
Embedded File Provider
// Serve files từ embedded resources
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new EmbeddedFileProvider(
typeof(Program).Assembly,
"MyApp.EmbeddedFiles")
});
Composite File Provider
// Kết hợp nhiều file providers
var compositeProvider = new CompositeFileProvider(
new PhysicalFileProvider(Path.Combine(builder.Environment.ContentRootPath, "wwwroot")),
new EmbeddedFileProvider(typeof(Program).Assembly, "MyApp.EmbeddedFiles")
);
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = compositeProvider
});
Content Types
Default Content Types
// ASP.NET Core tự động detect content type từ file extension
// .css -> text/css
// .js -> application/javascript
// .png -> image/png
// .html -> text/html
Custom Content Types
var provider = new FileExtensionContentTypeProvider();
// Add custom mapping
provider.Mappings[".webp"] = "image/webp";
provider.Mappings[".custom"] = "application/x-custom";
// Remove mapping
provider.Mappings.Remove(".rtf");
app.UseStaticFiles(new StaticFileOptions
{
ContentTypeProvider = provider
});
Cache Headers
Default Behavior
// Static files middleware tự động thêm Last-Modified header
// Browser sẽ cache và send If-Modified-Since request
// Server trả về 304 Not Modified nếu file không thay đổi
Custom Cache Headers
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// Cache 1 năm cho versioned files
const int durationInSeconds = 60 * 60 * 24 * 365;
ctx.Context.Response.Headers["Cache-Control"] =
$"public, max-age={durationInSeconds}";
}
});
Response Caching Middleware
// Add response caching
app.UseResponseCaching();
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers["Cache-Control"] = "public, max-age=3600";
ctx.Context.Response.Headers["Vary"] = "Accept-Encoding";
}
});
Security Considerations
Block Sensitive Files
// Block access to sensitive file types
app.UseStaticFiles(new StaticFileOptions
{
ServeUnknownFileTypes = false, // Default: false
OnPrepareResponse = ctx =>
{
var path = ctx.Context.Request.Path.Value;
if (path.EndsWith(".json") || path.EndsWith(".config"))
{
ctx.Context.Response.StatusCode = 403;
}
}
});
Best Practices
┌─────────────────────────────────────────────────────────────────┐
│ STATIC FILE SECURITY │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ✅ DO: │
│ - Chỉ serve từ wwwroot hoặc folder chỉ định │
│ - Disable directory browsing trong production │
│ - Set appropriate cache headers │
│ - Use CDN for production static files │
│ │
│ ❌ DON'T: │
│ - Serve từ folders chứa sensitive data │
│ - Enable directory browsing không cần thiết │
│ - Serve .config, .json, .env files │
│ - Forget to validate file uploads │
│ │
└─────────────────────────────────────────────────────────────────┘
Default Files
Serve Default File
// Serve default.html khi access root
app.UseDefaultFiles(new DefaultFilesOptions
{
DefaultFileNames = new List<string> { "index.html", "default.html" }
});
// Phải đặt TRƯỚC UseStaticFiles
app.UseStaticFiles();
// http://localhost/ -> wwwroot/index.html
UseFileServer (Combined)
// Kết hợp UseDefaultFiles + UseStaticFiles + UseDirectoryBrowser
app.UseFileServer();
// Hoặc với custom options
app.UseFileServer(new FileServerOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(builder.Environment.ContentRootPath, "wwwroot")),
EnableDirectoryBrowsing = false
});
File Upload & Download
Overview Questions
- Làm sao để upload file trong ASP.NET Core?
IFormFilelà gì và sử dụng như thế nào?- Streaming upload khác buffering như thế nào?
- Làm sao để handle large file uploads?
- Download file được implement ra sao?
File Upload với IFormFile
Single File Upload
[HttpPost("upload")]
public async Task<IActionResult> UploadFile(IFormFile file)
{
if (file == null || file.Length == 0)
{
return BadRequest("No file uploaded");
}
// Validate file size (max 10MB)
if (file.Length > 10 * 1024 * 1024)
{
return BadRequest("File too large");
}
// Validate file type
var allowedExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif" };
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowedExtensions.Contains(extension))
{
return BadRequest("Invalid file type");
}
// Save file
var fileName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Combine("uploads", fileName);
using var stream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(stream);
return Ok(new { fileName, filePath });
}
Multiple File Upload
[HttpPost("upload-multiple")]
public async Task<IActionResult> UploadFiles(List<IFormFile> files)
{
if (files == null || files.Count == 0)
{
return BadRequest("No files uploaded");
}
var uploadedFiles = new List<string>();
foreach (var file in files)
{
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var filePath = Path.Combine("uploads", fileName);
using var stream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(stream);
uploadedFiles.Add(fileName);
}
return Ok(uploadedFiles);
}
Upload với Form Data
public class ProductForm
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public IFormFile? Image { get; set; }
public List<IFormFile>? Documents { get; set; }
}
[HttpPost("products")]
public async Task<IActionResult> CreateProduct([FromForm] ProductForm form)
{
// Process form data
var product = new Product
{
Name = form.Name,
Price = form.Price
};
// Process image
if (form.Image != null && form.Image.Length > 0)
{
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(form.Image.FileName)}";
var filePath = Path.Combine("uploads", "images", fileName);
using var stream = new FileStream(filePath, FileMode.Create);
await form.Image.CopyToAsync(stream);
product.ImageUrl = $"/uploads/images/{fileName}";
}
// Process documents
if (form.Documents != null)
{
foreach (var doc in form.Documents)
{
// Save documents
}
}
return Ok(product);
}
Streaming Upload (Large Files)
Streaming vs Buffering
┌─────────────────────────────────────────────────────────────────┐
│ UPLOAD METHODS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Buffering (Default): │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client │───▶│ Memory │───▶│ Disk │ │
│ │ Upload │ │ (Form) │ │ (Save) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ - Toàn bộ file load vào memory trước │
│ - Phù hợp cho files nhỏ (< 64KB) │
│ │
│ Streaming: │
│ ┌──────────┐ ┌──────────┐ │
│ │ Client │───▶│ Disk │ │
│ │ Upload │ │ (Save) │ │
│ └──────────┘ └──────────┘ │
│ - Stream trực tiếp vào disk │
│ - Phù hợp cho files lớn │
│ │
└─────────────────────────────────────────────────────────────────┘
Streaming Implementation
[HttpPost("upload-stream")]
[RequestSizeLimit(100 * 1024 * 1024)] // 100MB
public async Task<IActionResult> UploadStream()
{
var fileName = Request.Headers["X-File-Name"].FirstOrDefault();
if (string.IsNullOrEmpty(fileName))
{
return BadRequest("Missing file name header");
}
var filePath = Path.Combine("uploads", fileName);
// Stream trực tiếp vào file
using var stream = new FileStream(filePath, FileMode.Create);
await Request.Body.CopyToAsync(stream);
return Ok(new { fileName, size = stream.Length });
}
Multipart Streaming
[HttpPost("upload-multipart-stream")]
[DisableFormValueModelBinding] // Custom attribute để disable automatic binding
public async Task<IActionResult> UploadMultipart()
{
if (!Request.HasFormContentType)
{
return BadRequest("Invalid content type");
}
var form = await Request.ReadFormAsync();
var file = form.Files["file"];
if (file == null || file.Length == 0)
{
return BadRequest("No file");
}
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var filePath = Path.Combine("uploads", fileName);
using var stream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(stream);
return Ok(new { fileName });
}
// Attribute để disable form binding
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<FormFileValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context) { }
}
File Download
Download từ File
[HttpGet("download/{fileName}")]
public IActionResult Download(string fileName)
{
var filePath = Path.Combine("uploads", fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound();
}
var contentType = GetContentType(fileName);
return PhysicalFile(filePath, contentType, fileName);
}
private string GetContentType(string fileName)
{
var extension = Path.GetExtension(fileName).ToLowerInvariant();
return extension switch
{
".jpg" or ".jpeg" => "image/jpeg",
".png" => "image/png",
".pdf" => "application/pdf",
".txt" => "text/plain",
_ => "application/octet-stream"
};
}
Download từ Stream
[HttpGet("download-stream/{fileName}")]
public IActionResult DownloadStream(string fileName)
{
var filePath = Path.Combine("uploads", fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound();
}
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return File(stream, "application/octet-stream", fileName);
}
Download từ Byte Array
[HttpGet("download-bytes")]
public IActionResult DownloadBytes()
{
var bytes = GeneratePdf(); // Generate file in memory
return File(bytes, "application/pdf", "report.pdf");
}
Configuration
File Size Limits
// Program.cs
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = 100 * 1024 * 1024; // 100MB
options.ValueLengthLimit = int.MaxValue;
options.MultipartHeadersLengthLimit = int.MaxValue;
});
// Hoặc trong appsettings.json
{
"Kestrel": {
"Limits": {
"MaxRequestBodySize": 104857600 // 100MB
}
}
}
Request Size Limit Attribute
// Per-action limit
[HttpPost("upload")]
[RequestSizeLimit(50 * 1024 * 1024)] // 50MB
public async Task<IActionResult> Upload(IFormFile file)
{
// ...
}
// No limit
[HttpPost("upload-large")]
[DisableRequestSizeLimit]
public async Task<IActionResult> UploadLarge(IFormFile file)
{
// ...
}
Best Practices
1. Validate File Uploads
public class FileUploadValidator
{
private static readonly Dictionary<string, string[]> AllowedExtensions = new()
{
["image"] = new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" },
["document"] = new[] { ".pdf", ".doc", ".docx" },
["video"] = new[] { ".mp4", ".avi", ".mov" }
};
public static (bool IsValid, string? Error) Validate(IFormFile file, string category)
{
if (file == null || file.Length == 0)
return (false, "No file uploaded");
if (file.Length > 100 * 1024 * 1024)
return (false, "File too large (max 100MB)");
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!AllowedExtensions.TryGetValue(category, out var allowed) ||
!allowed.Contains(extension))
{
return (false, $"Invalid file type. Allowed: {string.Join(", ", allowed)}");
}
return (true, null);
}
}
2. Use Safe File Names
// ✅ Good - generate safe file name
var safeFileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
// ❌ Bad - use original file name (security risk)
var fileName = file.FileName; // Could contain path traversal
3. Scan for Malware
// Integrate with antivirus scanning
public async Task<bool> ScanForMalwareAsync(Stream fileStream)
{
// Use antivirus API or external service
// Return true if clean
return true;
}
Hosted Services & Background Tasks
Overview Questions
- Hosted Service là gì và khi nào cần sử dụng?
IHostedServicevàBackgroundServicekhác nhau như thế nào?- Làm sao để thực thi task theo schedule?
- Quản lý cancellation token trong background tasks ra sao?
- Worker Service là gì và cách tạo?
IHostedService
Interface
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
Implementation
public class TimedHostedService : IHostedService, IDisposable
{
private readonly ILogger<TimedHostedService> _logger;
private Timer? _timer;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Hosted Service running.");
// Chạy mỗi 1 phút
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromMinutes(1));
return Task.CompletedTask;
}
private void DoWork(object? state)
{
_logger.LogInformation("Timed Hosted Service is working.");
// Thực thi công việc background
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Hosted Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
// Đăng ký
builder.Services.AddHostedService<TimedHostedService>();
BackgroundService
Abstract Class
// BackgroundService là implementation base của IHostedService
// Cung cấp ExecuteAsync method dễ sử dụng hơn
public abstract class BackgroundService : IHostedService, IDisposable
{
protected abstract Task ExecuteAsync(CancellationToken stoppingToken);
public virtual Task StartAsync(CancellationToken cancellationToken);
public virtual Task StopAsync(CancellationToken cancellationToken);
public virtual void Dispose();
}
Implementation
public class QueueProcessorService : BackgroundService
{
private readonly ILogger<QueueProcessorService> _logger;
private readonly Channel<string> _channel;
public QueueProcessorService(
ILogger<QueueProcessorService> logger,
Channel<string> channel)
{
_logger = logger;
_channel = channel;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queue Processor Service starting");
await foreach (var item in _channel.Reader.ReadAllAsync(stoppingToken))
{
try
{
_logger.LogInformation("Processing: {Item}", item);
await ProcessItemAsync(item, stoppingToken);
}
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
{
_logger.LogError(ex, "Error processing item: {Item}", item);
}
}
_logger.LogInformation("Queue Processor Service stopping");
}
private async Task ProcessItemAsync(string item, CancellationToken token)
{
// Simulate processing
await Task.Delay(100, token);
}
}
// Đăng ký
builder.Services.AddSingleton(Channel.CreateUnbounded<string>());
builder.Services.AddHostedService<QueueProcessorService>();
Periodic Background Service
Pattern với Timer
public class PeriodicService : BackgroundService
{
private readonly ILogger<PeriodicService> _logger;
private readonly TimeSpan _period = TimeSpan.FromSeconds(30);
public PeriodicService(ILogger<PeriodicService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogInformation("Doing periodic work at {Time}", DateTimeOffset.UtcNow);
await DoWorkAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in periodic work");
}
await Task.Delay(_period, stoppingToken);
}
}
private async Task DoWorkAsync(CancellationToken token)
{
// Thực thi công việc
await Task.CompletedTask;
}
}
Scheduled Tasks
Cron-based Scheduler
dotnet add package Coravel
// Program.cs
builder.Services.AddScheduler();
// Tạo scheduled task
public class CleanupTask : IInvocable
{
private readonly ILogger<CleanupTask> _logger;
public CleanupTask(ILogger<CleanupTask> logger)
{
_logger = logger;
}
public async Task Invoke()
{
_logger.LogInformation("Running cleanup task");
// Cleanup logic
}
}
// Đăng ký schedule
builder.Services.AddTransient<CleanupTask>();
// Trong Program.cs
var app = builder.Build();
app.Services.UseScheduler(scheduler =>
{
scheduler.Schedule<CleanupTask>()
.EveryFiveMinutes()
.PreventOverlapping();
});
app.Run();
Hangfire
dotnet add package Hangfire
// Program.cs
builder.Services.AddHangfire(config =>
config.UseSqlServerStorage(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddHangfireServer();
var app = builder.Build();
app.UseHangfireDashboard();
// Recurring job
RecurringJob.AddOrUpdate<ICleanupService>(
"cleanup",
service => service.Cleanup(),
Cron.Daily);
app.Run();
Worker Service Template
Tạo Worker Service
dotnet new worker -n MyWorker
Structure
// Program.cs
using MyWorker;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
// Worker.cs
namespace MyWorker;
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
}
Run as Windows Service
dotnet add package Microsoft.Extensions.Hosting.WindowsServices
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
// Add Windows Service support
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "My Worker Service";
});
var host = builder.Build();
host.Run();
Best Practices
1. Handle Cancellation Properly
// ✅ Good
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken);
await Task.Delay(1000, stoppingToken);
}
}
// ❌ Bad - ignore cancellation
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (true) // Infinite loop, no cancellation check
{
await DoWorkAsync(CancellationToken.None);
}
}
2. Handle Exceptions
// ✅ Good - catch và log exceptions
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoWorkAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in background service");
}
await Task.Delay(1000, stoppingToken);
}
}
3. Use Scoped Services Correctly
// ✅ Good - tạo scope cho scoped services
public class ScopedWorker : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ScopedWorker> _logger;
public ScopedWorker(IServiceProvider serviceProvider, ILogger<ScopedWorker> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Tạo scope mới cho mỗi iteration
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await ProcessAsync(dbContext, stoppingToken);
await Task.Delay(5000, stoppingToken);
}
}
}
Health Checks
Overview Questions
- Health Checks là gì và tại sao cần thiết?
- Làm sao để cấu hình health check endpoints?
- Custom health check được viết như thế nào?
- Health check UI là gì và cách tích hợp?
- Health check response format ra sao?
Basic Health Checks
Configuration
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add health checks
builder.Services.AddHealthChecks();
var app = builder.Build();
// Map health check endpoint
app.MapHealthChecks("/health");
// Basic health check
app.MapGet("/health/basic", () => Results.Ok("Healthy"));
app.Run();
Response
// Healthy
{
"status": "Healthy"
}
// Unhealthy
{
"status": "Unhealthy",
"results": {
"database": {
"status": "Unhealthy",
"description": "Connection failed"
}
}
}
Built-in Health Checks
Database Health Check
dotnet add package AspNetCore.HealthChecks.SqlServer
builder.Services.AddHealthChecks()
.AddSqlServer(
builder.Configuration.GetConnectionString("Default"),
name: "sqlserver",
failureStatus: HealthStatus.Unhealthy,
tags: new[] { "db", "sql" });
Redis Health Check
dotnet add package AspNetCore.HealthChecks.Redis
builder.Services.AddHealthChecks()
.AddRedis(
"localhost:6379",
name: "redis",
failureStatus: HealthStatus.Degraded);
HTTP Endpoint Health Check
builder.Services.AddHealthChecks()
.AddUrlGroup(
new Uri("https://api.external.com/health"),
name: "external-api",
failureStatus: HealthStatus.Degraded);
Multiple Health Checks
builder.Services.AddHealthChecks()
.AddSqlServer(
builder.Configuration.GetConnectionString("Default"),
name: "database")
.AddRedis("localhost:6379", name: "cache")
.AddUrlGroup(new Uri("https://api.external.com"), name: "external");
Custom Health Checks
IHealthCheck Implementation
public class DiskSpaceHealthCheck : IHealthCheck
{
private readonly ILogger<DiskSpaceHealthCheck> _logger;
private readonly long _thresholdBytes = 1024 * 1024 * 100; // 100MB
public DiskSpaceHealthCheck(ILogger<DiskSpaceHealthCheck> logger)
{
_logger = logger;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var drive = new DriveInfo(Path.GetPathRoot(Directory.GetCurrentDirectory())!);
var freeSpace = drive.AvailableFreeSpace;
if (freeSpace > _thresholdBytes)
{
return HealthCheckResult.Healthy(
$"Free disk space: {freeSpace / 1024 / 1024}MB");
}
return HealthCheckResult.Unhealthy(
$"Low disk space: {freeSpace / 1024 / 1024}MB remaining");
}
}
// Đăng ký
builder.Services.AddHealthChecks()
.AddCheck<DiskSpaceHealthCheck>("disk-space");
Delegate-based Health Check
builder.Services.AddHealthChecks()
.AddCheck("memory", () =>
{
var memoryUsed = GC.GetGCMemoryInfo().HeapSizeBytes;
var memoryLimit = 512L * 1024 * 1024; // 512MB
return memoryUsed < memoryLimit
? HealthCheckResult.Healthy($"Memory: {memoryUsed / 1024 / 1024}MB")
: HealthCheckResult.Unhealthy($"High memory: {memoryUsed / 1024 / 1024}MB");
});
Health Check Options
Response Writer
app.MapHealthChecks("/health", new HealthCheckOptions
{
// Custom response writer
ResponseWriter = async (context, report) =>
{
var result = new
{
status = report.Status.ToString(),
duration = report.TotalDuration,
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description,
data = e.Value.Data
}),
timestamp = DateTime.UtcNow
};
context.Response.ContentType = "application/json";
await context.Response.WriteAsJsonAsync(result);
}
});
Predicate & Tags
// Chỉ check database
app.MapHealthChecks("/health/db", new HealthCheckOptions
{
Predicate = (check) => check.Tags.Contains("db")
});
// Chỉ check cache
app.MapHealthChecks("/health/cache", new HealthCheckOptions
{
Predicate = (check) => check.Tags.Contains("cache")
});
Status Code Mapping
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResultStatusCodes = new Dictionary<HealthStatus, int>
{
[HealthStatus.Healthy] = 200,
[HealthStatus.Degraded] = 200,
[HealthStatus.Unhealthy] = 503
}
});
Health Check UI
Setup
dotnet add package AspNetCore.HealthChecks.UI
dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage
// Program.cs
builder.Services.AddHealthChecksUI()
.AddInMemoryStorage();
var app = builder.Build();
app.MapHealthChecks("/health");
app.MapHealthChecksUI("/health-ui");
app.Run();
Configuration
{
"HealthChecksUI": {
"HealthChecks": [
{
"Name": "API Health",
"Uri": "https://localhost:5001/health"
}
],
"EvaluationTimeInSeconds": 30,
"MinimumSecondsBetweenFailureNotifications": 60
}
}
Kubernetes Probes
Liveness & Readiness
// Liveness - App is running
app.MapHealthChecks("/healthz", new HealthCheckOptions
{
Predicate = _ => false // No checks, just return 200
});
// Readiness - App is ready to receive traffic
app.MapHealthChecks("/ready", new HealthCheckOptions
{
Predicate = check => true // Run all checks
});
// Startup - App has started
app.MapHealthChecks("/started", new HealthCheckOptions
{
Predicate = _ => false
});
Kubernetes Config
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 80
initialDelaySeconds: 10
periodSeconds: 5
Best Practices
1. Use Appropriate Checks
// ✅ Good - lightweight checks
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString, name: "database")
.AddRedis(redisConnection, name: "cache");
// ❌ Bad - expensive checks
builder.Services.AddHealthChecks()
.AddCheck("full-database-scan", async () =>
{
// Expensive query - avoid in health check
var count = await db.Products.CountAsync();
return count > 0 ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy();
});
2. Separate Liveness and Readiness
┌─────────────────────────────────────────────────────────────────┐
│ HEALTH CHECK TYPES │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Liveness (/healthz): │
│ - App is running │
│ - No deadlock or infinite loop │
│ - Kubernetes restarts if failed │
│ │
│ Readiness (/ready): │
│ - App can handle requests │
│ - Dependencies are available │
│ - Kubernetes removes from service if failed │
│ │
│ Startup (/started): │
│ - App has finished initialization │
│ - Used during startup probe │
│ │
└─────────────────────────────────────────────────────────────────┘
3. Don’t Overload Health Checks
// ✅ Good - simple checks
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString, name: "db")
.AddRedis(redisConnection, name: "cache");
// ❌ Bad - too many external dependencies
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString)
.AddRedis(redisConnection)
.AddUrlGroup(externalApi1)
.AddUrlGroup(externalApi2)
.AddUrlGroup(externalApi3); // External APIs có thể down
Minimal APIs Advanced
Overview Questions
- Route Groups là gì và khi nào sử dụng?
- Endpoint Filters hoạt động như thế nào?
- Làm sao để organize large Minimal APIs?
- Parameter binding trong Minimal APIs có gì đặc biệt?
- So sánh Minimal APIs vs Controllers khi nào dùng cái nào?
Route Groups (.NET 7+)
Basic Route Groups
var app = builder.Build();
// Group với common prefix
var products = app.MapGroup("/api/products");
products.MapGet("/", () => new[] { "Product 1", "Product 2" });
products.MapGet("/{id}", (int id) => new { Id = id, Name = "Product" });
products.MapPost("/", (Product product) => Results.Created($"/api/products/{product.Id}", product));
products.MapPut("/{id}", (int id, Product product) => Results.NoContent());
products.MapDelete("/{id}", (int id) => Results.NoContent());
app.Run();
Route Groups với Common Configuration
var app = builder.Build();
// Group với common middleware
var products = app.MapGroup("/api/products")
.RequireAuthorization()
.WithTags("Products")
.WithOpenApi();
products.MapGet("/", () => new[] { "Product 1" });
products.MapGet("/{id}", (int id) => new { Id = id });
products.MapPost("/", (Product product) => Results.Created($"/api/products/{product.Id}", product));
app.Run();
Nested Route Groups
var app = builder.Build();
var api = app.MapGroup("/api")
.RequireAuthorization();
var products = api.MapGroup("/products")
.WithTags("Products");
products.MapGet("/", () => new[] { "Product 1" });
products.MapGet("/{id}", (int id) => new { Id = id });
var orders = api.MapGroup("/orders")
.WithTags("Orders");
orders.MapGet("/", () => new[] { "Order 1" });
orders.MapGet("/{id}", (int id) => new { Id = id });
app.Run();
Endpoint Filters (.NET 7+)
Basic Filter
var app = builder.Build();
app.MapGet("/api/products/{id}", (int id, AppDbContext db) =>
{
return db.Products.Find(id);
})
.AddEndpointFilter(async (context, next) =>
{
var id = context.GetArgument<int>(0);
if (id <= 0)
{
return Results.BadRequest("Invalid ID");
}
return await next(context);
});
app.Run();
Logging Filter
public class LoggingFilter : IEndpointFilter
{
private readonly ILogger<LoggingFilter> _logger;
public LoggingFilter(ILogger<LoggingFilter> logger)
{
_logger = logger;
}
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var startTime = DateTime.UtcNow;
_logger.LogInformation("Starting {Method} {Path}",
context.HttpContext.Request.Method,
context.HttpContext.Request.Path);
var result = await next(context);
_logger.LogInformation("Completed in {Elapsed}ms",
(DateTime.UtcNow - startTime).TotalMilliseconds);
return result;
}
}
// Đăng ký
app.MapGet("/api/products", async (AppDbContext db) =>
{
return await db.Products.ToListAsync();
})
.AddEndpointFilter<LoggingFilter>();
Validation Filter
public class ValidationFilter<T> : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
// Find the argument to validate
for (var i = 0; i < context.Arguments.Count; i++)
{
if (context.Arguments[i] is T item)
{
var validationContext = new ValidationContext<T>(item);
var validator = context.HttpContext.RequestServices
.GetRequiredService<IValidator<T>>();
var result = await validator.ValidateAsync(validationContext);
if (!result.IsValid)
{
var errors = result.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(
g => g.Key,
g => g.Select(e => e.ErrorMessage).ToArray());
return Results.ValidationProblem(errors);
}
}
}
return await next(context);
}
}
// Sử dụng
app.MapPost("/api/products", (Product product, AppDbContext db) =>
{
db.Products.Add(product);
db.SaveChanges();
return Results.Created($"/api/products/{product.Id}", product);
})
.AddEndpointFilter<ValidationFilter<Product>>();
Parameter Binding
Built-in Binding
// FromRoute
app.MapGet("/api/products/{id}", (int id) => id);
// FromQuery
app.MapGet("/api/products", (int page, int pageSize) => new { page, pageSize });
// FromBody
app.MapPost("/api/products", (Product product) => product);
// FromServices
app.MapGet("/api/products", (AppDbContext db) => db.Products.ToList());
// FromHeader
app.MapGet("/api/secure", ([FromHeader(Name = "X-API-Key")] string apiKey) => apiKey);
// HttpContext
app.MapGet("/api/context", (HttpContext context) => context.Request.Path);
// HttpRequest/HttpResponse
app.MapGet("/api/request", (HttpRequest request) => request.Headers);
Custom Binding
// Bind từ query string vào complex type
app.MapGet("/api/products", ([AsParameters] PagingParams p) =>
{
return new { p.Page, p.PageSize, p.Search };
});
public record PagingParams(
int Page = 1,
int PageSize = 10,
string? Search = null);
TryParse Binding
// Custom type với TryParse
public class ProductId
{
public int Value { get; }
public ProductId(int value) => Value = value;
public static bool TryParse(string? value, out ProductId? result)
{
if (int.TryParse(value, out var id))
{
result = new ProductId(id);
return true;
}
result = null;
return false;
}
}
// Binding tự động qua TryParse
app.MapGet("/api/products/{id}", (ProductId id) => new { id.Value });
Organizing Large Minimal APIs
Extension Methods Pattern
// Program.cs
var app = builder.Build();
app.MapProductEndpoints();
app.MapOrderEndpoints();
app.MapUserEndpoints();
app.Run();
// ProductEndpoints.cs
public static class ProductEndpoints
{
public static void MapProductEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/products")
.WithTags("Products")
.WithOpenApi();
group.MapGet("/", async (AppDbContext db) =>
await db.Products.ToListAsync());
group.MapGet("/{id}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product product
? Results.Ok(product)
: Results.NotFound());
group.MapPost("/", async (Product product, AppDbContext db) =>
{
db.Products.Add(product);
await db.SaveChangesAsync();
return Results.Created($"/api/products/{product.Id}", product);
});
group.MapPut("/{id}", async (int id, Product product, AppDbContext db) =>
{
var existing = await db.Products.FindAsync(id);
if (existing == null) return Results.NotFound();
existing.Name = product.Name;
existing.Price = product.Price;
await db.SaveChangesAsync();
return Results.NoContent();
});
group.MapDelete("/{id}", async (int id, AppDbContext db) =>
{
var product = await db.Products.FindAsync(id);
if (product == null) return Results.NotFound();
db.Products.Remove(product);
await db.SaveChangesAsync();
return Results.NoContent();
});
}
}
Interface-based Pattern
public interface IEndpoint
{
void MapEndpoint(IEndpointRouteBuilder app);
}
public class ProductEndpoint : IEndpoint
{
public void MapEndpoint(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/products");
group.MapGet("/", () => new[] { "Product 1" });
group.MapGet("/{id}", (int id) => new { Id = id });
}
}
// Program.cs
var app = builder.Build();
var scope = app.Services.CreateScope();
var endpoints = scope.ServiceProvider.GetServices<IEndpoint>();
foreach (var endpoint in endpoints)
{
endpoint.MapEndpoint(app);
}
app.Run();
Minimal APIs vs Controllers
Comparison
| Aspect | Minimal APIs | Controllers |
|---|---|---|
| Code size | Ít hơn | Nhiều hơn |
| Complexity | Đơn giản | Phức tạp hơn |
| Features | Đầy đủ cho most cases | Full MVC features |
| Filters | Endpoint Filters | Action/Result Filters |
| Model Binding | Tự động | Attribute-based |
| Testing | Khó hơn | Dễ hơn |
| Organization | Extension methods | Controllers folder |
| Use case | Small APIs, microservices | Large applications |
When to Use
┌─────────────────────────────────────────────────────────────────┐
│ WHEN TO USE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Minimal APIs: │
│ - Microservices │
│ - Small to medium APIs │
│ - Quick prototypes │
│ - Serverless functions │
│ - Simple CRUD operations │
│ │
│ Controllers: │
│ - Large enterprise applications │
│ - Complex business logic │
│ - Need MVC features (Views, Razor Pages) │
│ - Team với nhiều developers │
│ - Need extensive filter pipeline │
│ │
└─────────────────────────────────────────────────────────────────┘
Content Negotiation
Overview Questions
- Content Negotiation là gì và tại sao cần thiết?
- ASP.NET Core xử lý input/output formatting như thế nào?
- Làm sao để thêm XML support?
- Custom formatter được viết ra sao?
- Response format negotiation hoạt động như thế nào?
Content Negotiation Basics
What is Content Negotiation?
┌─────────────────────────────────────────────────────────────────┐
│ CONTENT NEGOTIATION FLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Client Request: │
│ Accept: application/json │
│ Content-Type: application/json │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Input Formatter │ │
│ │ (JSON, XML, etc.) │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Controller │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Output Formatter │ │
│ │ (JSON, XML, etc.) │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ Client Response: │
│ Content-Type: application/json │
│ │
└─────────────────────────────────────────────────────────────────┘
Default Configuration
// ASP.NET Core mặc định chỉ support JSON
builder.Services.AddControllers();
// JSON là default formatter
// System.Text.Json được sử dụng
Input Formatters
JSON Input (Default)
// Request
// POST /api/products
// Content-Type: application/json
// {"name": "Laptop", "price": 999.99}
[HttpPost]
public IActionResult Create(Product product)
{
// product được deserialize từ JSON
return Ok(product);
}
XML Input
dotnet add package Microsoft.AspNetCore.Mvc.NewtonsoftJson
builder.Services.AddControllers()
.AddXmlSerializerFormatters(); // Add XML support
// Request
// POST /api/products
// Content-Type: application/xml
// <Product><Name>Laptop</Name><Price>999.99</Price></Product>
[HttpPost]
public IActionResult Create(Product product)
{
return Ok(product);
}
Custom Input Formatter
public class CsvInputFormatter : InputFormatter
{
public CsvInputFormatter()
{
SupportedMediaTypes.Add("text/csv");
}
protected override bool CanReadType(Type type)
{
return type == typeof(List<Product>);
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(
InputFormatterContext context)
{
var request = context.HttpContext.Request;
using var reader = new StreamReader(request.Body);
var csv = await reader.ReadToEndAsync();
var products = ParseCsv(csv);
return await InputFormatterResult.SuccessAsync(products);
}
private List<Product> ParseCsv(string csv)
{
// Parse CSV logic
return new List<Product>();
}
}
// Đăng ký
builder.Services.AddControllers(options =>
{
options.InputFormatters.Add(new CsvInputFormatter());
});
Output Formatters
JSON Output (Default)
[HttpGet]
public IActionResult Get()
{
return Ok(new { name = "Product", price = 99.99 });
}
// Response
// Content-Type: application/json
// {"name":"Product","price":99.99}
XML Output
builder.Services.AddControllers()
.AddXmlSerializerFormatters();
[HttpGet]
[Produces("application/xml")]
public IActionResult Get()
{
return Ok(new Product { Name = "Product", Price = 99.99 });
}
// Response
// Content-Type: application/xml
// <Product><Name>Product</Name><Price>99.99</Price></Product>
Content Negotiation
[HttpGet]
public IActionResult Get()
{
var product = new Product { Name = "Product", Price = 99.99 };
return Ok(product);
}
// Request với Accept: application/json
// Response: JSON
// Request với Accept: application/xml
// Response: XML
JSON Configuration
System.Text.Json Options
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
// CamelCase property names
options.JsonSerializerOptions.PropertyNamingPolicy =
JsonNamingPolicy.CamelCase;
// Ignore null values
options.JsonSerializerOptions.DefaultIgnoreCondition =
JsonIgnoreCondition.WhenWritingNull;
// Write numbers as strings
options.JsonSerializerOptions.NumberHandling =
JsonNumberHandling.WriteAsString;
// Allow trailing commas
options.JsonSerializerOptions.AllowTrailingCommas = true;
// Custom converters
options.JsonSerializerOptions.Converters.Add(
new JsonStringEnumConverter());
});
Newtonsoft.Json Options
builder.Services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver();
options.SerializerSettings.NullValueHandling =
NullValueHandling.Ignore;
options.SerializerSettings.DateFormatString =
"yyyy-MM-dd HH:mm:ss";
});
Custom Output Formatter
public class CsvOutputFormatter : TextOutputFormatter
{
public CsvOutputFormatter()
{
SupportedMediaTypes.Add("text/csv");
SupportedEncodings.Add(Encoding.UTF8);
}
protected override bool CanWriteType(Type type)
{
return typeof(IEnumerable<Product>).IsAssignableFrom(type);
}
public override async Task WriteResponseBodyAsync(
OutputFormatterWriteContext context,
Encoding selectedEncoding)
{
var response = context.HttpContext.Response;
var products = context.Object as IEnumerable<Product>;
using var writer = new StreamWriter(response.Body, selectedEncoding);
// Write header
writer.WriteLine("Id,Name,Price");
// Write data
foreach (var product in products ?? Enumerable.Empty<Product>())
{
writer.WriteLine($"{product.Id},{product.Name},{product.Price}");
}
}
}
// Đăng ký
builder.Services.AddControllers(options =>
{
options.OutputFormatters.Add(new CsvOutputFormatter());
});
Best Practices
1. Use JSON by Default
// ✅ JSON là default và được support tốt nhất
builder.Services.AddControllers();
// ❌ Tránh thêm quá nhiều formatters không cần thiết
builder.Services.AddControllers()
.AddXmlSerializerFormatters()
.AddXmlDataContractSerializerFormatters();
2. Configure Consistent JSON Settings
// ✅ Configure JSON settings globally
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy =
JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.WriteIndented = true;
});
3. Use [Produces] Attribute
// ✅ Explicit về response format
[HttpGet]
[Produces("application/json")]
public IActionResult Get() => Ok(products);
// ❌ Không rõ response format
[HttpGet]
public IActionResult Get() => Ok(products);
Output Caching
Overview Questions
- Output Caching là gì và khác Response Caching ra sao?
- Làm sao để cấu hình output caching trong .NET 7+?
- Cache policies hoạt động như thế nào?
- Khi nào nên dùng output caching?
- Cached tagged responses là gì?
Output Caching vs Response Caching
Comparison
┌─────────────────────────────────────────────────────────────────┐
│ CACHING COMPARISON │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Output Caching (.NET 7+): │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client │───▶│ Server │───▶│ Cache │ │
│ │ Request │ │ (App) │ │ (Mem) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ - Cache trên server │
│ - Không gửi cache headers cho client │
│ - Phù hợp cho server-side caching │
│ │
│ Response Caching: │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client │───▶│ Server │───▶│ Client │ │
│ │ Request │ │ (App) │ │ Cache │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ - Cache trên client/proxy │
│ - Gửi cache headers (Cache-Control) │
│ - Phù hợp cho CDN/browser caching │
│ │
└─────────────────────────────────────────────────────────────────┘
Output Caching Configuration
Basic Setup
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add output caching
builder.Services.AddOutputCache();
var app = builder.Build();
// Enable caching for endpoints
app.MapGet("/api/products", () => GetProducts())
.CacheOutput();
app.Run();
Cache Policies
builder.Services.AddOutputCache(options =>
{
// Default policy - cache 60 seconds
options.AddBasePolicy(builder => builder.Cache());
// Custom policy - cache 5 minutes
options.AddPolicy("ShortCache", builder =>
builder.Cache().Expire(TimeSpan.FromMinutes(5)));
// Custom policy - no cache
options.AddPolicy("NoCache", builder => builder.NoCache());
// Custom policy - vary by header
options.AddPolicy("VaryByHeader", builder =>
builder.Cache().VaryByHeader("Accept-Language"));
});
Endpoint Caching
Basic Caching
// Cache với default policy
app.MapGet("/api/products", async (AppDbContext db) =>
{
return await db.Products.ToListAsync();
})
.CacheOutput();
// Cache với custom policy
app.MapGet("/api/products", async (AppDbContext db) =>
{
return await db.Products.ToListAsync();
})
.CacheOutput(p => p.WithPolicyName("ShortCache"));
Vary By Query
// Cache khác nhau cho mỗi query param
app.MapGet("/api/products", async (int page, AppDbContext db) =>
{
return await db.Products
.Skip((page - 1) * 10)
.Take(10)
.ToListAsync();
})
.CacheOutput(b => b.VaryByQuery("page"));
// Vary by multiple query params
app.MapGet("/api/products", async (int page, string? category, AppDbContext db) =>
{
var query = db.Products.AsQueryable();
if (!string.IsNullOrEmpty(category))
query = query.Where(p => p.Category == category);
return await query.Skip((page - 1) * 10).Take(10).ToListAsync();
})
.CacheOutput(b => b.VaryByQuery("page", "category"));
Vary By Header
// Cache khác nhau cho mỗi Accept-Language
app.MapGet("/api/products", async (AppDbContext db) =>
{
return await db.Products.ToListAsync();
})
.CacheOutput(b => b.VaryByHeader("Accept-Language"));
Vary By Route
// Cache khác nhau cho mỗi route param
app.MapGet("/api/products/{category}", async (string category, AppDbContext db) =>
{
return await db.Products.Where(p => p.Category == category).ToListAsync();
})
.CacheOutput(b => b.VaryByRouteValue("category"));
Cache Expiration
Time-based Expiration
// Expire after 1 minute
app.MapGet("/api/products", () => GetProducts())
.CacheOutput(b => b.Expire(TimeSpan.FromMinutes(1)));
// Expire after 1 hour
app.MapGet("/api/products", () => GetProducts())
.CacheOutput(b => b.Expire(TimeSpan.FromHours(1)));
// Expire after 1 day
app.MapGet("/api/products", () => GetProducts())
.CacheOutput(b => b.Expire(TimeSpan.FromDays(1)));
No Caching
// Disable caching for this endpoint
app.MapGet("/api/products", () => GetProducts())
.CacheOutput(b => b.NoCache());
// No store - don't cache at all
app.MapGet("/api/products", () => GetProducts())
.CacheOutput(b => b.NoStore());
Cache Tags
Tag-based Invalidation
// Cache với tags
app.MapGet("/api/products", async (AppDbContext db) =>
{
return await db.Products.ToListAsync();
})
.CacheOutput(b => b.Tag("products"));
app.MapGet("/api/products/{id}", async (int id, AppDbContext db) =>
{
return await db.Products.FindAsync(id);
})
.CacheOutput(b => b.Tag($"product-{id}"));
// Invalidate cache khi update
app.MapPut("/api/products/{id}", async (int id, Product product, AppDbContext db, IOutputCacheStore cache) =>
{
var existing = await db.Products.FindAsync(id);
if (existing == null) return Results.NotFound();
existing.Name = product.Name;
existing.Price = product.Price;
await db.SaveChangesAsync();
// Evict cache
await cache.EvictByTagAsync($"product-{id}", CancellationToken.None);
return Results.NoContent();
});
Response Caching (Client-side)
Configuration
// Add response caching middleware
builder.Services.AddResponseCaching();
var app = builder.Build();
app.UseResponseCaching();
app.MapGet("/api/products", () => GetProducts());
Cache Headers
app.MapGet("/api/products", () =>
{
var response = Results.Ok(GetProducts());
return response;
})
.AddEndpointFilter(async (context, next) =>
{
var response = await next(context);
context.HttpContext.Response.Headers["Cache-Control"] =
"public, max-age=60"; // Cache 60 seconds
return response;
});
Best Practices
1. Cache Appropriately
// ✅ Cache read-heavy endpoints
app.MapGet("/api/products", () => GetProducts())
.CacheOutput(b => b.Expire(TimeSpan.FromMinutes(5)));
// ❌ Don't cache write endpoints
app.MapPost("/api/products", (Product p) => CreateProduct(p));
2. Use Tags for Invalidation
// ✅ Use tags for granular invalidation
app.MapGet("/api/products/{id}", (int id) => GetProduct(id))
.CacheOutput(b => b.Tag($"product-{id}"));
// Invalidate when updated
await cache.EvictByTagAsync($"product-{id}", CancellationToken.None);
3. Vary by Appropriate Keys
// ✅ Vary by relevant parameters
app.MapGet("/api/products", (string? category, int page) => GetProducts(category, page))
.CacheOutput(b => b.VaryByQuery("category", "page"));
// ❌ Don't vary by unnecessary parameters
app.MapGet("/api/products", (string? trackingId) => GetProducts())
.CacheOutput(b => b.VaryByQuery("trackingId")); // Tracking ID không nên vary
Kestrel Configuration
Overview Questions
- Kestrel là gì và tại sao nó quan trọng?
- Làm sao để cấu hình HTTPS cho Kestrel?
- Endpoint configuration hoạt động như thế nào?
- Request limits được cấu hình ra sao?
- Kestrel vs IIS/Nginx - khi nào dùng reverse proxy?
Kestrel Basics
What is Kestrel?
┌─────────────────────────────────────────────────────────────────┐
│ KESTREL ARCHITECTURE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client │───▶│ Kestrel │───▶│ ASP.NET │ │
│ │ (HTTP) │ │ Server │ │ Core │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ - Cross-platform web server │
│ - Default server cho ASP.NET Core │
│ - High performance │
│ - Có thể dùng standalone hoặc với reverse proxy │
│ │
└─────────────────────────────────────────────────────────────────┘
Basic Configuration
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
// Configure Kestrel here
});
var app = builder.Build();
app.Run();
Endpoint Configuration
appsettings.json
{
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://localhost:5000"
},
"Https": {
"Url": "https://localhost:5001"
}
}
}
}
Code Configuration
builder.WebHost.ConfigureKestrel(options =>
{
// HTTP endpoint
options.ListenLocalhost(5000);
// HTTPS endpoint
options.ListenLocalhost(5001, listenOptions =>
{
listenOptions.UseHttps();
});
// Custom IP and port
options.Listen(IPAddress.Parse("127.0.0.1"), 5002);
// Unix socket (Linux)
options.ListenUnixSocket("/tmp/kestrel.sock");
});
HTTPS Configuration
Development Certificate
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(5001, listenOptions =>
{
listenOptions.UseHttps(); // Uses development certificate
});
});
Production Certificate
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(5001, listenOptions =>
{
listenOptions.UseHttps("certificate.pfx", "password");
});
});
Certificate from Store
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(5001, listenOptions =>
{
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var cert = store.Certificates
.Find(X509FindType.FindByThumbprint, "CERT_THUMBPRINT", validOnly: false)
.OfType<X509Certificate2>()
.FirstOrDefault();
if (cert != null)
{
listenOptions.UseHttps(cert);
}
});
});
HTTPS Redirection
// Program.cs
var app = builder.Build();
// Redirect HTTP to HTTPS
app.UseHttpsRedirection();
app.MapGet("/", () => "Hello HTTPS!");
app.Run();
Request Limits
Configure Limits
builder.WebHost.ConfigureKestrel(options =>
{
// Max request body size (100MB)
options.Limits.MaxRequestBodySize = 100 * 1024 * 1024;
// Max request header size (32KB)
options.Limits.MaxRequestHeadersTotalSize = 32 * 1024;
// Max request header count
options.Limits.MaxRequestHeaderCount = 100;
// Keep-alive timeout
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
// Request headers timeout
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
// Min response data rate
options.Limits.MinResponseDataRate = new MinDataRate(
bytesPerSecond: 100,
gracePeriod: TimeSpan.FromSeconds(10));
});
Per-Request Limits
app.MapPost("/upload", async (HttpContext context) =>
{
// Override limit for this request
context.Features.Get<IHttpMaxRequestBodySizeFeature>()
?.MaxRequestBodySize = 200 * 1024 * 1024; // 200MB
// Process upload
await context.Request.Body.CopyToAsync(Stream.Null);
return Results.Ok();
});
HTTP/2 and HTTP/3
HTTP/2 Configuration
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(5001, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
listenOptions.UseHttps();
});
});
HTTP/3 Configuration
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(5002, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http3;
});
});
Protocol Selection
// Check protocol
app.MapGet("/protocol", (HttpContext context) =>
{
var protocol = context.Request.Protocol;
return new { Protocol = protocol };
});
Reverse Proxy
When to Use Reverse Proxy
┌─────────────────────────────────────────────────────────────────┐
│ REVERSE PROXY SETUP │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client │───▶│ Nginx/ │───▶│ Kestrel │ │
│ │ │ │ IIS │ │ (App) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Use reverse proxy for: │
│ - Multiple apps trên cùng port 80/443 │
│ - Static file serving │
│ - Response compression │
│ - Additional security layer │
│ - Load balancing │
│ │
│ Kestrel standalone OK cho: │
│ - Internal services │
│ - Container deployments │
│ - Development │
│ │
└─────────────────────────────────────────────────────────────────┘
Forwarded Headers
// Khi dùng reverse proxy
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto
});
Best Practices
1. Use HTTPS in Production
// ✅ Always use HTTPS in production
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(5001, o => o.UseHttps("cert.pfx", "password"));
});
// ❌ Never use HTTP only in production
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(5000); // No HTTPS!
});
2. Set Appropriate Limits
// ✅ Set reasonable limits
options.Limits.MaxRequestBodySize = 50 * 1024 * 1024; // 50MB
// ❌ Don't remove limits entirely
options.Limits.MaxRequestBodySize = null; // Unlimited!
3. Configure for Environment
if (builder.Environment.IsDevelopment())
{
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(5000);
options.ListenLocalhost(5001, o => o.UseHttps());
});
}
else
{
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(5001, o =>
o.UseHttps("prod-cert.pfx", config["CertPassword"]));
});
}
Localization (i18n)
Overview Questions
- Localization trong ASP.NET Core hoạt động như thế nào?
- Resource files là gì và cách sử dụng?
- Làm sao để localize controllers và views?
- Culture providers hoạt động ra sao?
- Localize data annotations và validation messages?
Localization Setup
Configuration
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
builder.Services.AddControllersWithViews()
.AddViewLocalization(LanguageViewLocationFormatSuffixFormat.Suffix)
.AddDataAnnotationsLocalization();
var app = builder.Build();
// Supported cultures
var supportedCultures = new[] { "en-US", "vi-VN", "fr-FR" };
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture(supportedCultures[0])
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
app.Run();
Resource Files
File Structure
Project/
├── Resources/
│ ├── Controllers/
│ │ ├── HomeController.en-US.resx
│ │ ├── HomeController.vi-VN.resx
│ │ └── HomeController.fr-FR.resx
│ ├── Views/
│ │ ├── Home/
│ │ │ ├── Index.en-US.resx
│ │ │ └── Index.vi-VN.resx
│ │ └── Shared/
│ │ └── _Layout.en-US.resx
│ ├── SharedResource.en-US.resx
│ └── SharedResource.vi-VN.resx
└── Program.cs
Resource File Content
<!-- SharedResource.en-US.resx -->
<data name="Hello" xml:space="preserve">
<value>Hello</value>
</data>
<data name="Welcome" xml:space="preserve">
<value>Welcome to our application</value>
</data>
<!-- SharedResource.vi-VN.resx -->
<data name="Hello" xml:space="preserve">
<value>Xin chào</value>
</data>
<data name="Welcome" xml:space="preserve">
<value>Chào mừng bạn đến với ứng dụng</value>
</data>
Using IStringLocalizer
Basic Usage
public class HomeController : Controller
{
private readonly IStringLocalizer<SharedResource> _localizer;
public HomeController(IStringLocalizer<SharedResource> localizer)
{
_localizer = localizer;
}
public IActionResult Index()
{
ViewData["Message"] = _localizer["Hello"];
ViewData["Welcome"] = _localizer["Welcome"];
return View();
}
}
With Parameters
// Resource file
// "Greeting" = "Hello {0}, welcome to {1}"
public IActionResult Index(string name, string city)
{
var message = _localizer["Greeting", name, city];
// Output: "Hello John, welcome to New York"
return View();
}
IStringLocalizer vs IHtmlLocalizer
// IStringLocalizer - plain text
public class MyController : Controller
{
private readonly IStringLocalizer<MyController> _localizer;
public MyController(IStringLocalizer<MyController> localizer)
{
_localizer = localizer;
}
}
// IHtmlLocalizer - HTML content (không encode HTML)
public class MyViewComponent : ViewComponent
{
private readonly IHtmlLocalizer<SharedResource> _localizer;
public MyViewComponent(IHtmlLocalizer<SharedResource> localizer)
{
_localizer = localizer;
}
public IViewComponentResult Invoke()
{
// Resource: "Bold" = "<strong>Bold text</strong>"
var html = _localizer["Bold"]; // Không encode HTML
return View(html);
}
}
View Localization
In Razor Views
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
<h1>@Localizer["Welcome"]</h1>
<p>@Localizer["Hello"]</p>
<!-- Hoặc dùng IHtmlLocalizer -->
@inject IHtmlLocalizer<SharedResource> HtmlLocalizer
<p>@HtmlLocalizer["Description"]</p>
View with Model
@model Product
@inject IViewLocalizer Localizer
<h1>@Localizer["ProductDetails"]</h1>
<div>
<label>@Localizer["Name"]</label>
<span>@Model.Name</span>
</div>
<div>
<label>@Localizer["Price"]</label>
<span>@Model.Price</span>
</div>
Data Annotations Localization
Localized Validation Messages
public class Product
{
[Required(ErrorMessage = "NameRequired")]
[Display(Name = "ProductName")]
public string Name { get; set; } = string.Empty;
[Range(0.01, 9999.99, ErrorMessage = "PriceRange")]
[Display(Name = "ProductPrice")]
public decimal Price { get; set; }
}
// Resource file
// "NameRequired" = "Tên sản phẩm là bắt buộc"
// "ProductName" = "Tên sản phẩm"
// "PriceRange" = "Giá phải từ 0.01 đến 9999.99"
// "ProductPrice" = "Giá sản phẩm"
Configuration
builder.Services.AddControllersWithViews()
.AddViewLocalization()
.AddDataAnnotationsLocalization(options =>
{
options.DataAnnotationLocalizerProvider =
(type, factory) => factory.Create(typeof(SharedResource));
});
Culture Providers
Request Culture Providers
┌─────────────────────────────────────────────────────────────────┐
│ CULTURE PROVIDERS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Thứ tự kiểm tra (theo mặc định): │
│ 1. QueryStringRequestCultureProvider │
│ ?culture=vi-VN&ui-culture=vi-VN │
│ │
│ 2. CookieRequestCultureProvider │
│ Cookie: .AspNetCore.Culture=c=vi-VN\|uic=vi-VN │
│ │
│ 3. AcceptLanguageHeaderRequestCultureProvider │
│ Accept-Language: vi-VN, en-US;q=0.9 │
│ │
│ Custom Provider: │
│ - Route data (/vi/products, /en/products) │
│ - Subdomain (vi.example.com, en.example.com) │
│ - Custom header (X-Culture: vi-VN) │
│ │
└─────────────────────────────────────────────────────────────────┘
Custom Culture Provider
public class RouteDataRequestCultureProvider : RequestCultureProvider
{
public int RouteDataIndex = 0;
public override Task<ProviderCultureResult> DetermineProviderCultureResult(
HttpContext httpContext)
{
var culture = httpContext.Request.RouteValues["culture"]?.ToString();
if (string.IsNullOrEmpty(culture))
{
return NullProviderCultureResult;
}
var result = new ProviderCultureResult(culture);
return Task.FromResult(result);
}
}
// Đăng ký
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture("en-US")
.AddSupportedCultures("en-US", "vi-VN");
// Thêm custom provider vào đầu danh sách
localizationOptions.RequestCultureProviders.Insert(
0, new RouteDataRequestCultureProvider());
app.UseRequestLocalization(localizationOptions);
Culture Cookie
// Set culture cookie
app.MapGet("/set-culture/{culture}", (string culture, HttpContext context) =>
{
context.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(
new RequestCulture(culture)));
return Results.Redirect("/");
});
Best Practices
1. Use Shared Resources
// ✅ Good - centralized resources
public class SharedResource { } // Empty class
// Inject shared localizer
public class HomeController : Controller
{
private readonly IStringLocalizer<SharedResource> _localizer;
public HomeController(IStringLocalizer<SharedResource> localizer)
{
_localizer = localizer;
}
}
2. Fallback Culture
var localizationOptions = new RequestLocalizationOptions()
.SetDefaultCulture("en-US")
.AddSupportedCultures("en-US", "vi-VN")
.AddSupportedUICultures("en-US", "vi-VN")
.SetFallbackCulture("en-US"); // Fallback nếu không tìm thấy culture
3. Culture in URLs
// Route với culture
app.MapControllerRoute(
name: "localized",
pattern: "{culture=en-US}/{controller=Home}/{action=Index}");
// URLs:
// /en-US/Home/Index
// /vi-VN/Home/Index
SignalR
Overview Questions
- SignalR là gì và khi nào cần sử dụng?
- Hub là gì và cách tạo Hub?
- Client-server communication hoạt động ra sao?
- Groups và Users trong SignalR khác nhau như thế nào?
- Scale SignalR với Redis backplane ra sao?
SignalR Basics
What is SignalR?
┌─────────────────────────────────────────────────────────────────┐
│ SIGNALR ARCHITECTURE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Client │◀──▶│ Hub │◀──▶│ Client │ │
│ │ (Web) │ │ (Server)│ │ (Web) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │
│ │ ┌──────────┐ │ │
│ └────────▶│ Client │◀──────────┘ │
│ │ (Mobile)│ │
│ └──────────┘ │
│ │
│ - Real-time communication │
│ - Server push to clients │
│ - WebSocket, Server-Sent Events, Long Polling │
│ - Chat, notifications, live updates │
│ │
└─────────────────────────────────────────────────────────────────┘
Setup
dotnet add package Microsoft.AspNetCore.SignalR
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<ChatHub>("/chat");
app.Run();
Hub
Basic Hub
public class ChatHub : Hub
{
// Client gọi method này
public async Task SendMessage(string user, string message)
{
// Server gọi method trên tất cả clients
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
// Gửi cho người gọi
public async Task SendMessageToCaller(string user, string message)
{
await Clients.Caller.SendAsync("ReceiveMessage", user, message);
}
// Gửi cho người khác (trừ người gọi)
public async Task SendMessageToOthers(string user, string message)
{
await Clients.Others.SendAsync("ReceiveMessage", user, message);
}
}
Hub with Dependency Injection
public class ChatHub : Hub
{
private readonly ILogger<ChatHub> _logger;
private readonly IChatService _chatService;
public ChatHub(ILogger<ChatHub> logger, IChatService chatService)
{
_logger = logger;
_chatService = chatService;
}
public async Task SendMessage(string user, string message)
{
_logger.LogInformation("Message from {User}: {Message}", user, message);
// Save to database
await _chatService.SaveMessage(user, message);
// Broadcast
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
Client Communication
Server to Client
public class NotificationHub : Hub
{
// Gửi cho tất cả
public async Task Broadcast(string message)
{
await Clients.All.SendAsync("OnBroadcast", message);
}
// Gửi cho client cụ thể (theo ConnectionId)
public async Task SendToClient(string connectionId, string message)
{
await Clients.Client(connectionId).SendAsync("OnMessage", message);
}
// Gửi cho nhóm
public async Task SendToGroup(string group, string message)
{
await Clients.Group(group).SendAsync("OnMessage", message);
}
// Gửi cho user cụ thể (theo UserId)
public async Task SendToUser(string userId, string message)
{
await Clients.User(userId).SendAsync("OnMessage", message);
}
}
Client to Server
// JavaScript Client
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chat")
.build();
// Receive message from server
connection.on("ReceiveMessage", (user, message) => {
console.log(`${user}: ${message}`);
});
// Send message to server
connection.invoke("SendMessage", "John", "Hello!").catch(err => console.error(err));
// Start connection
connection.start().catch(err => console.error(err));
Groups
Group Management
public class ChatHub : Hub
{
// Join group
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync(
"OnUserJoined",
$"{Context.ConnectionId} joined {groupName}");
}
// Leave group
public async Task LeaveGroup(string groupName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync(
"OnUserLeft",
$"{Context.ConnectionId} left {groupName}");
}
// Send to group
public async Task SendToGroup(string groupName, string message)
{
await Clients.Group(groupName).SendAsync("OnMessage", message);
}
}
Group Lifecycle
public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "General");
await Clients.Caller.SendAsync("OnConnected", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "General");
await Clients.Others.SendAsync(
"OnDisconnected",
Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}
Strongly-typed Hubs
Interface-based Hub
// Define client methods interface
public interface IChatClient
{
Task ReceiveMessage(string user, string message);
Task UserJoined(string user);
Task UserLeft(string user);
}
// Strongly-typed Hub
public class ChatHub : Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
// Type-safe client calls
await Clients.All.ReceiveMessage(user, message);
}
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).UserJoined(Context.ConnectionId);
}
}
Authentication & Authorization
Hub Authorization
// Program.cs
app.MapHub<ChatHub>("/chat", options =>
{
options.Transports = HttpTransportType.WebSockets |
HttpTransportType.ServerSentEvents;
});
// Hub với authorization
[Authorize]
public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
{
var userId = Context.UserIdentifier; // From JWT claim
var userName = Context.User?.Identity?.Name;
await base.OnConnectedAsync();
}
}
Scaling
Redis Backplane
dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis
builder.Services.AddSignalR()
.AddStackExchangeRedis("localhost:6379");
Azure SignalR Service
dotnet add package Microsoft.Azure.SignalR
builder.Services.AddSignalR()
.AddAzureSignalR(options =>
{
options.ConnectionString =
builder.Configuration["Azure:SignalR:ConnectionString"];
});
// Map hub
app.MapHub<ChatHub>("/chat");
Best Practices
1. Handle Connection Lifecycle
// ✅ Good - handle connection events
public class ChatHub : Hub
{
public override async Task OnConnectedAsync()
{
_logger.LogInformation("Client connected: {ConnectionId}", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
_logger.LogInformation("Client disconnected: {ConnectionId}", Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}
2. Use Groups for Targeted Messages
// ✅ Good - use groups for targeted messages
public async Task SendToRoom(string roomId, string message)
{
await Clients.Group($"room-{roomId}").SendAsync("OnMessage", message);
}
// ❌ Bad - send to all when only room needs it
public async Task SendToRoom(string roomId, string message)
{
await Clients.All.SendAsync("OnMessage", message); // All clients receive!
}
3. Handle Reconnection
// JavaScript Client - handle reconnection
connection.onclose(async () => {
await start();
});
async function start() {
try {
await connection.start();
console.log("SignalR connected");
} catch (err) {
console.log("SignalR connection failed");
setTimeout(() => start(), 5000);
}
}
gRPC in ASP.NET Core
Overview Questions
- gRPC là gì và khác REST API như thế nào?
- Protocol Buffers (protobuf) là gì?
- Làm sao để tạo gRPC service trong ASP.NET Core?
- Unary, Server Streaming, Client Streaming, Bidirectional Streaming là gì?
- Khi nào nên dùng gRPC thay vì REST?
gRPC Basics
What is gRPC?
┌─────────────────────────────────────────────────────────────────┐
│ gRPC vs REST │
├─────────────────────────────────────────────────────────────────┤
│ │
│ REST: │
│ ┌──────────┐ HTTP/JSON ┌──────────┐ │
│ │ Client │◀───────────────▶│ Server │ │
│ └──────────┘ └──────────┘ │
│ - Text-based (JSON) │
│ - Human readable │
│ - Larger payload size │
│ - Request/Response only │
│ │
│ gRPC: │
│ ┌──────────┐ HTTP/2/Proto ┌──────────┐ │
│ │ Client │◀───────────────▶│ Server │ │
│ └──────────┘ buf └──────────┘ │
│ - Binary (Protocol Buffers) │
│ - Not human readable │
│ - Smaller payload size │
│ - Streaming support │
│ - Strongly typed contracts │
│ │
└─────────────────────────────────────────────────────────────────┘
When to Use gRPC
┌─────────────────────────────────────────────────────────────────┐
│ WHEN TO USE gRPC │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Use gRPC for: │
│ - Microservices communication │
│ - Real-time streaming │
│ - Low latency requirements │
│ - Polyglot environments (multiple languages) │
│ - Internal service-to-service calls │
│ │
│ Use REST for: │
│ - Public APIs │
│ - Browser clients │
│ - Simple CRUD operations │
│ - When human readability matters │
│ - When caching is important │
│ │
└─────────────────────────────────────────────────────────────────┘
Protocol Buffers
.proto File
// Protos/greet.proto
syntax = "proto3";
option csharp_namespace = "MyGrpcService";
package greet;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
// Streaming: server sends multiple responses
rpc SayHelloStream (HelloRequest) returns (stream HelloReply);
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greeting.
message HelloReply {
string message = 1;
}
Project Configuration
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.57.0" />
</ItemGroup>
</Project>
gRPC Service
Basic Service
public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;
public GreeterService(ILogger<GreeterService> logger)
{
_logger = logger;
}
public override Task<HelloReply> SayHello(
HelloRequest request,
ServerCallContext context)
{
_logger.LogInformation("Saying hello to {Name}", request.Name);
return Task.FromResult(new HelloReply
{
Message = $"Hello {request.Name}"
});
}
}
Server Configuration
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();
var app = builder.Build();
app.MapGrpcService<GreeterService>();
app.Run();
Streaming
Server Streaming
service ChatService {
// Server sends multiple messages
rpc Subscribe (SubscribeRequest) returns (stream ChatMessage);
}
message SubscribeRequest {
string channel = 1;
}
message ChatMessage {
string user = 1;
string message = 2;
int64 timestamp = 3;
}
public class ChatService : ChatService.ChatServiceBase
{
private readonly IChannelReader<ChatMessage> _channel;
public ChatService(IChannelReader<ChatMessage> channel)
{
_channel = channel;
}
public override async Task Subscribe(
SubscribeRequest request,
IServerStreamWriter<ChatMessage> responseStream,
ServerCallContext context)
{
await foreach (var message in _channel.ReadAllAsync(context.CancellationToken))
{
if (message.Channel == request.Channel)
{
await responseStream.WriteAsync(message);
}
}
}
}
Client Streaming
service UploadService {
// Client sends multiple messages
rpc Upload (stream FileChunk) returns (UploadResponse);
}
message FileChunk {
bytes data = 1;
int32 chunk_number = 2;
}
message UploadResponse {
string file_id = 1;
int64 total_size = 2;
}
public class UploadService : UploadService.UploadServiceBase
{
public override async Task<UploadResponse> Upload(
IAsyncStreamReader<FileChunk> requestStream,
ServerCallContext context)
{
var totalSize = 0L;
var fileId = Guid.NewGuid().ToString();
await foreach (var chunk in requestStream.ReadAllAsync())
{
totalSize += chunk.Data.Length;
// Process chunk
}
return new UploadResponse
{
FileId = fileId,
TotalSize = totalSize
};
}
}
Bidirectional Streaming
service ChatService {
// Both client and server stream
rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}
public class ChatService : ChatService.ChatServiceBase
{
public override async Task Chat(
IAsyncStreamReader<ChatMessage> requestStream,
IServerStreamWriter<ChatMessage> responseStream,
ServerCallContext context)
{
// Read from client and broadcast to all
await foreach (var message in requestStream.ReadAllAsync())
{
// Process and broadcast
await responseStream.WriteAsync(new ChatMessage
{
User = "Server",
Message = $"Echo: {message.Message}"
});
}
}
}
gRPC Client
Setup Client
dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools
Client Code
// Program.cs
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);
// Unary call
var response = await client.SayHelloAsync(
new HelloRequest { Name = "World" });
Console.WriteLine(response.Message);
gRPC Client Factory
builder.Services.AddGrpcClient<Greeter.GreeterClient>(options =>
{
options.Address = new Uri("https://localhost:5001");
});
// Usage
public class MyService
{
private readonly Greeter.GreeterClient _client;
public MyService(Greeter.GreeterClient client)
{
_client = client;
}
public async Task<string> Greet(string name)
{
var response = await _client.SayHelloAsync(
new HelloRequest { Name = name });
return response.Message;
}
}
Interceptors
Server Interceptor
public class LoggingInterceptor : Interceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
{
_logger = logger;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
_logger.LogInformation("Starting call {Method}", context.Method);
try
{
return await continuation(request, context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in call {Method}", context.Method);
throw;
}
}
}
// Register
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LoggingInterceptor>();
});
Best Practices
1. Use Appropriate Call Types
// ✅ Unary for simple request/response
var response = await client.SayHelloAsync(request);
// ✅ Server streaming for real-time updates
await foreach (var message in client.SubscribeAsync(request))
{
Console.WriteLine(message.Message);
}
// ✅ Client streaming for uploads
var uploadResponse = await client.UploadAsync(requestStream);
// ✅ Bidirectional for chat
await foreach (var msg in client.ChatAsync(requestStream))
{
// Process
}
2. Handle Errors Properly
try
{
var response = await client.SayHelloAsync(request);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
Console.WriteLine("Resource not found");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable)
{
Console.WriteLine("Service unavailable");
}
catch (RpcException ex)
{
Console.WriteLine($"gRPC error: {ex.StatusCode}");
}
3. Use Deadlines
// Set deadline for call
var deadline = DateTime.UtcNow.AddSeconds(5);
var response = await client.SayHelloAsync(
request,
deadline: deadline);
3. Xây dựng Web API
Giới thiệu
Phần này trình bày cách xây dựng RESTful API với ASP.NET Core.
Nội dung chính
RESTful API
- Thiết kế API đúng chuẩn - REST principles
- Model Binding - Binding request data
- Model Validation - Validation với Data Annotations
Bảo mật
- Authentication - JWT, JwtBearer configuration
- Authorization - Role-based và Policy-based
- CORS - Cross-Origin Resource Sharing
Phiên bản & Tài liệu
- API Versioning - Version management
- Swagger/OpenAPI - Documentation
RESTful API
Overview Questions
- REST là gì và tại sao nó quan trọng?
- Các HTTP methods nào được sử dụng và khi nào dùng?
- Status codes nào nên dùng cho từng trường hợp?
- URL naming conventions như thế nào cho đúng chuẩn?
- Paging, filtering, sorting được implement ra sao?
Thiết kế API đúng chuẩn REST
REST Principles
- Client-Server - Tách biệt client và server
- Stateless - Mỗi request chứa đủ thông tin
- Cacheable - Response có thể được cache
- Uniform Interface - Sử dụng HTTP methods và status codes đúng cách
- Layered System - Có thể có nhiều layers
HTTP Methods
| Method | Purpose | Idempotent |
|---|---|---|
GET | Lấy resource | ✅ Yes |
POST | Tạo resource mới | ❌ No |
PUT | Thay thế toàn bộ resource | ✅ Yes |
PATCH | Cập nhật một phần resource | ❌ No |
DELETE | Xóa resource | ✅ Yes |
Status Codes
| Code | Meaning | Usage |
|---|---|---|
200 | OK | GET, PUT, PATCH thành công |
201 | Created | POST tạo mới thành công |
204 | No Content | DELETE thành công |
400 | Bad Request | Validation failed |
401 | Unauthorized | Chưa authenticate |
403 | Forbidden | Không có permission |
404 | Not Found | Resource không tồn tại |
500 | Internal Server Error | Server error |
URL Naming Conventions
// ✅ Tốt
GET /api/products // Lấy danh sách products
GET /api/products/1 // Lấy product có id = 1
POST /api/products // Tạo product mới
PUT /api/products/1 // Cập nhật product 1
DELETE /api/products/1 // Xóa product 1
// ❌ Tránh
GET /api/getProducts
GET /api/ProductController/GetAll
POST /api/createProduct
Model Binding & Validation
Note: Model Binding và Model Validation đã được chuyển sang các file riêng để dễ học hơn:
- Model Binding - Chi tiết về binding sources, custom binders
- Model Validation - Data Annotations, FluentValidation, custom validation
Quick Reference
[ApiController]
public class ProductsController : ControllerBase
{
// FromRoute - Từ URL path
[HttpGet("{id}")]
public IActionResult GetById([FromRoute] int id)
{
return Ok(id);
}
// FromQuery - Từ query string
[HttpGet]
public IActionResult Search([FromQuery] string name, [FromQuery] int page = 1)
{
return Ok(new { name, page });
}
// FromBody - Từ request body (JSON)
[HttpPost]
public IActionResult Create([FromBody] Product product)
{
// [ApiController] tự động validate ModelState
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}
Paging, Filtering, Sorting
[HttpGet]
public IActionResult GetProducts(
[FromQuery] PagingParameters paging,
[FromQuery] ProductFilter filter,
[FromQuery] string sortBy = "Name")
{
var query = _context.Products.AsQueryable();
// Filtering
if (!string.IsNullOrEmpty(filter.Category))
query = query.Where(p => p.Category == filter.Category);
if (filter.MinPrice.HasValue)
query = query.Where(p => p.Price >= filter.MinPrice.Value);
// Sorting
query = sortBy?.ToLower() switch
{
"price" => query.OrderBy(p => p.Price),
"pricedesc" => query.OrderByDescending(p => p.Price),
_ => query.OrderBy(p => p.Name)
};
// Paging
var total = query.Count();
var items = query
.Skip((paging.Page - 1) * paging.PageSize)
.Take(paging.PageSize)
.ToList();
return Ok(new PagedResult(items, total, paging.Page, paging.PageSize));
}
public class PagingParameters
{
[FromQuery(Name = "page")]
public int Page { get; set; } = 1;
[FromQuery(Name = "pageSize")]
public int PageSize { get; set; } = 10;
}
public class ProductFilter
{
[FromQuery(Name = "category")]
public string Category { get; set; }
[FromQuery(Name = "minPrice")]
public decimal? MinPrice { get; set; }
[FromQuery(Name = "maxPrice")]
public decimal? MaxPrice { get; set; }
}
Bảo mật
Authentication (Xác thực)
JWT (JSON Web Token)
JWT là một chuẩn token để truyền thông tin an toàn giữa các parties dưới dạng JSON.
Cấu trúc JWT
┌─────────────────────────────────────────────────────────────────────┐
│ JWT STRUCTURE │
├─────────────────┬─────────────────┬───────────────────────────────┤
│ HEADER │ PAYLOAD │ SIGNATURE │
│ (Base64URL) │ (Base64URL) │ (Base64URL) │
├─────────────────┼─────────────────┼───────────────────────────────┤
│ { │ { │ HmacSHA256( │
│ "alg": │ "sub": │ header + "." + payload, │
│ "HS256", │ "1234567890",│ secret_key │
│ "typ": │ "name": │ ) │
│ "JWT" │ "John Doe", │ │
│ } │ "iat": │ │
│ │ 1516239022, │ │
│ │ "exp": │ │
│ │ 1516242622 │ │
│ │ } │ │
└─────────────────┴─────────────────┴───────────────────────────────┘
Cấu hình JwtBearer trong .NET Core
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]))
};
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Tạo JWT Token
public class JwtService
{
private readonly JwtSettings _settings;
public string GenerateToken(User user)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(ClaimTypes.Role, user.Role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_settings.SecretKey));
var credentials = new SigningCredentials(
key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _settings.Issuer,
audience: _settings.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_settings.ExpiryMinutes),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
Authorization (Phân quyền)
[Authorize] Attribute
// Yêu cầu authenticate
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile() { }
// Yêu cầu role cụ thể
[Authorize(Roles = "Admin")]
[HttpGet("admin")]
public IActionResult GetAdminData() { }
// Yêu cầu nhiều roles (AND logic)
[Authorize(Roles = "Admin,Manager")]
[HttpGet("manage")]
public IActionResult Manage() { }
// Yếu tố OR - dùng Policy
[Authorize(Policy = "AdminOrManager")]
[HttpGet("manage")]
public IActionResult Manage() { }
Role-based Authorization
[Authorize(Roles = "Admin")]
public class AdminController : ControllerBase
{
[HttpGet("users")]
public IActionResult GetAllUsers()
{
// Only admins can access
return Ok();
}
}
Policy-based Authorization
// Đăng ký policy trong Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdultOnly", policy =>
policy.RequireClaim("Age", "18", "19", "20", "21", "22", "23", "24", "25"));
options.AddPolicy("PremiumUser", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c => c.Type == "Subscription" &&
c.Value == "Premium")));
options.AddPolicy("CanDeleteProduct", policy =>
policy.RequireAssertion(context =>
context.User.IsInRole("Admin") ||
(context.User.IsInRole("Manager") &&
context.User.HasClaim(c => c.Type == "CanDelete"))));
});
// Sử dụng
[Authorize(Policy = "PremiumUser")]
[HttpGet("premium-content")]
public IActionResult GetPremiumContent() { }
CORS (Cross-Origin Resource Sharing)
Vấn đề
Browser chặn requests từ một domain khác với server (cross-origin requests) vì lý do bảo mật. CORS cho phép server chỉ định origins nào được phép truy cập.
Cấu hình CORS
// Program.cs
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("http://localhost:3000", "https://myapp.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // Chỉ dùng với specific origins
});
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
app.UseCors("AllowFrontend");
CORS với named policy
[ApiController]
[Route("api/[controller]")]
[EnableCors("AllowFrontend")]
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
[HttpGet]
[DisableCors] // Disable CORS cho action cụ thể
public IActionResult GetSecret() => Ok();
}
Preflight Request
┌─────────────────────────────────────────────────────────────────────┐
│ CORS REQUEST FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Browser │
│ │ │
│ │ 1. OPTIONS /api/products │
│ │ Access-Control-Request-Method: GET │
│ │ Access-Control-Request-Headers: Content-Type │
│ ├────────────────────────────────────────────────────────► │
│ │◄────────────────────────────────────────────────────────┤ │
│ │ 2. 200 OK │
│ │ Access-Control-Allow-Origin: http://localhost:3000 │
│ │ Access-Control-Allow-Methods: GET, POST, PUT, DELETE │
│ │ Access-Control-Allow-Headers: Content-Type │
│ │ │
│ │ 3. GET /api/products │
│ ├────────────────────────────────────────────────────────► │
│ │◄────────────────────────────────────────────────────────┤ │
│ │ 4. 200 OK │
│ │ Access-Control-Allow-Origin: http://localhost:3000 │
│ │
└─────────────────────────────────────────────────────────────────────┘
Phiên bản & Tài liệu
API Versioning
Cài đặt
dotnet add package Microsoft.AspNetCore.Mvc.Versioning
Cấu hình
// Program.cs
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("X-Api-Version"),
new QueryStringApiVersionReader("v"));
});
Versioning Strategies
1. URL Path (phổ biến nhất)
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{v:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1() => Ok(new { version = "1.0" });
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2() => Ok(new { version = "2.0", extra = "data" });
}
2. Query String
GET /api/products?api-version=1.0
GET /api/products?api-version=2.0
3. Header
GET /api/products
X-Api-Version: 1.0
Deprecation
[ApiVersion("1.0")]
[ApiVersion("2.0", Deprecated = true)] // Mark as deprecated
[Route("api/v{v:apiVersion}/[controller]")]
public class ProductsController : ControllerBase { }
Swagger/OpenAPI
Cài đặt
dotnet add package Swashbuckle.AspNetCore
Cấu hình
// Program.cs
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "My API",
Version = "v1",
Description = "API Description",
Contact = new OpenApiContact
{
Name = "Support",
Email = "support@example.com"
}
});
// Add JWT Authentication to Swagger
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1");
c.RoutePrefix = "swagger"; // Access at /swagger
});
XML Documentation
// Program.cs
builder.Services.AddSwaggerGen(c =>
{
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
/// <summary>
/// Get all products
/// </summary>
/// <param name="category">Filter by category</param>
/// <returns>List of products</returns>
[HttpGet]
[ProducesResponseType(typeof(List<Product>), 200)]
[ProducesResponseType(400)]
public IActionResult GetProducts([FromQuery] string category) { }
Swagger Response Attributes
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult Create([FromBody] Product product)
{
// ...
}
Best Practices cho API Versioning
1. Chọn chiến lược version phù hợp
| Strategy | Pros | Cons |
|---|---|---|
| URL Path | Rõ ràng, dễ debug | Phải update client URLs |
| Query String | Không thay đổi URL | Ít visible |
| Header | Linh hoạt | Khó 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ế
- Code First vs Database First - Hai cách tiếp cận
- Migrations - Quản lý schema
- DbContext và Change Tracker - Cơ chế tracking
Tối ưu hiệu suất
- N+1 Query Problem - Vấn đề và giải pháp
- Include và ThenInclude - Eager loading
- AsNoTracking - Read-only queries
Giao dịch & Đồng thời
- Transactions - Quản lý transactions
- Concurrency - Xử lý xung đột
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
| State | Description |
|---|---|
Detached | Không được track |
Added | Mới, chưa có trong database |
Unchanged | Khô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
| Method | Tracking | Identity Resolution | Performance |
|---|---|---|---|
AsNoTracking() | ❌ No | ❌ No | Fastest |
AsTracking() | ✅ Yes | ✅ Yes | Default |
AsNoTrackingWithIdentityResolution() | ❌ No | ✅ Yes | Fast |
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 Microservice - Single responsibility, loose coupling
- API Gateway - Ocelot
- Message Bus - RabbitMQ, Kafka
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
| Characteristic | Description |
|---|---|
| Single Responsibility | Mỗi service chỉ làm một việc |
| Loose Coupling | Services giao tiếp qua APIs |
| Independent Deploy | Deploy không ảnh hưởng services khác |
| Technology Diversity | Mỗi service có thể dùng công nghệ khác |
| Ownership | Team 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
| Aspect | RabbitMQ | Kafka |
|---|---|---|
| Protocol | AMQP | Binary (custom) |
| Use Case | Task queues, simple messaging | Event streaming, high throughput |
| Message Retention | Per-queue (short-term) | Topic-based (long-term) |
| Ordering | Per-queue | Per-partition |
| Scalability | Horizontal | Very high |
| Complexity | Simpler | More complex |
| Delivery Guarantee | At-least-once, Exactly-once | At-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
- In-Memory Cache - IMemoryCache
- Distributed Cache - Redis
- Response Caching - Response caching headers
Xử lý tải
- Rate Limiting - Giới hạn requests
- Load Balancing - Phân phối tải
- Health Checks - Kiểm tra health
Bất đồng bộ
- IAsyncEnumerable - Stream dữ liệu
- Kỹ thuật stream - Large data handling
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
}
| Aspect | Task | ValueTask |
|---|---|---|
| Allocation | Always heap | Avoided if sync |
| Use case | Standard async | Hot path |
| Synchronous return | Not allowed | Allowed |
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
- RabbitMQ - Message broker với queue model
- Kafka - Distributed event streaming
- Pub/Sub Pattern - Publish/Subscribe model
Container & Cloud
- Docker - Containerization
- Kubernetes - Orchestration
- Azure/AWS - Cloud deployment
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
| Feature | RabbitMQ | Kafka |
|---|---|---|
| Delivery Model | Queue-based | Log-based |
| Message Retention | Until consumed (default) | Configurable time |
| Ordering | Per-queue | Per-partition |
| Throughput | Moderate | Very High |
| Latency | Low | Very Low |
| Use Cases | Task queues, RPC | Event streaming, audit log |
| Complexity | Lower | Higher |
| Scaling | Horizontal | Horizontal + 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
| Factor | Description |
|---|---|
| Codebase | One codebase tracked in version control |
| Dependencies | Explicitly declare dependencies |
| Config | Store config in environment |
| Backing Services | Treat backing services as attached resources |
| Build/Release/Run | Strictly separate build and run stages |
| Processes | Execute app as one or more stateless processes |
| Port Binding | Export HTTP as a service by port binding |
| Concurrency | Scale out via process model |
| Disposability | Fast startup and graceful shutdown |
| Dev/Prod Parity | Keep development, staging, production similar |
| Logs | Treat logs as event streams |
| Admin Processes | Run 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 với xUnit - Viết unit tests
- Mocking với Moq - Mock dependencies
- Integration Testing - Test toàn bộ pipeline
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 |
|---|---|---|
| Platform | Cross-platform (Windows, Linux, macOS) | Windows only |
| Source | Open source | Mostly closed source |
| Performance | Better, optimized runtime | Legacy |
| Modularity | Package-based (NuGet) | Full framework |
| CLI | Full CLI support | Limited |
| Side-by-side | Multiple versions | Single version per machine |
| Use case | Modern apps, microservices | Legacy 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
| Feature | MVC | Web API | Minimal API |
|---|---|---|---|
| Views | ✅ Yes | ❌ No | ❌ No |
| Controllers | ✅ Yes | ✅ Yes | ❌ No |
| Routing | Convention + Attribute | Attribute | MapGet/MapPost |
| Model Binding | Full | Full | Limited |
| Testability | Medium | Medium | High |
| Use case | Web apps | APIs | Microservices |
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
| Aspect | Abstract Class | Interface |
|---|---|---|
| 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 |
| Inheritance | Single | Multiple |
| 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
| Aspect | IEnumerable | IQueryable |
|---|---|---|
| Location | Client-side | Server-side |
| Execution | In-memory | Database query |
| SQL Translation | ❌ No | ✅ Yes |
| Deferred | Yes | Yes |
| Use case | In-memory collections | Database queries |
| Performance | Slow with large data | Optimized |
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
| Aspect | var | dynamic |
|---|---|---|
| Type checking | Compile-time | Runtime |
| IntelliSense | ✅ Yes | ❌ No |
| Performance | Fast | Slower |
| Use case | Type known at compile | Late binding |
const vs readonly
| Aspect | const | readonly |
|---|---|---|
| Evaluation | Compile-time | Runtime |
| Value | Must be known at compile | Set at runtime |
| Static | Always static | Can be instance-level |
| Use case | Compile-time constants | Runtime constants |
string vs StringBuilder
| Aspect | string | StringBuilder |
|---|---|---|
| Type | Immutable | Mutable |
| Memory | New allocation per change | Dynamic buffer |
| Use case | Small strings, no changes | Large 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);