FunctionalDev.ExpressionHelpers 2.4.3

dotnet add package FunctionalDev.ExpressionHelpers --version 2.4.3
                    
NuGet\Install-Package FunctionalDev.ExpressionHelpers -Version 2.4.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FunctionalDev.ExpressionHelpers" Version="2.4.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FunctionalDev.ExpressionHelpers" Version="2.4.3" />
                    
Directory.Packages.props
<PackageReference Include="FunctionalDev.ExpressionHelpers" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FunctionalDev.ExpressionHelpers --version 2.4.3
                    
#r "nuget: FunctionalDev.ExpressionHelpers, 2.4.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FunctionalDev.ExpressionHelpers@2.4.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FunctionalDev.ExpressionHelpers&version=2.4.3
                    
Install as a Cake Addin
#tool nuget:?package=FunctionalDev.ExpressionHelpers&version=2.4.3
                    
Install as a Cake Tool

FunctionalDev.ExpressionHelpers

Overview

FunctionalDev.ExpressionHelpers is a powerful library for bidirectional lambda expression serialization that enables dynamic LINQ predicate generation from strings, with seamless Entity Framework integration and advanced expression capabilities.

Transform strings into LINQ-compatible expressions:

var predicate = ExpressionParser.Parse<Customer>("Age > '25' And City = 'London'");
var customers = await dbContext.Customers.Where(predicate).ToListAsync();

Convert expressions back to strings for storage:

Expression<Func<Customer, bool>> expr = c => c.Age > 25 && c.IsActive;
string serialized = ExpressionParser.Parse(expr); // "Age > '25' And IsActive = 'True'"

Key Features

  • Bidirectional Serialization: Convert expressions ↔ strings for configuration storage
  • LINQ Integration: Generate Expression<Func<T, bool>> predicates for Entity Framework and IQueryable
  • Property Selectors: Dynamic projections with Expression<Func<T, TResult>>
  • Static Class Support: DateTime.Now, String methods, and custom static members
  • Method Chaining: Complex expressions like BirthDate.AddDays('-1').Date
  • Null-Safe Navigation: Address?.Line1 syntax with automatic null checking
  • Token Substitution: Runtime expression placeholders with $token$ syntax
  • Collection Operations: Any, All, Count, Contains with nested predicates
  • Performance Optimized: Expression trees compile to efficient delegates

Primary Use Cases

  1. Dynamic API Filtering: Accept filter expressions as API parameters
  2. Entity Framework Integration: Generate database queries from string expressions
  3. Configuration-Driven Business Rules: Store and execute rules from config files
  4. Report Builders: Dynamic filtering and projection for reporting systems
  5. User-Defined Filters: Let users create custom filtering logic

Quick Start

Basic LINQ Filtering

// Generate predicate from string
var predicate = ExpressionParser.Parse<Customer>("Age > '25' And IsActive = 'True'");

// Use with LINQ to Objects
var results = customers.Where(predicate.Compile()).ToList();

// Use with Entity Framework
var dbResults = await dbContext.Customers.Where(predicate).ToListAsync();

Property Selectors

// Generate property selector for dynamic projections
var selector = ExpressionParser.Parse<Customer, string>("Address?.City");
var cities = customers.Select(selector.Compile()).ToList();

Bidirectional Serialization

// Convert expression to string for storage
Expression<Func<Customer, bool>> expr = c => c.Age > 25 && c.IsActive;
string serialized = ExpressionParser.Parse(expr); // "Age > '25' And IsActive = 'True'"

// Convert back to expression
var deserialized = ExpressionParser.Parse<Customer>(serialized);

Entity Framework Integration

Dynamic API Endpoints

Create flexible APIs that accept filter expressions as parameters:

[HttpGet("customers")]
public async Task<ActionResult<List<Customer>>> GetCustomers(string filter = null)
{
    var query = dbContext.Customers.AsQueryable();
    
    if (!string.IsNullOrEmpty(filter))
    {
        var predicate = ExpressionParser.Parse<Customer>(filter);
        query = query.Where(predicate);
    }
    
    return await query.ToListAsync();
}

// Usage: GET /api/customers?filter=Age > '25' And City = 'London'
// Usage: GET /api/customers?filter=Orders.Any(Total > '1000') And IsActive = 'True'

