C# Day 7: LINQ Cơ Bản

Sau khi học:

Day 5
→ Collections

Day 6
→ Delegate
→ Func
→ Action
→ Lambda

Day 7 là thời điểm rất hợp lý để học LINQ.

LINQ sử dụng rất nhiều:

IEnumerable<T>
+
extension methods
+
delegate
+
lambda

Mục tiêu của ngày 7 là hiểu:

  • LINQ là gì?
  • IEnumerable<T> liên quan gì tới LINQ?
  • Where và Select khác nhau thế nào?
  • Any và All dùng khi nào?
  • First, FirstOrDefault, Single, SingleOrDefault khác nhau ra sao?
  • OrderBy, ThenBy
  • GroupBy
  • SelectMany
  • aggregation
  • Distinct
  • ToList() làm gì?
  • deferred execution là gì?
  • vì sao multiple enumeration có thể gây vấn đề?

1. LINQ là gì?

LINQ là viết tắt của:

Language Integrated Query

Nó cho phép query dữ liệu trực tiếp trong C#.

Ví dụ:

var activeUsers = users
    .Where(x => x.IsActive)
    .ToList();

Thay vì tự viết:

var activeUsers = new List<User>();

foreach (var user in users)
{
    if (user.IsActive)
    {
        activeUsers.Add(user);
    }
}

Mental model:

LINQ
→ mô tả dữ liệu mình muốn

thay vì luôn viết từng bước imperative bằng loop.


2. LINQ làm việc với IEnumerable<T>

Ví dụ:

List<User> users = ...;

List<User> implement:

IEnumerable<User>

Do đó LINQ có thể làm việc với nó.

Ví dụ:

users.Where(...)

Where không phải method riêng được viết trực tiếp trong List<T>.

Nó là extension method trên:

IEnumerable<T>

Mental model:

List<T>
↓
IEnumerable<T>
↓
LINQ extension methods

3. Where

Where dùng để filter.

Ví dụ:

var activeUsers = users
    .Where(user => user.IsActive);

Lambda:

user => user.IsActive

có shape gần như:

Func<User, bool>

Flow:

User
↓
predicate
↓
true
→ giữ

false
→ bỏ

Mental model:

Where
→ filter

4. Where không thay đổi Type của Item

Nếu input là:

IEnumerable<User>

thì:

users.Where(...)

vẫn cho:

IEnumerable<User>

Ví dụ:

User
User
User
↓ Where
User
User

Nó chỉ chọn item nào được giữ lại.


5. Select

Select dùng để transform / project.

Ví dụ:

var names = users
    .Select(user => user.Name);

Input:

User

Output:

string

Shape:

Func<User, string>

Mental model:

Select
→ transform

6. Where vs Select

Đây là distinction rất quan trọng.

Where
→ chọn item nào được giữ

Select
→ biến item thành dạng khác

Ví dụ:

var activeUsers = users
    .Where(x => x.IsActive);

vẫn là:

IEnumerable<User>

Còn:

var names = users
    .Select(x => x.Name);

trở thành:

IEnumerable<string>

7. Chain Where + Select

Ví dụ:

var activeUserNames = users
    .Where(x => x.IsActive)
    .Select(x => x.Name)
    .ToList();

Flow:

Users
↓
Where
→ chỉ active users
↓
Select
→ Name
↓
ToList
→ List<string>

8. Any

Any trả về bool.

Ví dụ:

bool hasActiveUser =
    users.Any(x => x.IsActive);

Mental model:

Any
→ có ít nhất một item match không?

Kết quả:

true / false

9. Any() không có Predicate

Có thể:

bool hasUsers =
    users.Any();

Nghĩa là:

collection có ít nhất một phần tử không?

Đây thường là cách diễn đạt intent tốt hơn:

users.Count() > 0

khi chỉ cần biết có phần tử hay không.


10. Any() vs Count() > 0

Nếu mục tiêu chỉ là:

Có item nào không?

thì:

users.Any()

thường rõ intent hơn.

Với sequence cần enumerate, Any() có thể dừng ngay khi thấy item đầu tiên.

Trong khi Count() có thể cần đếm toàn bộ tùy source.

Mental model:

Any
→ existence

Count
→ quantity

