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, materialize khác nhau thế nào?
  • AsEnumerable() khác ToList() ra sao?
  • Union, Intersect, Except, Distinct
  • DistinctBy() gặp duplicate thì xử lý sao?
  • ToLookup() vì sao được xem là read-oriented / immutable-ish?
  • TakeWhile, SkipWhile khác Where như 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:

DistinctBy chỉ 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

  1. IEnumerable<T> có phải LINQ không?
  2. LINQ to Objects là gì?
  3. sequence khác collection ở đâu?
  4. enumerate có phải materialize không?
  5. foreach có materialize không?
  6. ToList() làm gì?
  7. AsEnumerable() có query DB ngay không?
  8. phần sau AsEnumerable() chạy ở đâu?
  9. vì sao AsEnumerable() quá sớm có thể nguy hiểm?
  10. Union khác Concat thế nào?
  11. Intersect lấy gì?
  12. Except có direction không?
  13. DistinctBy gặp duplicate thì sao?
  14. ToLookup khác ToDictionary ra sao?
  15. vì sao lookup chỉ là immutable-ish?
  16. TakeWhile khác Where ở đâu?
  17. SkipWhile dừng skip khi nào?
  18. vì sao LINQ method có không đồng nghĩa EF Core translate được?
  19. SequenceEqual có quan tâm order không?
  20. SequenceEqual vớ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"