Repository Pattern Integration

public class CustomerRepository
{
    private readonly DbContext _context;
    
    public async Task<List<Customer>> GetAsync(string filterExpression = null, Dictionary<string, Expression> tokens = null)
    {
        var query = _context.Customers.AsQueryable();
        
        if (!string.IsNullOrEmpty(filterExpression))
        {
            var predicate = ExpressionParser.Parse<Customer>(filterExpression, tokens);
            query = query.Where(predicate);
        }
        
        return await query.ToListAsync();
    }
    
    public async Task<List<TResult>> SelectAsync<TResult>(string selectorExpression)
    {
        var selector = ExpressionParser.Parse<Customer, TResult>(selectorExpression);
        return await _context.Customers.Select(selector).ToListAsync();
    }
}

// Usage
var highValueCustomers = await repository.GetAsync("Orders.Any(Total > '5000') And Age > '30'");
var customerNames = await repository.SelectAsync<string>("FirstName + ' ' + LastName");

Complex Database Queries

// Nested object filtering
var filter = "Address.City = 'New York' And Orders.Count() > '10'";
var customers = await dbContext.Customers
    .Include(c => c.Address)
    .Include(c => c.Orders)
    .Where(ExpressionParser.Parse<Customer>(filter))
    .ToListAsync();

// Date range filtering with static classes
var dateFilter = "CreatedDate >= DateTime.UtcNow.AddDays('-30') And Status = 'Active'";
var recentOrders = await dbContext.Orders
    .Where(ExpressionParser.Parse<Order>(dateFilter))
    .ToListAsync();

// Collection operations
var customerFilter = "Orders.Any(Items.Any(Product.Category = 'Electronics') And Total > '500')";
var electronicsCustomers = await dbContext.Customers
    .Include(c => c.Orders)
        .ThenInclude(o => o.Items)
            .ThenInclude(i => i.Product)
    .Where(ExpressionParser.Parse<Customer>(customerFilter))
    .ToListAsync();

Advanced Expression Features

Static Class Support

Access static properties and methods in expressions:

// DateTime operations
"BirthDate > DateTime.UtcNow.AddYears('-18')"
"LastLoginDate < DateTime.Today.AddDays('-30')"
"CreatedDate.Date = DateTime.UtcNow.Date"

// String operations  
"Name != String.Empty"
"Description.Length > '50'"

// Custom static classes (if accessible)
"Amount > MyCompany.Constants.MinimumOrderValue"

Method Chaining

Build complex expressions with chained method calls:

// Date manipulations
"BirthDate.AddDays('-1').Date < DateTime.UtcNow.Date"
"CreatedDate.AddMonths('6').Year = '2024'"

// String operations
"Name.ToUpper().Contains('JOHN')"
"Email.ToLower().EndsWith('.com')"

// Collection chaining  
"Orders.Where(Status = 'Pending').Count() > '5'"
"Items.Select(Quantity).Sum() > '100'"

Null-Safe Navigation

Safely navigate nullable properties:

// Null-safe property access
"Address?.Line1 = 'Main Street'"
"Company?.Address?.City = 'Seattle'"
"Manager?.Department?.Name = 'Engineering'"

// Works with method calls
"Profile?.Settings?.GetPreference('theme') = 'dark'"
"Order?.Customer?.Address?.GetFormattedAddress().Contains('NY')"

Contents

  1. Quick Start

  2. Entity Framework Integration

  3. Advanced Expression Features

  4. Basic Literals

  5. Tokens - Runtime expression substitution

  6. Examples

  7. Performance and Best Practices

  8. Version Control

Basic Literals

Basic Literals - Overview

A string literal, in the context of this library, is a string which represents a serialised lambda expression. This can be broken down into segments which represent boolean conditions, chained together with chaining operators.

A string literal is built up with the following for constant/member evaluation. Members and constants can exist either side of the equation.

Constant values must be wrapped in single quotes, this denotes the internal expression as a constant value. "{Member/Constant} {Evaluation Symbol} {Member/Constant}" Methods can also be used. "{Member}.{Method Name}({Arguments as comma separated literals})" Note that nullable types and null avoidance are handled in deseralisation.

Basic Literals - Equality Comparison Operators

