Chuyển tới nội dung chính

LINQ

Định nghĩa (Definition)

LINQ (Language Integrated Query — Truy vấn tích hợp ngôn ngữ) là một tập hợp các tính năng ngôn ngữ và API cho phép bạn truy vấn các nguồn dữ liệu (collection, cơ sở dữ liệu, XML, v.v.) sử dụng cú pháp thống nhất, an toàn kiểu trực tiếp trong C#. Nó thu hẹp khoảng cách giữa thế giới hướng đối tượng và thế giới dữ liệu bằng cách coi các truy vấn là công dân hạng nhất (First-class Citizens) trong ngôn ngữ.

Tại sao LINQ tồn tại

Vấn đề không có LINQGiải pháp với LINQ
API khác nhau cho mỗi nguồn dữ liệu (SQL, XML, collection)Một cú pháp truy vấn thống nhất cho tất cả nguồn
Vòng lặp và điều kiện thủ công dài dòng và dễ lỗiTruy vấn khai báo (Declarative), dễ đọc
Sai lệch kiểu phát hiện tại runtimeKiểm tra kiểu tại thời điểm biên dịch
Truy vấn cơ sở dữ liệu xây dựng bằng nối chuỗi (nguy cơ SQL injection)LINQ to Entities tạo SQL được tham số hóa
// Không có LINQ — dài dòng, mệnh lệnh (Imperative)
List<string> result = new List<string>();
foreach (var student in students)
{
if (student.Age > 18 && student.Grade > 80)
{
result.Add(student.Name.ToUpper());
}
}
result.Sort();

// Với LINQ — khai báo (Declarative), ngắn gọn
var result = students
.Where(s => s.Age > 18 && s.Grade > 80)
.Select(s => s.Name.ToUpper())
.OrderBy(n => n)
.ToList();
Lợi ích chính
  • Cú pháp thống nhất trên tất cả các nguồn dữ liệu
  • Kiểm tra tại thời điểm biên dịch cho truy vấn và kiểu
  • Phong cách khai báo (Declarative Style) — mô tả cái gì bạn muốn, không phải như thế nào để lấy
  • Khả năng kết hợp (Composable) — chuỗi các thao tác trôi chảy
  • Dễ đọc (Readable) — ý định rõ ràng từ chính truy vấn

Khái niệm cốt lõi (Core Concepts)

LINQ hoạt động bên dưới như thế nào

Extension Method

Tất cả các toán tử LINQ là extension method được định nghĩa trên IEnumerable<T>IQueryable<T> trong namespace System.Linq. Đây là lý do tại sao chúng xuất hiện dưới dạng instance method trên bất kỳ collection nào — trình biên dịch phân giải source.Where(...) thành Enumerable.Where(source, ...).

// Những gì bạn viết
var result = students.Where(s => s.Grade > 90);

// Những gì trình biên dịch tạo ra
var result = Enumerable.Where(students, s => s.Grade > 90);

Extension method không thể truy cập private members của kiểu chúng mở rộng, và chúng không sửa đổi kiểu gốc. Chúng hoàn toàn là syntactic sugar (cú pháp đường) mà trình biên dịch phân giải tại thời điểm biên dịch.

Dịch cú pháp truy vấn (Query Syntax Translation)

Trình biên dịch C# dịch cú pháp truy vấn thành cú pháp phương thức trước khi tạo IL. Mỗi từ khóa truy vấn ánh xạ đến một phương thức tương ứng:

Từ khóa truy vấnPhương thức tương đương
where.Where()
select.Select()
orderby.OrderBy() / .OrderByDescending()
join ... on ... equals.Join()
join ... into.GroupJoin()
group ... by.GroupBy()
let.Select() với transparent identifier
// Cú pháp truy vấn
var query = from s in students
let average = s.Grade
where average > 90
orderby average descending
select new { s.Name, average };

// Trình biên dịch dịch thành (xấp xỉ)
var query = students
.Select(s => new { s, average = s.Grade })
.Where(x => x.average > 90)
.OrderByDescending(x => x.average)
.Select(x => new { x.s.Name, x.average });

Cú pháp truy vấn được loại bỏ hoàn toàn trong quá trình biên dịch — chỉ còn lại các lệnh gọi phương thức trong IL.

Pattern Iterator và Đánh giá lười (Iterator Pattern and Lazy Evaluation)

