(feature): New expression extension methods

This commit is contained in:
Maksym Sadovnychyy 2025-01-10 18:45:43 +01:00
parent f0dc409be9
commit 01ad1ddb8e
5 changed files with 146 additions and 15 deletions

View File

@ -6,14 +6,15 @@ using MaksIT.Core.Extensions;
namespace MaksIT.Core.Tests.Extensions;
public class ExpressionExtensionsTests {
[Fact]
public void CombineWith_ShouldCombineTwoPredicates() {
public void AndAlso_ShouldCombineTwoPredicatesWithAndCondition() {
// Arrange
Expression<Func<TestEntity, bool>> firstPredicate = x => x.Age > 18;
Expression<Func<TestEntity, bool>> secondPredicate = x => (x.Name ?? "").StartsWith("A");
// Act
var combinedPredicate = firstPredicate.CombineWith(secondPredicate);
var combinedPredicate = firstPredicate.AndAlso(secondPredicate);
var compiledPredicate = combinedPredicate.Compile();
// Assert
@ -22,10 +23,98 @@ public class ExpressionExtensionsTests {
Assert.False(compiledPredicate(new TestEntity { Age = 20, Name = "Bob" }));
}
[Fact]
public void OrElse_ShouldCombineTwoPredicatesWithOrCondition() {
// Arrange
Expression<Func<TestEntity, bool>> firstPredicate = x => x.Age > 18;
Expression<Func<TestEntity, bool>> secondPredicate = x => (x.Name ?? "").StartsWith("A");
// Act
var combinedPredicate = firstPredicate.OrElse(secondPredicate);
var compiledPredicate = combinedPredicate.Compile();
// Assert
Assert.True(compiledPredicate(new TestEntity { Age = 20, Name = "Alice" }));
Assert.True(compiledPredicate(new TestEntity { Age = 17, Name = "Alice" }));
Assert.True(compiledPredicate(new TestEntity { Age = 20, Name = "Bob" }));
Assert.False(compiledPredicate(new TestEntity { Age = 17, Name = "Bob" }));
}
[Fact]
public void Not_ShouldNegatePredicate() {
// Arrange
Expression<Func<TestEntity, bool>> predicate = x => x.Age > 18;
// Act
var negatedPredicate = predicate.Not();
var compiledPredicate = negatedPredicate.Compile();
// Assert
Assert.False(compiledPredicate(new TestEntity { Age = 20 }));
Assert.True(compiledPredicate(new TestEntity { Age = 17 }));
}
[Fact]
public void AndAlso_ShouldHandleNullValues() {
// Arrange
Expression<Func<TestEntity, bool>> firstPredicate = x => x.Name != null;
Expression<Func<TestEntity, bool>> secondPredicate = x => (x.Name ?? "").Length > 3;
// Act
var combinedPredicate = firstPredicate.AndAlso(secondPredicate);
var compiledPredicate = combinedPredicate.Compile();
// Assert
Assert.False(compiledPredicate(new TestEntity { Name = null }));
Assert.True(compiledPredicate(new TestEntity { Name = "John" }));
}
[Fact]
public void Not_ShouldThrowExceptionForNullExpression() {
// Act & Assert
Assert.Throws<ArgumentNullException>(() => ExpressionExtensions.Not<TestEntity>(null!));
}
[Fact]
public void AndAlso_ShouldThrowExceptionForNullExpression() {
// Arrange
Expression<Func<TestEntity, bool>> firstPredicate = null!;
Expression<Func<TestEntity, bool>> secondPredicate = x => x.Age > 18;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => firstPredicate.AndAlso(secondPredicate));
}
[Fact]
public void OrElse_ShouldThrowExceptionForNullExpression() {
// Arrange
Expression<Func<TestEntity, bool>> firstPredicate = x => x.Age > 18;
Expression<Func<TestEntity, bool>> secondPredicate = null!;
// Act & Assert
Assert.Throws<ArgumentNullException>(() => firstPredicate.OrElse(secondPredicate));
}
[Fact]
public void Batch_ShouldDivideCollectionIntoBatchesOfGivenSize() {
// Arrange
var source = Enumerable.Range(1, 10);
int batchSize = 3;
// Act
var batches = source.Batch(batchSize).ToList();
// Assert
Assert.Equal(4, batches.Count);
Assert.Equal(new List<int> { 1, 2, 3 }, batches[0]);
Assert.Equal(new List<int> { 4, 5, 6 }, batches[1]);
Assert.Equal(new List<int> { 7, 8, 9 }, batches[2]);
Assert.Equal(new List<int> { 10 }, batches[3]);
}
private class TestEntity {
public Guid Id { get; set; }
public int Age { get; set; }
public string? Name { get; set; }
}
}

View File

@ -10,13 +10,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PackageReference Include="coverlet.collector" Version="6.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -1,14 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Linq.Expressions;
namespace MaksIT.Core.Extensions;
public static class ExpressionExtensions {
public static Expression<Func<T, bool>> CombineWith<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) {
public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) {
ArgumentNullException.ThrowIfNull(first);
ArgumentNullException.ThrowIfNull(second);
var parameter = first.Parameters[0];
var visitor = new SubstituteParameterVisitor(second.Parameters[0], parameter);
var secondBody = visitor.Visit(second.Body);
@ -17,6 +17,27 @@ public static class ExpressionExtensions {
return Expression.Lambda<Func<T, bool>>(combinedBody, parameter);
}
public static Expression<Func<T, bool>> OrElse<T>(this Expression<Func<T, bool>> first, Expression<Func<T, bool>> second) {
ArgumentNullException.ThrowIfNull(first);
ArgumentNullException.ThrowIfNull(second);
var parameter = first.Parameters[0];
var visitor = new SubstituteParameterVisitor(second.Parameters[0], parameter);
var secondBody = visitor.Visit(second.Body);
var combinedBody = Expression.OrElse(first.Body, secondBody);
return Expression.Lambda<Func<T, bool>>(combinedBody, parameter);
}
public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expression) {
if (expression == null) throw new ArgumentNullException(nameof(expression));
var parameter = expression.Parameters[0];
var body = Expression.Not(expression.Body);
return Expression.Lambda<Func<T, bool>>(body, parameter);
}
private class SubstituteParameterVisitor : ExpressionVisitor {
private readonly ParameterExpression _oldParameter;
private readonly ParameterExpression _newParameter;
@ -31,4 +52,21 @@ public static class ExpressionExtensions {
return node == _oldParameter ? _newParameter : base.VisitParameter(node);
}
}
public static IEnumerable<List<T>> Batch<T>(this IEnumerable<T> source, int batchSize) {
var batch = new List<T>(batchSize);
foreach (var item in source) {
batch.Add(item);
if (batch.Count == batchSize) {
yield return batch;
batch = new List<T>(batchSize);
}
}
if (batch.Any()) {
yield return batch;
}
}
}

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata -->
<PackageId>MaksIT.Core</PackageId>
<Version>1.3.4</Version>
<Version>1.3.5</Version>
<Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company>
<Product>MaksIT.Core</Product>

View File

@ -13,14 +13,18 @@ public class PagedRequest : RequestModelBase {
public bool IsAscending { get; set; } = true;
public Expression<Func<T, bool>> BuildFilterExpression<T>() {
if (string.IsNullOrWhiteSpace(Filters))
return BuildFilterExpression<T>(Filters);
}
public virtual Expression<Func<T, bool>> BuildFilterExpression<T>(string? filters) {
if (string.IsNullOrWhiteSpace(filters))
return x => true; // Returns an expression that doesn't filter anything.
// Get the type of T
var type = typeof(T);
// Adjust Filters to make Contains, StartsWith, EndsWith, ==, and != case-insensitive
string adjustedFilters = Filters;
string adjustedFilters = filters;
// Regex to find property names and methods
adjustedFilters = Regex.Replace(adjustedFilters, @"(\w+)\.(Contains|StartsWith|EndsWith)\(\""(.*?)\""\)", m => {