11. All

Ví dụ:

bool allActive =
    users.All(x => x.IsActive);

Mental model:

All
→ tất cả item đều thỏa condition?

Ví dụ:

true
true
true
→ true

true
false
true
→ false

12. Contains

Ví dụ:

var numbers = new[]
{
    1, 2, 3
};

bool exists =
    numbers.Contains(2);

Kết quả:

true

Với custom type, equality semantics của type/comparer ảnh hưởng kết quả.

Phần này nối lại Day 2 và Day 5:

Equals()
GetHashCode()
Equality Comparer

13. First

var user = users
    .First(x => x.IsActive);

Nghĩa là:

Lấy item đầu tiên match

Nếu không có item:

throw exception

Mental model:

First
→ tôi kỳ vọng có ít nhất một item

14. FirstOrDefault

var user = users
    .FirstOrDefault(
        x => x.IsActive
    );

Nếu có:

lấy item đầu

Nếu không có:

default

Với reference type:

null

thường là default.

Mental model:

FirstOrDefault
→ có thì lấy đầu tiên
→ không có thì default

15. Single

var user = users
    .Single(x => x.Id == id);

Semantics:

0 item
→ exception

1 item
→ return item

>1 item
→ exception

Mental model:

Single
→ tôi kỳ vọng EXACTLY ONE

16. SingleOrDefault

var user = users
    .SingleOrDefault(
        x => x.Id == id
    );

Semantics:

0 item
→ default

1 item
→ return

>1 item
→ exception

Mental model:

SingleOrDefault
→ zero or one
→ nhưng không chấp nhận duplicate

17. FirstOrDefault vs SingleOrDefault

Đây là câu interview rất thường gặp.

FirstOrDefault:

0
→ default

1+
→ lấy item đầu

SingleOrDefault:

0
→ default

1
→ item

2+
→ exception

Nếu business rule nói:

Email phải unique

thì:

users.SingleOrDefault(
    x => x.Email == email
);

có thể thể hiện invariant rõ hơn.

Nhưng trong database code cần cân nhắc generated query/performance và constraint thực tế.


18. OrderBy

var sorted = users
    .OrderBy(x => x.Name);

Mental model:

OrderBy
→ ascending

Descending:

var sorted = users
    .OrderByDescending(
        x => x.CreatedAt
    );

19. ThenBy

Nếu nhiều item có cùng primary key:

var sorted = users
    .OrderBy(x => x.LastName)
    .ThenBy(x => x.FirstName);

Mental model:

OrderBy
→ sort key chính

ThenBy
→ sort key phụ

Descending secondary:

.ThenByDescending(x => x.CreatedAt)

20. GroupBy

Ví dụ:

var groups = users
    .GroupBy(x => x.Department);

Mental model:

users
↓
group theo Department

IT
→ User A
→ User B

HR
→ User C

Finance
→ User D

Mỗi group có:

Key
+
items

Ví dụ:

foreach (var group in groups)
{
    Console.WriteLine(group.Key);

    foreach (var user in group)
    {
        Console.WriteLine(user.Name);
    }
}

21. GroupBy + Projection

Ví dụ:

var result = users
    .GroupBy(x => x.Department)
    .Select(group => new
    {
        Department = group.Key,
        Count = group.Count()
    });

Conceptually:

IT → 10
HR → 4
Finance → 6

22. SelectMany

Đây là method dễ nhầm nhưng rất quan trọng.

Giả sử:

class User
{
    public List<string> Roles { get; set; }
        = new();
}

Dữ liệu:

User A
→ Admin
→ Editor

User B
→ Viewer
→ Editor

Nếu:

var roles = users
    .Select(x => x.Roles);

kết quả conceptually:

IEnumerable<List<string>>

tức:

[
  [Admin, Editor],
  [Viewer, Editor]
]

Nếu:

var roles = users
    .SelectMany(x => x.Roles);

thì flatten:

[
  Admin,
  Editor,
  Viewer,
  Editor
]

Mental model:

Select
→ one item → one result

SelectMany
→ one item → many results
→ flatten

23. Distinct

var values = new[]
{
    1, 2, 2, 3, 3
};

var unique =
    values.Distinct();

Result:

1, 2, 3