Thực thi trì hoãn (Deferred Execution) hoạt động vì các toán tử LINQ trả về iterator được xây dựng bằng yield return. Khi bạn gọi Where(), không có lọc nào xảy ra — nó trả về một object iterator sẽ lọc các phần tử khi bạn liệt kê nó.

// Triển khai đơn giản hóa của Where
public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (T item in source)
{
if (predicate(item))
{
yield return item; // lười — trả về một phần tử tại một thời điểm
}
}
}

Mỗi toán tử LINQ bao bọc cái trước đó, tạo thành chuỗi iterator. Khi bạn liệt kê kết quả cuối cùng, mỗi phần tử đi qua toàn bộ chuỗi một lần — dựa trên kéo (Pull-based), không dựa trên đẩy (Push-based).

Điều này có nghĩa là:

  • Không có collection trung gian được tạo — các phần tử đi qua từng cái một
  • Hiệu quả bộ nhớ — bạn không bao giờ giữ toàn bộ tập kết quả trong bộ nhớ (trừ khi bạn gọi ToList())
  • Đánh giá lại trên mỗi lần liệt kê — chuỗi chạy lại từ đầu mỗi lần

Chuỗi và Kết hợp (Chaining and Composition)

Vì mỗi toán tử trả về IEnumerable<T>, chúng kết hợp tự nhiên. Đầu ra của cái này trở thành đầu vào của cái tiếp theo, tạo thành một pipeline.

// Điều này tạo một pipeline, không phải một loạt collection
var pipeline = students // IEnumerable<Student>
.Where(Filter) // IEnumerable<Student> (trì hoãn)
.OrderBy(Sort) // IOrderedEnumerable<Student> (trì hoãn)
.Select(Project) // IEnumerable<string> (trì hoãn)
.Take(10); // IEnumerable<string> (trì hoãn)

// Không gì thực thi cho đến khi liệt kê
foreach (var name in pipeline) // BÂY GIỜ pipeline chạy từ đầu đến cuối
{
Console.WriteLine(name);
}

Cú pháp truy vấn vs Cú pháp phương thức (Query Syntax vs Method Syntax)

LINQ cung cấp hai cú pháp tương đương. Cú pháp phương thức (Fluent API) phổ biến hơn trong thực tế, nhưng cú pháp truy vấn có thể dễ đọc hơn cho các join và group phức tạp.

// Cú pháp truy vấn
var honorRoll = from s in students
where s.Grade >= 90
orderby s.Name
select new { s.Name, s.Grade };

// Cú pháp phương thức (tương đương)
var honorRoll = students
.Where(s => s.Grade >= 90)
.OrderBy(s => s.Name)
.Select(s => new { s.Name, s.Grade });
Nên sử dụng cái nào?
  • Sử dụng cú pháp phương thức cho hầu hết các truy vấn — đây là phong cách phổ biến trong các codebase .NET.
  • Sử dụng cú pháp truy vấn khi bạn cần let, join, hoặc group ... into — những cái này không có phương thức tương đương trực tiếp và yêu cầu biến trung gian nếu không.

Thực thi trì hoãn vs Thực thi ngay lập tức (Deferred vs Immediate Execution)

Một trong những khái niệm quan trọng nhất trong LINQ là hiểu khi nào một truy vấn thực sự chạy. Các toán tử LINQ được chia thành hai loại dựa trên thời điểm thực thi.

Thực thi trì hoãn (Deferred Execution) có nghĩa là truy vấn không chạy khi bạn định nghĩa nó — nó chỉ chạy khi bạn liệt kê kết quả (ví dụ: với foreach). Các toán tử đơn giản xây dựng một pipeline; không có gì xảy ra cho đến khi bạn tiêu thụ đầu ra. Điều này mạnh mẽ vì bạn có thể kết hợp các truy vấn phức tạp mà không có chi phí cho đến khi bạn thực sự cần dữ liệu.

Thực thi ngay lập tức (Immediate Execution) có nghĩa là truy vấn chạy ngay tại điểm bạn định nghĩa nó. Các toán tử như ToList(), Count(), First(), và Any() ép buộc toàn bộ pipeline thực thi vì chúng cần một kết quả cụ thể — một danh sách, một số, hoặc một phần tử duy nhất.

Thực thi trì hoãn (Deferred Execution)

