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?WherevàSelectkhác nhau thế nào?AnyvàAlldùng khi nào?First,FirstOrDefault,Single,SingleOrDefaultkhác nhau ra sao?OrderBy,ThenByGroupBySelectMany- aggregation
DistinctToList()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