Với custom type:

equality semantics

quyết định item có duplicate hay không.

Trong C# hiện đại còn có:

DistinctBy(x => x.Id)

rất hữu ích khi muốn distinct theo key.


24. Count

int count =
    users.Count();

Hoặc condition:

int activeCount =
    users.Count(x => x.IsActive);

Nhưng nhớ:

IEnumerable<T>.Count()

có thể phải enumerate source tùy implementation.

Không phải mọi IEnumerable<T> đều có cheap count.


25. Aggregation

LINQ có:

Sum
Min
Max
Average
Count

Ví dụ:

var total =
    orders.Sum(x => x.Total);
var max =
    orders.Max(x => x.Total);
var average =
    orders.Average(x => x.Total);

26. ToList

Ví dụ:

var query = users
    .Where(x => x.IsActive);

query thường vẫn là:

IEnumerable<User>

Khi:

var result = query.ToList();

LINQ enumerates source và tạo:

List<User>

Mental model:

ToList()
→ enumerate
→ materialize
→ snapshot kết quả vào List

27. ToArray

Tương tự:

var array = query.ToArray();

Kết quả:

User[]

28. Deferred Execution là gì?

Ví dụ:

var query = users
    .Where(x => x.IsActive);

Nhiều LINQ operator như Where và Select không nhất thiết enumerate ngay.

Nó tạo một query pipeline.

Conceptually:

users
↓
Where condition
↓
query description

Khi:

foreach (var user in query)
{
}

mới enumerate.

Hoặc:

query.ToList();

mới materialize.

Mental model:

Where / Select
→ build pipeline

foreach / ToList
→ execute enumeration

29. Ví dụ Deferred Execution

var numbers =
    new List<int>
    {
        1, 2, 3
    };

var query =
    numbers.Where(x => x > 1);

numbers.Add(4);

foreach (var number in query)
{
    Console.WriteLine(number);
}

Kết quả có thể là:

2
3
4

Vì query chưa được materialize trước khi add 4.


30. Nếu dùng ToList() sớm hơn

var numbers =
    new List<int>
    {
        1, 2, 3
    };

var result =
    numbers
        .Where(x => x > 1)
        .ToList();

numbers.Add(4);

result vẫn là:

2
3

vì đã materialize snapshot trước khi 4 được thêm.


31. Immediate vs Deferred

Mental model:

Deferred:
Where
Select
OrderBy
...

Immediate/materializing:
ToList
ToArray
Count
Any
First
Single
...

Nuance:

Một số terminal operators không tạo collection nhưng vẫn phải enumerate source để trả result.

Ví dụ:

Any()
Count()
First()

trigger enumeration.


32. Multiple Enumeration

Ví dụ:

var query = users
    .Where(x => x.IsActive);

Sau đó:

var count = query.Count();

foreach (var user in query)
{
}

Có thể sequence bị enumerate hai lần.

Conceptually:

Count()
→ enumeration 1

foreach
→ enumeration 2

Nếu source rẻ như List<T> có thể không quá nghiêm trọng.

Nhưng nếu source:

expensive iterator
network-backed abstraction
database query
heavy calculation

multiple enumeration có thể tốn chi phí.

Nếu cần dùng result nhiều lần:

var result = query.ToList();

sau đó:

result.Count
foreach (...)

33. Không nên ToList() quá sớm

Ví dụ:

var users = source
    .ToList()
    .Where(x => x.IsActive);

Nếu source có khả năng optimize query ở tầng khác, materialize quá sớm có thể làm mất optimization.

Đặc biệt với EF Core:

IQueryable<T>

phần này sẽ học kỹ hơn Day 8.

Mental note:

ToList()
→ execution boundary rất quan trọng

34. LINQ Method Syntax

Ví dụ:

var result = users
    .Where(x => x.IsActive)
    .OrderBy(x => x.Name)
    .Select(x => x.Name);

Đây gọi là method syntax.

Trong backend C# hiện đại, bạn sẽ thấy kiểu này rất nhiều.


35. Query Syntax

LINQ cũng có query syntax:

var result =
    from user in users
    where user.IsActive
    orderby user.Name
    select user.Name;

Conceptually tương đương nhiều trường hợp với method syntax.

