C# Day 8: LINQ Nâng Cao — Những Khái Niệm Dễ Nhầm
Bài này tổng hợp lại các điểm mình đã đào sâu thêm trong Day 8:
- LINQ to Objects là gì?
IEnumerable<T>liên quan gì tới LINQ?sequence,enumerate,materializekhác nhau thế nào?AsEnumerable()khácToList()ra sao?Union,Intersect,Except,DistinctDistinctBy()gặp duplicate thì xử lý sao?ToLookup()vì sao được xem là read-oriented / immutable-ish?TakeWhile,SkipWhilekhácWherenhư thế nào?- dùng các operator này với EF Core cần lưu ý gì?
SequenceEqual()thật sự so sánh điều gì?
1. LINQ to Objects là gì?
Không nên hiểu:
LINQ to Objects
vs
IEnumerable<T>
vì hai khái niệm này không cùng cấp.
Quan hệ đúng:
IEnumerable<T>
→ abstraction của một sequence có thể enumerate
LINQ to Objects
→ các LINQ operators chạy trên IEnumerable<T>
Ví dụ:
List<User> users = GetUsers();
var result = users
.Where(x => x.IsActive)
.Select(x => x.Name);
List<User> implement:
IEnumerable<User>
nên dùng được:
Where
Select
Any
All
First
GroupBy
...
Các extension method này thuộc System.Linq.Enumerable.
Mental model:
Objects trong memory
↓
IEnumerable<T>
↓
Enumerable.Where / Select / ...
↓
LINQ to Objects
2. LINQ to Objects không luôn trả List<object>
Ví dụ:
var result = users
.Where(x => x.IsActive);
kết quả thường là:
IEnumerable<User>
Nếu muốn thành list:
var result = users
.Where(x => x.IsActive)
.ToList();
lúc này mới là:
List<User>
Select còn có thể đổi type:
var names = users
.Select(x => x.Name);
Input:
User
Output:
string
Type:
IEnumerable<string>
3. Sequence là gì?
Sequence là một dãy item có thể được enumerate.
Ví dụ:
List<int> numbers = [1, 2, 3, 4];
Ta có:
1 → 2 → 3 → 4
Nhưng sequence không nhất thiết phải là List<T>.
Ví dụ:
IEnumerable<int> query =
numbers.Where(x => x > 2);
query cũng đại diện cho một sequence.
Mental model:
IEnumerable<T>
≈ "Tôi có thể cung cấp từng item khi anh yêu cầu."
4. Enumerate là gì?
Enumerate nghĩa là bắt đầu đọc sequence từng phần tử.
Ví dụ:
var query = numbers
.Where(x => x > 2);
Khi:
foreach (var number in query)
{
Console.WriteLine(number);
}
thì sequence bắt đầu được enumerate.
Conceptually:
GetEnumerator()
↓
MoveNext()
↓
Current
↓
MoveNext()
↓
Current
...
Mental model ngắn:
Enumerate
→ "Cho tôi item tiếp theo."
5. Materialize là gì?
Materialize là lấy kết quả thật từ sequence và lưu thành collection cụ thể.
Ví dụ:
var query = numbers
.Where(x => x > 2);
Khi:
var result = query.ToList();
flow:
sequence
↓
enumerate
↓
3
4
↓
create List<int>
↓
[3, 4]
Đây là materialization.
6. Enumerate không đồng nghĩa Materialize
foreach (var item in query)
{
}
là:
enumerate ✅
materialize ❌
Còn:
var list = query.ToList();
là:
enumerate ✅
materialize ✅
Câu cần nhớ:
Mọi materialization đều cần consume/enumerate sequence, nhưng enumerate không nhất thiết phải materialize.
7. Deferred Execution
Ví dụ:
var query = numbers
.Where(x => x > 1);
Nhiều operator như:
Where
Select
Take
Skip
thường chưa chạy ngay.
Chúng build pipeline.
Khi:
foreach (var item in query)
hoặc:
query.ToList()
thì mới enumerate.
Mental model:
Where / Select
→ build pipeline
foreach / ToList
→ trigger enumeration
8. Ví dụ Deferred Execution
var numbers = new List<int>
{
1, 2, 3
};
var query =
numbers.Where(x => x > 1);
numbers.Add(4);
Khi enumerate query, kết quả là:
2
3
4
Vì query chưa materialize trước khi 4 được thêm.
Nếu:
var result = numbers
.Where(x => x > 1)
.ToList();
numbers.Add(4);
thì result vẫn là:
2
3
vì ToList() đã tạo snapshot.
9. AsEnumerable() là gì?
AsEnumerable() không có nghĩa:
"Query DB ngay."
Nó có nghĩa:
"Từ đây trở đi, dùng IEnumerable/LINQ to Objects semantics."
Ví dụ:
var query = dbContext.Users
.Where(x => x.IsActive)
.AsEnumerable()
.Where(x => CustomCheck(x));
Flow:
DbSet<User>
↓
Where(IsActive)
→ IQueryable
→ provider có thể translate SQL
↓ AsEnumerable()
IEnumerable<User>
↓
Where(CustomCheck)
→ chạy bằng .NET
10. AsEnumerable() có call DB ngay không?
Thông thường:
AsEnumerable()
→ chưa query DB ngay
Nó chỉ switch abstraction:
IQueryable<T>
→ IEnumerable<T>
Khi:
var result = query.ToList();
thì mới enumerate.
11. ToList() thường Trigger Query
Ví dụ:
var result = dbContext.Users
.Where(x => x.IsActive)
.ToList();
Flow:
ToList
↓
enumerate IQueryable
↓
EF Core translate
↓
SQL
↓
DB execute
↓
rows returned
↓
materialize List<User>
Với Web API, thường dùng async:
var result =
await dbContext.Users
.Where(x => x.IsActive)
.ToListAsync();
12. AsEnumerable() vs ToList()
| Method | Mental model |
|---|---|
AsEnumerable() |
switch sang LINQ to Objects |
AsEnumerable() |
chưa materialize |
AsEnumerable() |
chưa nhất thiết hit DB ngay |
ToList() |
enumerate |
ToList() |
trigger execution nếu source là query |
ToList() |
materialize thành List<T> |
Câu nhớ nhanh:
AsEnumerable
→ đổi cách xử lý
ToList
→ lấy data thật + tạo List
13. Query trước và sau AsEnumerable()
var result = dbContext.Users
.Where(x => x.IsActive)
.AsEnumerable()
.Where(x => CustomCheck(x))
.ToList();
Phần trước AsEnumerable() có thể được provider translate xuống DB.
Phần sau:
.Where(x => CustomCheck(x))
chạy bằng .NET trên backend.
Nếu AsEnumerable() quá sớm, backend có thể kéo nhiều data hơn cần thiết.
14. Set Operators
Bốn method quan trọng:
Union
Intersect
Except
Distinct
Mental model:
Union
→ A hoặc B
Intersect
→ A và B
Except
→ A có, B không có
Distinct
→ unique trong A
Giả sử:
var a = new[] { 1, 2, 2, 3, 4 };
var b = new[] { 3, 4, 4, 5, 6 };
15. Union
var result = a.Union(b);
Kết quả:
1 2 3 4 5 6
Mental model:
A ∪ B
Khác Concat:
Concat
→ nối sequence
→ giữ duplicate
Union
→ set semantics
→ loại duplicate
16. Intersect
var result = a.Intersect(b);
Kết quả:
3 4
Mental model:
A ∩ B
→ "Hai bên có chung cái gì?"
17. Except
var result = a.Except(b);
Kết quả:
1 2
Mental model:
A - B
→ "A có gì mà B không có?"
Except có direction:
a.Except(b)
khác:
b.Except(a)
18. Distinct
var result = a.Distinct();
Input:
1 2 2 3 4
Output:
1 2 3 4
Mental model:
Distinct
→ remove duplicate trong một sequence
19. Equality trong Set Operators
Với custom class:
class User
{
public int Id { get; set; }
}
hai object có cùng Id chưa chắc đã equal nếu type chưa định nghĩa equality phù hợp.
Các set operators dựa trên:
Equals()
GetHashCode()
IEqualityComparer<T>
Đây là phần nối lại kiến thức Day 2 và Day 5.
20. Các phiên bản By
Có:
DistinctBy
UnionBy
IntersectBy
ExceptBy
Mental model:
By
→ "Đừng so cả object.
Hãy so theo key tôi chọn."
Ví dụ:
users.DistinctBy(x => x.Id);
nghĩa là unique theo Id.
21. DistinctBy() gặp Duplicate
Ví dụ:
var users = new[]
{
new User(1, "A"),
new User(2, "B"),
new User(1, "C")
};
var result =
users.DistinctBy(x => x.Id);
Flow:
Id = 1
→ chưa gặp
→ giữ A
Id = 2
→ chưa gặp
→ giữ B
Id = 1
→ đã gặp
→ bỏ C
Kết quả:
A
B
DistinctBy không:
merge
update
throw
Nó chỉ giữ một item cho mỗi selected key.
22. Nếu Duplicate cần Business Rule khác
Nếu muốn:
latest UpdatedAt wins
thì có thể dùng:
var result = users
.GroupBy(x => x.Id)
.Select(group =>
group
.OrderByDescending(
x => x.UpdatedAt
)
.First()
)
.ToList();
Nếu duplicate là lỗi, cần validate và throw rõ ràng.
Câu quan trọng:
DistinctBychỉ giải quyết “mỗi key một item”, không tự hiểu business rule khi duplicate.
23. ToLookup() là gì?
ToLookup() tạo cấu trúc:
Key
→ many values
Ví dụ:
var lookup =
orders.ToLookup(
x => x.UserId
);
Mental model:
User 1
→ Order A
→ Order B
User 2
→ Order C
Lấy data:
var userOrders =
lookup[userId];
24. Vì sao gọi ToLookup() là immutable-ish?
Sau khi tạo lookup, bạn không mutate mapping trực tiếp kiểu:
lookup.Add(...);
lookup.Remove(...);
lookup[key] = ...;
Nó là read-oriented structure.
Nhưng object bên trong vẫn có thể mutable:
var firstOrder =
lookup[userId].First();
firstOrder.Total = 999;
Nếu Order mutable, việc này vẫn được.
Mental model:
Lookup structure
→ không sửa mapping trực tiếp
Items bên trong
→ vẫn có thể mutable
Nên gọi là immutable-ish, không phải fully immutable.
25. ToLookup() vs ToDictionary()
ToDictionary:
Key
→ single value
Nếu duplicate key:
throw exception
ToLookup:
Key
→ many values
Duplicate key hoàn toàn hợp lệ.
Ví dụ:
UserId = 1
→ Order A
→ Order B
→ Order C
26. TakeWhile
var numbers =
new[] { 1, 2, 20, 3, 4 };
var result =
numbers.TakeWhile(
x => x < 10
);
Kết quả:
1
2
Vì gặp 20 là fail lần đầu và dừng luôn.
Mental model:
TakeWhile
→ lấy liên tục cho tới first failure
27. SkipWhile
var result =
numbers.SkipWhile(
x => x < 10
);
Kết quả:
20
3
4
Flow:
1 → skip
2 → skip
20 → false
↓
dừng skip
↓
lấy phần còn lại
Mental model:
SkipWhile
→ bỏ tới first failure
→ sau đó lấy tất cả phần còn lại
28. TakeWhile khác Where
Dữ liệu:
1 2 20 3 4
Where(x < 10):
1 2 3 4
TakeWhile(x < 10):
1 2
Mental model:
Where
→ filter toàn sequence
TakeWhile
→ lấy prefix cho tới first failure
29. SkipWhile khác Where
Với:
1 2 20 3 4
Where(x >= 10):
20
Nhưng:
SkipWhile(x => x < 10)
ra:
20
3
4
Vì sau first failure, nó ngừng skip.
30. TakeWhile / SkipWhile với EF Core
Điểm quan trọng:
Có LINQ method trên API không có nghĩa EF Core provider chắc chắn translate được xuống SQL.
Với query phức tạp hoặc operator ít phổ biến, cần:
verify provider support
inspect generated SQL
test query
Không nên assume:
LINQ method tồn tại
→ SQL translation chắc chắn tồn tại
Nếu chuyển sang:
.AsEnumerable()
.TakeWhile(...)
thì TakeWhile chạy trong .NET.
Điều này có thể kéo nhiều data về backend nên cần cân nhắc kỹ.
31. SequenceEqual()
SequenceEqual() không hỏi:
"Hai bên có cùng tập giá trị không?"
Nó hỏi:
"Hai sequence có giống nhau theo từng vị trí không?"
Ví dụ:
var a = new[] { 1, 2, 3 };
var b = new[] { 1, 2, 3 };
bool result =
a.SequenceEqual(b);
Kết quả:
true
32. SequenceEqual() kiểm tra gì?
Mental model:
1. cùng số lượng
2. cùng order
3. từng cặp item cùng vị trí equal
Ví dụ:
A = [1,2,3]
B = [3,2,1]
Kết quả:
false
Dù hai bên có cùng values.
33. SequenceEqual() với Object
Ví dụ:
var a = new[]
{
new User { Id = 1 }
};
var b = new[]
{
new User { Id = 1 }
};
Nếu User là class bình thường chưa custom equality:
a.SequenceEqual(b)
có thể là false.
Nếu muốn compare theo Id:
var result = a
.Select(x => x.Id)
.SequenceEqual(
b.Select(x => x.Id)
);
34. SequenceEqual khác Set Equality
Ví dụ:
A = [1,2,3]
B = [3,2,1]
SequenceEqual:
false
Nếu business requirement là:
"Chỉ cần cùng values, không quan tâm order"
thì đó là bài toán khác.
Có thể dùng:
HashSet.SetEquals
hoặc
sort + SequenceEqual
tùy semantics.
35. Mental Model Tổng Hợp
LINQ to Objects
→ LINQ operators chạy trên IEnumerable<T>
IEnumerable<T>
→ sequence có thể enumerate
Sequence
→ dãy item
Enumerate
→ lấy từng item
Materialize
→ lưu result vào collection cụ thể
AsEnumerable
→ switch sang LINQ to Objects
ToList
→ enumerate + materialize
36. Mental Model Set Operators
Union
→ A hoặc B
Intersect
→ A và B
Except
→ A có, B không có
Distinct
→ remove duplicate trong A
DistinctBy
→ distinct theo selected key
37. Mental Model Sequence Operators
Where
→ filter toàn sequence
TakeWhile
→ lấy tới first failure
SkipWhile
→ bỏ tới first failure
SequenceEqual
→ same length
+ same order
+ pair-by-pair equality
38. Câu Hỏi Ôn Tập
IEnumerable<T>có phải LINQ không?- LINQ to Objects là gì?
- sequence khác collection ở đâu?
- enumerate có phải materialize không?
foreachcó materialize không?ToList()làm gì?AsEnumerable()có query DB ngay không?- phần sau
AsEnumerable()chạy ở đâu? - vì sao
AsEnumerable()quá sớm có thể nguy hiểm? UnionkhácConcatthế nào?Intersectlấy gì?Exceptcó direction không?DistinctBygặp duplicate thì sao?ToLookupkhácToDictionaryra sao?- vì sao lookup chỉ là
immutable-ish? TakeWhilekhácWhereở đâu?SkipWhiledừng skip khi nào?- vì sao LINQ method có không đồng nghĩa EF Core translate được?
SequenceEqualcó quan tâm order không?SequenceEqualvới custom class dựa vào equality nào?
Key Takeaways
Điểm quan trọng nhất của LINQ nâng cao không phải là nhớ thật nhiều method.
Hãy luôn hỏi:
Source này là IEnumerable hay IQueryable?
Query đã execute chưa?
Sequence đã được enumerate chưa?
Data đã được materialize chưa?
AsEnumerable nằm ở đâu?
Filter này chạy ở DB hay memory?
Equality semantics là gì?
Operator này xử lý sequence hay set?
Order có quan trọng không?
Nếu trả lời được các câu này, bạn đã đi từ mức:
"biết viết LINQ"
sang:
"hiểu execution model và trade-off của LINQ"