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
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);
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
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
Binding Sources
[ApiController]
public class ProductsController : ControllerBase
{
// FromRoute - Từ URL path
[HttpGet("{id}")]
public IActionResult GetById([FromRoute] int id)
{
return Ok(id);
}
// FromQuery - Từ query string
[HttpGet]
public IActionResult Search([FromQuery] string name, [FromQuery] int page = 1)
{
return Ok(new { name, page });
}
// FromBody - Từ request body (JSON)
[HttpPost]
public IActionResult Create([FromBody] Product product)
{
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
// FromForm - Từ form data
[HttpPost("upload")]
public IActionResult Upload([FromForm] IFormFile file)
{
return Ok();
}
// FromHeader - Từ HTTP header
[HttpGet]
public IActionResult Get([FromHeader(Name = "X-Request-Id")] string requestId)
{
return Ok(requestId);
}
}
Complex Binding
// Request URL: /api/products?category=electronics&price[min]=10&price[max]=100
public IActionResult Search(
[FromQuery] string category,
[FromQuery] PriceRange price)
{
return Ok();
}
public class PriceRange
{
public decimal Min { get; set; }
public decimal Max { get; set; }
}
Model Validation
Data Annotations
public class Product
{
public int Id { get; set; }
[Required(ErrorMessage = "Name is required")]
[StringLength(100, MinimumLength = 2)]
public string Name { get; set; }
[Range(0.01, 9999.99)]
public decimal Price { get; set; }
[EmailAddress]
public string Email { get; set; }
[Url]
public string Website { get; set; }
[Phone]
public string Phone { get; set; }
[CreditCard]
public string CreditCard { get; set; }
[RegularExpression(@"^[A-Z]{3}$", ErrorMessage = "Invalid code")]
public string Code { get; set; }
}
Custom Validation
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("Name is required");
RuleFor(x => x.Price)
.GreaterThan(0)
.When(x => x.IsActive)
.WithMessage("Price must be greater than 0");
RuleFor(x => x.StartDate)
.LessThan(x => x.EndDate)
.WithMessage("Start date must be before end date");
}
}
Validation trong Controller
[HttpPost]
public IActionResult Create([FromBody] Product product)
{
// ✅ Tự động validate nếu dùng [ApiController]
// ModelState.IsValid sẽ được check tự động
// Hoặc validate thủ công
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
Validation Error Response
{
"errors": {
"Name": [
"Name is required"
],
"Price": [
"Price must be greater than 0"
]
}
}
Paging, Filtering, Sorting
[HttpGet]
public IActionResult GetProducts(
[FromQuery] PagingParameters paging,
[FromQuery] ProductFilter filter,
[FromQuery] string sortBy = "Name")
{
var query = _context.Products.AsQueryable();
// Filtering
if (!string.IsNullOrEmpty(filter.Category))
query = query.Where(p => p.Category == filter.Category);
if (filter.MinPrice.HasValue)
query = query.Where(p => p.Price >= filter.MinPrice.Value);
// Sorting
query = sortBy?.ToLower() switch
{
"price" => query.OrderBy(p => p.Price),
"pricedesc" => query.OrderByDescending(p => p.Price),
_ => query.OrderBy(p => p.Name)
};
// Paging
var total = query.Count();
var items = query
.Skip((paging.Page - 1) * paging.PageSize)
.Take(paging.PageSize)
.ToList();
return Ok(new PagedResult(items, total, paging.Page, paging.PageSize));
}
public class PagingParameters
{
[FromQuery(Name = "page")]
public int Page { get; set; } = 1;
[FromQuery(Name = "pageSize")]
public int PageSize { get; set; } = 10;
}
public class ProductFilter
{
[FromQuery(Name = "category")]
public string Category { get; set; }
[FromQuery(Name = "minPrice")]
public decimal? MinPrice { get; set; }
[FromQuery(Name = "maxPrice")]
public decimal? MaxPrice { get; set; }
}
Bảo mật
Authentication (Xác thực)
JWT (JSON Web Token)
JWT là một chuẩn token để truyền thông tin an toàn giữa các parties dưới dạng JSON.
Cấu trúc JWT
┌─────────────────────────────────────────────────────────────────────┐
│ JWT STRUCTURE │
├─────────────────┬─────────────────┬───────────────────────────────┤
│ HEADER │ PAYLOAD │ SIGNATURE │
│ (Base64URL) │ (Base64URL) │ (Base64URL) │
├─────────────────┼─────────────────┼───────────────────────────────┤
│ { │ { │ HmacSHA256( │
│ "alg": │ "sub": │ header + "." + payload, │
│ "HS256", │ "1234567890",│ secret_key │
│ "typ": │ "name": │ ) │
│ "JWT" │ "John Doe", │ │
│ } │ "iat": │ │
│ │ 1516239022, │ │
│ │ "exp": │ │
│ │ 1516242622 │ │
│ │ } │ │
└─────────────────┴─────────────────┴───────────────────────────────┘
Cấu hình JwtBearer trong .NET Core
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]))
};
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
Tạo JWT Token
public class JwtService
{
private readonly JwtSettings _settings;
public string GenerateToken(User user)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(ClaimTypes.Role, user.Role),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_settings.SecretKey));
var credentials = new SigningCredentials(
key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _settings.Issuer,
audience: _settings.Audience,
claims: claims,
expires: DateTime.UtcNow.AddMinutes(_settings.ExpiryMinutes),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
Authorization (Phân quyền)
[Authorize] Attribute
// Yêu cầu authenticate
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile() { }
// Yêu cầu role cụ thể
[Authorize(Roles = "Admin")]
[HttpGet("admin")]
public IActionResult GetAdminData() { }
// Yêu cầu nhiều roles (AND logic)
[Authorize(Roles = "Admin,Manager")]
[HttpGet("manage")]
public IActionResult Manage() { }
// Yếu tố OR - dùng Policy
[Authorize(Policy = "AdminOrManager")]
[HttpGet("manage")]
public IActionResult Manage() { }
Role-based Authorization
[Authorize(Roles = "Admin")]
public class AdminController : ControllerBase
{
[HttpGet("users")]
public IActionResult GetAllUsers()
{
// Only admins can access
return Ok();
}
}
Policy-based Authorization
// Đăng ký policy trong Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdultOnly", policy =>
policy.RequireClaim("Age", "18", "19", "20", "21", "22", "23", "24", "25"));
options.AddPolicy("PremiumUser", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c => c.Type == "Subscription" &&
c.Value == "Premium")));
options.AddPolicy("CanDeleteProduct", policy =>
policy.RequireAssertion(context =>
context.User.IsInRole("Admin") ||
(context.User.IsInRole("Manager") &&
context.User.HasClaim(c => c.Type == "CanDelete"))));
});
// Sử dụng
[Authorize(Policy = "PremiumUser")]
[HttpGet("premium-content")]
public IActionResult GetPremiumContent() { }
CORS (Cross-Origin Resource Sharing)
Vấn đề
Browser chặn requests từ một domain khác với server (cross-origin requests) vì lý do bảo mật. CORS cho phép server chỉ định origins nào được phép truy cập.
Cấu hình CORS
// Program.cs
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("http://localhost:3000", "https://myapp.com")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // Chỉ dùng với specific origins
});
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
app.UseCors("AllowFrontend");
CORS với named policy
[ApiController]
[Route("api/[controller]")]
[EnableCors("AllowFrontend")]
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok();
[HttpGet]
[DisableCors] // Disable CORS cho action cụ thể
public IActionResult GetSecret() => Ok();
}
Preflight Request
┌─────────────────────────────────────────────────────────────────────┐
│ CORS REQUEST FLOW │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Browser │
│ │ │
│ │ 1. OPTIONS /api/products │
│ │ Access-Control-Request-Method: GET │
│ │ Access-Control-Request-Headers: Content-Type │
│ ├────────────────────────────────────────────────────────► │
│ │◄────────────────────────────────────────────────────────┤ │
│ │ 2. 200 OK │
│ │ Access-Control-Allow-Origin: http://localhost:3000 │
│ │ Access-Control-Allow-Methods: GET, POST, PUT, DELETE │
│ │ Access-Control-Allow-Headers: Content-Type │
│ │ │
│ │ 3. GET /api/products │
│ ├────────────────────────────────────────────────────────► │
│ │◄────────────────────────────────────────────────────────┤ │
│ │ 4. 200 OK │
│ │ Access-Control-Allow-Origin: http://localhost:3000 │
│ │
└─────────────────────────────────────────────────────────────────────┘
Phiên bản & Tài liệu
API Versioning
Cài đặt
dotnet add package Microsoft.AspNetCore.Mvc.Versioning
Cấu hình
// Program.cs
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("X-Api-Version"),
new QueryStringApiVersionReader("v"));
});
Versioning Strategies
1. URL Path (phổ biến nhất)
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{v:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1() => Ok(new { version = "1.0" });
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2() => Ok(new { version = "2.0", extra = "data" });
}
2. Query String
GET /api/products?api-version=1.0
GET /api/products?api-version=2.0
3. Header
GET /api/products
X-Api-Version: 1.0
Deprecation
[ApiVersion("1.0")]
[ApiVersion("2.0", Deprecated = true)] // Mark as deprecated
[Route("api/v{v:apiVersion}/[controller]")]
public class ProductsController : ControllerBase { }
Swagger/OpenAPI
Cài đặt
dotnet add package Swashbuckle.AspNetCore
Cấu hình
// Program.cs
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "My API",
Version = "v1",
Description = "API Description",
Contact = new OpenApiContact
{
Name = "Support",
Email = "support@example.com"
}
});
// Add JWT Authentication to Swagger
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1");
c.RoutePrefix = "swagger"; // Access at /swagger
});
XML Documentation
// Program.cs
builder.Services.AddSwaggerGen(c =>
{
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
/// <summary>
/// Get all products
/// </summary>
/// <param name="category">Filter by category</param>
/// <returns>List of products</returns>
[HttpGet]
[ProducesResponseType(typeof(List<Product>), 200)]
[ProducesResponseType(400)]
public IActionResult GetProducts([FromQuery] string category) { }
Swagger Response Attributes
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public IActionResult Create([FromBody] Product product)
{
// ...
}
Best Practices cho API Versioning
1. Chọn chiến lược version phù hợp
| 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);