Hầu hết các toán tử LINQ (Where, Select, OrderBy, SelectMany, v.v.) là lười (Lazy) — chúng không thực thi cho đến khi bạn liệt kê kết quả.

var query = students.Where(s => s.Grade > 90); // Chưa chạy gì

// Thực thi khi liệt kê
foreach (var student in query) // Truy vấn chạy BÂY GIỜ
{
Console.WriteLine(student.Name);
}

// Thựcsci LẠI trên lần liệt kê thứ hai
foreach (var student in query) // Truy vấn chạy LẠI
{
Console.WriteLine(student.Name);
}

Thực thi ngay lập tức (Immediate Execution)

Các toán tử trả về một giá trị duy nhất hoặc collection cụ thể thực thi ngay lập tức.

// Ngay lập tức — trả về List<T> cụ thể
var list = students.Where(s => s.Grade > 90).ToList();

// Ngay lập tức — trả về một giá trị duy nhất
int count = students.Count(s => s.Grade > 90);
Student first = students.First(s => s.Grade > 90);
bool any = students.Any(s => s.Grade > 90);
int max = students.Max(s => s.Grade);

// Ngay lập tức — tích lũy (Aggregate)
double average = students.Average(s => s.Grade);
Lỗi thường gặp

Liệt kê một truy vấn trì hoãn nhiều lần thực thi lại toàn bộ pipeline. Nếu nguồn là cơ sở dữ liệu, điều này có nghĩa là nhiều round trip. Cache kết quả bằng ToList() hoặc ToArray() nếu bạn cần lặp lại nhiều hơn một lần.

Kiến trúc và Nhà cung cấp (Architecture and Providers)

Mô hình Nhà cung cấp LINQ (LINQ Provider Model)

LINQ không phải là một công nghệ đơn lẻ — nó là một pattern với nhiều nhà cung cấp (Provider) triển khai cùng interface cho các nguồn dữ liệu khác nhau.

ProviderInterfaceNguồn dữ liệuCách hoạt động
LINQ to ObjectsIEnumerable<T>Collection trong bộ nhớExtension method với delegate
LINQ to EntitiesIQueryable<T>Cơ sở dữ liệu (EF Core)Expression tree sang SQL
LINQ to XMLIEnumerable<XElement>Tài liệu XMLDuyệt cây XElement
PLINQParallelQuery<T>Trong bộ nhớ (song song)Phân chia công việc qua các luồng
Tùy chỉnh (Custom)IQueryable<T>Bất kỳ nguồn nàoTriển khai IQueryProvider

Cách các Provider dịch Truy vấn

Một IQueryable<T> có một IQueryProvider liên kết quyết định cách thực thi truy vấn:

  1. Xây dựng (Build) — mỗi lệnh gọi .Where(), .Select() bao bọc biểu thức vào một expression tree lớn hơn
  2. Dịch (Translate) — khi liệt kê bắt đầu, provider duyệt expression tree và dịch nó (ví dụ: sang SQL)
  3. Thực thi (Execute) — provider thực thi truy vấn đã dịch trên nguồn dữ liệu
  4. Vật chất hóa (Materialize) — kết quả được chuyển đổi trở lại thành object
// IQueryable xây dựng một expression tree
IQueryable<Student> query = db.Students
.Where(s => s.Grade > 90)
.OrderBy(s => s.Name)
.Select(s => s);

// Provider (EF Core) dịch expression tree thành:
// SELECT * FROM Students WHERE Grade > 90 ORDER BY Name

IEnumerable vs IQueryable

IEnumerable<T> là interface tiêu chuẩn cho collection trong bộ nhớ. Khi bạn gọi các toán tử LINQ trên nó, trình biên dịch sử dụng static class Enumerable — mỗi toán tử nhận một delegate (Func<T, bool>) và thực thi lambda trực tiếp trong bộ nhớ. Toàn bộ pipeline chạy dưới dạng code .NET.

IQueryable<T> mở rộng IEnumerable<T> nhưng thêm một expression tree và một query provider. Khi bạn gọi các toán tử LINQ trên nó, trình biên dịch sử dụng static class Queryable — mỗi toán tử nhận một expression (Expression<Func<T, bool>>) và nối nó vào expression tree. Provider (ví dụ: EF Core) sau đó duyệt cây này và dịch nó sang SQL hoặc ngôn ngữ truy vấn khác.

