diff --git a/src/MaksIT.Core.Tests/Extensions/ExpressionExtensionsTests.cs b/src/MaksIT.Core.Tests/Extensions/ExpressionExtensionsTests.cs index 7998c52..6239dd8 100644 --- a/src/MaksIT.Core.Tests/Extensions/ExpressionExtensionsTests.cs +++ b/src/MaksIT.Core.Tests/Extensions/ExpressionExtensionsTests.cs @@ -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> firstPredicate = x => x.Age > 18; Expression> 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> firstPredicate = x => x.Age > 18; + Expression> 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> 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> firstPredicate = x => x.Name != null; + Expression> 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(() => ExpressionExtensions.Not(null!)); + } + + [Fact] + public void AndAlso_ShouldThrowExceptionForNullExpression() { + // Arrange + Expression> firstPredicate = null!; + Expression> secondPredicate = x => x.Age > 18; + + // Act & Assert + Assert.Throws(() => firstPredicate.AndAlso(secondPredicate)); + } + + [Fact] + public void OrElse_ShouldThrowExceptionForNullExpression() { + // Arrange + Expression> firstPredicate = x => x.Age > 18; + Expression> secondPredicate = null!; + + // Act & Assert + Assert.Throws(() => 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 { 1, 2, 3 }, batches[0]); + Assert.Equal(new List { 4, 5, 6 }, batches[1]); + Assert.Equal(new List { 7, 8, 9 }, batches[2]); + Assert.Equal(new List { 10 }, batches[3]); + } + private class TestEntity { public Guid Id { get; set; } public int Age { get; set; } public string? Name { get; set; } } -} - +} \ No newline at end of file diff --git a/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj b/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj index 198808b..76169c8 100644 --- a/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj +++ b/src/MaksIT.Core.Tests/MaksIT.Core.Tests.csproj @@ -10,13 +10,13 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/MaksIT.Core/Extensions/ExpressionExtensions.cs b/src/MaksIT.Core/Extensions/ExpressionExtensions.cs index eff7665..c2e718e 100644 --- a/src/MaksIT.Core/Extensions/ExpressionExtensions.cs +++ b/src/MaksIT.Core/Extensions/ExpressionExtensions.cs @@ -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> CombineWith(this Expression> first, Expression> second) { + public static Expression> AndAlso(this Expression> first, Expression> 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>(combinedBody, parameter); } + public static Expression> OrElse(this Expression> first, Expression> 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>(combinedBody, parameter); + } + + public static Expression> Not(this Expression> expression) { + if (expression == null) throw new ArgumentNullException(nameof(expression)); + + var parameter = expression.Parameters[0]; + var body = Expression.Not(expression.Body); + + return Expression.Lambda>(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> Batch(this IEnumerable source, int batchSize) { + var batch = new List(batchSize); + foreach (var item in source) { + batch.Add(item); + if (batch.Count == batchSize) { + yield return batch; + batch = new List(batchSize); + } + } + if (batch.Any()) { + yield return batch; + } + } + + + } diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj index 8e1ce51..a03cd5d 100644 --- a/src/MaksIT.Core/MaksIT.Core.csproj +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -8,7 +8,7 @@ MaksIT.Core - 1.3.4 + 1.3.5 Maksym Sadovnychyy MAKS-IT MaksIT.Core diff --git a/src/MaksIT.Core/Webapi/Models/PagedRequest.cs b/src/MaksIT.Core/Webapi/Models/PagedRequest.cs index 85296d6..eb2b5fc 100644 --- a/src/MaksIT.Core/Webapi/Models/PagedRequest.cs +++ b/src/MaksIT.Core/Webapi/Models/PagedRequest.cs @@ -13,14 +13,18 @@ public class PagedRequest : RequestModelBase { public bool IsAscending { get; set; } = true; public Expression> BuildFilterExpression() { - if (string.IsNullOrWhiteSpace(Filters)) + return BuildFilterExpression(Filters); + } + + public virtual Expression> BuildFilterExpression(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 => {