The following comparisons are possible.

Symbol Description
= Equals
!= Not Equals
> Greater Than
>= Greater Than or Equal
< Less Than
<= Less Than or Equal

Basic Literals - Chaining Operators

Symbol Description
And The left part of the expression must resolve to true before the right part of the expression is evaluated. Both sides must resolve to true before the full expression returns true. Comparable to AndAlso.
Or If either the left or the right side of the expression resolves to true then the full expression returns true. The right side of the Or operator is only evaluated if the left resolves to false. Comparable to OrElse.

Tokens

It is possible to tokenise a runtime expression. When calling to convert expression to string ExpressionParser.Parse use the ExpressionToken.Generate method to create a token expression which is then serialized as $Identifier$.

When calling ExpressionParser.Parse to convert from string to expression, use TokenDictionaryBuilder to provide an expression to resolve the token to.

Please see the following code for an example. Example - Tokens

Examples

For the following class, some examples are listed below.

public class Person
{
	public string FirstName { set; get; }
	public int Age { set; get; }
	public IEnumerable<Person> Friends { set; get; }
}

Examples - Calling Parser

Examples - Calling Parser - String Literal to Expression

Convert a string literal to an expression.

Expression<Func<Person, bool>> parsed = ExpressionParser.Parse<Person>("FirstName = 'Fred'");

Examples - Calling Parser - String Literal to Func

Compile the expression to a func (a delegate which can be executed at runtime).

Func<Person, bool> parsed = ExpressionParser.Parse<Person>("FirstName = 'Fred'").Compile();

Examples - Calling Parser - Expression to String Literal

Convert an expression to a string literal.

// literal will contain the value "Firstname = 'Fred'".
var literal = ExpressionParser.Parse((Person x) => x.FirstName == "Fred");

Also note that ExpressionParser contains non generic method alternatives for when the type is not known at compile time.

Examples - Basic Equality

Note that constant values are always surrounded by single quotes, integer and boolean values included.

Examples - Basic Equality - String

The following two examples compare a property to a constant value.

var basicMemberLiteral = "FirstName = 'Fred'";
var alternativeMemberLiteral = "'Fred' = FirstName";

Examples - Basic Equality - Integer

The following two examples compare a property to a constant value.

var numericalLiteral = "Age = '51'";

Examples - Method Calls

All parts of an expression literal are evaluated the same, when an argument is found for a method call it's treated in the same way, constants, method calls and literals.

Most methods could be included but the service running the parser from literal to expression must be able to understand the method calls.

Examples - Method Calls - Constant Arguments

In the example below Contains is called on the string with an argument of "F".

var methodLiteral = "FirstName.Contains('F')";

Examples - Method Calls - Lambda Arguments

In the example below a lambda is being used in the Any method call.

var nestedLambdaMethodLiteral = "Friends.Any(Age = '50')";

Examples - Chaining

Examples of chaining expression trees.

Examples - Chaining - Mixing Method And Member Comparisons

Below are some examples of mixing functions (which need to resolve to a boolean) and chaining the response with other expressions.

var chainedAndLiteral = "Friends.Any(Age = '50') And FirstName = 'Fred'";
var chainedOrLiteral = "Friends.Any(Age = '50') Or FirstName = 'Fred'";
var chainedComplexLiteral = "(Friends.Any(Age = '50') Or FirstName = 'Fred') And Age = '60'";

Examples - Null

Null can be used in an equality expression, the null text represents the Null monad.

var nullLiteral = "FirstName = null";

Examples - Tokens

Tokens enable runtime expression substitution for dynamic values, making expressions reusable across different contexts.

Basic Token Usage

// Define tokens for dynamic values
var tokens = new TokenDictionaryBuilder
{
    {"MinAge", () => 18},
    {"CurrentYear", () => DateTime.UtcNow.Year},
    {"CompanyCity", () => "Seattle"}
}.Built;

// Use tokens in expressions
var filter = "Age >= $MinAge$ And BirthDate.Year = $CurrentYear$ And City = $CompanyCity$";
var predicate = ExpressionParser.Parse<Person>(filter, tokens);

Configuration-Driven Filtering

