(feature): New expression extension methods
This commit is contained in:
parent
f0dc409be9
commit
01ad1ddb8e
@ -6,14 +6,15 @@ using MaksIT.Core.Extensions;
|
|||||||
namespace MaksIT.Core.Tests.Extensions;
|
namespace MaksIT.Core.Tests.Extensions;
|
||||||
|
|
||||||
public class ExpressionExtensionsTests {
|
public class ExpressionExtensionsTests {
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CombineWith_ShouldCombineTwoPredicates() {
|
public void AndAlso_ShouldCombineTwoPredicatesWithAndCondition() {
|
||||||
// Arrange
|
// Arrange
|
||||||
Expression<Func<TestEntity, bool>> firstPredicate = x => x.Age > 18;
|
Expression<Func<TestEntity, bool>> firstPredicate = x => x.Age > 18;
|
||||||
Expression<Func<TestEntity, bool>> secondPredicate = x => (x.Name ?? "").StartsWith("A");
|
Expression<Func<TestEntity, bool>> secondPredicate = x => (x.Name ?? "").StartsWith("A");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var combinedPredicate = firstPredicate.CombineWith(secondPredicate);
|
var combinedPredicate = firstPredicate.AndAlso(secondPredicate);
|
||||||
var compiledPredicate = combinedPredicate.Compile();
|
var compiledPredicate = combinedPredicate.Compile();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@ -22,10 +23,98 @@ public class ExpressionExtensionsTests {
|
|||||||
Assert.False(compiledPredicate(new TestEntity { Age = 20, Name = "Bob" }));
|
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 {
|
private class TestEntity {
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public int Age { get; set; }
|
public int Age { get; set; }
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10,13 +10,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
<PackageReference Include="coverlet.collector" Version="6.0.3">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
using System;
|
using System.Linq.Expressions;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.Core.Extensions;
|
namespace MaksIT.Core.Extensions;
|
||||||
|
|
||||||
public static class ExpressionExtensions {
|
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 parameter = first.Parameters[0];
|
||||||
var visitor = new SubstituteParameterVisitor(second.Parameters[0], parameter);
|
var visitor = new SubstituteParameterVisitor(second.Parameters[0], parameter);
|
||||||
var secondBody = visitor.Visit(second.Body);
|
var secondBody = visitor.Visit(second.Body);
|
||||||
@ -17,6 +17,27 @@ public static class ExpressionExtensions {
|
|||||||
return Expression.Lambda<Func<T, bool>>(combinedBody, parameter);
|
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 class SubstituteParameterVisitor : ExpressionVisitor {
|
||||||
private readonly ParameterExpression _oldParameter;
|
private readonly ParameterExpression _oldParameter;
|
||||||
private readonly ParameterExpression _newParameter;
|
private readonly ParameterExpression _newParameter;
|
||||||
@ -31,4 +52,21 @@ public static class ExpressionExtensions {
|
|||||||
return node == _oldParameter ? _newParameter : base.VisitParameter(node);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<!-- NuGet package metadata -->
|
<!-- NuGet package metadata -->
|
||||||
<PackageId>MaksIT.Core</PackageId>
|
<PackageId>MaksIT.Core</PackageId>
|
||||||
<Version>1.3.4</Version>
|
<Version>1.3.5</Version>
|
||||||
<Authors>Maksym Sadovnychyy</Authors>
|
<Authors>Maksym Sadovnychyy</Authors>
|
||||||
<Company>MAKS-IT</Company>
|
<Company>MAKS-IT</Company>
|
||||||
<Product>MaksIT.Core</Product>
|
<Product>MaksIT.Core</Product>
|
||||||
|
|||||||
@ -13,14 +13,18 @@ public class PagedRequest : RequestModelBase {
|
|||||||
public bool IsAscending { get; set; } = true;
|
public bool IsAscending { get; set; } = true;
|
||||||
|
|
||||||
public Expression<Func<T, bool>> BuildFilterExpression<T>() {
|
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.
|
return x => true; // Returns an expression that doesn't filter anything.
|
||||||
|
|
||||||
// Get the type of T
|
// Get the type of T
|
||||||
var type = typeof(T);
|
var type = typeof(T);
|
||||||
|
|
||||||
// Adjust Filters to make Contains, StartsWith, EndsWith, ==, and != case-insensitive
|
// Adjust Filters to make Contains, StartsWith, EndsWith, ==, and != case-insensitive
|
||||||
string adjustedFilters = Filters;
|
string adjustedFilters = filters;
|
||||||
|
|
||||||
// Regex to find property names and methods
|
// Regex to find property names and methods
|
||||||
adjustedFilters = Regex.Replace(adjustedFilters, @"(\w+)\.(Contains|StartsWith|EndsWith)\(\""(.*?)\""\)", m => {
|
adjustedFilters = Regex.Replace(adjustedFilters, @"(\w+)\.(Contains|StartsWith|EndsWith)\(\""(.*?)\""\)", m => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user