Khi nào sử dụng cái nào:

  • Sử dụng IEnumerable<T> khi làm việc với dữ liệu trong bộ nhớ (Lists, Arrays, v.v.) — tất cả truy vấn LINQ to Objects.
  • Sử dụng IQueryable<T> khi làm việc với nguồn dữ liệu từ xa (cơ sở dữ liệu, API) — bạn muốn server thực hiện lọc/sắp xếp, không phải ứng dụng của bạn.
  • Chuyển từ IQueryable sang IEnumerable bằng .AsEnumerable() khi bạn cần chạy code C# không thể dịch sang SQL.
// IQueryable<T> — khả năng kết hợp, dịch sang SQL
IQueryable<Student> query = db.Students.Where(s => s.Grade > 90);
query = query.OrderBy(s => s.Name); // Thêm vào SQL expression tree
var result = query.ToList(); // Một truy vấn SQL duy nhất

// IEnumerable<T> — trong bộ nhớ, sử dụng delegate
IEnumerable<Student> items = db.Students.Where(s => s.Grade > 90).AsEnumerable();
items = items.OrderBy(s => s.Name); // Sắp xếp trong bộ nhớ — tất cả dòng đã được tải
var result = items.ToList();

Expression Tree (Cây biểu thức)

Expression tree là cấu trúc dữ liệu biểu diễn code dưới dạng cây các node, nơi mỗi node là một thao tác (lệnh gọi phương thức, toán tử nhị phân, truy cập thuộc tính, v.v.). Khác với delegate (là code biên dịch có thể thực thi), expression tree có thể được kiểm tra, phân tích và dịch tại runtime.

Đây là cơ chế chính khiến LINQ to Entities hoạt động: khi bạn viết s => s.Grade > 90 trong ngữ cảnh IQueryable, trình biên dịch không biên dịch nó thành IL thực thi — thay vào đó, nó xây dựng một cây nói "tham số s, truy cập thuộc tính Grade, so sánh với 90 bằng lớn hơn." Provider EF Core sau đó có thể duyệt cây này và phát ra WHERE Grade > 90 trong SQL.

Expression tree cũng được sử dụng trong truy vấn động (Dynamic Querying), rule engine, và xây dựng API phân tích bộ lọc do người dùng định nghĩa tại runtime.

// Func<T> — delegate thực thi được (LINQ to Objects)
Func<Student, bool> predicate = s => s.Grade > 90;

// Expression<T> — cấu trúc dữ liệu biểu diễn code (LINQ to Entities)
Expression<Func<Student, bool>> predicate = s => s.Grade > 90;

// Expression tree có thể được kiểm tra, sửa đổi và dịch sang SQL
string body = predicate.Body.ToString(); // "s.Grade > 90"
mẹo

IQueryable<T>.Where() chấp nhận Expression<Func<T, bool>>, trong khi IEnumerable<T>.Where() chấp nhận Func<T, bool>. Trình biên dịch tự động bao lambda trong expression khi đích là IQueryable.

LINQ to Objects vs LINQ to Entities

Khía cạnhLINQ to ObjectsLINQ to Entities (EF Core)
NguồnIEnumerable<T> trong bộ nhớIQueryable<T> ánh xạ đến cơ sở dữ liệu
Thực thiDelegate trong bộ nhớDịch sang SQL và thực thi trên cơ sở dữ liệu
PredicateBất kỳ code C# nàoChỉ expression có thể dịch sang SQL
Hiệu suấtTốc độ trong bộ nhớPhụ thuộc vào SQL được tạo và index
// LINQ to Objects — hoạt động với bất kỳ phương thức C# nào
var result = students.Where(s => CustomFilter(s)).ToList();

// LINQ to Entities — phải có thể dịch sang SQL
var result = db.Students
.Where(s => s.Grade > 90) // OK — dịch sang SQL WHERE
.Where(s => s.Name.StartsWith("A")) // OK — dịch sang LIKE 'A%'
.Where(s => CustomFilter(s)) // LỖI — phương thức client-side không thể dịch
.ToList();
Đánh giá Phía Client (Client-Side Evaluation)

EF Core có thể đánh giá một phần truy vấn trên client nếu chúng không thể dịch sang SQL. Điều này có thể gây tải toàn bộ bảng vào bộ nhớ. Theo dõi cảnh báo trong console và sử dụng .ToList() tường minh tại đúng điểm để kiểm soát nơi ranh giới SQL nằm.