// Store filter templates in configuration
var filterTemplate = configuration["Filters:HighValueCustomer"]; 
// "Orders.Any(Total > $MinOrderValue$) And Age >= $MinAge$"

var tokens = new TokenDictionaryBuilder
{
    {"MinOrderValue", () => configuration.GetValue<decimal>("BusinessRules:MinOrderValue")},
    {"MinAge", () => configuration.GetValue<int>("BusinessRules:MinAge")}
}.Built;

var customers = await dbContext.Customers
    .Where(ExpressionParser.Parse<Customer>(filterTemplate, tokens))
    .ToListAsync();

API with Token Parameters

[HttpGet("customers")]
public async Task<ActionResult<List<Customer>>> GetCustomers(
    string filter, 
    decimal? minOrderValue = null, 
    DateTime? dateFrom = null)
{
    var tokens = new TokenDictionaryBuilder
    {
        {"MinOrderValue", () => minOrderValue ?? 0},
        {"DateFrom", () => dateFrom ?? DateTime.Today.AddMonths(-12)}
    }.Built;

    var predicate = ExpressionParser.Parse<Customer>(filter, tokens);
    return await dbContext.Customers.Where(predicate).ToListAsync();
}

// Usage: GET /api/customers?filter=Orders.Any(Total > $MinOrderValue$) And CreatedDate >= $DateFrom$&minOrderValue=1000

Dynamic Business Rules

public class BusinessRuleEngine
{
    public async Task<List<T>> ApplyRule<T>(string ruleName, IQueryable<T> query)
    {
        var rule = await GetRuleByName(ruleName); // "Status = $ActiveStatus$ And LastUpdate >= $CutoffDate$"
        var tokens = await GetRuleTokens(ruleName);
        
        var predicate = ExpressionParser.Parse<T>(rule, tokens);
        return await query.Where(predicate).ToListAsync();
    }
    
    private async Task<Dictionary<string, Expression>> GetRuleTokens(string ruleName)
    {
        // Load from database, cache, or configuration
        return new TokenDictionaryBuilder
        {
            {"ActiveStatus", () => "Active"},
            {"CutoffDate", () => DateTime.UtcNow.AddDays(-30)}
        }.Built;
    }
}

Serialization with Tokens

// Generate expression with tokens
var expr = ExpressionParser.Parse<Order>(
    order => order.Total > ExpressionToken.Generate<decimal>("MinTotal") && 
             order.CreatedDate > ExpressionToken.Generate<DateTime>("StartDate"));

// Serializes to: "Total > $MinTotal$ And CreatedDate > $StartDate$"
var serialized = ExpressionParser.Parse(expr);

// Store in configuration/database for later use
await SaveFilterTemplate("HighValueOrders", serialized);

// Later, deserialize with actual values
var tokens = new TokenDictionaryBuilder
{
    {"MinTotal", () => 1000m},
    {"StartDate", () => DateTime.Today.AddDays(-7)}
}.Built;

var filter = await LoadFilterTemplate("HighValueOrders");
var predicate = ExpressionParser.Parse<Order>(filter, tokens);

Performance and Best Practices

Expression Tree Benefits

Expression trees offer significant performance advantages over alternative approaches:

Compiled Performance:

// Expression trees compile to efficient IL code
var predicate = ExpressionParser.Parse<Customer>("Age > '25'");
var compiled = predicate.Compile(); // Compiles to native IL

// Much faster than reflection-based approaches
customers.Where(compiled).ToList(); // High performance filtering

Database Query Translation:

// Expressions translate directly to SQL with Entity Framework
var predicate = ExpressionParser.Parse<Customer>("Orders.Any(Total > '1000')");

// Generates efficient SQL query, not in-memory filtering
await dbContext.Customers.Where(predicate).ToListAsync();
// SQL: SELECT * FROM Customers WHERE EXISTS (SELECT 1 FROM Orders WHERE CustomerId = Customers.Id AND Total > 1000)

Best Practices

1. Cache Compiled Expressions

// Cache frequently used compiled expressions
private static readonly ConcurrentDictionary<string, Func<Customer, bool>> _compiledCache = new();

public Func<Customer, bool> GetCompiledPredicate(string filter)
{
    return _compiledCache.GetOrAdd(filter, f => 
        ExpressionParser.Parse<Customer>(f).Compile());
}

