Skip to content

Commit 3e79d17

Browse files
committed
FilterBy extension method was added with operator selection
1 parent 9e23f5f commit 3e79d17

File tree

6 files changed

+321
-1
lines changed

6 files changed

+321
-1
lines changed

.idea/.idea.GenericRepository/.idea/.name

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/.idea.GenericRepository/.idea/indexLayout.xml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/.idea.GenericRepository/.idea/vcs.xml

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Extensions/FilterByExtension.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
namespace GenericRepository.Extensions;
2+
3+
public static class FilterByExtension
4+
{
5+
public static IQueryable<T> FilterBy<T>(this IQueryable<T> collection, string filterBy) =>
6+
ParseFilterBy(filterBy).Aggregate(collection, ApplyFilterBy);
7+
8+
private static IQueryable<T> ApplyFilterBy<T>(IQueryable<T> collection, FilterByInfo filterByInfo)
9+
{
10+
var type = typeof(T);
11+
var arg = Expression.Parameter(type, "x");
12+
var property = type.GetProperty(filterByInfo.PropertyName);
13+
var propertyAccess = Expression.MakeMemberAccess(arg, property!);
14+
var constant = Expression.Constant(Convert.ChangeType(filterByInfo.Value, property!.PropertyType));
15+
var body = GetExpressionBody(filterByInfo.Operator, propertyAccess, constant);
16+
var lambda = Expression.Lambda<Func<T, bool>>(body, arg);
17+
return collection.Where(lambda);
18+
}
19+
20+
private static Expression GetExpressionBody(FilterOperator filterOperator, Expression propertyAccess,
21+
ConstantExpression constant)
22+
{
23+
return filterOperator switch
24+
{
25+
FilterOperator.Equal => Expression.Equal(propertyAccess, constant),
26+
FilterOperator.NotEqual => Expression.NotEqual(propertyAccess, constant),
27+
FilterOperator.GreaterThan => Expression.GreaterThan(propertyAccess, constant),
28+
FilterOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(propertyAccess, constant),
29+
FilterOperator.LessThan => Expression.LessThan(propertyAccess, constant),
30+
FilterOperator.LessThanOrEqual => Expression.LessThanOrEqual(propertyAccess, constant),
31+
FilterOperator.Contains => Expression.Call(propertyAccess,
32+
typeof(string).GetMethod("Contains", [typeof(string)])!, constant),
33+
FilterOperator.StartsWith => Expression.Call(propertyAccess,
34+
typeof(string).GetMethod("StartsWith", [typeof(string)])!, constant),
35+
FilterOperator.EndsWith => Expression.Call(propertyAccess,
36+
typeof(string).GetMethod("EndsWith", [typeof(string)])!, constant),
37+
_ => throw new ArgumentOutOfRangeException(nameof(filterOperator), filterOperator, null)
38+
};
39+
}
40+
41+
private static IEnumerable<FilterByInfo> ParseFilterBy(string filterBy)
42+
{
43+
if (string.IsNullOrEmpty(filterBy)) yield break;
44+
45+
var items = filterBy.Split(',');
46+
foreach (var item in items)
47+
{
48+
var pair = item.Trim().Split(' ');
49+
if (pair.Length != 3)
50+
throw new ArgumentException(
51+
$"Invalid FilterBy string '{item}'. Filter By Format: PropertyName Operator Value");
52+
53+
var propertyName = pair[0];
54+
var filterOperator = Enum.Parse<FilterOperator>(pair[1], true);
55+
var value = ParseValue(pair[2]);
56+
57+
yield return new FilterByInfo
58+
{
59+
PropertyName = propertyName,
60+
Operator = filterOperator,
61+
Value = value
62+
};
63+
}
64+
}
65+
66+
private static object ParseValue(string value)
67+
{
68+
if (bool.TryParse(value, out var boolResult))
69+
return boolResult;
70+
if (int.TryParse(value, out var intResult))
71+
return intResult;
72+
if (float.TryParse(value, out var floatResult))
73+
return floatResult;
74+
if (decimal.TryParse(value, out var decimalResult))
75+
return decimalResult;
76+
return value;
77+
}
78+
}
79+
80+
internal class FilterByInfo
81+
{
82+
public string PropertyName { get; init; } = string.Empty;
83+
public object Value { get; init; } = string.Empty;
84+
public FilterOperator Operator { get; init; }
85+
}
86+
87+
public enum FilterOperator
88+
{
89+
Equal,
90+
NotEqual,
91+
GreaterThan,
92+
GreaterThanOrEqual,
93+
LessThan,
94+
LessThanOrEqual,
95+
Contains,
96+
StartsWith,
97+
EndsWith
98+
}

GenericRepository.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
<ItemGroup>
1010
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
11+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
12+
<PackageReference Include="xunit" Version="2.9.2" />
1113
</ItemGroup>
1214

1315
</Project>