PLINQ (Parallel LINQ)

PLINQ là triển khai song song của LINQ to Objects phân phối công việc qua nhiều core CPU. Khi bạn thêm .AsParallel() vào truy vấn, collection nguồn được phân chia thành các khối, mỗi khối được xử lý trên luồng riêng biệt, và kết quả được gộp lại.

PLINQ hữu ích khi bạn có collection lớn và mỗi phần tử yêu cầu công việc CPU tốn kém (ví dụ: tính toán phức tạp, xử lý hình ảnh, mã hóa). Đối với collection nhỏ hoặc thao tác rẻ, PLINQ thực tế có thể chậm hơn do overhead phân chia, phối hợp luồng và gộp kết quả.

// Chuyển sang thực thi song song
var result = students
.AsParallel()
.Where(s => ExpensiveFilter(s))
.Select(s => Transform(s))
.ToList();

// Với mức độ song song
var result = students
.AsParallel()
.WithDegreeOfParallelism(4)
.Where(s => ExpensiveFilter(s));

// Giữ thứ tự (có chi phí hiệu suất)
var result = students
.AsParallel()
.AsOrdered()
.Select(s => Transform(s));

// Ép tuần tự cho các phần không hưởng lợi từ song song
var result = students
.AsParallel()
.Where(s => ExpensiveFilter(s))
.AsSequential()
.Take(10);
Lưu ý PLINQ
  • PLINQ hữu ích cho thao tác CPU-bound, không phải I/O-bound (sử dụng Task.WhenAll thay thế).
  • Có overhead trong phân chia, gộp và phối hợp luồng — collection nhỏ hoặc nhanh có thể chạy chậm hơn với PLINQ.
  • An toàn luồng là trách nhiệm của bạn — thân lambda phải an toàn cho thực thi đồng thời.
  • Thứ tự không được bảo toàn theo mặc định — sử dụng .AsOrdered() nếu bạn cần.

Toán tử truy vấn tiêu chuẩn (Standard Query Operators)

Lọc (Filtering)

// Where — lọc theo predicate
var adults = people.Where(p => p.Age >= 18);

// OfType — lọc theo kiểu
var errors = mixedList.OfType<Exception>();

Chiếu (Projection)

// Select — biến đổi mỗi phần tử
var names = students.Select(s => s.Name);

// SelectMany — làm phẳng collection lồng nhau
var allCourses = students.SelectMany(s => s.Courses);

// Chiếu kiểu ẩn danh (Anonymous Type Projection)
var summary = students.Select(s => new { s.Name, s.Grade, Status = s.Grade >= 60 ? "Pass" : "Fail" });

Sắp xếp (Ordering)

var sorted = students.OrderBy(s => s.Grade); // tăng dần
var sorted = students.OrderByDescending(s => s.Grade); // giảm dần
var sorted = students
.OrderBy(s => s.LastName)
.ThenBy(s => s.FirstName); // đa cấp

// Đảo ngược
var reversed = students.Reverse();

Tích lũy (Aggregation)

int count = students.Count();
int count = students.Count(s => s.Grade > 90);
int sum = orders.Sum(o => o.Total);
double avg = students.Average(s => s.Grade);
int min = students.Min(s => s.Grade);
int max = students.Max(s => s.Grade);

// Aggregate — tích lũy tùy chỉnh
string combined = words.Aggregate((a, b) => a + ", " + b);

// Aggregate với seed
int totalLength = words.Aggregate(0, (sum, word) => sum + word.Length);

Nhóm (Grouping)

// Nhóm theo thuộc tính
var byGrade = students.GroupBy(s => s.Grade >= 90 ? "A" : "Other");

foreach (var group in byGrade)
{
Console.WriteLine($"Group: {group.Key}, Count: {group.Count()}");
}

// GroupBy với chiếu
var byDept = employees
.GroupBy(e => e.Department, e => e.Name);

// Nhóm với cú pháp truy vấn
var grouped = from s in students
group s by s.Major into g
select new { Major = g.Key, Count = g.Count(), AvgGrade = g.Average(s => s.Grade) };

Kết nối (Joining)

// Inner join (cú pháp phương thức)
var result = students.Join(
enrollments,
s => s.Id,
e => e.StudentId,
(s, e) => new { s.Name, e.CourseId });

