5.10 — Mở rộng và đào sâu
Những chủ đề C# nâng cao nối tiếp Module 5, kèm điều kiện cụ thể. Hai mục đáng dùng ngay: IAsyncEnumerable (stream dữ liệu mà không nạp hết vào bộ nhớ — giải quyết đúng lớp bug làm sập container khi export dữ liệu lớn) và source generator có sẵn của System.Text.Json (bỏ reflection khi serialize, chỉ cần thêm một lớp). Hai mục còn lại — expression tree và Span — bạn sẽ dùng gián tiếp hàng ngày mà không tự viết: expression tree là thứ khiến _db.Leads.Where(l => ...) dịch được thành SQL, còn Span nằm bên dưới hầu hết API xử lý chuỗi hiện đại.
Mục tiêu bài học
Sau bài này bạn có thể:
- Dùng
IAsyncEnumerableđể stream dữ liệu lớn. - Bật source generator cho JSON serialization.
- Hiểu expression tree và vì sao EF Core cần nó.
- Biết covariance và contravariance giải quyết vấn đề gì.
Nội dung bài học
5.10.1 — IAsyncEnumerable: dùng ngay
Điều kiện kích hoạt: xử lý tập dữ liệu có thể rất lớn.
// Nạp HẾT vào bộ nhớ — 200.000 bản ghi = OOM
public async Task<List<Lead>> GetAllAsync(CancellationToken ct)
=> await _db.Leads.ToListAsync(ct);
// Stream từng bản ghi — bộ nhớ là HẰNG SỐ
public async IAsyncEnumerable<Lead> GetAllAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var lead in _db.Leads.AsAsyncEnumerable().WithCancellation(ct))
yield return lead;
}
// Sử dụng
await foreach (var lead in _service.GetAllAsync(ct))
{
await ProcessAsync(lead, ct);
}
Thuộc tính [EnumeratorCancellation] là chi tiết dễ quên nhưng quan trọng: không có nó, CancellationToken truyền qua WithCancellation không tới được phần thân phương thức, nên việc huỷ không có tác dụng.
Đây là công cụ giải quyết đúng lớp bug làm container bị OOM kill khi export dữ liệu (bài 19.13).
Trong ASP.NET Core, trả về IAsyncEnumerable trực tiếp từ controller cũng stream được kết quả:
[HttpGet("stream")]
public IAsyncEnumerable<LeadDto> Stream(CancellationToken ct)
=> _db.Leads.AsNoTracking().Select(l => new LeadDto(l.Id, l.Name)).AsAsyncEnumerable();
5.10.2 — Source generator: dùng cái có sẵn
Điều kiện kích hoạt: serialize JSON trong đường xử lý nóng, hoặc cần tương thích Native AOT.
Bạn không cần viết source generator, nhưng nên dùng cái có sẵn:
[JsonSerializable(typeof(LeadDto))]
[JsonSerializable(typeof(List<LeadDto>))]
internal partial class AppJsonContext : JsonSerializerContext { }
// Dung
var json = JsonSerializer.Serialize(lead, AppJsonContext.Default.LeadDto);
Trình biên dịch sinh ra code serialize lúc biên dịch thay vì dùng reflection lúc chạy. Lợi ích: nhanh hơn, cấp phát ít hơn, và chạy được dưới Native AOT (bài 19.11).
Đăng ký cho toàn bộ ứng dụng:
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default));
Source generator khác biệt với reflection ở một điểm quan trọng: nó hoàn toàn tường minh. Bạn thấy được code sinh ra, gỡ lỗi được, và trình biên dịch kiểm tra được — khác với reflection vốn chỉ lộ lỗi lúc chạy.
5.10.3 — Expression tree
Điều kiện kích hoạt: bạn dùng nó hàng ngày qua EF Core; chỉ cần tự viết khi xây thư viện.
// Delegate — code ĐÃ BIÊN DỊCH, chỉ chạy được
Func<Lead, bool> predicate = l => l.Value > 1000;
// Expression tree — CẤU TRÚC DỮ LIỆU mô tả code, ĐỌC và PHÂN TÍCH được
Expression<Func<Lead, bool>> expression = l => l.Value > 1000;
Console.WriteLine(expression.Body); // (l.Value > 1000)
Console.WriteLine(expression.Body.NodeType); // GreaterThan
Đây chính là lý do EF Core dịch được LINQ sang SQL:
// EF Core ĐỌC cây biểu thức và sinh SQL
_db.Leads.Where(l => l.Value > 1000)
// -> SELECT * FROM Leads WHERE Value > 1000
Và cũng giải thích một lỗi rất hay gặp:
// Hàm C# — EF Core KHÔNG đọc được bên trong
_db.Leads.Where(l => IsValid(l))
// -> InvalidOperationException: could not be translated
Where của IQueryable nhận Expression<Func<...>>, không phải Func<...>. Một lời gọi phương thức thường là một nút "gọi hàm" trong cây, và EF Core không biết dịch nó thành SQL gì.
Hiểu điều này giúp bạn đọc được thông báo lỗi "could not be translated" và biết ngay phải sửa ở đâu (bài 13.6).
5.10.4 — Span và Memory
Điều kiện kích hoạt: hot path đã đo được là tốn cấp phát; hoặc bạn viết thư viện.
// Cấp phát: Substring tạo chuỗi MỚI
string code = input.Substring(0, 3);
// Không cấp phát: Span trỏ vào bộ nhớ SẴN CÓ
ReadOnlySpan<char> code = input.AsSpan(0, 3);
Span<T> là một "cửa sổ" nhìn vào vùng nhớ có sẵn, không sao chép gì. Với code chạy hàng triệu lần, khác biệt về số lần cấp phát là đáng kể.
Hai giới hạn cần biết:
Span<T>làref struct— không dùng được trong phương thứcasync, không lưu được vào trường của lớp. Nó bị giới hạn ở stack.- Khi cần những thứ đó, dùng
Memory<T>— linh hoạt hơn nhưng chậm hơn một chút.
Thực tế: bạn dùng nó gián tiếp liên tục. int.Parse(ReadOnlySpan<char>), string.Split phiên bản mới, và phần lớn API xử lý chuỗi trong .NET hiện đại đều nhận Span. Tự viết code dùng Span chỉ đáng khi profiler đã chỉ ra một hot path cụ thể.
5.10.5 — Covariance và contravariance
Điều kiện kích hoạt: thiết kế interface generic dùng lại nhiều nơi.
// Covariance (out) — dùng được kiểu CỤ THỂ HƠN ở đầu ra
IEnumerable<Lead> leads = leadList;
IEnumerable<object> objects = leads; // OK vì IEnumerable<out T>
// Contravariance (in) — dùng được kiểu TỔNG QUÁT HƠN ở đầu vào
Action<object> handleObject = o => Console.WriteLine(o);
Action<Lead> handleLead = handleObject; // OK vì Action<in T>
// Trong thiết kế của bạn
public interface IReadOnlyRepository<out T> // chỉ TRẢ VỀ T
{
Task<T?> GetByIdAsync(Guid id, CancellationToken ct);
}
// Giờ dùng được:
IReadOnlyRepository<Lead> leadRepo = ...;
IReadOnlyRepository<IEntity> entityRepo = leadRepo; // hợp lệ
Quy tắc dễ nhớ: out cho kiểu chỉ xuất hiện ở đầu ra, in cho kiểu chỉ xuất hiện ở đầu vào. Nếu kiểu xuất hiện ở cả hai (ví dụ IList<T> vừa đọc vừa ghi), không đánh dấu được — và đó là lý do IList<Lead> không gán được cho IList<object>.
Ít khi bạn cần tự khai báo, nhưng hiểu nó giải thích được vì sao một số phép gán generic hợp lệ còn số khác thì không.
5.10.6 — Thứ tự nên học
| Ưu tiên | Chủ đề | Vì sao |
|---|---|---|
| Cao | IAsyncEnumerable | Giải quyết bug OOM khi xử lý dữ liệu lớn |
| Cao | Source generator cho JSON | Thêm một lớp, nhanh hơn ngay |
| Trung bình | Expression tree (hiểu) | Giải thích lỗi "could not be translated" |
| Trung bình | Covariance, contravariance | Giải thích lỗi gán generic |
| Thấp | Tự viết code dùng Span | Chỉ khi profiler chỉ ra hot path |
| Thấp | Tự viết source generator | Cho thư viện |
Sau Module 5, bước tiếp theo là Module 6 — Async Programming.
5.10.7 — Rà lại code của bạn
Danh sách rà soát C# nâng cao
- •Endpoint trả danh sách lớn dùng IAsyncEnumerable thay vì ToListAsync.
- •Phương thức IAsyncEnumerable có thuộc tính EnumeratorCancellation.
- •Đã cân nhắc source generator cho JSON trong đường xử lý nóng.
- •Hiểu vì sao gọi hàm C# trong Where của IQueryable gây lỗi.
- •Không tự viết code dùng Span khi chưa có số liệu profiler.
- •Interface chỉ đọc dùng out để tăng khả năng tái sử dụng.
Bài tập áp dụng
Bài 1 — Stream dữ liệu
Viết một endpoint trả 100.000 bản ghi bằng ToListAsync rồi bằng IAsyncEnumerable, so sánh bộ nhớ đỉnh.
Tiêu chí hoàn thành: bạn đo được cả hai, và biết hai điều kiện để streaming thật sự có tác dụng — thiếu một trong hai thì không.
Gợi ý và lời giải — Bài 1
Gợi ý. Đo cả heap quản lý lẫn RSS đỉnh. Hai con số kể hai phần của cùng một câu chuyện.
Lời giải:
// A — nạp hết rồi xử lý
var ds = await db.Leads.AsNoTracking().ToListAsync(ct);
long tong = 0;
foreach (var l in ds) tong += l.HoTen.Length;
// B — đọc từng bản ghi
long tong = 0;
await foreach (var l in db.Leads.AsNoTracking().AsAsyncEnumerable().WithCancellation(ct))
tong += l.HoTen.Length;
Kết quả đo trên .NET 9.0.203, EF Core 9.0.0, SQLite, 100.000 bản ghi (mỗi bản ghi có một trường ghi chú 200 ký tự):
ToListAsync | heap cuối 66,2 MB | RSS đỉnh 163,5 MB | 915 ms
AsAsyncEnumerable | heap cuối 13,1 MB | RSS đỉnh 130,9 MB | 527 ms
ToListAsync | AsAsyncEnumerable | |
|---|---|---|
| Heap cuối | 66,2 MB | 13,1 MB |
| RSS đỉnh | 163,5 MB | 130,9 MB |
| Thời gian | 915 ms | 527 ms |
| Kết quả tính được | 1.888.890 | 1.888.890 |
Ba nhận xét:
1. Heap giảm 5 lần — đó là phần rõ nhất.
ToListAsync giữ 100.000 đối tượng Lead cùng lúc
AsAsyncEnumerable giữ vài chục đối tượng tại một thời điểm
-> phần còn lại bị thu gom ngay trong gen-0
2. Nhưng RSS chỉ giảm 20%, không giảm 5 lần.
RSS gồm cả: bộ đệm của SQLite, mã đã JIT, stack của luồng, bộ đệm mạng
-> phần heap chỉ là một phần của RSS
Với dữ liệu lớn hơn, khoảng cách này giãn ra:
heap của ToListAsync tăng tuyến tính, heap của streaming thì không.
3. Streaming còn NHANH hơn — 527 so với 915 ms.
Không phải vì đọc nhanh hơn, mà vì:
- không phải cấp phát rồi mở rộng một List 100.000 phần tử
- ít áp lực GC hơn -> ít lần tạm dừng hơn
Và đây là phần quan trọng nhất — hai điều kiện để streaming có tác dụng:
Điều kiện 1: phía tiêu thụ cũng phải là streaming.
// VÔ NGHĨA — vẫn gom hết vào bộ nhớ
var ds = new List<LeadDto>();
await foreach (var l in query.AsAsyncEnumerable())
ds.Add(ToDto(l)); // gom lại -> mất hết lợi ích
return ds;
// ĐÚNG — trả thẳng IAsyncEnumerable cho ASP.NET Core
[HttpGet("stream")]
public async IAsyncEnumerable<LeadDto> Stream([EnumeratorCancellation] CancellationToken ct)
{
await foreach (var l in _db.Leads.AsNoTracking().AsAsyncEnumerable().WithCancellation(ct))
yield return ToDto(l);
}
ASP.NET Core ghi từng phần tử ra Response.Body khi nhận được
-> bộ nhớ hằng số từ database tới tận trình duyệt
Điều kiện 2: kết nối database phải giữ mở suốt quá trình.
AsAsyncEnumerable giữ một DataReader mở trong suốt vòng lặp.
-> kết nối bị chiếm lâu hơn nhiều so với ToListAsync
ToListAsync: mở, đọc hết trong 300 ms, đóng
AsAsyncEnumerable: mở, và giữ suốt thời gian xử lý — có thể vài phút
Hệ quả: với pool 30 kết nối, 30 request streaming đồng thời làm cạn pool
-> và request thứ 31 chờ, dù CPU còn rỗi
Đây là đánh đổi thật, và nó dẫn t ới quy tắc chọn:
| Tình huống | Chọn |
|---|---|
| Xuất dữ liệu, báo cáo lớn, ít người dùng đồng thời | IAsyncEnumerable |
| Endpoint danh sách có phân trang (dưới 100 bản ghi) | ToListAsync — đơn giản hơn |
| Job nền xử lý toàn bộ bảng | IAsyncEnumerable |
| Nhiều request đồng thời, mỗi request lấy ít dữ liệu | ToListAsync — giữ kết nối ngắn |
Và một cái bẫy riêng của IAsyncEnumerable trong endpoint: lỗi giữa chừng.
ToListAsync hỏng -> chưa ghi gì ra response -> trả 500 sạch sẽ
IAsyncEnumerable hỏng ở bản ghi thứ 40.000
-> 40.000 bản ghi đầu ĐÃ được ghi ra, kèm mã trạng thái 200
-> client nhận JSON cụt, không có dấu hiệu lỗi nào
Cách giảm nhẹ: dùng định dạng mỗi dòng một bản ghi (NDJSON)
và kèm một dòng kết thúc có đánh dấu
-> client biết được nó đã nhận đủ hay chưa
Bài 2 — Bật source generator
Thêm JsonSerializerContext cho một DTO và benchmark serialize trước/sau.
Tiêu chí hoàn thành: bạn có số đo cho cả serialize lẫn deserialize, và biết lý do chính để dùng nó không phải tốc độ.
Gợi ý và lời giải — Bài 2
Gợi ý. Đo cả hai chiều. Chúng cho kết quả khác nhau rõ rệt, và sự khác nhau đó chính là câu trả lời.
Lời giải:
public sealed record LeadDto(
int Id, string HoTen, string Email, string DienThoai,
string TrangThai, DateTime NgayTao, decimal GiaTri);
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(LeadDto))]
[JsonSerializable(typeof(List<LeadDto>))]
public partial class CrmJsonContext : JsonSerializerContext { }
[Benchmark(Baseline = true)] public string SerReflection() => JsonSerializer.Serialize(_ds);
[Benchmark] public string SerSourceGen() => JsonSerializer.Serialize(_ds, CrmJsonContext.Default.ListLeadDto);
[Benchmark] public List<LeadDto>? DeReflection() => JsonSerializer.Deserialize<List<LeadDto>>(_json);
[Benchmark] public List<LeadDto>? DeSourceGen() => JsonSerializer.Deserialize(_json, CrmJsonContext.Default.ListLeadDto);
Kết quả đo trên .NET 9.0.203, BenchmarkDotNet với iterationCount: 5:
| Method | Count | Mean | Error | Ratio | Allocated |
|--------------------------------- |------ |------------:|------------:|------:|----------:|
| 'Serialize - reflection' | 1 | 973,8 ns | 297,0 ns | 1.00 | 624 B |
| 'Serialize - source generator' | 1 | 621,1 ns | 168,5 ns | 0.64 | 312 B |
| 'Deserialize - reflection' | 1 | 1.747,7 ns | 410,3 ns | 1.80 | 944 B |
| 'Deserialize - source generator' | 1 | 1.735,4 ns | 425,1 ns | 1.79 | 944 B |
| | | | | | |
| 'Serialize - reflection' | 100 | 65.190,7 ns | 19.346,2 ns | 1.00 | 29.480 B |
| 'Serialize - source generator' | 100 | 48.222,0 ns | 15.449,6 ns | 0.74 | 29.168 B |
| 'Deserialize - reflection' | 100 | 168.681,4 ns | 15.133,0 ns | 2.60 | 41.064 B |
| 'Deserialize - source generator' | 100 | 142.238,2 ns | 40.744,1 ns | 2.19 | 41.064 B |
Serialize nhanh hơn rõ (0,64× và 0,74×). Deserialize thì không kết luận được:
1.747,7 so với 1.735,4 ns — chênh 0,7%
Error là 410 ns, tức 23% của Mean
-> hai khoảng tin cậy chồng lên nhau hoàn toàn
-> không đủ bằng chứng để nói cái nào nhanh hơn
Với một đối tượng, cấp phát giảm một nửa (624 → 312 B) — đó là metadata mà reflection phải dựng lại mỗi lần gọi.
Và lý do chính để dùng source generator không nằm trong bảng trên:
| Lý do | Mức độ |
|---|---|
| Tương thích Native AOT và trimming | Quyết định |
| Khởi động nhanh hơn (không dựng metadata lúc chạy) | Cao |
| Serialize nhanh hơn | Trung bình |
| Deserialize nhanh hơn | Chưa khẳng định được ở bài thử này |
| Lỗi phát hiện lúc biên dịch | Cao |
PublishTrimmed + reflection:
trimmer xoá thuộc tính nó không thấy ai dùng
-> chạy lên, JSON THIẾU TRƯỜNG, không có lỗi nào
-> loại lỗi tệ nhất: sai âm thầm
Source generator: mã tuần tự hoá được SINH RA lúc biên dịch
-> trimmer thấy chúng được dùng -> giữ lại
Bật cho toàn ứng dụng:
builder.Services.ConfigureHttpJsonOptions(o =>
o.SerializerOptions.TypeInfoResolverChain.Insert(0, CrmJsonContext.Default));
Ba giới hạn cần biết trước khi chuyển:
1. Phải khai báo TRƯỚC mọi kiểu sẽ tuần tự hoá
-> quên một kiểu: JsonTypeInfo metadata for type ... was not provided
2. Không hỗ trợ một số tính năng động
-> converter tuỳ biến phụ thuộc kiểu lúc chạy, đa hình không khai báo trước
3. Thời gian build tăng theo số kiểu khai báo
Quyết định:
Native AOT hoặc PublishTrimmed -> BẮT BUỘC
Serverless, quan tâm cold start -> nên dùng
API thường, JSON không phải nghẽn -> chưa cần vội
Với một endpoint mất 200 ms, tiết kiệm 17 µs khi serialize 100 đối tượng là 0,008% — đúng loại tối ưu mà micro-benchmark đo được còn hệ thống thì không.
Bài 3 — Đọc expression tree
In ra Body và NodeType của vài biểu thức LINQ khác nhau.
Tiêu chí hoàn thành: bạn in được cấu trúc cây của ít nhất ba dạng biểu thức, và giải thích được vì sao Func<T,bool> không dịch được sang SQL còn Expression<Func<T,bool>> thì đư ợc.
Gợi ý và lời giải — Bài 3
Gợi ý. Func là code đã biên dịch. Expression là mô tả của code, còn đọc được từng nút.
Lời giải:
Expression<Func<Lead, bool>> e1 = l => l.GiaTri > 1_000_000;
Expression<Func<Lead, bool>> e2 = l => l.HoTen.Contains("Nguyen") && l.TrangThai == 2;
Expression<Func<Lead, string>> e3 = l => l.HoTen.ToUpper();
foreach (var (ten, ex) in new (string, LambdaExpression)[] { ("e1", e1), ("e2", e2), ("e3", e3) })
Console.WriteLine($"{ten}: NodeType={ex.Body.NodeType,-12} Type={ex.Body.Type.Name,-8} Body={ex.Body}");
Kết quả chạy trên .NET 9.0.203:
e1: NodeType=GreaterThan Type=Boolean Body=(l.GiaTri > 1000000)
e2: NodeType=AndAlso Type=Boolean Body=(l.HoTen.Contains("Nguyen") AndAlso (l.TrangThai == 2))
e3: NodeType=Call Type=String Body=l.HoTen.ToUpper()
Đi sâu vào một nút:
var be = (BinaryExpression)e1.Body;
Console.WriteLine($"Left : {be.Left.NodeType} -> {be.Left}");
Console.WriteLine($"Right : {be.Right.NodeType} -> {be.Right} (giá trị: {((ConstantExpression)be.Right).Value})");
Console.WriteLine($"biên dịch rồi chạy: {e1.Compile()(new Lead { GiaTri = 2_000_000 })}");
Left : MemberAccess -> l.GiaTri
Right : Constant -> 1000000 (giá trị: 1000000)
biên dịch rồi chạy: True
Cây của e1 trông như sau:
GreaterThan
├── MemberAccess : l.GiaTri
└── Constant : 1000000
Và đây là câu trả lời cho câu hỏi chính — vì sao Func không dịch được sang SQL:
Func<Lead, bool> f = l => l.GiaTri > 1_000_000;
Func = một con trỏ tới MÃ MÁY đã biên dịch.
Chỉ làm được một việc: gọi nó với một Lead và nhận về true/false.
Không có cách nào hỏi "anh so sánh trường nào?" hay "với giá trị bao nhiêu?"
-> EF Core không có thông tin để dựng câu WHERE
Expression<Func<Lead, bool>> e = l => l.GiaTri > 1_000_000;
Expression = một CÂY ĐỐI TƯỢNG mô tả chính đoạn code đó.
EF Core duyệt cây:
GreaterThan -> sinh ra >
MemberAccess l.GiaTri -> tra bảng ánh xạ -> cột [GiaTri]
Constant 1000000 -> tham số @p0
-> WHERE [l].[GiaTri] > @p0
Hệ quả thực tế — hai dòng trông giống hệt nhau, chạy hoàn toàn khác:
// IQueryable.Where nhận Expression -> lọc ở DATABASE
var a = await _db.Leads.Where(l => l.GiaTri > 1_000_000).ToListAsync(ct);
// IEnumerable.Where nhận Func -> nạp HẾT rồi lọc trong bộ nhớ
var b = await _db.Leads.ToListAsync(ct);
var c = b.Where(l => l.GiaTri > 1_000_000).ToList();
-- a:
SELECT ... FROM [Leads] WHERE [GiaTri] > @p0
-- b:
SELECT ... FROM [Leads] -- TOÀN BỘ bảng
Chuyển từ IQueryable sang IEnumerable là ranh giới quyết định điều này.
Và nó xảy ra âm thầm:
.AsEnumerable() -> tường minh
.ToList() -> tường minh
.AsQueryable() -> KHÔNG đưa ngược lại được về database
// Bẫy hay gặp: kiểu trả về của một phương thức
public IEnumerable<Lead> LayTheoTenant(int id) => _db.Leads.Where(l => l.TenantId == id);
// ^^^^^^^^^^^^^^^ mọi Where nối thêm sau đó chạy TRONG BỘ NHỚ
public IQueryable<Lead> LayTheoTenant(int id) => _db.Leads.Where(l => l.TenantId == id);
// ^^^^^^^^^^^^^^^ Where nối thêm vẫn dịch thành SQL
Ứng dụng thực tế của expression tree: dựng bộ lọc động.
public static IQueryable<T> LocNeuCo<T>(
this IQueryable<T> q, bool dieuKien, Expression<Func<T, bool>> loc)
=> dieuKien ? q.Where(loc) : q;
var kq = await _db.Leads
.LocNeuCo(tuKhoa is not null, l => l.HoTen.Contains(tuKhoa!))
.LocNeuCo(trangThai.HasValue, l => l.TrangThai == trangThai!.Value)
.LocNeuCo(tuNgay.HasValue, l => l.NgayTao >= tuNgay!.Value)
.ToListAsync(ct);
Chỉ những điều kiện được chọn mới vào câu SQL.
Không có Expression thì phải dựng chuỗi SQL bằng tay — và mở cửa cho SQL injection.
Và một giới hạn cần biết: không phải biểu thức nào cũng dịch được.
await _db.Leads.Where(l => TinhDiem(l) > 80).ToListAsync(ct);
InvalidOperationException: The LINQ expression '... TinhDiem(l) ...'
could not be translated.
EF Core thấy một MethodCall tới một phương thức của bạn,
nó không biết phương thức đó làm gì -> không dịch được.
Từ EF Core 3.0 trở đi, trường hợp này NÉM LỖI thay vì âm thầm
chuyển sang lọc trong bộ nhớ như các phiên bản cũ.
-> đây là một thay đổi tốt: lỗi rõ ràng hơn là chậm âm thầm.
Tự kiểm tra
Frequently asked questions
IAsyncEnumerable giải quyết vấn đề gì?
Nó cho phép xử lý từng bản ghi một thay vì nạp hết vào bộ nhớ, nên bộ nhớ là hằng số bất kể dữ liệu lớn tới đâu. Đây là cách sửa đúng cho lớp bug làm container bị OOM khi export dữ liệu lớn.
Vì sao cần thuộc tính EnumeratorCancellation?
Không có nó, CancellationToken truyền qua WithCancellation không tới được phần thân phương thức, nên việc huỷ không có tác dụng gì.
Expression tree khác delegate thế nào?
Delegate là code đã biên dịch, chỉ chạy được. Expression tree là cấu trúc dữ liệu mô tả code, nên đọc và phân tích được. Đó là lý do EF Core dịch được LINQ sang SQL.
Vì sao gọi hàm C# trong Where của IQueryable gây lỗi?
Vì lời gọi phương thức chỉ là một nút gọi hàm trong cây biểu thức, và EF Core không biết dịch nó thành SQL gì. Thông báo could not be translated chính là điều đó.
Hai giới hạn của Span là gì?
Nó là ref struct nên không dùng được trong phương thức async và không lưu được vào trường của lớp, vì nó bị giới hạn ở stack. Khi cần những điều đó thì dùng Memory, linh hoạt hơn nhưng chậm hơn một chút.
Quy tắc dễ nhớ cho out và in trong generic là gì?
Dùng out khi kiểu chỉ xuất hiện ở đầu ra, dùng in khi nó chỉ xuất hiện ở đầu vào. Nếu xuất hiện ở cả hai như IList thì không đánh dấu được, và đó là lý do IList của kiểu con không gán được cho IList của kiểu cha.
Kết luận
Ba điều đáng nhớ nhất:
IAsyncEnumerablegiữ bộ nhớ ở mức hằng số — công cụ quan trọng nhất trong bài.- Expression tree là thứ khiến LINQ dịch được sang SQL, và hiểu nó giải thích lỗi thường gặp.
Spanbạn dùng gián tiếp hàng ngày; tự viết chỉ khi profiler chỉ ra.
Tham khảo
Điều hướng
- Bài trước: 5.8 — 8. Tuples và Deconstruction
- Bài tiếp theo: 5.10 — Mini case study
- Về module: Trang mục lục