You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在C#/EF Core中基于运行时定义的字段构建Where子句?

Dynamic Where Clauses with EF Core (No Extra Dependencies)

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:

  1. Check for null first, then compare the underlying value
  2. 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.MakeMemberAccess is valid, but Expression.Property is cleaner for property-specific access
  • Always handle nullable types by checking HasValue and accessing Value (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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 14:39:07