From 6ea88940e6c9fd81a9d3aefc48076305b8e206dd Mon Sep 17 00:00:00 2001 From: vivet Date: Fri, 30 Oct 2026 09:18:58 +0100 Subject: [PATCH 1/2] Added NotIn, Updated NuGets --- .github/workflows/build-and-deploy.yml | 2 +- .../CriteriaBuilderTest.cs | 30 ++++++++++++++ .../Tests.DynamicExpression.csproj | 6 +-- DynamicExpression/CriteriaBuilder.cs | 40 ++++++++++++++++++- DynamicExpression/DynamicExpression.csproj | 2 +- README.md | 3 ++ 6 files changed, 76 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 9cd00dd..7a29f02 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -8,7 +8,7 @@ on: - master env: APP_NAME: DynamicExpression - VERSION: 10.0.6 + VERSION: 10.0.7 NUGET_HOST: https://api.nuget.org/v3/index.json NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }} jobs: diff --git a/.tests/Tests.DynamicExpression/CriteriaBuilderTest.cs b/.tests/Tests.DynamicExpression/CriteriaBuilderTest.cs index a21c657..747bf31 100644 --- a/.tests/Tests.DynamicExpression/CriteriaBuilderTest.cs +++ b/.tests/Tests.DynamicExpression/CriteriaBuilderTest.cs @@ -680,6 +680,36 @@ public void BuildWhenReferenceCollectionTest() Assert.AreEqual("x.Orders.Any(i => ((i.Payment.Id != null) AndAlso (i.Payment.Id == \"value\")))", expression.Body.ToString()); } + [TestMethod] + public void BuildWhenNotEqualReferenceCollectionTest() + { + var criteriaExpression = new CriteriaExpression(); + criteriaExpression.NotEqual("Orders[Payment.Id]", "value"); + + var expression = CriteriaBuilder.Build(criteriaExpression); + + Assert.IsNotNull(expression); + Assert.IsNotNull(expression.Compile()); + Assert.AreEqual("Not(x.Orders.Any(i => ((i.Payment.Id != null) AndAlso (i.Payment.Id == \"value\"))))", expression.Body.ToString()); + } + + [TestMethod] + public void BuildWhenNotEqualReferenceCollectionLeavesOutMatchesTest() + { + var criteriaExpression = new CriteriaExpression(); + criteriaExpression.NotEqual("Orders[Payment.Id]", "value"); + + var predicate = CriteriaBuilder.Build(criteriaExpression).Compile(); + + var withMatch = new Customer { Orders = [new Order { Payment = new Payment { Id = "value" } }, new Order { Payment = new Payment { Id = "other" } }] }; + var withoutMatch = new Customer { Orders = [new Order { Payment = new Payment { Id = "other" } }] }; + var withoutOrders = new Customer { Orders = [] }; + + Assert.IsFalse(predicate(withMatch)); + Assert.IsTrue(predicate(withoutMatch)); + Assert.IsTrue(predicate(withoutOrders)); + } + [TestMethod] public void BuildWhenRefernceAndReferenceCollectionTest() { diff --git a/.tests/Tests.DynamicExpression/Tests.DynamicExpression.csproj b/.tests/Tests.DynamicExpression/Tests.DynamicExpression.csproj index 6a18eb3..884749a 100644 --- a/.tests/Tests.DynamicExpression/Tests.DynamicExpression.csproj +++ b/.tests/Tests.DynamicExpression/Tests.DynamicExpression.csproj @@ -9,9 +9,9 @@ - - - + + + diff --git a/DynamicExpression/CriteriaBuilder.cs b/DynamicExpression/CriteriaBuilder.cs index 565a0cf..8d05237 100644 --- a/DynamicExpression/CriteriaBuilder.cs +++ b/DynamicExpression/CriteriaBuilder.cs @@ -121,7 +121,6 @@ private static Expression GetExpression(Expression parameter, Criteria criteria, throw new ArgumentException($"Criteria for property '{name}' cannot be applied: operation '{operationType}' with value type '{value.Type.Name}' is not compatible with the property's type '{member.Type.Name}'.", ex); } } - private static Expression GetExpressionCore(Expression member, Expression value, Expression value2, OperationType operationType) { if (value.Type.IsEnum) @@ -429,6 +428,27 @@ private static Expression GetExpressionCore(Expression member, Expression value, throw new NotSupportedException($"'{operationType}' is not supported by '{value.Type}' "); } + private static bool TryGetPositiveOperation(OperationType operationType, out OperationType positiveOperation) + { + switch (operationType) + { + case OperationType.NotEqual: + positiveOperation = OperationType.Equal; + return true; + + case OperationType.NotIn: + positiveOperation = OperationType.In; + return true; + + case OperationType.NotContains: + positiveOperation = OperationType.Contains; + return true; + + default: + positiveOperation = operationType; + return false; + } + } private static Expression BuildExpression(CriteriaExpression criteriaExpression, Expression parameter) { var prevLogicalType = LogicalType.And; @@ -467,10 +487,26 @@ private static Expression BuildExpression(CriteriaExpression criteriaExpression, var methodAny = typeof(Enumerable).GetRuntimeMethods().First(x => x.Name == "Any" && x.GetParameters().Length == 2).MakeGenericMethod(type); var memberAny = CriteriaBuilder.GetMember(paramNested, baseName); var parameterAny = Expression.Parameter(type, "i"); - var expressionAny = CriteriaBuilder.GetExpression(parameterAny, criteria, name); + var isNegated = CriteriaBuilder.TryGetPositiveOperation(criteria.OperationType, out var positiveOperation); + var elementCriteria = isNegated + ? new Criteria + { + Property = criteria.Property, + Value = criteria.Value, + Value2 = criteria.Value2, + LogicalType = criteria.LogicalType, + OperationType = positiveOperation + } + : criteria; + var expressionAny = CriteriaBuilder.GetExpression(parameterAny, elementCriteria, name); var expr2 = Expression.Lambda(expressionAny, parameterAny); innerExpression = Expression.Call(methodAny, memberAny, expr2); + + if (isNegated) + { + innerExpression = Expression.Not(innerExpression); + } } else { diff --git a/DynamicExpression/DynamicExpression.csproj b/DynamicExpression/DynamicExpression.csproj index 9d4db28..e138e27 100644 --- a/DynamicExpression/DynamicExpression.csproj +++ b/DynamicExpression/DynamicExpression.csproj @@ -47,7 +47,7 @@ - + diff --git a/README.md b/README.md index 7e29f99..58d26f9 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,9 @@ Note, that not all operations are valid on all data types, but it should be appa * IsEmpty * IsNotEmpty +A property on a collection is written in brackets, such as `Orders[Total]`, and the operation must match at least one element: `GreaterThan("Orders[Total]", 100)` is `x.Orders.Any(i => i.Total > 100)`. +The negated operations (NotEqual, NotIn and NotContains) on a collection mean that no element matches: `NotEqual("Orders[Id]", id)` is `!x.Orders.Any(i => i.Id == id)`. + #### Linq Extensions The library comes with a few ```IQueryable``` extension methods. They serve to convert the ```CriteriaExpression``` returned by the ```GetExpression()```, into a valid linq expression. From 0c287aa6c575d3a4a70fa47aba630a0d394f7d0c Mon Sep 17 00:00:00 2001 From: vivet Date: Fri, 30 Oct 2026 09:40:54 +0100 Subject: [PATCH 2/2] Updated --- DynamicExpression/DynamicExpression.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/DynamicExpression/DynamicExpression.csproj b/DynamicExpression/DynamicExpression.csproj index e138e27..b4153ce 100644 --- a/DynamicExpression/DynamicExpression.csproj +++ b/DynamicExpression/DynamicExpression.csproj @@ -16,6 +16,7 @@ README.md - Updated NuGets + - Added NotIn expression https://raw.githubusercontent.com/vivet/DynamicExpression/master/LICENSE LICENSE