diff --git a/Ctoss.Example/JsonExamples.cs b/Ctoss.Example/JsonExamples.cs new file mode 100644 index 0000000..e0e6fc4 --- /dev/null +++ b/Ctoss.Example/JsonExamples.cs @@ -0,0 +1,51 @@ +namespace Ctoss.Example; + +public static class JsonExamples +{ + /// + /// This JSON snippet contains an example of the date range filter with + /// an illustration that field is case-insensitive. + /// + public const string PlainDateRangeFilter = + """ + { + "filters":{ + "PrOpErTy":{ + "filterType":"date", + "type":"inRange", + "dateFrom":"10/10/2002", + "dateTo":"10/12/2020" + } + } + } + """; + + /// + /// This illustrates how to define multiple conditions, and use CTOSS configuration to define the virtual field ms + /// + public const string MultipleConditionsNumberFilter = + """ + { + "filters":{ + "mc":{ + "filterType":"number", + "operator": "and", + "conditions":[ + { + "filterType":"number", + "type":"GreaterThan", + "filter":"10" + }, + { + "filterType":"number", + "type":"LessThan", + "filter":"15" + } + ] + } + } + } + """; + + +} diff --git a/Ctoss.Example/Program.cs b/Ctoss.Example/Program.cs index 35167f8..9c62d4d 100644 --- a/Ctoss.Example/Program.cs +++ b/Ctoss.Example/Program.cs @@ -1,5 +1,4 @@ -using Ctoss.Configuration; -using Ctoss.Configuration.Builders; +using Ctoss.Configuration.Builders; using Ctoss.Example; using Ctoss.Extensions; using Ctoss.Models; @@ -18,83 +17,8 @@ .Property(x => x.TextField, settings => { settings.IgnoreCase = true; }) .Apply(); -const string jsonFilter = - """ - { - "PrOpErTy": { - "filterType": "date", - "condition1": { - "filterType": "date", - "type": "inRange", - "dateFrom": "10/10/2002", - "dateTo": "10/12/2020" - }, - "conditions": [ - { - "filterType": "date", - "type": "inRange", - "date": "10/10/2002", - "dateTo": "10/12/2020" - } - ] - } - } - """; - -const string jsonNumericFilter = - """ - { - "mc": { - "filterType": "number", - "condition1": { - "filterType": "number", - "type": "inRange", - "filter": "1", - "filterTo": "20" - }, - "conditions": [ - { - "filterType": "number", - "type": "GreaterThan", - "filter": "10" - } - ] - } - } - """; - -const string jsonTextFilter = - """ - { - "TextField": { - "filterType": "text", - "condition1": { - "filterType": "text", - "type": "contains", - "filter": "a" - }, - "conditions": [ - { - "filterType": "text", - "type": "contains", - "filter": "a" - } - ] - } - } - """; - -/* - * The CTOSS gives you three overloads of the method WithFilter which evaluates a given filter and provides you with - * a filtering Expression> fully compatible with IQueryable and EF. - * - * Overloads: - * - WithFilter(this IQueryable query, string jsonFilter) - * - WithFilter(this IQueryable query, string propertyName, Filter filter) - * - WithFilter(this IQueryable query, Dictionary filters) - */ var entities = ExampleEntityFaker.GetN(100).AsQueryable() - .WithFilter(jsonFilter) // <-- This is the extension method from the ctoss library + .WithFilter(JsonExamples.PlainDateRangeFilter) .ToList(); Console.WriteLine("Filtered entities:"); @@ -112,23 +36,10 @@ }; var numericEntities = ExampleNumericEntityFaker.GetN(100).AsQueryable() - .WithFilter(jsonNumericFilter) // <-- This is the extension method from the ctoss library + .WithFilter(JsonExamples.MultipleConditionsNumberFilter) .WithSorting(sortings) .WithPagination(1, 10) .ToList(); foreach (var entity in numericEntities) Console.WriteLine($"A: {entity.A}, B: {entity.B}, SubEntity = ({entity.SubEntity.A + entity.SubEntity.B})"); - -Console.WriteLine("\nText entities:"); - -var textEntity = new ExampleTextEntity() -{ - TextField = "abc" -}; - -var textEntities = new List { textEntity }.AsQueryable() - .WithFilter(jsonTextFilter) // <-- This is the extension method from the ctoss library - .ToList(); - -foreach (var entity in textEntities) Console.WriteLine(entity.TextField); \ No newline at end of file diff --git a/Ctoss.Tests/DateFilterTests.cs b/Ctoss.Tests/DateFilterTests.cs index 4d6d647..cb86de2 100644 --- a/Ctoss.Tests/DateFilterTests.cs +++ b/Ctoss.Tests/DateFilterTests.cs @@ -1,7 +1,6 @@ using Ctoss.Builders.Filters; -using Ctoss.Models; -using Ctoss.Models.Conditions; using Ctoss.Models.Enums; +using Ctoss.Models.V2; using Ctoss.Tests.Models; namespace Ctoss.Tests; @@ -12,35 +11,19 @@ public class DateFilterTests private readonly List _testEntities = [ - new TestEntity - { - NumericProperty = 10, StringProperty = "abc", DateTimeProperty = new DateOnly(2022, 1, 1) - }, - new TestEntity - { - NumericProperty = 20, StringProperty = "def", DateTimeProperty = new DateOnly(2023, 2, 2) - }, - new TestEntity - { - NumericProperty = 30, StringProperty = "ghi", DateTimeProperty = new DateOnly(2024, 3, 3) - } + new TestEntity(numericProperty: 10, stringProperty: "abc", dateTimeProperty: new DateOnly(2022, 1, 1)), + new TestEntity(numericProperty: 20, stringProperty: "def", dateTimeProperty: new DateOnly(2023, 2, 2)), + new TestEntity(numericProperty: 30, stringProperty: "ghi", dateTimeProperty: new DateOnly(2024, 3, 3)) ]; [Fact] public void DateFilter_Equals_Success() { - var condition = new DateFilterCondition - { - DateFrom = "01/01/2022", - FilterType = "date", - Type = DateFilterOptions.Equals - }; - - var filter = new Filter + var filter = new FilterModel { FilterType = "date", - Condition1 = condition, - Conditions = new List { condition } + DateFrom = "01/01/2022", + Type = "equals" }; var expr = _filterBuilder.GetExpression("DateTimeProperty", filter)!; @@ -53,18 +36,11 @@ public void DateFilter_Equals_Success() [Fact] public void DateFilter_GreaterThen_Success() { - var condition = new DateFilterCondition + var filter = new FilterModel { - DateFrom = "02/02/2023", FilterType = "date", - Type = DateFilterOptions.GreaterThen - }; - - var filter = new Filter - { - FilterType = "date", - Condition1 = condition, - Conditions = new List { condition } + DateFrom = "02/02/2023", + Type = "GreaterThen" }; var expr = _filterBuilder.GetExpression("DateTimeProperty", filter)!; @@ -77,17 +53,10 @@ public void DateFilter_GreaterThen_Success() [Fact] public void DateFilter_Blank_Success() { - var condition = new DateFilterCondition - { - FilterType = "date", - Type = DateFilterOptions.Blank - }; - - var filter = new Filter + var filter = new FilterModel { FilterType = "date", - Condition1 = condition, - Conditions = new List { condition } + Type = "Blank" }; var expr = _filterBuilder.GetExpression("DateTimeProperty", filter)!; @@ -99,18 +68,11 @@ public void DateFilter_Blank_Success() [Fact] public void DateFilter_LessThen_Success() { - var condition = new DateFilterCondition - { - DateFrom = "01/01/2023", - FilterType = "date", - Type = DateFilterOptions.LessThen - }; - - var filter = new Filter + var filter = new FilterModel { FilterType = "date", - Condition1 = condition, - Conditions = new List { condition } + DateFrom = "01/01/2023", + Type = "LessThen" }; var expr = _filterBuilder.GetExpression("DateTimeProperty", filter)!; @@ -123,17 +85,10 @@ public void DateFilter_LessThen_Success() [Fact] public void DateFilter_NotBlank_Success() { - var condition = new DateFilterCondition + var filter = new FilterModel { FilterType = "date", - Type = DateFilterOptions.NotBlank - }; - - var filter = new Filter - { - FilterType = "date", - Condition1 = condition, - Conditions = new List { condition } + Type = "NotBlank" }; var expr = _filterBuilder.GetExpression("DateTimeProperty", filter)!; @@ -145,19 +100,12 @@ public void DateFilter_NotBlank_Success() [Fact] public void DateFilter_InRange_Success() { - var condition = new DateFilterCondition + var filter = new FilterModel { + FilterType = "date", DateFrom = "06/06/2021", DateTo = "09/09/2024", - FilterType = "date", - Type = DateFilterOptions.InRange - }; - - var filter = new Filter - { - FilterType = "date", - Condition1 = condition, - Conditions = new List { condition } + Type = "InRange" }; var expr = _filterBuilder.GetExpression("DateTimeProperty", filter)!; @@ -169,26 +117,24 @@ public void DateFilter_InRange_Success() [Fact] public void DateFilter_NotEquals_Success() { - var condition1 = new DateFilterCondition + var condition1 = new DateCondition { DateFrom = "01/01/2022", FilterType = "date", Type = DateFilterOptions.NotEquals }; - var condition2 = new DateFilterCondition + var condition2 = new DateCondition { DateFrom = "03/03/2024", FilterType = "date", Type = DateFilterOptions.NotEquals }; - var filter = new Filter + var filter = new FilterModel { FilterType = "date", Operator = Operator.And, - Condition1 = condition1, - Condition2 = condition2, - Conditions = new List { condition1, condition2 } + Conditions = new List { condition1, condition2 } }; var expr = _filterBuilder.GetExpression("DateTimeProperty", filter)!; @@ -201,27 +147,25 @@ public void DateFilter_NotEquals_Success() [Fact] public void DateFilter_Composed_Success() { - var condition1 = new DateFilterCondition + var condition1 = new DateCondition { DateFrom = "01/01/2022", FilterType = "date", Type = DateFilterOptions.NotEquals }; - var condition2 = new DateFilterCondition + var condition2 = new DateCondition { DateFrom = "03/03/2024", FilterType = "date", Type = DateFilterOptions.LessThen }; - var filter = new Filter + var filter = new FilterModel { - Operator = Operator.And, FilterType = "date", - Condition1 = condition1, - Condition2 = condition2, - Conditions = new List + Operator = Operator.And, + Conditions = new List { condition1, condition2 } @@ -233,4 +177,4 @@ public void DateFilter_Composed_Success() Assert.Single(result); Assert.Equal(new DateOnly(2023, 02, 02), result.First().DateTimeProperty); } -} +} \ No newline at end of file diff --git a/Ctoss.Tests/Models/TestEntity.cs b/Ctoss.Tests/Models/TestEntity.cs index 773ded5..c3f9212 100644 --- a/Ctoss.Tests/Models/TestEntity.cs +++ b/Ctoss.Tests/Models/TestEntity.cs @@ -2,6 +2,17 @@ public class TestEntity { + public TestEntity() + { + } + + public TestEntity(int? numericProperty, string? stringProperty, DateOnly dateTimeProperty) + { + NumericProperty = numericProperty; + StringProperty = stringProperty; + DateTimeProperty = dateTimeProperty; + } + public string? StringProperty { get; set; } = null!; public DateOnly DateTimeProperty { get; set; } public int? NumericProperty { get; set; } diff --git a/Ctoss.Tests/NumberFilterTests.cs b/Ctoss.Tests/NumberFilterTests.cs index cf3dab0..0dca290 100644 --- a/Ctoss.Tests/NumberFilterTests.cs +++ b/Ctoss.Tests/NumberFilterTests.cs @@ -1,7 +1,6 @@ using Ctoss.Builders.Filters; -using Ctoss.Models; -using Ctoss.Models.Conditions; using Ctoss.Models.Enums; +using Ctoss.Models.V2; using Ctoss.Tests.Models; namespace Ctoss.Tests; @@ -29,18 +28,11 @@ public class FilterTests [Fact] public void NumericFilter_Equals_Success() { - var condition = new NumberFilterCondition + var filter = new FilterModel { Filter = "10", FilterType = "number", - Type = NumberFilterOptions.Equals - }; - - var filter = new Filter - { - FilterType = "number", - Condition1 = condition, - Conditions = new List { condition } + Type = "Equals" }; var expr = _filterBuilder.GetExpression("NumericProperty", filter)!; @@ -53,20 +45,12 @@ public void NumericFilter_Equals_Success() [Fact] public void NumericFilter_GreaterThen_Success() { - var condition = new NumberFilterCondition - { - Filter = "20", - FilterType = "number", - Type = NumberFilterOptions.GreaterThan - }; - - var filter = new Filter + var filter = new FilterModel { FilterType = "number", - Condition1 = condition, - Conditions = new List { condition } + Filter = "20", + Type = "GreaterThan" }; - var expr = _filterBuilder.GetExpression("NumericProperty", filter)!; var result = _testEntities.AsQueryable().Where(expr).ToList(); @@ -77,18 +61,11 @@ public void NumericFilter_GreaterThen_Success() [Fact] public void NumericFilter_GreaterThenOrEquals_Success() { - var condition = new NumberFilterCondition + var filter = new FilterModel { Filter = "30", FilterType = "number", - Type = NumberFilterOptions.GreaterThanOrEqual - }; - - var filter = new Filter - { - FilterType = "number", - Condition1 = condition, - Conditions = new List { condition } + Type = "GreaterThanOrEqual" }; var expr = _filterBuilder.GetExpression("NumericProperty", filter)!; @@ -101,18 +78,11 @@ public void NumericFilter_GreaterThenOrEquals_Success() [Fact] public void NumericFilter_LessThen_Success() { - var condition = new NumberFilterCondition + var filter = new FilterModel { Filter = "20", FilterType = "number", - Type = NumberFilterOptions.LessThan - }; - - var filter = new Filter - { - FilterType = "number", - Condition1 = condition, - Conditions = new List { condition } + Type = "LessThan" }; var expr = _filterBuilder.GetExpression("NumericProperty", filter)!; @@ -125,18 +95,11 @@ public void NumericFilter_LessThen_Success() [Fact] public void NumericFilter_LessThenOrEquals_Success() { - var condition = new NumberFilterCondition + var filter = new FilterModel { Filter = "10", FilterType = "number", - Type = NumberFilterOptions.LessThanOrEqual - }; - - var filter = new Filter - { - FilterType = "number", - Condition1 = condition, - Conditions = new List { condition } + Type = "LessThanOrEqual" }; var expr = _filterBuilder.GetExpression("NumericProperty", filter)!; @@ -149,19 +112,12 @@ public void NumericFilter_LessThenOrEquals_Success() [Fact] public void NumericFilter_InRange_Success() { - var condition = new NumberFilterCondition + var filter = new FilterModel { Filter = "0", FilterTo = "12", FilterType = "number", - Type = NumberFilterOptions.InRange - }; - - var filter = new Filter - { - FilterType = "number", - Condition1 = condition, - Conditions = new List { condition } + Type = "InRange" }; var expr = _filterBuilder.GetExpression("NumericProperty", filter)!; @@ -174,26 +130,24 @@ public void NumericFilter_InRange_Success() [Fact] public void NumericFilter_NotEquals_Success() { - var condition1 = new NumberFilterCondition + var condition1 = new NumberCondition { Filter = "10", FilterType = "number", Type = NumberFilterOptions.NotEquals }; - var condition2 = new NumberFilterCondition + var condition2 = new NumberCondition { Filter = "20", FilterType = "number", Type = NumberFilterOptions.NotEquals }; - var filter = new Filter + var filter = new FilterModel { FilterType = "number", Operator = Operator.And, - Condition1 = condition1, - Condition2 = condition2, - Conditions = new List { condition1, condition2 } + Conditions = new List { condition1, condition2 } }; var expr = _filterBuilder.GetExpression("NumericProperty", filter)!; @@ -206,18 +160,11 @@ public void NumericFilter_NotEquals_Success() [Fact] public void NumericFilter_NotBlank_Success() { - var condition1 = new NumberFilterCondition + var filter = new FilterModel { Filter = "10", FilterType = "number", - Type = NumberFilterOptions.NotBlank - }; - - var filter = new Filter - { - FilterType = "number", - Condition1 = condition1, - Conditions = new List { condition1 } + Type = "NotBlank" }; var expr = _filterBuilder.GetExpression("NumericProperty", filter)!; @@ -229,50 +176,40 @@ public void NumericFilter_NotBlank_Success() [Fact] public void NumericFilter_Blank_Success() { - var condition1 = new NumberFilterCondition - { - Filter = "10", - FilterType = "number", - Type = NumberFilterOptions.Blank - }; - - var filter = new Filter + var filter = new FilterModel { FilterType = "number", - Condition1 = condition1, - Conditions = new List { condition1 } + Type = "Blank" }; var expr = _filterBuilder.GetExpression("NumericProperty", filter)!; var result = _testEntities.AsQueryable().Where(expr).ToList(); - Assert.Equal(0, result.Count); + Assert.Empty(result); } [Fact] public void NumericFilter_Composed_Success() { - var condition1 = new NumberFilterCondition + var condition1 = new NumberCondition { Filter = "25", FilterType = "number", Type = NumberFilterOptions.LessThan }; - var condition2 = new NumberFilterCondition + var condition2 = new NumberCondition { Filter = "10", FilterType = "number", Type = NumberFilterOptions.NotEquals }; - var filter = new Filter + var filter = new FilterModel { Operator = Operator.And, FilterType = "number", - Condition1 = condition1, - Condition2 = condition2, - Conditions = new List + Conditions = new List { condition1, condition2 } @@ -284,4 +221,4 @@ public void NumericFilter_Composed_Success() Assert.Single(result); Assert.Equal(20, result.First().NumericProperty); } -} +} \ No newline at end of file diff --git a/Ctoss.Tests/TextFilterTests.cs b/Ctoss.Tests/TextFilterTests.cs index 4e3d063..8b5f8be 100644 --- a/Ctoss.Tests/TextFilterTests.cs +++ b/Ctoss.Tests/TextFilterTests.cs @@ -1,7 +1,6 @@ using Ctoss.Builders.Filters; -using Ctoss.Models; -using Ctoss.Models.Conditions; using Ctoss.Models.Enums; +using Ctoss.Models.V2; using Ctoss.Tests.Models; namespace Ctoss.Tests; @@ -29,18 +28,11 @@ public class TextFilterTests [Fact] public void TextFilter_Equals_Success() { - var condition = new TextFilterCondition + var filter = new FilterModel { Filter = "abc", FilterType = "text", - Type = TextFilterOptions.Equals - }; - - var filter = new Filter - { - FilterType = "text", - Condition1 = condition, - Conditions = new List { condition } + Type = "Equals" }; var expr = _filterBuilder.GetExpression("StringProperty", filter)!; @@ -53,18 +45,11 @@ public void TextFilter_Equals_Success() [Fact] public void TextFilter_StartsWith_Success() { - var condition = new TextFilterCondition + var filter = new FilterModel { Filter = "a", FilterType = "text", - Type = TextFilterOptions.StartsWith - }; - - var filter = new Filter - { - FilterType = "text", - Condition1 = condition, - Conditions = new List { condition } + Type = "StartsWith" }; var expr = _filterBuilder.GetExpression("StringProperty", filter)!; @@ -77,18 +62,11 @@ public void TextFilter_StartsWith_Success() [Fact] public void TextFilter_EndsWith_Success() { - var condition = new TextFilterCondition + var filter = new FilterModel { Filter = "c", FilterType = "text", - Type = TextFilterOptions.EndsWith - }; - - var filter = new Filter - { - FilterType = "text", - Condition1 = condition, - Conditions = new List { condition } + Type = "EndsWith" }; var expr = _filterBuilder.GetExpression("StringProperty", filter)!; @@ -101,17 +79,10 @@ public void TextFilter_EndsWith_Success() [Fact] public void TextFilter_NotBlank_Success() { - var condition = new TextFilterCondition - { - FilterType = "text", - Type = TextFilterOptions.NotBlank - }; - - var filter = new Filter + var filter = new FilterModel { FilterType = "text", - Condition1 = condition, - Conditions = new List { condition } + Type = "NotBlank" }; var expr = _filterBuilder.GetExpression("StringProperty", filter)!; @@ -123,18 +94,11 @@ public void TextFilter_NotBlank_Success() [Fact] public void TextFilter_Contains_Success() { - var condition = new TextFilterCondition + var filter = new FilterModel { Filter = "ab", FilterType = "text", - Type = TextFilterOptions.Contains - }; - - var filter = new Filter - { - FilterType = "text", - Condition1 = condition, - Conditions = new List { condition } + Type = "Contains" }; var expr = _filterBuilder.GetExpression("StringProperty", filter)!; @@ -147,26 +111,24 @@ public void TextFilter_Contains_Success() [Fact] public void TextFilter_NotEquals_Success() { - var condition1 = new TextFilterCondition + var condition1 = new TextCondition { Filter = "abc", FilterType = "text", Type = TextFilterOptions.NotEquals }; - var condition2 = new TextFilterCondition + var condition2 = new TextCondition { Filter = "ghi", FilterType = "text", Type = TextFilterOptions.NotEquals }; - var filter = new Filter + var filter = new FilterModel { FilterType = "text", Operator = Operator.And, - Condition1 = condition1, - Condition2 = condition2, - Conditions = new List { condition1, condition2 } + Conditions = new List { condition1, condition2 } }; var expr = _filterBuilder.GetExpression("StringProperty", filter)!; @@ -179,27 +141,25 @@ public void TextFilter_NotEquals_Success() [Fact] public void TextFilter_Composed_Success() { - var condition1 = new TextFilterCondition + var condition1 = new TextCondition { Filter = "def", FilterType = "text", Type = TextFilterOptions.NotEquals }; - var condition2 = new TextFilterCondition + var condition2 = new TextCondition { Filter = "a", FilterType = "text", Type = TextFilterOptions.StartsWith }; - var filter = new Filter + var filter = new FilterModel { Operator = Operator.And, FilterType = "text", - Condition1 = condition1, - Condition2 = condition2, - Conditions = new List + Conditions = new List { condition1, condition2 } diff --git a/Ctoss/Builders/Filters/DateFilterBuilder.cs b/Ctoss/Builders/Filters/DateFilterBuilder.cs index 11ca072..bbbfb39 100644 --- a/Ctoss/Builders/Filters/DateFilterBuilder.cs +++ b/Ctoss/Builders/Filters/DateFilterBuilder.cs @@ -1,12 +1,12 @@ using System.Linq.Expressions; -using Ctoss.Models.Conditions; using Ctoss.Models.Enums; +using Ctoss.Models.V2; namespace Ctoss.Builders.Filters; -public class DateFilterBuilder : IPropertyFilterBuilder +public class DateFilterBuilder : IPropertyFilterBuilder { - public Expression> GetExpression(string property, DateFilterCondition condition) + public Expression> GetExpression(string property, DateCondition condition) { return condition.Type switch { @@ -18,7 +18,7 @@ DateFilterOptions.Blank or DateFilterOptions.NotBlank or DateFilterOptions.Empty }; } - private Expression> GetBlankExpression(string property, DateFilterCondition condition) + private Expression> GetBlankExpression(string property, DateCondition condition) { var propertyType = IPropertyBuilder.GetPropertyType(property); @@ -45,7 +45,7 @@ DateFilterOptions.Empty or DateFilterOptions.Blank }; } - private Expression> GetRangeExpression(string property, DateFilterCondition condition) + private Expression> GetRangeExpression(string property, DateCondition condition) { var propertyType = IPropertyBuilder.GetPropertyType(property); @@ -72,7 +72,7 @@ private Expression> GetRangeExpression(string property, DateFil Expression.AndAlso(greaterThan, lessThan), parameter); } - private Expression> GetComparisonExpression(string property, DateFilterCondition condition) + private Expression> GetComparisonExpression(string property, DateCondition condition) { var parameter = Expression.Parameter(typeof(T), "x"); diff --git a/Ctoss/Builders/Filters/FilterBuilder.cs b/Ctoss/Builders/Filters/FilterBuilder.cs index 327c579..942754a 100644 --- a/Ctoss/Builders/Filters/FilterBuilder.cs +++ b/Ctoss/Builders/Filters/FilterBuilder.cs @@ -1,38 +1,42 @@ using System.Linq.Expressions; using System.Reflection; using Ctoss.Extensions; -using Ctoss.Models; -using Ctoss.Models.Conditions; using Ctoss.Models.Enums; +using Ctoss.Models.V2; namespace Ctoss.Builders.Filters; public class FilterBuilder { - private readonly IPropertyFilterBuilder _textFilterBuilder = new TextFilterBuilder(); - private readonly IPropertyFilterBuilder _dateFilterBuilder = new DateFilterBuilder(); - private readonly IPropertyFilterBuilder _numberFilterBuilder = new NumberFilterBuilder(); + private readonly IPropertyFilterBuilder _textFilterBuilder = new TextFilterBuilder(); + private readonly IPropertyFilterBuilder _dateFilterBuilder = new DateFilterBuilder(); + private readonly IPropertyFilterBuilder _numberFilterBuilder = new NumberFilterBuilder(); + private readonly IPropertyFilterBuilder _setFilterBuilder = new SetFilterBuilder(); - public Expression>? GetExpression(Dictionary? filters) + public Expression>? GetExpression(AgGridFilter? filterSet) { - if (filters == null) + if (filterSet == null) return null; var expressions = new List>>(); - expressions.AddRange(filters.Select(filter => GetExpressionInternal(filter.Key, filter.Value))); + expressions.AddRange(filterSet.Filters + .Select(filter => GetExpressionInternal(filter.Key, filter.Value))); return expressions.Aggregate((acc, expr) => acc.AndAlso(expr)); } - public Expression>? GetExpression(string property, Filter filter) - => GetExpression(new Dictionary { { property, filter } }); + public Expression>? GetExpression(string property, FilterModel filter) + => GetExpression(new AgGridFilter + { + Filters = new Dictionary { { property, filter } } + }); - private Expression> GetExpressionInternal(string property, Filter? filter) + private Expression> GetExpressionInternal(string property, FilterModel? filter) { if (filter == null) return _ => true; - if (filter.Operator != Operator.NoOp) + if (filter.Operator != null && filter.Operator != Operator.NoOp) { return filter.Conditions? .Select(c => GetFilterExpr(property, c)) @@ -44,10 +48,46 @@ private Expression> GetExpressionInternal(string property, Filt })!; } - return GetFilterExpr(property, filter.Condition1); + return GetFilterExpr(property, MapPlainFilterToConditions(filter)); + } + + private static FilterConditionBase MapPlainFilterToConditions(FilterModel filter) + { + if (filter.Conditions is not null && filter.Conditions?.Count != 0) + throw new ArgumentException("The given filter is not a plain filter"); + + return filter.FilterType switch + { + "text" => new TextCondition + { + Type = Enum.Parse(filter.Type, ignoreCase: true), + Filter = filter.Filter!, + FilterType = filter.FilterType + }, + "date" => new DateCondition + { + Type = Enum.Parse(filter.Type, ignoreCase: true), + DateFrom = filter.DateFrom, + DateTo = filter.DateTo, + FilterType = filter.FilterType + }, + "number" => new NumberCondition + { + Type = Enum.Parse(filter.Type, ignoreCase: true), + Filter = filter.Filter!, + FilterTo = filter.FilterTo!, + FilterType = filter.FilterType + }, + "set" => new SetCondition + { + FilterType = filter.FilterType, + Values = filter.Values + }, + _ => throw new ArgumentException("Unknown filter type") + }; } - private Expression> GetFilterExpr(string property, FilterCondition? condition) + private Expression> GetFilterExpr(string property, FilterConditionBase? condition) { // NOTE: first of all, we're trying to get a real property name from the given one. // If we find it, we can use it to work with an expression. Else the given property name will be used. @@ -57,13 +97,15 @@ private Expression> GetFilterExpr(string property, FilterCondit var propertyName = normalizedProperty?.Name ?? property; return condition switch { - TextFilterCondition textCondition + TextCondition textCondition => _textFilterBuilder.GetExpression(propertyName, textCondition), - DateFilterCondition dateCondition + DateCondition dateCondition => _dateFilterBuilder.GetExpression(propertyName, dateCondition), - NumberFilterCondition numberCondition + NumberCondition numberCondition => _numberFilterBuilder.GetExpression(propertyName, numberCondition), + SetCondition setCondition + => _setFilterBuilder.GetExpression(propertyName, setCondition), _ => _ => true }; } -} +} \ No newline at end of file diff --git a/Ctoss/Builders/Filters/NumberFilterBuilder.cs b/Ctoss/Builders/Filters/NumberFilterBuilder.cs index 2353980..e7440a0 100644 --- a/Ctoss/Builders/Filters/NumberFilterBuilder.cs +++ b/Ctoss/Builders/Filters/NumberFilterBuilder.cs @@ -1,12 +1,12 @@ using System.Linq.Expressions; -using Ctoss.Models.Conditions; using Ctoss.Models.Enums; +using Ctoss.Models.V2; namespace Ctoss.Builders.Filters; -public class NumberFilterBuilder : IPropertyFilterBuilder +public class NumberFilterBuilder : IPropertyFilterBuilder { - public Expression> GetExpression(string property, NumberFilterCondition condition) + public Expression> GetExpression(string property, NumberCondition condition) { return condition.Type switch { @@ -18,7 +18,7 @@ NumberFilterOptions.Blank or NumberFilterOptions.NotBlank or NumberFilterOptions }; } - private Expression> GetBlankExpression(string property, NumberFilterCondition condition) + private Expression> GetBlankExpression(string property, NumberCondition condition) { var propertyType = IPropertyBuilder.GetPropertyType(property); @@ -49,7 +49,7 @@ NumberFilterOptions.Empty or NumberFilterOptions.NotBlank }; } - private Expression> GetRangeExpression(string property, NumberFilterCondition condition) + private Expression> GetRangeExpression(string property, NumberCondition condition) { if (string.IsNullOrEmpty(condition.Filter)) throw new ArgumentException("Filter value is required."); @@ -77,7 +77,7 @@ private Expression> GetRangeExpression(string property, NumberF ); } - private Expression> GetComparisonExpression(string property, NumberFilterCondition condition) + private Expression> GetComparisonExpression(string property, NumberCondition condition) { if (string.IsNullOrEmpty(condition.Filter)) throw new ArgumentException("Filter value is required."); diff --git a/Ctoss/Builders/Filters/SetFilterBuilder.cs b/Ctoss/Builders/Filters/SetFilterBuilder.cs new file mode 100644 index 0000000..72ed70d --- /dev/null +++ b/Ctoss/Builders/Filters/SetFilterBuilder.cs @@ -0,0 +1,12 @@ +using System.Linq.Expressions; +using Ctoss.Models.V2; + +namespace Ctoss.Builders.Filters; + +internal class SetFilterBuilder : IPropertyFilterBuilder +{ + public Expression> GetExpression(string property, SetCondition condition) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Ctoss/Builders/Filters/TextFilterBuilder.cs b/Ctoss/Builders/Filters/TextFilterBuilder.cs index c9b7809..2de7cfa 100644 --- a/Ctoss/Builders/Filters/TextFilterBuilder.cs +++ b/Ctoss/Builders/Filters/TextFilterBuilder.cs @@ -1,13 +1,13 @@ using System.Linq.Expressions; using Ctoss.Configuration; -using Ctoss.Models.Conditions; using Ctoss.Models.Enums; +using Ctoss.Models.V2; namespace Ctoss.Builders.Filters; -public class TextFilterBuilder : IPropertyFilterBuilder +public class TextFilterBuilder : IPropertyFilterBuilder { - public Expression> GetExpression(string property, TextFilterCondition condition) + public Expression> GetExpression(string property, TextCondition condition) { var parameter = Expression.Parameter(typeof(T), "x"); var propertyExpression = IPropertyFilterBuilder diff --git a/Ctoss/Ctoss.csproj b/Ctoss/Ctoss.csproj index 53b40fa..b4e89ab 100644 --- a/Ctoss/Ctoss.csproj +++ b/Ctoss/Ctoss.csproj @@ -5,7 +5,7 @@ enable enable Ctoss - 10 + 11 diff --git a/Ctoss/Extensions/AgGridExtensions.cs b/Ctoss/Extensions/AgGridExtensions.cs index 5d32861..832e37e 100644 --- a/Ctoss/Extensions/AgGridExtensions.cs +++ b/Ctoss/Extensions/AgGridExtensions.cs @@ -6,7 +6,7 @@ public static class AgGridExtensions { public static AgGridQueryResult Apply(this IEnumerable all, AgGridQuery query) { - var applyFilter = query.FilterModel is { Count: > 0 }; + var applyFilter = query.FilterModel!.Filters is { Count: > 0 }; if (applyFilter) all = all.WithFilter(query.FilterModel!); @@ -17,7 +17,7 @@ public static AgGridQueryResult Apply(this IEnumerable all, AgGridQuery var array = all.ToArray(); var totalCount = array.Length; var paginated = array - .Skip(query.StartRow - 1) + .Skip(query.StartRow) .Take(query.EndRow - query.StartRow) .ToList(); @@ -26,7 +26,7 @@ public static AgGridQueryResult Apply(this IEnumerable all, AgGridQuery public static AgGridQueryResult Apply(this IQueryable all, AgGridQuery query) { - var applyFilter = query.FilterModel is { Count: > 0 }; + var applyFilter = query.FilterModel!.Filters is { Count: > 0 }; if (applyFilter) all = all.WithFilter(query.FilterModel!); @@ -36,7 +36,7 @@ public static AgGridQueryResult Apply(this IQueryable all, AgGridQuery var totalCount = all.Count(); var paginated = all - .Skip(query.StartRow - 1) + .Skip(query.StartRow) .Take(query.EndRow - query.StartRow) .ToList(); diff --git a/Ctoss/Extensions/EnumerableExtensions.cs b/Ctoss/Extensions/EnumerableExtensions.cs index cc562bf..1405eb7 100644 --- a/Ctoss/Extensions/EnumerableExtensions.cs +++ b/Ctoss/Extensions/EnumerableExtensions.cs @@ -4,6 +4,7 @@ using Ctoss.Json; using Ctoss.Models; using Ctoss.Models.Enums; +using Ctoss.Models.V2; namespace Ctoss.Extensions; @@ -70,24 +71,29 @@ public static IEnumerable WithSorting(this IEnumerable query, List WithFilter( this IEnumerable query, string jsonFilter) => query.WithFilter( - JsonSerializer.Deserialize>( + JsonSerializer.Deserialize( jsonFilter, CtossJsonDefaults.DefaultJsonOptions) ); public static IEnumerable WithFilter( - this IEnumerable query, string propertyName, Filter? filter) => + this IEnumerable query, string propertyName, FilterModel? filter) => filter is null ? query - : WithFilter(query, new Dictionary { { propertyName, filter } }); + : WithFilter( + query, + new AgGridFilter + { + Filters = new Dictionary { { propertyName, filter } } + }); public static IEnumerable WithFilter( - this IEnumerable query, Dictionary? filters) + this IEnumerable query, AgGridFilter? filterSet) { - if (filters is null || !filters.Any()) + if (filterSet is null || !filterSet.Filters.Any()) return query; var filterBuilder = new FilterBuilder(); - var predicate = filterBuilder.GetExpression(filters); + var predicate = filterBuilder.GetExpression(filterSet); if (predicate is null) throw new ArgumentException("Invalid filter"); diff --git a/Ctoss/Extensions/QueryableExtensions.cs b/Ctoss/Extensions/QueryableExtensions.cs index 02de9c4..c332327 100644 --- a/Ctoss/Extensions/QueryableExtensions.cs +++ b/Ctoss/Extensions/QueryableExtensions.cs @@ -4,6 +4,7 @@ using Ctoss.Json; using Ctoss.Models; using Ctoss.Models.Enums; +using Ctoss.Models.V2; namespace Ctoss.Extensions; @@ -70,24 +71,29 @@ public static IQueryable WithSorting(this IQueryable query, List WithFilter( this IQueryable query, string jsonFilter) => query.WithFilter( - JsonSerializer.Deserialize>( + JsonSerializer.Deserialize( jsonFilter, CtossJsonDefaults.DefaultJsonOptions) ); public static IQueryable WithFilter( - this IQueryable query, string propertyName, Filter? filter) => + this IQueryable query, string propertyName, FilterModel? filter) => filter is null ? query - : WithFilter(query, new Dictionary { { propertyName, filter } }); + : WithFilter( + query, + new AgGridFilter + { + Filters = new Dictionary { { propertyName, filter } } + }); public static IQueryable WithFilter( - this IQueryable query, Dictionary? filters) + this IQueryable query, AgGridFilter? filtersSet) { - if (filters is null || !filters.Any()) + if (filtersSet is null || !filtersSet.Filters.Any()) return query; var filterBuilder = new FilterBuilder(); - var predicate = filterBuilder.GetExpression(filters); + var predicate = filterBuilder.GetExpression(filtersSet); if (predicate is null) throw new ArgumentException("Invalid filter"); diff --git a/Ctoss/Json/FilterConditionConverter.cs b/Ctoss/Json/FilterConditionConverter.cs deleted file mode 100644 index e25b591..0000000 --- a/Ctoss/Json/FilterConditionConverter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using Ctoss.Models.Conditions; - -namespace Ctoss.Json; - -internal class FilterConditionConverter : JsonConverter -{ - public override FilterCondition Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - using var doc = JsonDocument.ParseValue(ref reader); - var root = doc.RootElement; - var filterType = root.GetProperty("filterType").GetString() - ?? throw new ArgumentException("filterType is required"); - - return (filterType switch - { - "text" => JsonSerializer.Deserialize(root.GetRawText(), options), - "number" => JsonSerializer.Deserialize(root.GetRawText(), options), - "date" => JsonSerializer.Deserialize(root.GetRawText(), options), - _ => throw new NotSupportedException($"FilterType {filterType} is not supported") - })!; - } - - public override void Write(Utf8JsonWriter writer, FilterCondition value, JsonSerializerOptions options) - { - JsonSerializer.Serialize(writer, (object)value, options); - } -} diff --git a/Ctoss/Json/FilterConverter.cs b/Ctoss/Json/FilterConverter.cs new file mode 100644 index 0000000..88ae9dc --- /dev/null +++ b/Ctoss/Json/FilterConverter.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Ctoss.Models.V2; + +public class FilterConverter : JsonConverter +{ + public override FilterConditionBase? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var doc = JsonDocument.ParseValue(ref reader); + var root = doc.RootElement; + var filterType = root.GetProperty("filterType").GetString(); + + return filterType switch + { + "text" => JsonSerializer.Deserialize(root.GetRawText(), options), + "number" => JsonSerializer.Deserialize(root.GetRawText(), options), + "date" => JsonSerializer.Deserialize(root.GetRawText(), options), + "set" => JsonSerializer.Deserialize(root.GetRawText(), options), + "multi" => JsonSerializer.Deserialize(root.GetRawText(), options), + _ => throw new NotSupportedException($"Filter type '{filterType}' is not supported") + }; + } + + public override void Write(Utf8JsonWriter writer, FilterConditionBase? value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value.GetType(), options); + } +} \ No newline at end of file diff --git a/Ctoss/Json/JsonDefaults.cs b/Ctoss/Json/JsonDefaults.cs index 8905544..127c661 100644 --- a/Ctoss/Json/JsonDefaults.cs +++ b/Ctoss/Json/JsonDefaults.cs @@ -1,5 +1,7 @@ using System.Text.Json; +using System.Text.Json.Serialization; using Ctoss.Models.Enums; +using Ctoss.Models.V2; namespace Ctoss.Json; @@ -9,7 +11,8 @@ public static class CtossJsonDefaults { Converters = { - new FilterConditionConverter(), + new FilterConverter(), + new NumberToStringConverter(), new JsonStringEnumConverter(), new JsonStringEnumConverter(), new JsonStringEnumConverter(), @@ -17,5 +20,6 @@ public static class CtossJsonDefaults new JsonStringEnumConverter() }, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + NumberHandling = JsonNumberHandling.WriteAsString }; } diff --git a/Ctoss/Json/NumberToStringConverter.cs b/Ctoss/Json/NumberToStringConverter.cs new file mode 100644 index 0000000..b723dd7 --- /dev/null +++ b/Ctoss/Json/NumberToStringConverter.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Ctoss.Json; + +public class NumberToStringConverter : JsonConverter +{ + public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.Number => reader.GetDouble().ToString(), + JsonTokenType.String => reader.GetString(), + _ => throw new JsonException("Unexpected token type") + }; + } + + public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } +} diff --git a/Ctoss/Models/AgGrid/AgGridQuery.cs b/Ctoss/Models/AgGrid/AgGridQuery.cs index e70aa3c..abe6e51 100644 --- a/Ctoss/Models/AgGrid/AgGridQuery.cs +++ b/Ctoss/Models/AgGrid/AgGridQuery.cs @@ -1,8 +1,10 @@ +using Ctoss.Models.V2; + namespace Ctoss.Models.AgGrid; public record AgGridQuery( int StartRow, int EndRow, List? SortModel, - Dictionary? FilterModel -); \ No newline at end of file + AgGridFilter? FilterModel +); diff --git a/Ctoss/Models/Conditions/DateFilterCondition.cs b/Ctoss/Models/Conditions/DateFilterCondition.cs deleted file mode 100644 index 4b6f05a..0000000 --- a/Ctoss/Models/Conditions/DateFilterCondition.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Ctoss.Models.Enums; - -namespace Ctoss.Models.Conditions; - -public record DateFilterCondition : FilterCondition -{ - public string? DateFrom { get; init; } - public string? DateTo { get; init; } - public DateFilterOptions? Type { get; init; } -} diff --git a/Ctoss/Models/Conditions/FilterCondition.cs b/Ctoss/Models/Conditions/FilterCondition.cs deleted file mode 100644 index b400fdf..0000000 --- a/Ctoss/Models/Conditions/FilterCondition.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Ctoss.Models.Conditions; - -public record FilterCondition -{ - public string FilterType { get; init; } = null!; -} diff --git a/Ctoss/Models/Conditions/NumberFilterCondition.cs b/Ctoss/Models/Conditions/NumberFilterCondition.cs deleted file mode 100644 index 6ae06ba..0000000 --- a/Ctoss/Models/Conditions/NumberFilterCondition.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Ctoss.Models.Enums; - -namespace Ctoss.Models.Conditions; - -public record NumberFilterCondition : FilterCondition -{ - public string? Filter { get; init; } - public string? FilterTo { get; init; } - public NumberFilterOptions? Type { get; init; } -} diff --git a/Ctoss/Models/Conditions/TextFilterCondition.cs b/Ctoss/Models/Conditions/TextFilterCondition.cs deleted file mode 100644 index e13e195..0000000 --- a/Ctoss/Models/Conditions/TextFilterCondition.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ctoss.Models.Conditions; - -public record TextFilterCondition : FilterCondition -{ - public string? Filter { get; init; } - public Enums.TextFilterOptions Type { get; init; } -} diff --git a/Ctoss/Models/Filter.cs b/Ctoss/Models/Filter.cs deleted file mode 100644 index 836a7fd..0000000 --- a/Ctoss/Models/Filter.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Ctoss.Models.Conditions; -using Ctoss.Models.Enums; - -namespace Ctoss.Models; - -public class Filter -{ - public string FilterType { get; set; } = null!; - public Operator Operator { get; set; } - public FilterCondition? Condition1 { get; set; } - public FilterCondition? Condition2 { get; set; } - public List? Conditions { get; set; } -} diff --git a/Ctoss/Models/V2/AgGridFilter.cs b/Ctoss/Models/V2/AgGridFilter.cs new file mode 100644 index 0000000..764f799 --- /dev/null +++ b/Ctoss/Models/V2/AgGridFilter.cs @@ -0,0 +1,6 @@ +namespace Ctoss.Models.V2; + +public record AgGridFilter +{ + public Dictionary Filters { get; init; } = null!; +} \ No newline at end of file diff --git a/Ctoss/Models/V2/DateCondition.cs b/Ctoss/Models/V2/DateCondition.cs new file mode 100644 index 0000000..be095c3 --- /dev/null +++ b/Ctoss/Models/V2/DateCondition.cs @@ -0,0 +1,10 @@ +using Ctoss.Models.Enums; + +namespace Ctoss.Models.V2; + +public record DateCondition : FilterConditionBase +{ + public DateFilterOptions Type { get; init; } + public string? DateFrom { get; init; } + public string? DateTo { get; init; } +} \ No newline at end of file diff --git a/Ctoss/Models/V2/FilterConditionBase.cs b/Ctoss/Models/V2/FilterConditionBase.cs new file mode 100644 index 0000000..0bd5cdb --- /dev/null +++ b/Ctoss/Models/V2/FilterConditionBase.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Ctoss.Models.V2; + +[JsonConverter(typeof(FilterConverter))] +public abstract record FilterConditionBase +{ + public string FilterType { get; init; } = null!; +} \ No newline at end of file diff --git a/Ctoss/Models/V2/FilterModel.cs b/Ctoss/Models/V2/FilterModel.cs new file mode 100644 index 0000000..2ab0c2c --- /dev/null +++ b/Ctoss/Models/V2/FilterModel.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; +using Ctoss.Json; +using Ctoss.Models.Enums; + +namespace Ctoss.Models.V2; + +public record FilterModel +{ + public string FilterType { get; init; } = null!; + public string Type { get; init; } = null!; + public Operator? Operator { get; init; } + public List? Conditions { get; init; } + + // This is done to support plain filters. Most of these properties will be empty + [JsonConverter(typeof(NumberToStringConverter))] + public string? Filter { get; set; } + [JsonConverter(typeof(NumberToStringConverter))] + public string? FilterTo { get; set; } + public string? DateFrom { get; set; } + public string? DateTo { get; set; } + public List Values { get; init; } = null!; +} \ No newline at end of file diff --git a/Ctoss/Models/V2/MultiCondition.cs b/Ctoss/Models/V2/MultiCondition.cs new file mode 100644 index 0000000..a11bf8c --- /dev/null +++ b/Ctoss/Models/V2/MultiCondition.cs @@ -0,0 +1,6 @@ +namespace Ctoss.Models.V2; + +public record MultiCondition : FilterConditionBase +{ + public List FilterModels { get; init; } = null!; +} \ No newline at end of file diff --git a/Ctoss/Models/V2/NumberCondition.cs b/Ctoss/Models/V2/NumberCondition.cs new file mode 100644 index 0000000..e7262da --- /dev/null +++ b/Ctoss/Models/V2/NumberCondition.cs @@ -0,0 +1,10 @@ +using Ctoss.Models.Enums; + +namespace Ctoss.Models.V2; + +public record NumberCondition : FilterConditionBase +{ + public NumberFilterOptions Type { get; init; } + public string? Filter { get; init; } + public string? FilterTo { get; init; } +} \ No newline at end of file diff --git a/Ctoss/Models/V2/SetCondition.cs b/Ctoss/Models/V2/SetCondition.cs new file mode 100644 index 0000000..e56101e --- /dev/null +++ b/Ctoss/Models/V2/SetCondition.cs @@ -0,0 +1,6 @@ +namespace Ctoss.Models.V2; + +public record SetCondition : FilterConditionBase +{ + public List Values { get; init; } = null!; +} \ No newline at end of file diff --git a/Ctoss/Models/V2/TextCondition.cs b/Ctoss/Models/V2/TextCondition.cs new file mode 100644 index 0000000..634bb71 --- /dev/null +++ b/Ctoss/Models/V2/TextCondition.cs @@ -0,0 +1,9 @@ +using Ctoss.Models.Enums; + +namespace Ctoss.Models.V2; + +public record TextCondition : FilterConditionBase +{ + public TextFilterOptions Type { get; init; } + public string Filter { get; init; } = null!; +} \ No newline at end of file