Day 7 chỉ cần nhận diện.

Không cần ép mọi LINQ thành query syntax.


36. Common Mistake — First() khi dữ liệu có thể rỗng

var user =
    users.First(
        x => x.Id == id
    );

Nếu không có:

exception

Nếu absence là case bình thường:

FirstOrDefault()

có thể hợp lý hơn.


37. Common Mistake — SingleOrDefault() không hiểu Duplicate

users.SingleOrDefault(
    x => x.Email == email
);

Nếu hai records cùng email:

exception

Đây có thể là điều tốt nếu business invariant là email unique.

Nhưng phải dùng có chủ đích.


38. Common Mistake — Count() > 0

Nếu chỉ cần existence:

query.Any()

thể hiện intent tốt hơn:

query.Count() > 0

39. Common Mistake — Side Effect trong LINQ

Ví dụ:

users.Select(user =>
{
    user.IsActive = false;

    return user;
});

LINQ thường dễ reason hơn khi operation mang tính transformation/query.

Nếu mục đích chính là mutate side effects, foreach có thể rõ ràng hơn.

Rule:

LINQ
→ query / transformation

foreach
→ thường rõ hơn cho side-effect-heavy logic

40. Common Mistake — Không hiểu Deferred Execution

var query =
    users.Where(x => x.IsActive);

Sau đó source thay đổi:

users.Add(...);

query có thể phản ánh dữ liệu mới khi enumerate.

Nếu muốn snapshot:

var snapshot =
    users
        .Where(x => x.IsActive)
        .ToList();

41. Production Example — User Lookup

var user = users
    .FirstOrDefault(
        x => x.Id == userId
    );

Nếu:

user is null

thì xử lý not found.


42. Production Example — Projection DTO

var result = users
    .Where(x => x.IsActive)
    .Select(x => new UserDto
    {
        Id = x.Id,
        Name = x.Name
    })
    .ToList();

Mental flow:

Entity
↓
filter
↓
projection
↓
DTO List

43. Production Example — Group Statistics

var statistics = orders
    .GroupBy(x => x.Status)
    .Select(group => new
    {
        Status = group.Key,
        Count = group.Count(),
        Total = group.Sum(x => x.Total)
    })
    .ToList();

44. Production Example — Flatten Nested Data

var roleNames = users
    .SelectMany(x => x.Roles)
    .Distinct()
    .ToList();

Flow:

Users
↓
Roles của từng User
↓
SelectMany
↓
flatten
↓
Distinct
↓
unique roles

45. LINQ và Delegate

Ví dụ:

users.Where(
    x => x.IsActive
);

Lambda:

x => x.IsActive

được dùng như predicate.

Day 6:

Func<User, bool>

Day 7:

Where(predicate)

Đây là điểm nối trực tiếp giữa delegate và LINQ.


46. LINQ và Closure

Ví dụ:

var organizationId =
    currentUser.OrganizationId;

var result = users
    .Where(
        x =>
            x.OrganizationId
            == organizationId
    );

organizationId là captured variable.

Tức là LINQ + lambda có thể sử dụng closure.

Phần này nối lại Day 6.


47. Mental Model Tổng Kết

Where
→ filter

Select
→ transform

Any
→ có ít nhất một?

All
→ tất cả đều đúng?

First
→ item đầu, không có thì throw

FirstOrDefault
→ item đầu hoặc default

Single
→ chính xác một

SingleOrDefault
→ zero hoặc one

OrderBy
→ sort

ThenBy
→ secondary sort

GroupBy
→ group

SelectMany
→ flatten

Distinct
→ unique

ToList
→ enumerate + materialize

48. Deferred Execution Mental Model

var query =
    users
        .Where(...)
        .Select(...);

Mental model:

Query Pipeline
→ chưa nhất thiết chạy

Khi:

query.ToList();

hoặc:

foreach (...)

thì:

enumeration xảy ra

49. Câu Hỏi Ôn Tập

1. LINQ là gì?

Language Integrated Query, API/query syntax để xử lý dữ liệu trong C#.

2. Where làm gì?

Filter item.

3. Select làm gì?

Transform/project item.

4. Where khác Select thế nào?

Where giữ cùng item type và chọn subset.