// Inner join (cú pháp truy vấn)
var result = from s in students
join e in enrollments on s.Id equals e.StudentId
select new { s.Name, e.CourseId };

// Group join — tạo kết quả phân cấp
var result = students.GroupJoin(
enrollments,
s => s.Id,
e => e.StudentId,
(s, es) => new { Student = s, Enrollments = es });

// Left outer join
var result = from s in students
join e in enrollments on s.Id equals e.StudentId into se
from e in se.DefaultIfEmpty()
select new { s.Name, CourseId = e?.CourseId };

Phép toán tập hợp (Set Operations)

var a = new[] { 1, 2, 3, 4 };
var b = new[] { 3, 4, 5, 6 };

a.Union(b); // { 1, 2, 3, 4, 5, 6 }
a.Concat(b); // { 1, 2, 3, 4, 3, 4, 5, 6 }
a.Intersect(b); // { 3, 4 }
a.Except(b); // { 1, 2 }
a.Distinct(); // loại bỏ trùng lặp

Toán tử phần tử (Element Operators)

Student first = students.First(); // ném nếu rỗng
Student? first = students.FirstOrDefault(); // trả về default nếu rỗng
Student last = students.Last(s => s.Grade > 90);
Student single = students.Single(s => s.Id == 42); // ném nếu 0 hoặc 2+ khớp
Student? single = students.SingleOrDefault(s => s.Id == 42);

Toán tử lượng hóa (Quantifiers)

bool anyHigh = students.Any(s => s.Grade > 90); // ít nhất một khớp
bool allPassed = students.All(s => s.Grade >= 60); // mọi phần tử khớp
bool contains = grades.Contains(95); // phần tử cụ thể tồn tại

Phân trang (Partitioning)

var top10 = students.OrderByDescending(s => s.Grade).Take(10);
var skip5 = students.Skip(5);
var page2 = students.Skip(20).Take(10); // phân trang

// TakeWhile / SkipWhile
var good = grades.TakeWhile(g => g >= 60); // lấy cho đến khi điều kiện sai
var rest = grades.SkipWhile(g => g >= 60); // bỏ qua cho đến khi điều kiện sai

Tạo (Generation)

// DefaultIfEmpty — cung cấp phần tử mặc định cho chuỗi rỗng
var result = students.Where(s => s.Grade > 100).DefaultIfEmpty();

// Range (phương thức tĩnh)
var numbers = Enumerable.Range(1, 10); // { 1, 2, 3, ..., 10 }

// Repeat
var zeros = Enumerable.Repeat(0, 5); // { 0, 0, 0, 0, 0 }

// Empty
var empty = Enumerable.Empty<Student>();

Cân nhắc hiệu suất (Performance Considerations)

Tránh liệt kê nhiều lần (Avoid Multiple Enumeration)

// Sai — truy vấn thực thi hai lần
var query = GetExpensiveQuery();
var count = query.Count(); // Thực thi lần một
var list = query.ToList(); // Thựcsci LẠI

// Đúng — vật chất hóa một lần
var list = GetExpensiveQuery().ToList();
var count = list.Count;

Sử dụng đúng toán tử (Use the Right Operator)

// Sai — lặp toàn bộ collection để kiểm tra tồn tại
bool exists = items.Where(x => x.Id == target).Any();

// Đúng — dừng tại kết quả khớp đầu tiên
bool exists = items.Any(x => x.Id == target);

Cẩn thận với truy cập theo chỉ số (Be Careful with Index-Based Access)

// Sai — O(n) cho mỗi lần truy cập với IEnumerable
var first = items.ElementAt(0); // lặp từ đầu

// Đúng — sử dụng IList<T> hoặc List<T> cho truy cập theo chỉ số
List<T> list = items.ToList();
var first = list[0]; // O(1)

Tối ưu kiểm tra Count (Optimize Count Checks)

// Sai — lặp toàn bộ collection
bool hasItems = items.Count() > 0;

// Đúng — dừng ngay lập tức
bool hasItems = items.Any();

Tối ưu truy vấn cho EF Core (Query Optimization for EF Core)

// Sai — vấn đề N+1
var orders = db.Orders.ToList(); // tải TẤT CẢ đơn hàng
foreach (var order in orders)
{
var items = db.Items.Where(i => i.OrderId == order.Id).ToList(); // truy vấn mỗi đơn hàng
}

