Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/enisn/AutoFilterer into …
Browse files Browse the repository at this point in the history
…develop
  • Loading branch information
enisn committed Jul 13, 2021
2 parents 550a92a + 981abac commit 76d22d8
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 10 deletions.
47 changes: 45 additions & 2 deletions src/AutoFilterer/Attributes/CompareToAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,88 @@
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; }

/// <summary>
/// Gets or sets CombineType parameter. All properties are combined with 'Or' by default.
/// </summary>
public CombineType CombineWith { get; set; } = CombineType.Or;

/// <summary>
/// Type must implement <see cref="IFilterableType"/> and must has parameterless constructor.
/// </summary>
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;
}

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<FilteringOptionsBaseAttribute>().FirstOrDefault(x => !(x is CompareToAttribute));

if (attribute != null)
{
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)))
Expand Down
15 changes: 15 additions & 0 deletions src/AutoFilterer/AutoFiltererConsts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using AutoFilterer.Types;

namespace AutoFilterer
{
public static class AutoFiltererConsts
{
public static bool IgnoreExceptions
{
set
{
FilterBase.IgnoreExceptions = value;
}
}
}
}
31 changes: 23 additions & 8 deletions src/AutoFilterer/Types/FilterBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ namespace AutoFilterer.Types
/// </summary>
public class FilterBase : IFilter
{
public static bool IgnoreExceptions { get; set; } = true;

[IgnoreFilter]
public virtual CombineType CombineWith { get; set; }

Expand Down Expand Up @@ -47,27 +49,40 @@ public virtual Expression BuildExpression(Type entityType, Expression body)
if (val == null || filterProperty.GetCustomAttribute<IgnoreFilterAttribute>() != null)
continue;

var attribute = filterProperty.GetCustomAttribute<CompareToAttribute>(inherit: true) ?? new CompareToAttribute(filterProperty.Name);
var attributes = filterProperty.GetCustomAttributes<CompareToAttribute>(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);
finalExpression = combined.Combine(body, CombineWith);
}
catch (Exception ex)
{
if (!IgnoreExceptions)
{
throw;
}

Debug.WriteLine(ex?.ToString());
}
}
Expand Down
126 changes: 126 additions & 0 deletions tests/AutoFilterer.Tests/Attributes/CompareToAttributeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -56,5 +59,128 @@ public void BuildExpression_MultipleFieldWithAnd_ShouldMatchCount(List<Book> 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<Book> books)
{
AutoFiltererConsts.IgnoreExceptions = false;

var argumentException = Assert.Throws<ArgumentException>(() =>
{
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<Book> 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<Book> 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<Book> 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)
{
}
}
}
}
}
5 changes: 5 additions & 0 deletions tests/AutoFilterer.Tests/Environment/Models/Book.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
}
}
}

0 comments on commit 76d22d8

Please sign in to comment.