Tests/ExtensionTests.cs

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
using Xunit;
2+
using Xunit.Abstractions;
3+
4+
namespace GenericRepository.Tests;
5+
6+
internal class TestEntity
7+
{
8+
public required int Id { get; set; }
9+
public required string Name { get; set; }
10+
public required bool IsActive { get; set; }
11+
public required DateTime CreatedAt { get; set; }
12+
public required float Price { get; set; }
13+
public required decimal Amount { get; set; }
14+
}
15+
16+
public class ExtensionTests(ITestOutputHelper testOutputHelper)
17+
{
18+
private readonly List<TestEntity> _collection =
19+
[
20+
new()
21+
{
22+
Id = 1,
23+
Name = "A",
24+
IsActive = true,
25+
CreatedAt = DateTime.Now,
26+
Price = 10.5f,
27+
Amount = 10.5m
28+
},
29+
new()
30+
{
31+
Id = 2,
32+
Name = "B",
33+
IsActive = true,
34+
CreatedAt = DateTime.Now,
35+
Price = 20.5f,
36+
Amount = 20.5m
37+
},
38+
new()
39+
{
40+
Id = 3,
41+
Name = "C",
42+
IsActive = false,
43+
CreatedAt = new DateTime(2022, 1, 1),
44+
Price = 30.5f,
45+
Amount = 30.5m
46+
}
47+
];
48+
49+
[Fact]
50+
public void OrderByTest()
51+
{
52+
// Arrange
53+
var queryable = _collection.AsQueryable();
54+
55+
// Act
56+
var result = queryable
57+
.OrderBy("Name DESC")
58+
.ToList();
59+
60+
// Assert
61+
Assert.Equal("C", result[0].Name);
62+
Assert.Equal("B", result[1].Name);
63+
Assert.Equal("A", result[2].Name);
64+
65+
testOutputHelper.WriteLine("Ordered By Name DESC");
66+
result.ForEach(x => testOutputHelper.WriteLine(x.Name));
67+
}
68+
69+
[Fact]
70+
public void FilterByTest()
71+
{
72+
// Arrange
73+
var queryable = _collection.AsQueryable();
74+
75+
// Act
76+
var result = queryable
77+
.FilterBy("Name Equal B")
78+
.ToList();
79+
80+
// Assert
81+
Assert.Single(result);
82+
Assert.Equal("B", result[0].Name);
83+
84+
testOutputHelper.WriteLine("Filtered By Name Equal B");
85+
result.ForEach(x => testOutputHelper.WriteLine(x.Name));
86+
}
87+
88+
// Filter By integer property
89+
[Fact]
90+
public void FilterById()
91+
{
92+
// Arrange
93+
var queryable = _collection.AsQueryable();
94+
95+
// Act
96+
var result = queryable
97+
.FilterBy("Id GreaterThan 1")
98+
.ToList();
99+
100+
// Assert
101+
Assert.Equal(2, result.Count);
102+
Assert.Equal("B", result[0].Name);
103+
Assert.Equal("C", result[1].Name);
104+
105+
testOutputHelper.WriteLine("Filtered By Id GreaterThan 1");
106+
result.ForEach(x => testOutputHelper.WriteLine(x.Name));
107+
}
108+
109+
// Filter By boolean property
110+
[Fact]
111+
public void FilterByIsActive()
112+
{
113+
// Arrange
114+
var queryable = _collection.AsQueryable();
115+
116+
// Act
117+
var result = queryable
118+
.FilterBy("IsActive Equal true")
119+
.ToList();
120+
121+
// Assert
122+
Assert.Equal(2, result.Count);
123+
Assert.Equal("A", result[0].Name);
124+
Assert.Equal("B", result[1].Name);
125+
126+
testOutputHelper.WriteLine("Filtered By IsActive Equal true");
127+
result.ForEach(x => testOutputHelper.WriteLine(x.Name));
128+
}
129+
130+
// Filter By DateTime property
131+
[Fact]
132+
public void FilterByCreatedAt()
133+
{
134+
// Arrange
135+
var queryable = _collection.AsQueryable();
136+
137+
// Act
138+
var result = queryable
139+
.FilterBy("CreatedAt GreaterThan 2022-01-01")
140+
.ToList();
141+
142+
// Assert
143+
Assert.Equal(2, result.Count);
144+
145+
testOutputHelper.WriteLine("Filtered By CreatedAt GreaterThan 2022-01-01");
146+
result.ForEach(x => testOutputHelper.WriteLine(x.Name));
147+
}
148+
149+
// Filter By float property
150+
[Fact]
151+
public void FilterByPrice()
152+
{
153+
// Arrange
154+
var queryable = _collection.AsQueryable();
155+
156+
// Act
157+
var result = queryable
158+
.FilterBy("Price GreaterThan 20.5")
159+
.ToList();
160+
161+
// Assert
162+
Assert.Single(result);
163+
Assert.Equal("C", result[0].Name);
164+
165+
testOutputHelper.WriteLine("Filtered By Price GreaterThan 20.5");
166+
result.ForEach(x => testOutputHelper.WriteLine(x.Name));
167+
}
168+
169+
// Filter By decimal property
170+
[Fact]
171+
public void FilterByAmount()
172+
{
173+
// Arrange
174+
var queryable = _collection.AsQueryable();
175+
176+
// Act
177+
var result = queryable
178+
.FilterBy("Amount GreaterThan 20.5")
179+
.ToList();
180+
181+
// Assert
182+
Assert.Single(result);
183+
Assert.Equal("C", result[0].Name);
184+
185+
testOutputHelper.WriteLine("Filtered By Amount GreaterThan 20.5");
186+
result.ForEach(x => testOutputHelper.WriteLine(x.Name));
187+
}
188+
189+
[Fact]
190+
public void FilterByAndOrderByTest()
191+
{
192+
// Arrange
193+
var queryable = _collection.AsQueryable();
194+
195+
// Act
196+
var result = queryable
197+
.FilterBy("Name NotEqual B")
198+
.OrderBy("Name DESC")
199+
.ToList();
200+
201+
// Assert
202+
Assert.Equal(2, result.Count);
203+
Assert.Equal("C", result[0].Name);
204+
Assert.Equal("A", result[1].Name);
205+
206+
testOutputHelper.WriteLine("Filtered By Name NotEqual B and Ordered By Name DESC");
207+
result.ForEach(x => testOutputHelper.WriteLine(x.Name));
208+
}
209+
}

0 commit comments

Comments
 (0)