// Đúng — eager loading với Include
var orders = db.Orders.Include(o => o.Items).ToList(); // một truy vấn với JOIN

// Đúng — chiếu chỉ những gì bạn cần
var orderSummaries = db.Orders
.Where(o => o.Date >= startDate)
.Select(o => new { o.Id, o.Total, ItemCount = o.Items.Count })
.ToList();

Khi nào sử dụng

  • Biến đổi dữ liệu trong bộ nhớ — lọc, sắp xếp, chiếu collection mọi kích thước
  • Truy vấn cơ sở dữ liệu — LINQ EF Core dịch sang SQL, cho truy cập cơ sở dữ liệu an toàn kiểu
  • Tổng hợp dữ liệu — đếm, tính tổng, tính trung bình, nhóm trên các tập dữ liệu
  • Join phức tạp — kết hợp nhiều nguồn dữ liệu với inner/left outer/group join
  • Phân trangSkip + Take cho bất kỳ nguồn dữ liệu nào
  • Phép toán tập hợp — union, intersection, difference giữa các collection
  • Tránh khi một vòng lặp foreach đơn giản rõ ràng hơn cho các thao tác một bước đơn giản không có biến đổi

Lỗi thường gặp (Common Pitfalls)

Sửa đổi Collection trong khi liệt kê (Modifying Collections During Enumeration)

// Sai — ném InvalidOperationException
foreach (var item in list)
{
if (item.ShouldBeRemoved)
list.Remove(item); // Không thể sửa đổi collection trong khi liệt kê
}

// Đúng — vật chất hóa các mục cần xóa
var toRemove = list.Where(x => x.ShouldBeRemoved).ToList();
toRemove.ForEach(x => list.Remove(x));

// Đúng — tạo danh sách đã lọc mới
var filtered = list.Where(x => !x.ShouldBeRemoved).ToList();

Bắt biến vòng lặp (Capturing Loop Variables)

// Sai — tất cả lambda bắt CÙNG một biến
var actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i)); // bắt biến 'i', không phải giá trị của nó
}
actions.ForEach(a => a()); // in 5, 5, 5, 5, 5

// Đúng — sao chép sang biến cục bộ
for (int i = 0; i < 5; i++)
{
int copy = i;
actions.Add(() => Console.WriteLine(copy));
}

Kết quả Null bất ngờ (Unexpected Null Results)

// FirstOrDefault có thể trả về null — luôn xử lý nó
var student = students.FirstOrDefault(s => s.Id == 999);
Console.WriteLine(student.Name); // NullReferenceException nếu không tìm thấy

// Cách tiếp cận an toàn
var student = students.FirstOrDefault(s => s.Id == 999);
if (student is not null)
{
Console.WriteLine(student.Name);
}

// Hoặc sử dụng pattern matching
if (students.FirstOrDefault(s => s.Id == 999) is { Name: var name })
{
Console.WriteLine(name);
}

Chuỗi sau khi vật chất hóa (Chaining After Materialization)

// Gây nhầm — AsEnumerable() chuyển sang client-side, rồi ToList() là thừa
var result = db.Students
.Where(s => s.Grade > 90)
.AsEnumerable()
.Where(s => CustomFilter(s)) // lọc client-side là ổn
.ToList();

// Thứ tự sai — lọc trong bộ nhớ thay vì cơ sở dữ liệu
var result = db.Students
.ToList() // tải TOÀN BỘ bảng vào bộ nhớ
.Where(s => s.Grade > 90); // lọc trong bộ nhớ

Điểm chính (Key Takeaways)

  • LINQ cung cấp cú pháp truy vấn thống nhất, khai báo hoạt động trên collection trong bộ nhớ, cơ sở dữ liệu, XML, v.v.
  • Tất cả toán tử LINQ là extension method — trình biên dịch dịch cú pháp truy vấn thành lệnh gọi phương thức.
  • Thực thi trì hoãn hoạt động qua chuỗi iterator (yield return) — các phần tử đi qua từng cái một qua pipeline mà không có collection trung gian.
  • Hiểu thực thi trì hoãn vs ngay lập tức — truy vấn trì hoãn đánh giá lại trên mỗi lần liệt kê.
  • Sử dụng cú pháp phương thức cho hầu hết truy vấn; sử dụng cú pháp truy vấn cho join và group phức tạp.
  • Biết sự khác biệt giữa IEnumerable<T> (trong bộ nhớ, delegate) và IQueryable<T> (có thể dịch, expression tree).
  • Nhà cung cấp LINQ (LINQ Providers) triển khai cùng interface cho các nguồn dữ liệu khác nhau — cùng một truy vấn có thể nhắm vào collection trong bộ nhớ, cơ sở dữ liệu, hoặc XML.
  • Tránh vấn đề truy vấn N+1 trong EF Core bằng cách sử dụng .Include() hoặc chiếu (Projection).
  • Sử dụng .Any() thay vì .Count() > 0.ToList() để tránh liệt kê nhiều lần.

