Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Constructing Dynamic Query Conditions in C# Using Expression Trees for FreeSql

Tech 1

When building data access layers, a common requirement is togggling between exact match and fuzzy search for string fields based on front-end input. Instead of writting two separate conditional branches for each filter, a reusable helper can encapsulate either equality or containment logic. Directly passing a field selector delegate to FreeSql’s Where method may produce incorrect SQL, missing the column name. This occurs because the underlying expression parser does not properly translate that delegate form into a member access expression.

Expression trees provide a precise way to build query predicates dynamically. By constructing the predicate manually, you retain full control over how the expression is interpreted by the ORM. The core idea is to take an expression that selects a string property and combine it with a constant value to produce either an equality check or a Contains method call.

Single Entity Implementation

For a single generic entity, the extension method accepts an Expression<Func<T1, string>>, a string value, and a boolean flag. Internally, it creates a constant expression for the value, then either wraps the field selector body in Expression.Equal or in a string.Contains call. The resulting lambda is passed directly to Where.

public static ISelect<T1> WhereLike<T1>(
    this ISelect<T1> query,
    Expression<Func<T1, string>> fieldSelector,
    string filterValue,
    bool enableFuzzy = false) where T1 : class
{
    var constantValue = Expression.Constant(filterValue);
    Expression predicateBody;

    if (enableFuzzy)
    {
        var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });
        predicateBody = Expression.Call(fieldSelector.Body, containsMethod, constantValue);
    }
    else
    {
        predicateBody = Expression.Equal(fieldSelector.Body, constantValue);
    }

    var lambda = Expression.Lambda<Func<T1, bool>>(predicateBody, fieldSelector.Parameters);
    return query.Where(lambda);
}

Joining Multiple Tables

When working with joined tables, FreeSql represents them using HzyTuple<T1, T2, ...>. The helper needs overloads that accept field selectors operating on those tuple types. The same expression-tree construction applies, just adapted for the tuple parameter.

public static ISelect<T1, T2> WhereLike<T1, T2>(
    this ISelect<T1, T2> query,
    Expression<Func<HzyTuple<T1, T2>, string>> fieldSelector,
    string filterValue,
    bool enableFuzzy = false) where T2 : class
{
    var constantValue = Expression.Constant(filterValue);
    Expression predicateBody = enableFuzzy
        ? Expression.Call(fieldSelector.Body,
            typeof(string).GetMethod("Contains", new[] { typeof(string) }), constantValue)
        : Expression.Equal(fieldSelector.Body, constantValue);

    var lambda = Expression.Lambda<Func<HzyTuple<T1, T2>, bool>>(predicateBody, fieldSelector.Parameters);
    return query.Where(lambda);
}

A variant that receives a two-parameter function (Func<T1, T2, string>) is also necessary for certain join scenarios:

public static ISelect<T1, T2> WhereLike<T1, T2>(
    this ISelect<T1, T2> query,
    Expression<Func<T1, T2, string>> fieldSelector,
    string filterValue,
    bool enableFuzzy = false) where T2 : class
{
    var constantValue = Expression.Constant(filterValue);
    var containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) });

    Expression predicateBody = enableFuzzy
        ? Expression.Call(fieldSelector.Body, containsMethod, constantValue)
        : Expression.Equal(fieldSelector.Body, constantValue);

    var lambda = Expression.Lambda<Func<T1, T2, bool>>(predicateBody, fieldSelector.Parameters);
    return query.Where(lambda);
}

When multiple overloads are needed for larger numbers of type parameters, code generation can be automated. A Python script can emit the repetitive generic overloads for both HzyTuple and non-tuple signatures, as well as conditional WhereLikeIf variants that short-circuit when a boolean condition is false.

#!/usr/bin/env python3

def emit_hzy_tuple_variant(count):
    type_params = ', '.join(f'T{i+1}' for i in range(count))
    constraints = '\n'.join(
        f'            where T{i+2} : class' for i in range(count - 1)
    )
    code = f'''
    public static ISelect<{type_params}> WhereLike<{type_params}>(
        this ISelect<{type_params}> query,
        Expression<Func<HzyTuple<{type_params}>, string>> fieldSelector,
        string filterValue,
        bool fuzzy = false)
{constraints}
        {{
            var constVal = Expression.Constant(filterValue);
            if (fuzzy)
            {{
                var contains = typeof(string).GetMethod("Contains", new[] {{ typeof(string) }});
                query = query.Where(Expression.Lambda<Func<HzyTuple<{type_params}>, bool>>(
                    Expression.Call(fieldSelector.Body, contains, constVal), fieldSelector.Parameters));
            }}
            else
            {{
                query = query.Where(Expression.Lambda<Func<HzyTuple<{type_params}>, bool>>(
                    Expression.Equal(fieldSelector.Body, constVal), fieldSelector.Parameters));
            }}
            return query;
        }}
    '''
    print(code)

# Generate for 2 up to 16 type parameters
for n in range(2, 17):
    emit_hzy_tuple_variant(n)

Extending this approach with a WhereLikeIf overload adds a conditional guard, avoiding unnecessary predicate construction when the filter is disabled:

public static ISelect<T1> WhereLikeIf<T1>(
    this ISelect<T1> query,
    bool condition,
    Expression<Func<T1, string>> fieldSelector,
    string filterValue,
    bool fuzzy = false) where T1 : class
{
    if (!condition) return query;

    var constVal = Expression.Constant(filterValue);
    var method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
    Expression body = fuzzy
        ? Expression.Call(fieldSelector.Body, method, constVal)
        : Expression.Equal(fieldSelector.Body, constVal);

    return query.Where(Expression.Lambda<Func<T1, bool>>(body, fieldSelector.Parameters));
}

By leveraging expression trees, query logic becomes concise and the generated SQL accuratley reflects column names, whether performing exact matches or pattern searches.

Tags: C#

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.