-
Notifications
You must be signed in to change notification settings - Fork 7
Description
I encountered a case where Linq.Expression.Optimizer was applied to a logical expression, but the resulting optimized expression was not fully simplified to its minimal form. After manual analysis, I found that the expression can be reduced further, which suggests that the optimizer is not performing complete logical simplifications in this case.
Original Expression
The following logical expression is my original input:
c => (
(
(
(
(!c.IsRestricted && !c.IsAnonymized) &&
!c.IsDeleted
) ||
(
(c.IsRestricted && !c.IsAnonymized) &&
!c.IsDeleted
)
) ||
(
c.IsAnonymized && !c.IsDeleted
)
) ||
c.IsDeleted
)
Optimized Expression (Generated by Linq.Expression.Optimizer)
c => (
(
(
!(
(c.IsRestricted || c.IsAnonymized) ||
c.IsDeleted
) ||
(
(c.IsRestricted && !c.IsAnonymized) &&
!c.IsDeleted
)
) ||
(
c.IsAnonymized && !c.IsDeleted
)
) ||
c.IsDeleted
)
While this version is slightly rewritten, it is not fully optimized.
Manual Simplification
By manually simplifying the logical structure, we can reduce it to:
c => (!c.IsDeleted || c.IsDeleted)
which always evaluates to true.
Expected Behavior
The optimizer should ideally recognize that the entire expression simplifies to true and return c => true instead of keeping unnecessary logical operations.
Environment
Linq.Expression.Optimizer Version: 1.0.24
.NET 9
Windows 11
Reproducible demo:
using System;
using System.Linq;
using LinqKit;
class Program
{
static void Main()
{
var statuses = Enum.GetValues(typeof(ClientStatus)).Cast<ClientStatus>();
var predicate = PredicateBuilder.New<Client>(false);
foreach (var status in statuses.Distinct())
{
predicate = status switch
{
ClientStatus.Active => predicate.Or(c => !c.IsRestricted && !c.IsAnonymized && !c.IsDeleted),
ClientStatus.Restricted => predicate.Or(c => c.IsRestricted && !c.IsAnonymized && !c.IsDeleted),
ClientStatus.Anonymized => predicate.Or(c => c.IsAnonymized && !c.IsDeleted),
ClientStatus.Deleted => predicate.Or(c => c.IsDeleted),
_ => throw new ArgumentOutOfRangeException(nameof(statuses), status, $@"Unsupported ClientStatus: {status}")
};
}
// Optimize expression
var optimized = OptimizeExtension.Optimize<Func<Client, bool>>(predicate);
Console.WriteLine("Original Expression:");
Console.WriteLine(predicate);
Console.WriteLine("\nOptimized Expression:");
Console.WriteLine(optimized);
}
}
public class Client
{
public bool IsRestricted { get; set; }
public bool IsAnonymized { get; set; }
public bool IsDeleted { get; set; }
}
public enum ClientStatus
{
Active,
Restricted,
Anonymized,
Deleted
}
Demo output
Original Expression:
c => (((((Not(c.IsRestricted) AndAlso Not(c.IsAnonymized)) AndAlso Not(c.IsDeleted)) OrElse ((c.IsRestricted AndAlso Not(c.IsAnonymized)) AndAlso Not(c.IsDeleted))) OrElse (c.IsAnonymized AndAlso Not(c.IsDeleted))) OrElse c.IsDeleted)
Optimized Expression:
c => (((Not(((c.IsRestricted OrElse c.IsAnonymized) OrElse c.IsDeleted)) OrElse ((c.IsRestricted AndAlso Not(c.IsAnonymized)) AndAlso Not(c.IsDeleted))) OrElse (c.IsAnonymized AndAlso Not(c.IsDeleted))) OrElse c.IsDeleted)
Bonus
The explanation for binary logic optimization for this particular case:
https://chatgpt.com/share/67d0ce83-e1dc-8004-a36d-94be9b45ab00