如何在C#/EF Core中基于运行时定义的字段构建Where子句?
Great work getting the foundational expression tree logic in place! Let's address the nullable field, date comparison issues, and clarify your questions about Expression.MakeMemberAccess to make this solution robust and maintainable.
First: Is Expression.MakeMemberAccess Correct?
Short answer: Yes, your usage is valid, but there's a more intuitive alternative.
Expression.MakeMemberAccess works for both properties and fields, which makes it a bit generic. Since you're explicitly targeting a property (you use GetProperty), you can simplify this line:
MemberExpression access = Expression.MakeMemberAccess(arg, typeof(TableObject).GetProperty(fieldName));
to:
MemberExpression access = Expression.Property(arg, fieldName);
Expression.Property is a specialized method for property access, so it makes your code more readable without changing functionality.
Fixing Nullable Field Comparisons
Nullable value types (like int?, DateTime?) need special handling because they wrap an underlying value. Directly using Expression.Equal or Expression.LessThan on a nullable property will throw a runtime error—those operators don't work on the nullable type itself. You have two solid options:
- Check for null first, then compare the underlying value
- Use the nullable type's lifted operators (EF Core supports these, but we need to build the expression correctly)
Here's adjusted logic to handle nullable types:
// Get property details to check for nullability var propertyType = propertyInfo.PropertyType; var isNullable = Nullable.GetUnderlyingType(propertyType) != null; Expression oprFunc; switch (filter.MatchMode.ToUpper()) { case "EQ": // Convert filter value to match the property's type (including nullable) var eqValue = Convert.ChangeType(filter.Value, isNullable ? Nullable.GetUnderlyingType(propertyType) : propertyType); var eqConstant = Expression.Constant(eqValue, propertyType); oprFunc = Expression.Equal(access, eqConstant); break; case "LT": var underlyingType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; var ltValue = Convert.ChangeType(filter.IntValue, underlyingType); var ltConstant = Expression.Constant(ltValue, propertyType); if (isNullable) { // Build logic: Only compare if the field has a value (adjust null behavior as needed) var hasValueExpr = Expression.Property(access, "HasValue"); var valueAccess = Expression.Property(access, "Value"); var compareExpr = Expression.LessThan(valueAccess, ltConstant); // Use AndAlso to exclude nulls, or Conditional to treat null as "less than" oprFunc = Expression.AndAlso(hasValueExpr, compareExpr); } else { oprFunc = Expression.LessThan(access, ltConstant); } break; // Add GT, LE, GE cases using similar logic default: throw new ArgumentException($"Invalid match mode: {filter.MatchMode}"); }
Handling Date Comparisons
Date comparisons usually fail due to type mismatches (e.g., passing a string filter.Value instead of a DateTime). Fix this by:
- Parsing the filter value to the correct
DateTime/DateTime?type before creating the constant expression - Applying the same nullable handling logic above for
DateTime?fields
Example for date equality checks:
case "EQ": if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)) { if (!DateTime.TryParse(filter.Value, out var dateValue)) { throw new ArgumentException($"Invalid date value: {filter.Value}"); } var dateConstant = Expression.Constant(dateValue, propertyType); oprFunc = Expression.Equal(access, dateConstant); } // Handle other types here break;
Making the Method Generic (Even More Reusable)
Since you need to support multiple fields across different entities, turn this into a generic method to avoid repeating code for each entity type:
private IQueryable<T> ProcessFilter<T>(IQueryable<T> query, FilterItem filter, string fieldName) where T : class { var entityType = typeof(T); var propertyInfo = entityType.GetProperty(fieldName); if (propertyInfo == null) { throw new ArgumentException($"Field {fieldName} does not exist on type {entityType.Name}"); } ParameterExpression arg = Expression.Parameter(entityType, "x"); MemberExpression access = Expression.Property(arg, fieldName); var propertyType = propertyInfo.PropertyType; var isNullable = Nullable.GetUnderlyingType(propertyType) != null; Expression oprFunc; switch (filter.MatchMode.ToUpper()) { case "EQ": var eqValue = Convert.ChangeType(filter.Value, isNullable ? Nullable.GetUnderlyingType(propertyType) : propertyType); var eqConstant = Expression.Constant(eqValue, propertyType); oprFunc = Expression.Equal(access, eqConstant); break; case "LT": var ltValue = Convert.ChangeType(filter.IntValue, isNullable ? Nullable.GetUnderlyingType(propertyType) : propertyType); var ltConstant = Expression.Constant(ltValue, propertyType); if (isNullable) { var hasValue = Expression.Property(access, "HasValue"); var value = Expression.Property(access, "Value"); var compare = Expression.LessThan(value, ltConstant); oprFunc = Expression.AndAlso(hasValue, compare); } else { oprFunc = Expression.LessThan(access, ltConstant); } break; // Add other match modes here default: throw new ArgumentException($"Invalid match mode: {filter.MatchMode}"); } var func = Expression.Lambda<Func<T, bool>>(oprFunc, arg); return query.Where(func); }
Key Takeaways
Expression.MakeMemberAccessis valid, butExpression.Propertyis cleaner for property-specific access- Always handle nullable types by checking
HasValueand accessingValue(adjust null behavior to fit your business rules) - Parse filter values to match the property's exact type (critical for date comparisons)
- Generic methods turn your filter logic into a reusable tool across all your EF Core entities
内容的提问来源于stack exchange,提问作者David Dombrowsky

