[asp.net] - Query trên các computed property không hỗ trợ LINQ
Trong một project gần đây ở công ty, tôi bàng hoàng nhận ra rằng:
LINQ và Entity Framework không hỗ trợ query trên các property được tính toán dựa trên các field khác
Vậy giờ ta phải làm sao? May mắn là vẫn có cách
1. Computed Property
Là một property chỉ có hàm get, và trong get đó, giá trị trả về được tính toán dựa trên các property khác
public class TestViewModel
{
[ ]
[ ]
public string FirstName { get; set; }
[ ]
[ ]
public string LastName { get; set; }
// Computed Property
[ ]
public string FullName => FirstName + LastName;
}
Theo các chuẩn thiết kế database, một column phải chứa dữ liệu mà không thể được suy ra từ các dữ liệu khác. Đoạn Attribute [NotMapped]
phục vụ cho việc đó. EF sẽ không sinh ra code generate column FullName
nếu bạn dùng code first, không cố gắng tìm column FullName
trong table nếu bạn dùng database first
2. Simple LINQ
Để query một giá trị nào đó trong Database dùng Entity Framework, bạn có thể dùng LINQ rất đơn giản như sau
// TableNameWithS is your table name in plural
var names = dbContext.TableNameWithS.Where(x => x.FirstName.Contains("test"));
Nhưng cũng đoạn code đó sẽ gây lỗi nếu bạn cố gắn dùng nó với Property FullName
// BUG BUG BUG
var names = dbContext.TableNameWithS.Where(x => x.FullName.Contains("test"));
3. Solution
TL;DR: The best solution
3.1. [Slow performance] Gọi ToList
var names = dbContext.TableNameWithS.ToList().Where(x => x.FullName.Contains("test"));
Gọi ToList sẽ làm Entity Framework gọi tới database, thực thi bất kỳ đoạn query nào trước đó, rồi mới thực thi tới đoạn LINQ có computed property của bạn
Nhược điểm của nó là EF sẽ query ra kết quả nhiều hơn so với cần thiết, làm giảm performance của ứng dụng
3.2. [DRY Principle violated] Viết biểu thức
Một cách khác là thay vì sử dụng computed property, ta có thể viết thẳng biểu thức của property đó vào câu query
var names = dbContext.TableNameWithS.Where(x => (x.FirstName + x.LastName).Contains("test"));
Nhược điểm của cách này là bạn đã vi phạm nguyên tắc “DRY” - Don’t repeat yourself. Một biểu thức mà phải code tới 2 lần. Nếu sau này bạn thay đổi biểu thức đó ở 1 chỗ, thì ở chỗ còn lại bạn cũng sẽ phải đổi theo. Nếu bạn quên -> BUG ngay và luôn
The DRY principle is stated as “Every piece of knowledge must have a single, unambiguous, authoritative representation within a system”
Source: https://en.wikipedia.org/wiki/Don%27t_repeat_yourself
4. The best solution
DelegateDecompiler - Một bộ thư viện cực khủng giúp bạn decompile biểu thức của computed property, và translate chúng thành LINQ, EF sau đó sẽ translate nó thành câu lệnh SQL.
Nhược điểm của nó là nó không thể dịch được khi bạn sử dụng các method, class mà bạn tự định nghĩa, không nằm trong .NET Framework
Sử dụng nó thì không còn gì dễ hơn
Bước 1: Cài đặt nuget DelegateDecompiler
Install-Package DelegateDecompiler
Bước 2: Trang trí property với attribute [Computed]
public class TestViewModel
{
[ ]
[ ]
public string FirstName { get; set; }
[ ]
[ ]
public string LastName { get; set; }
// Computed Property
[ ]
[ ]
public string FullName => FirstName + LastName;
}
Bước 3: Gọi method Decompile
var names = dbContext.TableNameWithS
.ToList()
.Where(x => x.FullName.Contains("test")).Decompile();
Thư viện này thậm chí còn hỗ trợ async, các advanced functions của EF như
Include
,AsNoTracking
với phần mở rộng DelegateDecompiler.EntityFramework
5. Make life easier
Bạn cũng có thể cấu hình cho asp tự xử lý các property có [NotMapped]
Tạo một class Configuration
public class DelegateDecompilerConfiguration : DefaultConfiguration
{
public override bool ShouldDecompile(MemberInfo memberInfo)
{
// Automatically decompile all NotMapped members
return base.ShouldDecompile(memberInfo) || memberInfo.GetCustomAttributes(typeof(NotMappedAttribute), true).Length > 0;
}
}
Rồi đăng ký nó trong method Startup như sau
DelegateDecompiler.Configuration
.Configure(new DelegateDecompilerConfiguration());