From 4d937f3a056356babec5fb1829bd60f4bf10487b Mon Sep 17 00:00:00 2001 From: enisn Date: Mon, 5 Jul 2021 20:12:42 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Introduce=20multiple=20comparet?= =?UTF-8?q?o=20attribute=20with=20IFilterableType?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Attributes/CompareToAttribute.cs | 42 +++++- src/AutoFilterer/AutoFiltererConsts.cs | 16 +++ src/AutoFilterer/Types/FilterBase.cs | 31 +++-- .../Attributes/CompareToAttributeTests.cs | 126 ++++++++++++++++++ .../Environment/Models/Book.cs | 5 + 5 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 src/AutoFilterer/AutoFiltererConsts.cs diff --git a/src/AutoFilterer/Attributes/CompareToAttribute.cs b/src/AutoFilterer/Attributes/CompareToAttribute.cs index 10e59b5..61696f0 100644 --- a/src/AutoFilterer/Attributes/CompareToAttribute.cs +++ b/src/AutoFilterer/Attributes/CompareToAttribute.cs @@ -6,16 +6,26 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System; namespace AutoFilterer.Attributes { + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] public class CompareToAttribute : FilteringOptionsBaseAttribute { + private Type filterableType; + public CompareToAttribute(params string[] propertyNames) { PropertyNames = propertyNames; } + public CompareToAttribute(Type filterableType, params string[] propertyNames) + { + FilterableType = filterableType; + PropertyNames = propertyNames; + } + public string[] PropertyNames { get; set; } /// @@ -23,14 +33,37 @@ public CompareToAttribute(params string[] propertyNames) /// public CombineType CombineWith { get; set; } = CombineType.Or; + /// + /// Type must implement and must has parameterless constructor. + /// + public Type FilterableType + { + get => filterableType; + set + { + if (!typeof(IFilterableType).IsAssignableFrom(value)) + { + throw new ArgumentException($"The {value.FullName} type must implement 'IFilterableType'", nameof(FilterableType)); + } + + filterableType = value; + } + } + public override Expression BuildExpression(Expression expressionBody, PropertyInfo targetProperty, PropertyInfo filterProperty, object value) { for (int i = 0; i < PropertyNames.Length; i++) { var targetPropertyName = PropertyNames[i]; var _targetProperty = targetProperty.DeclaringType.GetProperty(targetPropertyName); - - expressionBody = BuildExpressionForProperty(expressionBody, _targetProperty, filterProperty, value); + if (FilterableType != null) + { + expressionBody = ((IFilterableType)Activator.CreateInstance(FilterableType)).BuildExpression(expressionBody, _targetProperty, filterProperty, value); + } + else + { + expressionBody = BuildExpressionForProperty(expressionBody, _targetProperty, filterProperty, value); + } } return expressionBody; @@ -38,6 +71,11 @@ public override Expression BuildExpression(Expression expressionBody, PropertyIn public virtual Expression BuildExpressionForProperty(Expression expressionBody, PropertyInfo targetProperty, PropertyInfo filterProperty, object value) { + if (FilterableType != null) + { + return ((IFilterableType) Activator.CreateInstance(FilterableType)).BuildExpression(expressionBody, targetProperty, filterProperty, value); + } + var attribute = filterProperty.GetCustomAttributes().FirstOrDefault(x => !(x is CompareToAttribute)); if (attribute != null) diff --git a/src/AutoFilterer/AutoFiltererConsts.cs b/src/AutoFilterer/AutoFiltererConsts.cs new file mode 100644 index 0000000..13d7f35 --- /dev/null +++ b/src/AutoFilterer/AutoFiltererConsts.cs @@ -0,0 +1,16 @@ +using AutoFilterer.Types; + +namespace AutoFilterer +{ + public static class AutoFiltererConsts + { + public static bool IgnoreExceptions + { + set + { + FilterBase.IgnoreExceptions = value; + + } + } + } +} diff --git a/src/AutoFilterer/Types/FilterBase.cs b/src/AutoFilterer/Types/FilterBase.cs index c160e5b..b8547c0 100644 --- a/src/AutoFilterer/Types/FilterBase.cs +++ b/src/AutoFilterer/Types/FilterBase.cs @@ -17,6 +17,8 @@ namespace AutoFilterer.Types /// public class FilterBase : IFilter { + public static bool IgnoreExceptions { get; set; } = true; + [IgnoreFilter] public virtual CombineType CombineWith { get; set; } @@ -47,20 +49,28 @@ public virtual Expression BuildExpression(Type entityType, Expression body) if (val == null || filterProperty.GetCustomAttribute() != null) continue; - var attribute = filterProperty.GetCustomAttribute(inherit: true) ?? new CompareToAttribute(filterProperty.Name); + var attributes = filterProperty.GetCustomAttributes(inherit: true); + + if (!attributes.Any()) + { + attributes = new[] { new CompareToAttribute(filterProperty.Name) }; + } Expression innerExpression = null; - foreach (var targetPropertyName in attribute.PropertyNames) + foreach (var attribute in attributes) { - var targetProperty = entityType.GetProperty(targetPropertyName); - if (targetProperty == null) - continue; + foreach (var targetPropertyName in attribute.PropertyNames) + { + var targetProperty = entityType.GetProperty(targetPropertyName); + if (targetProperty == null) + continue; - var bodyParameter = finalExpression is MemberExpression ? finalExpression : body; + var bodyParameter = finalExpression is MemberExpression ? finalExpression : body; - var expression = attribute.BuildExpressionForProperty(bodyParameter, targetProperty, filterProperty, val); - innerExpression = innerExpression.Combine(expression, attribute.CombineWith); + var expression = attribute.BuildExpressionForProperty(bodyParameter, targetProperty, filterProperty, val); + innerExpression = innerExpression.Combine(expression, attribute.CombineWith); + } } var combined = finalExpression.Combine(innerExpression, CombineWith); @@ -68,6 +78,11 @@ public virtual Expression BuildExpression(Type entityType, Expression body) } catch (Exception ex) { + if (!IgnoreExceptions) + { + throw; + } + Debug.WriteLine(ex?.ToString()); } } diff --git a/tests/AutoFilterer.Tests/Attributes/CompareToAttributeTests.cs b/tests/AutoFilterer.Tests/Attributes/CompareToAttributeTests.cs index 121d280..a702b00 100644 --- a/tests/AutoFilterer.Tests/Attributes/CompareToAttributeTests.cs +++ b/tests/AutoFilterer.Tests/Attributes/CompareToAttributeTests.cs @@ -9,6 +9,9 @@ using AutoFilterer.Extensions; using AutoFilterer.Tests.Core; using Xunit; +using AutoFilterer.Types; +using System.ComponentModel.DataAnnotations; +using AutoFilterer.Attributes; namespace AutoFilterer.Tests.Attributes { @@ -56,5 +59,128 @@ public void BuildExpression_MultipleFieldWithAnd_ShouldMatchCount(List dum var actualResult = query.Where(x => x.Title.Contains(filter.Query) && x.Author.Contains(filter.Query)).ToList(); Assert.Equal(result.Count, actualResult.Count); } + + [Theory, AutoMoqData(count: 3)] + public void ShouldThrowException_WhenWrongFilterableTypeSet(List books) + { + AutoFiltererConsts.IgnoreExceptions = false; + + var argumentException = Assert.Throws(() => + { + books.AsQueryable().ApplyFilter(new WrongTypeSetFilter() { Filter = "A" }); + }); + } + + public class WrongTypeSetFilter : FilterBase + { + [CompareTo(typeof(Exception), "Title")] + public string Filter { get; set; } + } + + [Theory, AutoMoqData(count: 64)] + public void ShouldFilterWithTypeInAttribute(List dummyData) + { + // Arrange + var filter = new TypeCompareToFilter + { + Search = "titlea" + }; + + var query = dummyData.AsQueryable(); + + var expectedQuery = query.Where(x => x.Title.ToLower().Contains(filter.Search.ToLower())); + var expected = expectedQuery.ToList(); + + // Act + var actualQuery = query.ApplyFilter(filter); + var actual = actualQuery.ToList(); + + // Assert + Assert.Equal(expected.Count, actual.Count); + } + + public class TypeCompareToFilter : FilterBase + { + [CompareTo(typeof(ToLowerContainsComparisonAttribute), nameof(Book.Title))] + public string Search { get; set; } + } + + [Theory, AutoMoqData(count: 64)] + public void ShouldFilterWithTypeInAttributeWithMultipleAttribute(List dummyData) + { + // Arrange + var filter = new MultipleTypeCompareToFilter + { + Search = "af" + }; + + var query = dummyData.AsQueryable(); + + var expectedQuery = query.Where(x => + x.Title.ToLower().Contains(filter.Search.ToLower()) + || x.Author.StartsWith(filter.Search, StringComparison.InvariantCultureIgnoreCase)); + + var expected = expectedQuery.ToList(); + + // Act + var actualQuery = query.ApplyFilter(filter); + var actual = actualQuery.ToList(); + + // Assert + Assert.Equal(expected.Count, actual.Count); + } + + public class MultipleTypeCompareToFilter : FilterBase + { + [CompareTo(typeof(ToLowerContainsComparisonAttribute), nameof(Book.Title))] + [CompareTo(typeof(StartsWithAttribute), nameof(Book.Author))] + public string Search { get; set; } + + public class StartsWithAttribute : StringFilterOptionsAttribute + { + public StartsWithAttribute() : base(StringFilterOption.StartsWith, StringComparison.InvariantCultureIgnoreCase) + { + } + } + } + + [Theory, AutoMoqData(count: 64)] + public void ShouldFilterWithTypeInAttributeWithMultipleAttributeWithAndCombination(List dummyData) + { + // Arrange + var filter = new MultipleTypeCompareToAndComparisonFilter + { + Search = "9" + }; + + var query = dummyData.AsQueryable(); + + var expectedQuery = query.Where(x => + x.Title.ToLower().Contains(filter.Search.ToLower()) + && x.Author.EndsWith(filter.Search, StringComparison.InvariantCultureIgnoreCase)); + + var expected = expectedQuery.ToList(); + + // Act + var actualQuery = query.ApplyFilter(filter); + var actual = actualQuery.ToList(); + + // Assert + Assert.Equal(expected.Count, actual.Count); + } + + public class MultipleTypeCompareToAndComparisonFilter : FilterBase + { + [CompareTo(typeof(ToLowerContainsComparisonAttribute), nameof(Book.Title))] + [CompareTo(typeof(EndsWithAttribute), nameof(Book.Author), CombineWith = CombineType.And)] + public string Search { get; set; } + + public class EndsWithAttribute : StringFilterOptionsAttribute + { + public EndsWithAttribute() : base(StringFilterOption.EndsWith, StringComparison.InvariantCultureIgnoreCase) + { + } + } + } } } diff --git a/tests/AutoFilterer.Tests/Environment/Models/Book.cs b/tests/AutoFilterer.Tests/Environment/Models/Book.cs index e07ce5f..6a4728b 100644 --- a/tests/AutoFilterer.Tests/Environment/Models/Book.cs +++ b/tests/AutoFilterer.Tests/Environment/Models/Book.cs @@ -15,5 +15,10 @@ public class Book public int ReadCount { get; set; } public bool IsPublished { get; set; } public int? Views { get; set; } + + public override string ToString() + { + return $"[{Id}] {Title} - {Author} | TotalPage: {TotalPage} | ReadCount: {ReadCount}"; + } } } From cb5c9a18a5afc95133920226ed515b039433512c Mon Sep 17 00:00:00 2001 From: enisn Date: Tue, 6 Jul 2021 09:21:32 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BB=20Code-formatting=20&=20refactori?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AutoFilterer/Attributes/CompareToAttribute.cs | 7 ++++++- src/AutoFilterer/AutoFiltererConsts.cs | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/AutoFilterer/Attributes/CompareToAttribute.cs b/src/AutoFilterer/Attributes/CompareToAttribute.cs index 61696f0..0bf8a13 100644 --- a/src/AutoFilterer/Attributes/CompareToAttribute.cs +++ b/src/AutoFilterer/Attributes/CompareToAttribute.cs @@ -73,7 +73,7 @@ public virtual Expression BuildExpressionForProperty(Expression expressionBody, { if (FilterableType != null) { - return ((IFilterableType) Activator.CreateInstance(FilterableType)).BuildExpression(expressionBody, targetProperty, filterProperty, value); + return ((IFilterableType)Activator.CreateInstance(FilterableType)).BuildExpression(expressionBody, targetProperty, filterProperty, value); } var attribute = filterProperty.GetCustomAttributes().FirstOrDefault(x => !(x is CompareToAttribute)); @@ -83,6 +83,11 @@ public virtual Expression BuildExpressionForProperty(Expression expressionBody, return attribute.BuildExpression(expressionBody, targetProperty, filterProperty, value); } + return BuildDefaultExpression(expressionBody, targetProperty, filterProperty, value); + } + + public virtual Expression BuildDefaultExpression(Expression expressionBody, PropertyInfo targetProperty, PropertyInfo filterProperty, object value) + { if (value is IFilter filter) { if (typeof(ICollection).IsAssignableFrom(targetProperty.PropertyType) || (targetProperty.PropertyType.IsConstructedGenericType && typeof(IEnumerable).IsAssignableFrom(targetProperty.PropertyType))) diff --git a/src/AutoFilterer/AutoFiltererConsts.cs b/src/AutoFilterer/AutoFiltererConsts.cs index 13d7f35..80ced50 100644 --- a/src/AutoFilterer/AutoFiltererConsts.cs +++ b/src/AutoFilterer/AutoFiltererConsts.cs @@ -9,7 +9,6 @@ public static bool IgnoreExceptions set { FilterBase.IgnoreExceptions = value; - } } }