Select có thể biến input thành output type khác.

5. Any() dùng để làm gì?

Kiểm tra có ít nhất một item.

6. All() dùng để làm gì?

Kiểm tra tất cả item đều match condition.

7. First() không có item thì sao?

Throw exception.

8. FirstOrDefault() không có item thì sao?

Return default.

9. Single() khi có hai item match thì sao?

Throw exception.

10. SingleOrDefault() khác FirstOrDefault() thế nào?

SingleOrDefault() reject duplicate match.

FirstOrDefault() lấy item đầu.

11. SelectMany để làm gì?

Flatten nested sequences.

12. GroupBy trả concept gì?

Các group, mỗi group có key và sequence item.

13. Distinct dựa vào gì?

Equality semantics / comparer.

14. ToList() làm gì?

Enumerate source và materialize result thành List<T>.

15. Deferred execution là gì?

Query chưa necessarily execute khi được define; nó execute khi được enumerate.

16. foreach có thể trigger LINQ query không?

Có.

17. Any() có thể tốt hơn Count() > 0 ở đâu?

Intent rõ hơn và có thể short-circuit.

18. Multiple enumeration là gì?

Cùng một sequence/query bị enumerate nhiều lần.

19. Vì sao multiple enumeration có thể tốn chi phí?

Vì operation/query có thể được chạy lại mỗi lần enumerate.

20. Vì sao ToList() là một boundary quan trọng?

Nó materialize query thành dữ liệu cụ thể trong memory.


50. Bài Tập Ngày 7

Giả sử:

public class User
{
    public Guid Id { get; set; }

    public string Name { get; set; } = "";

    public bool IsActive { get; set; }

    public int Age { get; set; }

    public string Department { get; set; } = "";

    public List<string> Roles { get; set; }
        = new();
}

Bài 1 — Where

Lấy tất cả user:

IsActive == true

Bài 2 — Select

Từ:

User

project thành:

Name

Bài 3 — Any

Kiểm tra:

Có user nào Age >= 18 không?

Bài 4 — FirstOrDefault

Tìm user theo:

Id

Nếu không có:

null

Bài 5 — SingleOrDefault

Giả sử email là unique.

Tìm user theo email bằng:

SingleOrDefault

Sau đó thử dữ liệu duplicate để quan sát exception.


Bài 6 — OrderBy

Sort theo:

Department ASC
Name ASC

Bài 7 — GroupBy

Group user theo:

Department

và trả:

Department
Count

Bài 8 — SelectMany

Flatten tất cả:

User.Roles

sau đó:

Distinct()

để lấy danh sách role unique.


Bài 9 — Deferred Execution

var numbers =
    new List<int>
    {
        1, 2, 3
    };

var query =
    numbers.Where(x => x > 1);

numbers.Add(4);

Sau đó enumerate query.

Giải thích vì sao 4 xuất hiện.


Bài 10 — Materialization

Lặp lại bài trên nhưng thêm:

.ToList()

trước:

numbers.Add(4);

Giải thích vì sao result khác.


51. Production Exercise

Có:

List<Order> orders;

Mỗi order có:

Id
UserId
Status
Total
CreatedAt

Hãy dùng LINQ để:

1. Chỉ lấy order Completed

2. Sort CreatedAt DESC

3. Group theo UserId

4. Tính:
   - Count
   - TotalAmount

5. Project thành report DTO

6. Materialize bằng ToList()

Mental flow:

Orders
↓
Where
↓
OrderByDescending
↓
GroupBy
↓
Select
↓
ToList

Key Takeaways

LINQ
→ query + transformation abstraction

Where
→ filter

Select
→ transform

Any / All
→ boolean questions

First / Single
→ cardinality semantics

GroupBy
→ grouping

SelectMany
→ flatten

ToList
→ materialization boundary

Và phần quan trọng nhất của Day 7:

LINQ query
≠
luôn execute ngay

Hãy luôn hỏi:

Query đang được build?
hay
Data đã được materialize?

Đây là nền tảng để sang Day 8:

IEnumerable<T>
vs
IQueryable<T>

LINQ to Objects
vs
LINQ provider

Expression<Func<T, bool>>

EF Core SQL translation

Deferred execution sâu hơn