diff --git a/src/MaksIT.Core.Tests/Extensions/ExpressionExtensionsTests.cs b/src/MaksIT.Core.Tests/Extensions/ExpressionExtensionsTests.cs index f99cef3..3a1b58f 100644 --- a/src/MaksIT.Core.Tests/Extensions/ExpressionExtensionsTests.cs +++ b/src/MaksIT.Core.Tests/Extensions/ExpressionExtensionsTests.cs @@ -6,34 +6,6 @@ using MaksIT.Core.Extensions; namespace MaksIT.Core.Tests.Extensions; public class ExpressionExtensionsTests { - [Fact] - public void CreateContainsPredicate_ShouldReturnTrue_WhenIdIsInList() { - // Arrange - var ids = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; - var targetId = ids[1]; - var predicate = ExpressionExtensions.CreateContainsPredicate(ids, nameof(TestEntity.Id)); - - // Act - var result = predicate.Compile()(new TestEntity { Id = targetId }); - - // Assert - Assert.True(result); - } - - [Fact] - public void CreateContainsPredicate_ShouldReturnFalse_WhenIdIsNotInList() { - // Arrange - var ids = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; - var targetId = Guid.NewGuid(); - var predicate = ExpressionExtensions.CreateContainsPredicate(ids, nameof(TestEntity.Id)); - - // Act - var result = predicate.Compile()(new TestEntity { Id = targetId }); - - // Assert - Assert.False(result); - } - [Fact] public void CombineWith_ShouldCombineTwoPredicates() { // Arrange diff --git a/src/MaksIT.Core.Tests/Webapi/Models/PagedRequestTests.cs b/src/MaksIT.Core.Tests/Webapi/Models/PagedRequestTests.cs new file mode 100644 index 0000000..685d632 --- /dev/null +++ b/src/MaksIT.Core.Tests/Webapi/Models/PagedRequestTests.cs @@ -0,0 +1,136 @@ + +using MaksIT.Core.Webapi.Models; + +namespace MaksIT.Core.Tests.Webapi.Models; + +public class PagedRequestTests { + [Fact] + public void BuildFilterExpression_ShouldReturnNull_WhenFilterIsEmpty() { + // Arrange + var request = new PagedRequest(); + + // Act + var result = request.BuildFilterExpression(null); + + // Assert + Assert.Null(result); + } + + [Fact] + public void BuildFilterExpression_ShouldHandleEqualsOperator() { + // Arrange + var request = new PagedRequest { + Filters = "Name='John'" + }; + + // Act + var expression = request.BuildFilterExpression(request.Filters); + var compiled = expression!.Compile(); + + // Assert + var testEntity = new TestEntity { Name = "John" }; + Assert.True(compiled(testEntity)); + } + + [Fact] + public void BuildFilterExpression_ShouldHandleNotEqualsOperator() { + // Arrange + var request = new PagedRequest { + Filters = "Name!='John'" + }; + + // Act + var expression = request.BuildFilterExpression(request.Filters); + var compiled = expression!.Compile(); + + // Assert + var testEntity = new TestEntity { Name = "John" }; + Assert.False(compiled(testEntity)); + } + + [Fact] + public void BuildFilterExpression_ShouldHandleGreaterThanOperator() { + // Arrange + var request = new PagedRequest { + Filters = "Age>30" + }; + + // Act + var expression = request.BuildFilterExpression(request.Filters); + var compiled = expression!.Compile(); + + // Assert + var testEntity = new TestEntity { Age = 31 }; + Assert.True(compiled(testEntity)); + } + + [Fact] + public void BuildFilterExpression_ShouldHandleLessThanOperator() { + // Arrange + var request = new PagedRequest { + Filters = "Age<30" + }; + + // Act + var expression = request.BuildFilterExpression(request.Filters); + var compiled = expression!.Compile(); + + // Assert + var testEntity = new TestEntity { Age = 29 }; + Assert.True(compiled(testEntity)); + } + + [Fact] + public void BuildFilterExpression_ShouldHandleAndOperator() { + // Arrange + var request = new PagedRequest { + Filters = "Name='John' && Age>30" + }; + + // Act + var expression = request.BuildFilterExpression(request.Filters); + var compiled = expression!.Compile(); + + // Assert + var testEntity = new TestEntity { Name = "John", Age = 31 }; + Assert.True(compiled(testEntity)); + } + + [Fact] + public void BuildFilterExpression_ShouldHandleOrOperator() { + // Arrange + var request = new PagedRequest { + Filters = "Name='John' || Age>30" + }; + + // Act + var expression = request.BuildFilterExpression(request.Filters); + var compiled = expression!.Compile(); + + // Assert + var testEntity = new TestEntity { Name = "Doe", Age = 31 }; + Assert.True(compiled(testEntity)); + } + + [Fact] + public void BuildFilterExpression_ShouldHandleNegation() { + // Arrange + var request = new PagedRequest { + Filters = "!Name='John'" + }; + + // Act + var expression = request.BuildFilterExpression(request.Filters); + var compiled = expression!.Compile(); + + // Assert + var testEntity = new TestEntity { Name = "Doe" }; + Assert.True(compiled(testEntity)); + } +} + +// Helper class for testing purposes +public class TestEntity { + public string Name { get; set; } + public int Age { get; set; } +} \ No newline at end of file diff --git a/src/MaksIT.Core.Tests/Webapi/Models/PagedResponseTests.cs b/src/MaksIT.Core.Tests/Webapi/Models/PagedResponseTests.cs new file mode 100644 index 0000000..09229d9 --- /dev/null +++ b/src/MaksIT.Core.Tests/Webapi/Models/PagedResponseTests.cs @@ -0,0 +1,86 @@ +using MaksIT.Core.Webapi.Models; + + +namespace MaksIT.Core.Tests.Webapi.Models; + +public class PagedResponseTests { + [Fact] + public void TotalPages_ShouldCalculateCorrectly() { + // Arrange + var items = new List { "Item1", "Item2" }; + var response = new PagedResponse(items, 10, 1, 2); + + // Act + var totalPages = response.TotalPages; + + // Assert + Assert.Equal(5, totalPages); + } + + [Fact] + public void HasPreviousPage_ShouldReturnTrue_WhenPageNumberGreaterThan1() { + // Arrange + var items = new List { "Item1", "Item2" }; + var response = new PagedResponse(items, 10, 2, 2); + + // Act + var hasPreviousPage = response.HasPreviousPage; + + // Assert + Assert.True(hasPreviousPage); + } + + [Fact] + public void HasPreviousPage_ShouldReturnFalse_WhenPageNumberIs1() { + // Arrange + var items = new List { "Item1", "Item2" }; + var response = new PagedResponse(items, 10, 1, 2); + + // Act + var hasPreviousPage = response.HasPreviousPage; + + // Assert + Assert.False(hasPreviousPage); + } + + [Fact] + public void HasNextPage_ShouldReturnTrue_WhenPageNumberLessThanTotalPages() { + // Arrange + var items = new List { "Item1", "Item2" }; + var response = new PagedResponse(items, 10, 1, 2); + + // Act + var hasNextPage = response.HasNextPage; + + // Assert + Assert.True(hasNextPage); + } + + [Fact] + public void HasNextPage_ShouldReturnFalse_WhenPageNumberEqualsTotalPages() { + // Arrange + var items = new List { "Item1", "Item2" }; + var response = new PagedResponse(items, 10, 5, 2); + + // Act + var hasNextPage = response.HasNextPage; + + // Assert + Assert.False(hasNextPage); + } + + [Fact] + public void Constructor_ShouldInitializePropertiesCorrectly() { + // Arrange + var items = new List { "Item1", "Item2" }; + + // Act + var response = new PagedResponse(items, 10, 1, 2); + + // Assert + Assert.Equal(items, response.Items); + Assert.Equal(10, response.TotalCount); + Assert.Equal(1, response.PageNumber); + Assert.Equal(2, response.PageSize); + } +} \ No newline at end of file diff --git a/src/MaksIT.Core/Extensions/ExpressionExtensions.cs b/src/MaksIT.Core/Extensions/ExpressionExtensions.cs index 30d8b3c..58a98e1 100644 --- a/src/MaksIT.Core/Extensions/ExpressionExtensions.cs +++ b/src/MaksIT.Core/Extensions/ExpressionExtensions.cs @@ -8,15 +8,6 @@ namespace MaksIT.Core.Extensions; public static class ExpressionExtensions { - public static Expression> CreateContainsPredicate(IEnumerable ids, string propertyName) { - var parameter = Expression.Parameter(typeof(T), "x"); - var property = Expression.Property(parameter, propertyName); - var containsMethod = typeof(List).GetMethod("Contains", new[] { typeof(Guid) }); - var containsCall = Expression.Call(Expression.Constant(ids), containsMethod!, property); - - return Expression.Lambda>(containsCall, parameter); - } - public static Expression> CombineWith(this Expression> first, Expression> second) { var parameter = first.Parameters[0]; var visitor = new SubstituteParameterVisitor(second.Parameters[0], parameter); diff --git a/src/MaksIT.Core/MaksIT.Core.csproj b/src/MaksIT.Core/MaksIT.Core.csproj index 1f2665a..35944e1 100644 --- a/src/MaksIT.Core/MaksIT.Core.csproj +++ b/src/MaksIT.Core/MaksIT.Core.csproj @@ -8,7 +8,7 @@ MaksIT.Core - 1.1.2 + 1.1.3 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 340f203..a8d5541 100644 --- a/src/MaksIT.Core/Webapi/Models/PagedRequest.cs +++ b/src/MaksIT.Core/Webapi/Models/PagedRequest.cs @@ -1,112 +1,155 @@ using System.Linq.Expressions; - using MaksIT.Core.Extensions; using MaksIT.Core.Abstractions.Webapi; +namespace MaksIT.Core.Webapi.Models { + public class PagedRequest : RequestModelBase { + public int PageSize { get; set; } = 100; + public int PageNumber { get; set; } = 1; -namespace MaksIT.Core.Webapi.Models; + public string? Filters { get; set; } + public Dictionary? CollectionFilters { get; set; } -public class PagedRequest : RequestModelBase { - public int PageSize { get; set; } = 100; - public int PageNumber { get; set; } = 1; + public string? SortBy { get; set; } + public bool IsAscending { get; set; } = true; - public string? Filters { get; set; } - public Dictionary? CollectionFilters { get; set; } + public Expression>? BuildCollectionFilterExpression(string collectionName) { + Expression>? globalFilterExpression = null; + Expression>? collectionFilterExpression = null; - public string? SortBy { get; set; } - public bool IsAscending { get; set; } = true; - - public Expression>? BuildCollectionFilterExpression(string collectionName) { - Expression>? globalFilterExpression = null; - Expression>? collectionFilterExpression = null; - - if (!string.IsNullOrEmpty(Filters)) { - globalFilterExpression = BuildFilterExpression(Filters); - } - - if (CollectionFilters != null && CollectionFilters.TryGetValue(collectionName, out var collectionFilter) && !string.IsNullOrEmpty(collectionFilter)) { - collectionFilterExpression = BuildFilterExpression(collectionFilter); - } - - if (globalFilterExpression == null && collectionFilterExpression == null) { - return null; - } - - if (globalFilterExpression != null && collectionFilterExpression != null) { - return globalFilterExpression.CombineWith(collectionFilterExpression); - } - - return globalFilterExpression ?? collectionFilterExpression; - } - - public Expression>? BuildFilterExpression(string filter) { - if (string.IsNullOrEmpty(filter)) { - return null; - } - - var parameter = Expression.Parameter(typeof(T), "x"); - var expressions = new List(); - - var filters = filter.Split(new[] { "AND", "OR" }, StringSplitOptions.None); - - foreach (var subFilter in filters) { - Expression? expression = null; - - if (subFilter.Contains('=')) { - var parts = subFilter.Split('='); - var propertyName = parts[0].Trim(); - var value = parts[1].Trim().Replace("'", ""); - - var property = Expression.Property(parameter, propertyName); - var constant = Expression.Constant(ConvertValue(property.Type, value)); - - expression = Expression.Equal(property, constant); - } - else if (subFilter.Contains('>') || subFilter.Contains('<')) { - expression = BuildComparisonExpression(subFilter, parameter); + if (!string.IsNullOrEmpty(Filters)) { + globalFilterExpression = BuildFilterExpression(Filters); } - if (expression != null) { - expressions.Add(expression); + if (CollectionFilters != null && CollectionFilters.TryGetValue(collectionName, out var collectionFilter) && !string.IsNullOrEmpty(collectionFilter)) { + collectionFilterExpression = BuildFilterExpression(collectionFilter); } + + if (globalFilterExpression == null && collectionFilterExpression == null) { + return null; + } + + if (globalFilterExpression != null && collectionFilterExpression != null) { + return globalFilterExpression.CombineWith(collectionFilterExpression); + } + + return globalFilterExpression ?? collectionFilterExpression; } - if (!expressions.Any()) { - return null; + public Expression>? BuildFilterExpression(string filter) { + if (string.IsNullOrEmpty(filter)) { + return null; + } + + var parameter = Expression.Parameter(typeof(T), "x"); + Expression? combinedExpression = null; + + // Split filters based on && and || operators + var tokens = filter.Split(new[] { "&&", "||" }, StringSplitOptions.None); + var operators = new List(); + + // Extract operators (&&, ||) from the original filter string + int lastIndex = 0; + for (int i = 0; i < filter.Length; i++) { + if (filter.Substring(i).StartsWith("&&")) { + operators.Add("&&"); + lastIndex = i + 2; + } + else if (filter.Substring(i).StartsWith("||")) { + operators.Add("||"); + lastIndex = i + 2; + } + } + + for (int i = 0; i < tokens.Length; i++) { + string processedFilter = tokens[i].Trim(); + bool isNegated = false; + + // Check for '!' at the beginning for negation + if (processedFilter.StartsWith("!")) { + isNegated = true; + processedFilter = processedFilter.Substring(1).Trim(); // Remove '!' and trim + } + + Expression? expression = null; + + if (processedFilter.Contains("!=")) { + var parts = processedFilter.Split(new[] { "!=" }, StringSplitOptions.None); + expression = BuildEqualityExpression(parameter, parts[0], parts[1], isNegated: true); + } + else if (processedFilter.Contains('=')) { + var parts = processedFilter.Split('='); + expression = BuildEqualityExpression(parameter, parts[0], parts[1], isNegated: false); + } + else if (processedFilter.Contains('>') || processedFilter.Contains('<')) { + // Handle comparison (>, <, >=, <=) + expression = BuildComparisonExpression(processedFilter, parameter); + } + + // Apply negation if '!' was found at the beginning + if (isNegated && expression != null) { + expression = Expression.Not(expression); + } + + // Combine the current expression with the previous one using the correct operator + if (combinedExpression == null) { + combinedExpression = expression; + } + else if (i - 1 < operators.Count) // Ensure we don't exceed the operators list size + { + var operatorType = operators[i - 1]; + combinedExpression = operatorType == "&&" + ? Expression.AndAlso(combinedExpression, expression) + : Expression.OrElse(combinedExpression, expression); + } + } + + return combinedExpression != null + ? Expression.Lambda>(combinedExpression, parameter) + : null; } - var combinedExpression = expressions.Aggregate(Expression.AndAlso); + private static Expression BuildEqualityExpression(ParameterExpression parameter, string propertyName, string value, bool isNegated) { + var property = Expression.Property(parameter, propertyName.Trim()); + var constant = Expression.Constant(ConvertValue(property.Type, value.Trim().Replace("'", ""))); - return Expression.Lambda>(combinedExpression, parameter); - } + return isNegated + ? Expression.NotEqual(property, constant) + : Expression.Equal(property, constant); + } - private static Expression? BuildComparisonExpression(string subFilter, ParameterExpression parameter) { - var comparisonType = subFilter.Contains(">=") ? ">=" : - subFilter.Contains("<=") ? "<=" : - subFilter.Contains(">") ? ">" : "<"; + private static Expression? BuildComparisonExpression(string subFilter, ParameterExpression parameter) { + var comparisonType = subFilter.Contains(">=") + ? ">=" + : subFilter.Contains("<=") + ? "<=" + : subFilter.Contains(">") + ? ">" + : "<"; - var parts = subFilter.Split(new[] { '>', '<', '=', ' ' }, StringSplitOptions.RemoveEmptyEntries); - var propertyName = parts[0].Trim(); - var value = parts[1].Trim().Replace("'", ""); + var parts = subFilter.Split(new[] { '>', '<', '=', ' ' }, StringSplitOptions.RemoveEmptyEntries); + var propertyName = parts[0].Trim(); + var value = parts[1].Trim().Replace("'", ""); - var property = Expression.Property(parameter, propertyName); - var constant = Expression.Constant(ConvertValue(property.Type, value)); + var property = Expression.Property(parameter, propertyName); + var constant = Expression.Constant(ConvertValue(property.Type, value)); - return comparisonType switch { - ">" => Expression.GreaterThan(property, constant), - "<" => Expression.LessThan(property, constant), - ">=" => Expression.GreaterThanOrEqual(property, constant), - "<=" => Expression.LessThanOrEqual(property, constant), - _ => null - }; - } + return comparisonType switch { + ">" => Expression.GreaterThan(property, constant), + "<" => Expression.LessThan(property, constant), + ">=" => Expression.GreaterThanOrEqual(property, constant), + "<=" => Expression.LessThanOrEqual(property, constant), + _ => null + }; + } - private static object ConvertValue(Type type, string value) { - return type switch { - var t when t == typeof(int) => int.Parse(value), - var t when t == typeof(bool) => bool.Parse(value), - var t when t == typeof(DateTime) => DateTime.Parse(value), - _ => value - }; + private static object ConvertValue(Type type, string value) { + return type switch { + var t when t == typeof(int) => int.Parse(value), + var t when t == typeof(bool) => bool.Parse(value), + var t when t == typeof(DateTime) => DateTime.Parse(value), + _ => value + }; + } } }