2. Use Parameter Sanitization for Expression Composition

// When combining expressions, sanitize parameters
var expr1 = ExpressionParser.Parse<Customer>("Age > '25'");
var expr2 = ExpressionParser.Parse<Customer>("IsActive = 'True'");

// Sanitize before combining
var combined = expr1.SanitizeParameters().And(expr2.SanitizeParameters());

3. Leverage Property Selectors for Dynamic Projections

// More efficient than string-based projections
var selector = ExpressionParser.Parse<Customer, string>("FirstName + ' ' + LastName");
var names = customers.Select(selector.Compile()).ToList();

4. Use Tokens for Reusable Expressions

// Store expression templates, not hardcoded values
var template = "Age >= $MinAge$ And City = $TargetCity$";
// Reuse with different token values

Performance Considerations

Parsing Overhead:

  • Expression parsing has a one-time cost
  • Cache parsed expressions when possible
  • Consider pre-compiling frequently used expressions

Database Query Optimization:

  • Expressions translate to efficient SQL with Entity Framework
  • Complex expressions may require proper database indexing
  • Use Include() statements for related data to avoid N+1 queries

Memory Usage:

  • Expression trees are immutable and memory-efficient
  • Compiled expressions use minimal memory
  • Token dictionaries are lightweight

Common Pitfalls

1. Avoid String Concatenation for Dynamic Filters

// Wrong - SQL injection risk and poor performance
var sql = $"SELECT * FROM Customers WHERE Age > {age}";

// Right - Safe and efficient
var predicate = ExpressionParser.Parse<Customer>($"Age > '{age}'");

2. Don't Compile Expressions Repeatedly

// Wrong - Compiles on every call
customers.Where(ExpressionParser.Parse<Customer>("Age > '25'").Compile())

// Right - Compile once, reuse
var compiled = ExpressionParser.Parse<Customer>("Age > '25'").Compile();
customers.Where(compiled)

3. Use Appropriate Expression Types

// For boolean conditions
Expression<Func<T, bool>> predicate = ExpressionParser.Parse<T>("condition");

// For property selection
Expression<Func<T, TResult>> selector = ExpressionParser.Parse<T, TResult>("property");

Version Control

2.4.3 | 05/08/2025

  • Fixed critical TypeLoadException when enumerating extension methods. Added safe type checking to prevent crashes when encountering problematic types.
2.4.2 | 26/06/2025
  • Fixed an issue with types not being loaded where an assembly could not be found. Now loading types in a safer way.
2.4.1 | 13/05/2025
  • Fixed an issue with types not being loaded where an assembly could not be found. Now loading types in a safer way.
2.4.0 | 06/05/2025
  • Added support for chaining members and methods, for example BirthDate.AddDays('-1').Date < DateTime.UtcNow.Date. Previously expressions were limited to finishing with a method call
2.3.0 | 07/04/2025
  • Added support for using static classes, such as DateTimeProperty > DateTime.UtcNow.AddDays(180)
2.2.1 | 03/04/2025
  • Added support for token substitution in method arguments, such as PreviousAddresses.Any(Line1 = $Line1Token$)
2.2.0 | 27/03/2025
  • Added method support in equality expressions, such as Property.Count(Member = '5') >= '1'
2.1.3 | 17/09/2024
  • Fixed endless looping on invalid expression.
2.1.1 | 07/05/2024
  • Minor readme changes: fixed content link.
2.1.1 | 07/05/2024
  • Introduced ChangeLog
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.4.3 216 8/5/2025
2.4.2 147 6/26/2025
2.4.1 263 5/13/2025
2.4.0 159 5/6/2025
2.3.0 184 4/7/2025
2.2.1 173 4/3/2025
2.2.0 161 3/27/2025
2.1.3 231 9/17/2024
2.1.2 176 5/8/2024
2.1.1 146 5/7/2024
2.1.0 175 5/1/2024
2.0.1 178 4/4/2024
2.0.0 195 2/26/2024
1.1.1 165 2/7/2024
1.1.0 134 2/3/2024
1.0.4 597 1/27/2022
1.0.3 531 1/27/2022
1.0.2 514 1/26/2022
1.0.1 523 1/25/2022
1.0.0 531 1/25/2022