Câu hỏi phỏng vấn (Interview Questions)

Q: Sự khác biệt giữa thực thi trì hoãn và thực thi ngay lập tức trong LINQ là gì? Thực thi trì hoãn (Deferred Execution) có nghĩa là truy vấn không được đánh giá cho đến khi bạn liệt kê kết quả (ví dụ: Where, Select, OrderBy). Thực thi ngay lập tức (Immediate Execution) có nghĩa là truy vấn chạy ngay lập tức (ví dụ: ToList, Count, First, Any). Truy vấn trì hoãn thực thi lại mỗi lần chúng được liệt kê.

Q: Sự khác biệt giữa IEnumerable<T>IQueryable<T> là gì? IEnumerable<T> thực thi truy vấn trong bộ nhớ sử dụng delegate (LINQ to Objects). IQueryable<T> biểu diễn truy vấn dưới dạng expression tree mà provider (như EF Core) có thể dịch sang SQL và thực thi trên cơ sở dữ liệu.

Q: Expression tree là gì và tại sao chúng quan trọng trong LINQ? Expression tree là cấu trúc dữ liệu biểu diễn code dưới dạng object có thể duyệt. Trong LINQ, chúng cho phép provider IQueryable kiểm tra lambda expression và dịch chúng sang ngôn ngữ khác như SQL, thay vì thực thi trực tiếp. IEnumerable sử dụng Func<T> (delegate thực thi được), trong khi IQueryable sử dụng Expression<Func<T>> (cây có thể kiểm tra).

Q: Vấn đề truy vấn N+1 là gì và cách giải quyết? Vấn đề N+1 xảy ra khi bạn tải danh sách entity (1 truy vấn) và sau đó tải lười (Lazy Load) entity liên quan cho mỗi cái (N truy vấn). Giải quyết bằng cách sử dụng .Include() cho eager loading, chiếu với .Select(), hoặc chia tách truy vấn với .AsSplitQuery().

Q: Sự khác biệt giữa First()SingleOrDefault() là gì? First() trả về phần tử khớp đầu tiên và ném nếu chuỗi rỗng. SingleOrDefault() trả về phần tử khớp duy nhất, default nếu không có cái nào khớp, và ném nếu nhiều hơn một khớp. Sử dụng FirstOrDefault() khi bạn mong đợi 0 hoặc 1 kết quả và muốn xử lý sự thiếu vắng một cách linh hoạt.

Q: Sự khác biệt giữa SelectSelectMany là gì? Select ánh xạ mỗi phần tử sang giá trị mới (1-đến-1). SelectMany làm phẳng collection lồng nhau — nó ánh xạ mỗi phần tử sang một chuỗi và sau đó làm phẳng tất cả các chuỗi thành một (1-đến-nhiều).

Q: Thực thi trì hoãn hoạt động bên trong như thế nào? Các toán tử LINQ trả về iterator được xây dựng bằng yield return. Mỗi lệnh gọi bao bọc iterator trước đó, tạo thành một chuỗi. Không có code chạy cho đến khi bạn liệt kê — sau đó mỗi phần tử được kéo qua chuỗi từng cái một. Điều này có nghĩa là không có collection trung gian và streaming hiệu quả bộ nhớ, nhưng đánh giá lại trên mỗi lần liệt kê.

Q: PLINQ là gì và khi nào nên sử dụng? PLINQ (AsParallel()) song song hóa truy vấn LINQ qua nhiều core CPU. Sử dụng cho thao tác CPU-bound trên collection lớn trong bộ nhớ nơi công việc mỗi phần tử tốn kém. Tránh cho công việc I/O-bound (sử dụng Task.WhenAll) và collection nhỏ nơi overhead vượt quá lợi ích.

Tài liệu tham khảo (References)