FunctionalDev.ExpressionHelpers
2.4.3
dotnet add package FunctionalDev.ExpressionHelpers --version 2.4.3
NuGet\Install-Package FunctionalDev.ExpressionHelpers -Version 2.4.3
<PackageReference Include="FunctionalDev.ExpressionHelpers" Version="2.4.3" />
<PackageVersion Include="FunctionalDev.ExpressionHelpers" Version="2.4.3" />
<PackageReference Include="FunctionalDev.ExpressionHelpers" />
paket add FunctionalDev.ExpressionHelpers --version 2.4.3
#r "nuget: FunctionalDev.ExpressionHelpers, 2.4.3"
#:package FunctionalDev.ExpressionHelpers@2.4.3
#addin nuget:?package=FunctionalDev.ExpressionHelpers&version=2.4.3
#tool nuget:?package=FunctionalDev.ExpressionHelpers&version=2.4.3
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
- Dynamic API Filtering: Accept filter expressions as API parameters
- Entity Framework Integration: Generate database queries from string expressions
- Configuration-Driven Business Rules: Store and execute rules from config files
- Report Builders: Dynamic filtering and projection for reporting systems
- 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
-
- Overview - String representation syntax
- Equality Comparison Operators - Supported comparison symbols
- Chaining Operators - And/Or logical operators
Tokens - Runtime expression substitution
-
- Calling Parser - Core API usage
- Basic Equality - Simple comparisons
- Method Calls - Function invocation
- Chaining - Complex expressions
- Null Handling - Null comparisons
- Token Examples - Advanced token scenarios
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 | Versions 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. |
-
.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 |