├── .github
└── workflows
│ └── dotnet.yml
├── .gitignore
├── Core
├── Attributes
│ ├── ReFilterBuilder.cs
│ ├── ReFilterProperty.cs
│ └── ReFilterSpecialFilter.cs
├── Enums
│ ├── OperatorComparer.cs
│ └── SortDirection.cs
├── Models
│ ├── BasePagedRequest.cs
│ ├── Filtering
│ │ └── Contracts
│ │ │ ├── IReFilter.cs
│ │ │ ├── IReFilterRequest.cs
│ │ │ └── IReFilterable.cs
│ ├── PagedBase.cs
│ ├── PagedRequest.cs
│ ├── PagedResult.cs
│ ├── PagedResultBase.cs
│ └── PropertyFilterConfig.cs
└── ReFilter.Core.csproj
├── Docs
└── ReFilter.ts
├── README.md
├── ReFilter.sln
├── ReFilter
├── Attributes
│ ├── ReFilterBuilder.cs
│ ├── ReFilterProperty.cs
│ └── ReFilterSpecialFilter.cs
├── Converters
│ ├── DateOnlyConverter.cs
│ ├── DateOnlyNullableConverter.cs
│ ├── TimeOnlyConverter.cs
│ └── TimeOnlyNullableConverter.cs
├── Enums
│ ├── OperatorComparer.cs
│ └── SortDirection.cs
├── Extensions
│ ├── BasePagedRequestExtensions.cs
│ ├── IServiceCollectionExtension.cs
│ ├── ObjectExtensions.cs
│ ├── PagedResultExtensions.cs
│ ├── RangeFilterExtensions.cs
│ ├── SortDirectionExtensions.cs
│ ├── StringExtensions.cs
│ └── TypeExtensions.cs
├── Models
│ ├── BasePagedRequest.cs
│ ├── Filtering
│ │ └── Contracts
│ │ │ ├── IReFilter.cs
│ │ │ ├── IReFilterRequest.cs
│ │ │ ├── IReFilterable.cs
│ │ │ └── IReSort.cs
│ ├── PagedBase.cs
│ ├── PagedRequest.cs
│ ├── PagedResult.cs
│ ├── PagedResultBase.cs
│ ├── PropertyFilterConfig.cs
│ └── RangeFilter.cs
├── ReFilter.csproj
├── ReFilterActions
│ ├── IReFilterActions.cs
│ └── ReFilterActions.cs
├── ReFilterBuilder
│ └── IReFilterBuilder.cs
├── ReFilterConfigBuilder
│ ├── IReFilterConfigBuilder.cs
│ ├── IReSearchConfigBuilder.cs
│ └── IReSortConfigBuilder.cs
├── ReFilterExpressionBuilder
│ └── ReFilterExpressionBuilder.cs
├── ReSearchBuilder
│ └── IReSearchBuilder.cs
├── ReSortBuilder
│ └── IReSortBuilder.cs
└── Utilities
│ └── FilterHelper.cs
├── TestProject
├── Enums
│ └── Gender.cs
├── FilterBuilders
│ ├── SchoolFilterBuilder.cs
│ └── SchoolFilters
│ │ └── StudentNamesFilter.cs
├── Mappers
│ ├── SchoolMapper.cs
│ └── StudentMapper.cs
├── Models
│ ├── Building.cs
│ ├── Certificate.cs
│ ├── College.cs
│ ├── CollegeViewModel.cs
│ ├── Country.cs
│ ├── CountryFilterRequest.cs
│ ├── FilterRequests
│ │ ├── CollegeFilterRequest.cs
│ │ └── SchoolFilterRequest.cs
│ ├── School.cs
│ ├── SchoolViewModel.cs
│ ├── Student.cs
│ └── StudentViewModel.cs
├── RequiredImplementations
│ ├── ReFilterConfigBuilder.cs
│ └── ReSortConfigBuilder.cs
├── SortBuilders
│ ├── CountrySorter.cs
│ └── SchoolSortBuilder.cs
├── TestData
│ ├── SchoolServiceTestData.cs
│ └── StudentServiceTestData.cs
├── TestProject.csproj
├── TestServices
│ ├── SchoolService.cs
│ └── StudentService.cs
└── Tests
│ ├── SchoolServiceTest.cs
│ └── StudentServiceTest.cs
└── Tests
├── Enums
└── Gender.cs
├── Mappers
└── StudentMapper.cs
├── Models
├── Student.cs
└── StudentViewModel.cs
├── RequiredImplementations
└── ReFilterConfigBuilder.cs
├── TestData
└── StudentServiceTestData.cs
├── TestServices
└── StudentService.cs
├── Tests.csproj
└── Tests
└── StudentServiceTests.cs
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | name: .NET
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: windows-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Setup .NET Core
17 | uses: actions/setup-dotnet@v1
18 | with:
19 | dotnet-version: 3.1.x
20 | - name: Setup dotnet
21 | uses: actions/setup-dotnet@v1
22 | with:
23 | dotnet-version: '6.0.x'
24 | - name: Restore dependencies
25 | run: dotnet restore
26 | - name: Build
27 | run: dotnet build --no-restore
28 | - name: Test
29 | run: dotnet test --no-build --verbosity normal
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Visual Studio code coverage results
141 | *.coverage
142 | *.coveragexml
143 |
144 | # NCrunch
145 | _NCrunch_*
146 | .*crunch*.local.xml
147 | nCrunchTemp_*
148 |
149 | # MightyMoose
150 | *.mm.*
151 | AutoTest.Net/
152 |
153 | # Web workbench (sass)
154 | .sass-cache/
155 |
156 | # Installshield output folder
157 | [Ee]xpress/
158 |
159 | # DocProject is a documentation generator add-in
160 | DocProject/buildhelp/
161 | DocProject/Help/*.HxT
162 | DocProject/Help/*.HxC
163 | DocProject/Help/*.hhc
164 | DocProject/Help/*.hhk
165 | DocProject/Help/*.hhp
166 | DocProject/Help/Html2
167 | DocProject/Help/html
168 |
169 | # Click-Once directory
170 | publish/
171 |
172 | # Publish Web Output
173 | *.[Pp]ublish.xml
174 | *.azurePubxml
175 | # Note: Comment the next line if you want to checkin your web deploy settings,
176 | # but database connection strings (with potential passwords) will be unencrypted
177 | *.pubxml
178 | *.publishproj
179 |
180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
181 | # checkin your Azure Web App publish settings, but sensitive information contained
182 | # in these scripts will be unencrypted
183 | PublishScripts/
184 |
185 | # NuGet Packages
186 | *.nupkg
187 | # NuGet Symbol Packages
188 | *.snupkg
189 | # The packages folder can be ignored because of Package Restore
190 | **/[Pp]ackages/*
191 | # except build/, which is used as an MSBuild target.
192 | !**/[Pp]ackages/build/
193 | # Uncomment if necessary however generally it will be regenerated when needed
194 | #!**/[Pp]ackages/repositories.config
195 | # NuGet v3's project.json files produces more ignorable files
196 | *.nuget.props
197 | *.nuget.targets
198 |
199 | # Microsoft Azure Build Output
200 | csx/
201 | *.build.csdef
202 |
203 | # Microsoft Azure Emulator
204 | ecf/
205 | rcf/
206 |
207 | # Windows Store app package directories and files
208 | AppPackages/
209 | BundleArtifacts/
210 | Package.StoreAssociation.xml
211 | _pkginfo.txt
212 | *.appx
213 | *.appxbundle
214 | *.appxupload
215 |
216 | # Visual Studio cache files
217 | # files ending in .cache can be ignored
218 | *.[Cc]ache
219 | # but keep track of directories ending in .cache
220 | !?*.[Cc]ache/
221 |
222 | # Others
223 | ClientBin/
224 | ~$*
225 | *~
226 | *.dbmdl
227 | *.dbproj.schemaview
228 | *.jfm
229 | *.pfx
230 | *.publishsettings
231 | orleans.codegen.cs
232 |
233 | # Including strong name files can present a security risk
234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
235 | #*.snk
236 |
237 | # Since there are multiple workflows, uncomment next line to ignore bower_components
238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
239 | #bower_components/
240 |
241 | # RIA/Silverlight projects
242 | Generated_Code/
243 |
244 | # Backup & report files from converting an old project file
245 | # to a newer Visual Studio version. Backup files are not needed,
246 | # because we have git ;-)
247 | _UpgradeReport_Files/
248 | Backup*/
249 | UpgradeLog*.XML
250 | UpgradeLog*.htm
251 | ServiceFabricBackup/
252 | *.rptproj.bak
253 |
254 | # SQL Server files
255 | *.mdf
256 | *.ldf
257 | *.ndf
258 |
259 | # Business Intelligence projects
260 | *.rdl.data
261 | *.bim.layout
262 | *.bim_*.settings
263 | *.rptproj.rsuser
264 | *- [Bb]ackup.rdl
265 | *- [Bb]ackup ([0-9]).rdl
266 | *- [Bb]ackup ([0-9][0-9]).rdl
267 |
268 | # Microsoft Fakes
269 | FakesAssemblies/
270 |
271 | # GhostDoc plugin setting file
272 | *.GhostDoc.xml
273 |
274 | # Node.js Tools for Visual Studio
275 | .ntvs_analysis.dat
276 | node_modules/
277 |
278 | # Visual Studio 6 build log
279 | *.plg
280 |
281 | # Visual Studio 6 workspace options file
282 | *.opt
283 |
284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
285 | *.vbw
286 |
287 | # Visual Studio LightSwitch build output
288 | **/*.HTMLClient/GeneratedArtifacts
289 | **/*.DesktopClient/GeneratedArtifacts
290 | **/*.DesktopClient/ModelManifest.xml
291 | **/*.Server/GeneratedArtifacts
292 | **/*.Server/ModelManifest.xml
293 | _Pvt_Extensions
294 |
295 | # Paket dependency manager
296 | .paket/paket.exe
297 | paket-files/
298 |
299 | # FAKE - F# Make
300 | .fake/
301 |
302 | # CodeRush personal settings
303 | .cr/personal
304 |
305 | # Python Tools for Visual Studio (PTVS)
306 | __pycache__/
307 | *.pyc
308 |
309 | # Cake - Uncomment if you are using it
310 | # tools/**
311 | # !tools/packages.config
312 |
313 | # Tabs Studio
314 | *.tss
315 |
316 | # Telerik's JustMock configuration file
317 | *.jmconfig
318 |
319 | # BizTalk build output
320 | *.btp.cs
321 | *.btm.cs
322 | *.odx.cs
323 | *.xsd.cs
324 |
325 | # OpenCover UI analysis results
326 | OpenCover/
327 |
328 | # Azure Stream Analytics local run output
329 | ASALocalRun/
330 |
331 | # MSBuild Binary and Structured Log
332 | *.binlog
333 |
334 | # NVidia Nsight GPU debugger configuration file
335 | *.nvuser
336 |
337 | # MFractors (Xamarin productivity tool) working folder
338 | .mfractor/
339 |
340 | # Local History for Visual Studio
341 | .localhistory/
342 |
343 | # BeatPulse healthcheck temp database
344 | healthchecksdb
345 |
346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
347 | MigrationBackup/
348 |
349 | # Ionide (cross platform F# VS Code tools) working folder
350 | .ionide/
351 |
--------------------------------------------------------------------------------
/Core/Attributes/ReFilterBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ReFilter.Attributes
4 | {
5 | [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
6 | public sealed class ReFilterBuilder : Attribute
7 | {
8 | public Type FilterBuilderType { get; }
9 |
10 | public ReFilterBuilder(Type filterProviderType)
11 | {
12 | FilterBuilderType = filterProviderType;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Core/Attributes/ReFilterProperty.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ReFilter.Attributes
4 | {
5 | [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
6 | public sealed class ReFilterProperty : Attribute
7 | {
8 | public bool UsedForSearchQuery { get; set; } = true;
9 | public bool HasSpecialFilter { get; set; } = false;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Core/Attributes/ReFilterSpecialFilter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ReFilter.Attributes
4 | {
5 | [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
6 | public sealed class ReFilterSpecialFilter : Attribute
7 | {
8 | public string AttributeName { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Core/Enums/OperatorComparer.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 |
3 | namespace ReFilter.Enums
4 | {
5 | public enum OperatorComparer
6 | {
7 | Contains,
8 | StartsWith,
9 | EndsWith,
10 | Equals = ExpressionType.Equal,
11 | GreaterThan = ExpressionType.GreaterThan,
12 | GreaterThanOrEqual = ExpressionType.GreaterThanOrEqual,
13 | LessThan = ExpressionType.LessThan,
14 | LessThanOrEqual = ExpressionType.LessThanOrEqual,
15 | NotEqual = ExpressionType.NotEqual,
16 | Not = ExpressionType.Not,
17 | BetweenExclusive = 95,
18 | BetweenInclusive = 96,
19 | BetweenLowerInclusive = 97,
20 | BetweenHigherInclusive = 98,
21 | CustomFilter = 99
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Core/Enums/SortDirection.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 |
3 | namespace ReFilter.Enums
4 | {
5 | public enum SortDirection
6 | {
7 | ASC = 0,
8 | DESC = 1
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Core/Models/BasePagedRequest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Newtonsoft.Json.Linq;
5 | using ReFilter.Enums;
6 |
7 | namespace ReFilter.Models
8 | {
9 | public class BasePagedRequest : PagedBase
10 | {
11 | ///
12 | /// Where object for 1:1 mapping to entity to be filtered.
13 | /// Only requirenment is that property names are same
14 | ///
15 | public JObject Where { get; set; }
16 |
17 | ///
18 | /// Defines rules for sorting and filtering
19 | /// Can be left empty and in such way, the default values are used.
20 | /// Default values are no sort and Equals comparer
21 | ///
22 | public List PropertyFilterConfigs { get; set; }
23 |
24 | [Obsolete]
25 | ///
26 | /// Dictionary containing Keys matching PropertyNames and Value matching SortDirection
27 | ///
28 | public Dictionary Sorting { get; set; }
29 |
30 | ///
31 | /// String SearchQuery meant for searching ANY of the tagged property
32 | ///
33 | public string SearchQuery { get; set; }
34 |
35 | public PagedRequest GetPagedRequest(bool returnQuery = true, bool returnResults = false)
36 | {
37 | var pagedRequest = new PagedRequest
38 | {
39 | PageIndex = PageIndex,
40 | PageSize = PageSize,
41 | PropertyFilterConfigs = PropertyFilterConfigs,
42 | SearchQuery = SearchQuery,
43 | Sorting = Sorting,
44 | Where = Where,
45 | ReturnQuery = returnQuery,
46 | ReturnResults = returnResults
47 | };
48 |
49 | return pagedRequest;
50 | }
51 |
52 | public PagedRequest GetPagedRequest(bool returnQuery = true, bool returnResults = false) where T : class, new() where U : class, new()
53 | {
54 | var pagedRequest = new PagedRequest(this)
55 | {
56 | PageIndex = PageIndex,
57 | PageSize = PageSize,
58 | PropertyFilterConfigs = PropertyFilterConfigs,
59 | SearchQuery = SearchQuery,
60 | Sorting = Sorting,
61 | Where = Where,
62 | ReturnQuery = returnQuery,
63 | ReturnResults = returnResults
64 | };
65 |
66 | return pagedRequest;
67 | }
68 |
69 | public PagedRequest GetPagedRequest(Func, List> mappingFunction) where T : class, new() where U : class, new()
70 | {
71 | var pagedRequest = new PagedRequest(this)
72 | {
73 | ReturnQuery = false,
74 | ReturnResults = true,
75 | MappingFunction = mappingFunction
76 | };
77 |
78 | return pagedRequest;
79 | }
80 |
81 | public PagedRequest GetPagedRequest(Func, List> mappingProjection) where T : class, new() where U : class, new()
82 | {
83 | var pagedRequest = new PagedRequest(this)
84 | {
85 | ReturnQuery = false,
86 | ReturnResults = true,
87 | MappingProjection = mappingProjection
88 | };
89 |
90 | return pagedRequest;
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Core/Models/Filtering/Contracts/IReFilter.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 |
3 | namespace ReFilter.Models.Filtering.Contracts
4 | {
5 | public interface IReFilter where T : class, new()
6 | {
7 | IQueryable FilterQuery(IQueryable query);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/Core/Models/Filtering/Contracts/IReFilterRequest.cs:
--------------------------------------------------------------------------------
1 | namespace ReFilter.Models.Filtering.Contracts
2 | {
3 | public interface IReFilterRequest
4 | {
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Core/Models/Filtering/Contracts/IReFilterable.cs:
--------------------------------------------------------------------------------
1 | namespace ReFilter.Models.Filtering.Contracts
2 | {
3 | public interface IReFilterable where T : struct
4 | {
5 | T Id { get; set; }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/Core/Models/PagedBase.cs:
--------------------------------------------------------------------------------
1 | namespace ReFilter.Models
2 | {
3 | public class PagedBase
4 | {
5 | public int PageSize { get; set; }
6 | public int PageIndex { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Core/Models/PagedRequest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace ReFilter.Models
6 | {
7 | public class PagedRequest : BasePagedRequest
8 | {
9 | public PagedRequest()
10 | {
11 |
12 | }
13 |
14 | public PagedRequest(BasePagedRequest pagedRequest)
15 | {
16 | PageIndex = pagedRequest.PageIndex;
17 | PageSize = pagedRequest.PageSize;
18 | PropertyFilterConfigs = pagedRequest.PropertyFilterConfigs;
19 | SearchQuery = pagedRequest.SearchQuery;
20 | Sorting = pagedRequest.Sorting;
21 | Where = pagedRequest.Where;
22 | ReturnQuery = false;
23 | ReturnResults = true;
24 | }
25 |
26 | public bool ReturnQuery { get; set; } = true;
27 | public bool ReturnResults { get; set; } = false;
28 | }
29 |
30 | public class PagedRequest : PagedRequest where T : class, new() where U : class, new()
31 | {
32 | public PagedRequest(BasePagedRequest pagedRequest)
33 | {
34 | PageIndex = pagedRequest.PageIndex;
35 | PageSize = pagedRequest.PageSize;
36 | PropertyFilterConfigs = pagedRequest.PropertyFilterConfigs;
37 | SearchQuery = pagedRequest.SearchQuery;
38 | Sorting = pagedRequest.Sorting;
39 | Where = pagedRequest.Where;
40 | ReturnQuery = true;
41 | ReturnResults = false;
42 | MappingFunction = null;
43 | MappingProjection = null;
44 | }
45 |
46 | public Func, List> MappingFunction { get; set; } = null;
47 | public Func, List> MappingProjection { get; set; } = null;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Core/Models/PagedResult.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 |
4 | namespace ReFilter.Models
5 | {
6 | public class PagedResult : PagedResultBase where T : new()
7 | {
8 | public List Results { get; set; }
9 | public IQueryable ResultQuery { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Core/Models/PagedResultBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ReFilter.Models
4 | {
5 | public abstract class PagedResultBase : PagedBase
6 | {
7 | public int PageCount { get; set; }
8 | public int RowCount { get; set; }
9 |
10 | public int CurrentPage => PageIndex + 1;
11 |
12 | public int FirstRowOnPage => PageIndex * PageSize + 1;
13 |
14 | public int LastRowOnPage => Math.Min(CurrentPage * PageSize, RowCount);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Core/Models/PropertyFilterConfig.cs:
--------------------------------------------------------------------------------
1 | using ReFilter.Enums;
2 |
3 | namespace ReFilter.Models
4 | {
5 | public class PropertyFilterConfig
6 | {
7 | public string PropertyName { get; set; }
8 | public OperatorComparer? OperatorComparer { get; set; } = Enums.OperatorComparer.Equals;
9 | public SortDirection? SortDirection { get; set; }
10 | public object Value { get; set; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Core/ReFilter.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Docs/ReFilter.ts:
--------------------------------------------------------------------------------
1 | export interface PagedBase {
2 | pageSize: number;
3 | pageIndex: number;
4 | }
5 |
6 | export enum SortDirection {
7 | ASC = 0,
8 | DESC = 1
9 | }
10 |
11 | export enum OperatorComparer {
12 | Contains,
13 | StartsWith,
14 | EndsWith,
15 | Equals = 13,
16 | GreaterThan = 15,
17 | GreaterThanOrEqual = 16,
18 | LessThan = 20,
19 | LessThanOrEqual = 21,
20 | NotEqual = 35,
21 | Not = 34,
22 | CustomFilter = 99
23 | }
24 |
25 | export interface PropertyFilterConfig {
26 | propertyName: string;
27 | operatorComparer?: OperatorComparer | null;
28 | sortDirection?: SortDirection | null;
29 | value?: any;
30 | }
31 |
32 | export interface BasePagedRequest extends PagedBase {
33 | /**
34 | * Where object for 1:1 mapping to entity to be filtered.
35 | * Only requirenment is that property names are same
36 | */
37 | where?: T;
38 |
39 | /**
40 | * Defines rules for sorting and filtering
41 | * Can be left empty and in such way, the default values are used.
42 | * Default values are no sort and Equals comparer
43 | */
44 | propertyFilterConfigs?: PropertyFilterConfig[];
45 |
46 | /**String SearchQuery meant for searching ANY of the tagged property */
47 | searchQuery?: string;
48 | }
49 |
50 | export interface PagedResultBase extends PagedBase {
51 | pageCount: number;
52 | rowCount: number;
53 | currentPage: number;
54 | firstRowOnPage: number;
55 | lastRowOnPage: number;
56 | }
57 |
58 | export interface PagedResult> {
59 | results: V[];
60 | rowCount: number;
61 | }
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ReFilter
2 |
3 | ## A Package supporting Filtering, Sorting and Pagination
4 |
5 | This package is designed to facilitate both basic and advanced filtering, sorting, and pagination for queryable entities, including lists, arrays, and IQueryable
6 | It's meant to be used with EntityFramework and CodeFirst approach, although it will work with all other approaches as well.
7 | Filtering and sorting support simple property based scenarios or advanced override scenarios.
8 | Search feature uses attributes to determine which properties to do a search against and builds a ready to use OR predicate.
9 |
10 | To use it, you should inject it inside Startup.cs like so:
11 |
12 | ```cs
13 | services.AddReFilter(typeof(ReFilterConfigBuilder));
14 | ```
15 |
16 | This tells your DI to include the ReFilter bootstraper.
17 | Only `AddReFilter` is required. If you don't have any custom filters, you don't need to use anything else.
18 | If you do, then you add them as any other service inside your DI.
19 | For minimum implementation of required stuff check [Filtering and Sorting Examples](#filtering-and-sorting-examples)
20 |
21 | And now you're ready to use ReFilter.
22 |
23 | ## Basic Example
24 |
25 | ```cs
26 | public async Task> GetPaged(BasePagedRequest request)
27 | {
28 | var testQueryable = testList.AsQueryable(); // Any kind of User queryable
29 |
30 | var pagedRequest = request.GetPagedRequest(returnResults: true); // Transformation
31 |
32 | var result = await testReFilterActions.GetPaged(testQueryable, pagedRequest);
33 | // Calling ReFilter GetPaged action
34 |
35 | return result;
36 | }
37 | ```
38 |
39 | This would return a paginated list of users for the request you supply it.
40 | `BasePagedRequest` is the base request toward ReFilter and the main messanger object.
41 | Main parts are Where, PropertyFilterConfigs and SearchQuery. The rest gets populated inside ReFilter.
42 |
43 | ```cs
44 | ///
45 | /// Object meant for mapping into query conditions.
46 | /// Only requirenment is that property names match destination
47 | ///
48 | public JObject Where { get; set; }
49 |
50 | ///
51 | /// Defines rules for sorting and filtering
52 | /// Can be left empty and in such way, the default values are used.
53 | /// Default values are no sort and Equals comparer
54 | ///
55 | public List PropertyFilterConfigs { get; set; }
56 |
57 | ///
58 | /// String SearchQuery meant for searching ANY of the tagged property
59 | ///
60 | public string SearchQuery { get; set; }
61 |
62 | ///
63 | /// If you need to filter by multiple incompatible filters, this is the easiest way to do it
64 | /// Depending on set in parent BasePagedRequest, child requests are added either as AND or OR clauses
65 | /// Predicate is being built the same way every time so you are able to chain multiple complex filters
66 | ///
67 | public List PagedRequests { get; set; }
68 | ```
69 |
70 | ## Features
71 |
72 | ### Pagination
73 |
74 | Pagination is a basic feature so lets start with it.
75 | It's all based on the root `BasePagedRequest` and any following `PagedRequests` are ignored when Pagination is concearned.
76 | Basic params are `PageIndex` and `PageSize`.
77 | Index starts with 0.
78 | It's enought to use the basic request from example and call any version of `GetPaged` from IReFilterActions.
79 |
80 | ```cs
81 | new BasePagedRequest
82 | {
83 | PageIndex = 0,
84 | PageSize = 10
85 | }
86 | ```
87 |
88 | ### Search
89 |
90 | Search makes it possible to search over chosen string properties and fetch results. It's case insensitive and uses `OperatorComparer.Contains` mechanism using every property as OR clause.
91 | Setting up Search is quite easy and only requires setting an attribute over a property on _database_ model:
92 |
93 | ```cs
94 | [ReFilterProperty]
95 | public string Address { get; set; }
96 | ```
97 |
98 | This tells ReFilter that the property is available as a search parameter and it uses it in the query.
99 | To trigger Search you need to pass the `SearchQuery` value to ReFilter query and call any action from IReFilterActions.
100 |
101 | #### Considerations and Limitations
102 |
103 | Since V.1.1.0 it is possible to use Search values inside child entites and child collections.
104 | ReFilter recognizes any object or array type marked with "UsedForSearchQuery" as another branch for going through Search value.
105 | String search is expensive and going through a tree of entites searching for a string is very expensive.
106 | Search itself is combined as an OR clause but is combined with every other feature as an AND clause.
107 | Pending: Custom search provider in form of an `Expression>` => this would make search use custom implementation when desired (not only `OperatorComparer.Contains`).
108 |
109 | ### Filtering
110 |
111 | Filtering is achieved by a combination of `Where` property and `PropertyFilterConfigs`.
112 | If `PropertyFilterConfigs` is omitted then default filtering parameters are used: `OperatorComparer.Contains` for string and `OperatorComparer.Equals` for everything else.
113 | If `Where` is omitted then the filtering is based on `PropertyFilterConfigs`.
114 | Where is any arbitrary object but the keys used for it have to match the model which you want to filter.
115 | That means if you have a list of `User` objects, you want your `Where` to be a replica of nullable `User` object. The most correct way to think about it is `Partial` from TypeScript.
116 | Special case is `RangeFilter` which filters out by provided range and falls back to basic property filtering. It essentially unpacks into `PropertyFilterConfigs`.
117 | All the filter options from `OperatorComparer` use valid built in `ExpressionType` values such as: Contains, StartsWith, EndsWith, Equals, GreaterThan, LessThan, etc. .
118 |
119 | Since V.2.0.0 filtering received a major refactor and supports pretty much everything you can think of.
120 | The most important change was the introduction of `OR` option for filtering. Previously, with the exception of Search, all the filtering was done using `AND` clause. That did not reflect the real world needs and had to be upgraded.
121 | Because of that a new property was introduced: `PredicateOperator` on `BasePagedRequest` and `PropertyFilterConfigs`; with options of `AND` or `OR`.
122 | This means that you can use `BasePagedRequest` to wrap filtering by properties or special filters in a selected clause but properties themselves can have multiple AND/OR filters on them.
123 | Not only that but you can send multiple `BasePagedRequest` objects with different filters and choose to build either clause.
124 | The `Where` object is no longer necessary but it's still the simplest way to use ReFilter and a major syntactic sugar. I plan to always support this feature.
125 | While it can still be used the same way as previously, the base of filtering bacame `PropertyFilterConfigs` because it carries the information about the `OperatorComparer`, the `Value` used to filter/compare against and the `PredicateOperator` to be used.
126 |
127 | There are real life examples inside test project and I plan to add even more meaningful examples.
128 | Example of a model to filter over and matching IReFilterRequest used as Where from test project:
129 |
130 | ```cs
131 | class School
132 | {
133 | public int Id { get; set; }
134 | public int IdRange { get; set; }
135 | [ReFilterProperty]
136 | public string Name { get; set; }
137 | [ReFilterProperty]
138 | public string Address { get; set; }
139 |
140 | public Country Country { get; set; }
141 |
142 | public List Contacts { get; set; }
143 | public List Students { get; set; }
144 |
145 | public double Age { get; set; }
146 | public DateTime FoundingDate { get; set; }
147 | public DateOnly ValidOn { get; set; }
148 |
149 | public bool IsActive { get; set; }
150 | }
151 |
152 | class SchoolFilterRequest : IReFilterRequest
153 | {
154 | public int? Id { get; set; }
155 | public RangeFilter IdRange { get; set; }
156 | public string Name { get; set; }
157 | public string Address { get; set; }
158 |
159 | [ReFilterProperty(HasSpecialSort = true)]
160 | public CountryFilterRequest Country { get; set; }
161 |
162 | public List Contacts { get; set; }
163 |
164 | [ReFilterProperty(HasSpecialFilter = true)]
165 | public List StudentNames { get; set; }
166 |
167 | public RangeFilter Age { get; set; }
168 |
169 | public RangeFilter FoundingDate { get; set; }
170 | public RangeFilter ValidOn { get; set; }
171 |
172 | public bool? IsActive { get; set; }
173 | }
174 | ```
175 |
176 | When generating filters property by property, we need to know which property to filter by.
177 | When sending an object that has some properties set, we know to use those properties as filters. But in the case of sending a default School object, the Id would always have a value, even if default one. A default value is a valid value so we can't ignore it and it can never be null and therefore it can't be skipped when filtering.
178 | That's why the FilterRequest version has every property as nullable. As such, any value it gets is used for filtering and there can be no doubt about intentions.
179 | This is not a rule, but it's the most common case. If you need a property to always be defined you are free to set it up that way. If you don't need a nullable object for a filter, you don't need to set it up explicitly inside `ReFilterConfigBuilder`, meaning your `GetMatchingType` would fallback to default type, in this case School.
180 | Additionally, since V.2.0.0 the `Where` in `BasePagedRequest` is no longer necessary and `PropertyFilterConfigs` can be used instead to same effect.
181 |
182 | ### Sorting
183 |
184 | Sorting is a bit different from Filtering in a way that it only requires `PropertyFilterConfigs` set up correctly.
185 | To set them up correctly, the PFC needs to have a `SortDirection` set and `PropertyName` needs to match the case of the property name.
186 | Sorting also supports for multiple sorts at the same time.
187 |
188 | ### Filtering and Sorting Examples
189 |
190 | ReFilter needs to be setup in a central place inside your project.
191 | For filtering that's your implementation of `IReFilterConfigBuilder` and for sorting it's your implementation of `IReSortConfigBuilder`.
192 | Both serve the same purpose: matching nullable objects to database models and are nothing more than "controllers" for redirecting "requests".
193 | Both have 2 methods: `GetMatchingType` and `GetMatching[Filter/Sort]Builder`.
194 | They are intentionally separated since Sort and Filter don't need to have the same model.
195 | Also, both implementations are required for ReFilter to work.
196 | The minimum implementation (only filter is shown but filter and sort are mirrored) is as shown:
197 |
198 | ```cs
199 | class ReFilterConfigBuilder : IReFilterConfigBuilder
200 | {
201 | public Type GetMatchingType() where T : class, new()
202 | {
203 | switch (typeof(T))
204 | {
205 | default:
206 | return typeof(T);
207 | }
208 | }
209 |
210 | public IReFilterBuilder GetMatchingFilterBuilder() where T : class, new()
211 | {
212 | switch (typeof(T))
213 | {
214 | default:
215 | return null;
216 | }
217 | }
218 | }
219 | ```
220 |
221 | Then, when you have the basics setup, you can use the following:
222 | ```cs
223 | services.AddReFilter(typeof(ReFilterConfigBuilder), typeof(ReSortConfigBuilder));
224 | ```
225 |
226 | Other than the basic sorting and filtering scenarios, there are also advanced ones using custom implementations provided by you.
227 | The advanced custom scenarions are implemented via `IRe[Filter/Sort]Builder`s. Also, these are the most complicated ones and provide all the options.
228 |
229 | ```cs
230 | public interface IReFilterBuilder where T : class, new()
231 | {
232 | ///
233 | /// Gets all the custom filters and matches them to properties.
234 | ///
235 | ///
236 | ///
237 | IEnumerable> GetFilters(IReFilterRequest filterRequest);
238 | ///
239 | /// Entry point of filter builder. Builds default query for that entity.
240 | ///
241 | ///
242 | ///
243 | IQueryable BuildEntityQuery(IReFilterRequest filterRequest);
244 | ///
245 | /// First uses GetFilters and then applies them to the provided query.
246 | ///
247 | ///
248 | ///
249 | ///
250 | IQueryable BuildFilteredQuery(IQueryable query, IReFilterRequest filterRequest);
251 | ///
252 | /// Gets the list of Ids for the provided filter parameters in order to use it as an "IN ()" clause.
253 | ///
254 | ///
255 | ///
256 | List GetForeignKeys(IReFilterRequest filterRequest);
257 | }
258 | ```
259 |
260 | A real life example of a UserFilterBuilder will be shown below.
261 | We'll start with models and move upwards.
262 | Entity Framework Code First is used as database provider for this solution. The models themselves don't need anything extra in this case (only Search attribute is used directly on EF models).
263 | What it does require is the _FilterRequest_ class that has a blueprint of what goes into the custom _FilterBuilder_.
264 | This is the `UserFilterRequest`. Notice that it inherits `IReFilterRequest` and also defines special `ReFilterProperty` attributes over some properties.
265 | Those tell ReFilter how to handle them when they arrive via `Where` property.
266 | Once you're done with the configuration, you need to supply it with logic, in this case the `UserFilterBuilder` which takes care of the general implementation for filtering the _User_.
267 | For every property marked as `HasSpecialFilter` you will need a dedicated filter which will be applied to the query. That's achieved via `BuildFilteredQuery`and specific implementation of target filter showcased in `RoleFilter`.
268 | Finally, everything is connected inside `ReFilterConfigBuilder`.
269 |
270 | ```cs
271 | public class User : IdentityUser
272 | {
273 | public DateTime DateCreated { get; set; }
274 | public DateTime? DateModified { get; set; }
275 | public DatabaseEntityStatus Status { get; set; }
276 |
277 | public Person UserDetails { get; set; }
278 |
279 | public List UserRoles { get; set; }
280 | public List UserClaims { get; set; }
281 |
282 | public List UserRenewTokens { get; set; }
283 |
284 | public List CompanyUsers { get; set; }
285 | }
286 | ```
287 |
288 | ```cs
289 | public class Person : DatabaseEntity
290 | {
291 | [Required]
292 | [StringLength(255)]
293 | public string FirstName { get; set; }
294 | [StringLength(255)]
295 | public string MiddleName { get; set; }
296 | [Required]
297 | [StringLength(255)]
298 | public string LastName { get; set; }
299 |
300 | [StringLength(40)]
301 | public string Mobile { get; set; }
302 | [StringLength(40)]
303 | public string Phone { get; set; }
304 | [StringLength(40)]
305 | public string Email { get; set; }
306 |
307 | public Guid? UserId { get; set; }
308 | public User User { get; set; }
309 | }
310 | ```
311 |
312 | ```cs
313 | public class UserFilterRequest : IReFilterRequest
314 | {
315 | public string Email { get; set; }
316 | public string UserName { get; set; }
317 |
318 | [ReFilterProperty(HasSpecialFilter = true)]
319 | public int? CompanyId { get; set; }
320 | [ReFilterProperty(HasSpecialFilter = true)]
321 | public Guid? RoleId { get; set; }
322 |
323 | [ReFilterProperty(HasSpecialFilter = true, HasSpecialSort = true)]
324 | public string FirstName { get; set; }
325 | [ReFilterProperty(HasSpecialFilter = true, HasSpecialSort = true)]
326 | public string MiddleName { get; set; }
327 | [ReFilterProperty(HasSpecialFilter = true, HasSpecialSort = true)]
328 | public string LastName { get; set; }
329 |
330 | [ReFilterProperty(HasSpecialFilter = true, HasSpecialSort = true)]
331 | public string Mobile { get; set; }
332 | [ReFilterProperty(HasSpecialFilter = true, HasSpecialSort = true)]
333 | public string Phone { get; set; }
334 |
335 | public bool? EmailConfirmed { get; set; }
336 |
337 | [ReFilterProperty(HasSpecialFilter = true, HasSpecialSort = true)]
338 | public bool? IsActive { get; set; }
339 |
340 | [ReFilterProperty(HasSpecialFilter = true)]
341 | public bool? IsSuperAdmin { get; set; }
342 | }
343 | ```
344 |
345 | ```cs
346 | internal class UserFilterBuilder : IReFilterBuilder
347 | {
348 | private readonly IUnitOfWork unitOfWork;
349 | private readonly ApplicationSettings appSettings;
350 |
351 | // Since it's provided via DI, you can use any DI mechanism here
352 | public UserFilterBuilder(IOptions appSettings, IUnitOfWork unitOfWork)
353 | {
354 | this.appSettings = appSettings.Value;
355 | this.unitOfWork = unitOfWork;
356 | }
357 |
358 | public IQueryable BuildEntityQuery(IReFilterRequest filterRequest)
359 | {
360 | var query = unitOfWork.GetDbSet().AsQueryable();
361 |
362 | query = BuildFilteredQuery(query, filterRequest);
363 |
364 | return query;
365 | }
366 |
367 | public IQueryable BuildFilteredQuery(IQueryable query, IReFilterRequest filterRequest)
368 | {
369 | var filters = GetFilters(filterRequest).ToList();
370 |
371 | filters.ForEach(filter =>
372 | {
373 | query = filter.FilterQuery(query);
374 | });
375 |
376 | return query;
377 | }
378 |
379 | public IEnumerable> GetFilters(IReFilterRequest filterRequest)
380 | {
381 | List> filters = new List>();
382 |
383 | if (filterRequest == null)
384 | {
385 | return filters;
386 | }
387 |
388 | var realFilter = (UserFilterRequest)filterRequest;
389 | // Custom filter implementation for these properties
390 | if (!string.IsNullOrWhiteSpace(realFilter.FirstName))
391 | {
392 | filters.Add(new FirstNameFilter(realFilter.FirstName));
393 | }
394 |
395 | if (!string.IsNullOrWhiteSpace(realFilter.LastName))
396 | {
397 | filters.Add(new LastNameFilter(realFilter.LastName));
398 | }
399 |
400 | if (!string.IsNullOrWhiteSpace(realFilter.MiddleName))
401 | {
402 | filters.Add(new MiddleNameFilter(realFilter.MiddleName));
403 | }
404 |
405 | if (!string.IsNullOrWhiteSpace(realFilter.Mobile))
406 | {
407 | filters.Add(new MobileFilter(realFilter.Mobile));
408 | }
409 |
410 | if (!string.IsNullOrWhiteSpace(realFilter.Phone))
411 | {
412 | filters.Add(new PhoneFilter(realFilter.Phone));
413 | }
414 |
415 | if (realFilter.RoleId.HasValue)
416 | {
417 | filters.Add(new RoleFilter(realFilter.RoleId.Value));
418 | }
419 |
420 | if (realFilter.IsActive.HasValue)
421 | {
422 | filters.Add(new IsActiveFilter(realFilter.IsActive.Value));
423 | }
424 |
425 | if (realFilter.CompanyId.HasValue)
426 | {
427 | filters.Add(new CompanyFilter(realFilter.CompanyId.Value));
428 | }
429 |
430 | if (realFilter.IsSuperAdmin.HasValue)
431 | {
432 | filters.Add(new SuperAdminFilter(realFilter.IsSuperAdmin.Value, appSettings.SuperAdminRole));
433 | }
434 |
435 | return filters;
436 | }
437 |
438 | public List GetForeignKeys(IReFilterRequest filterRequest)
439 | {
440 | var query = unitOfWork.GetDbSet().Where(u => u.Status == DatabaseEntityStatus.Active);
441 |
442 | query = BuildFilteredQuery(query, filterRequest);
443 |
444 | return query.Select(e => e.Id)
445 | .Distinct()
446 | .ToList();
447 | }
448 | }
449 | ```
450 |
451 | ```cs
452 | internal class RoleFilter : IReFilter
453 | {
454 | private readonly Guid roleId;
455 |
456 | public RoleFilter(Guid roleId)
457 | {
458 | this.roleId = roleId;
459 | }
460 |
461 | public IQueryable FilterQuery(IQueryable query)
462 | {
463 | return query.Where(e => e.UserRoles.Any(ur => ur.RoleId == roleId));
464 | }
465 | }
466 | ```
467 |
468 | ```cs
469 | class ReFilterConfigBuilder : IReFilterConfigBuilder
470 | {
471 | private readonly UserFilterBuilder userFilterBuilder;
472 |
473 | public ReFilterConfigBuilder(UserFilterBuilder userFilterBuilder)
474 | {
475 | this.userFilterBuilder = userFilterBuilder;
476 | }
477 |
478 | public Type GetMatchingType() where T : class, new()
479 | {
480 | return EntityTypeMatcher.GetEntityTypeConfig().FilterRequestType ?? typeof(T);
481 | }
482 |
483 | public IReFilterBuilder GetMatchingFilterBuilder() where T : class, new()
484 | {
485 | return typeof(T) switch
486 | {
487 | Type user when user == typeof(User) => (IReFilterBuilder)userFilterBuilder,
488 | _ => null,
489 | };
490 | }
491 | }
492 | ```
493 |
494 | As a request you would use something like this:
495 |
496 | ```ts
497 | {
498 | 'Where': {
499 | 'Email': 'user@email.com',
500 | 'UserName': 'username',
501 | 'FirstName': 'user',
502 | 'IsActive': true
503 | }, // Supplies all the filter params to filter by
504 | 'PropertyFilterConfigs': [
505 | {
506 | 'PropertyName': 'UserName',
507 | 'OperatorComparer': 1,
508 | 'SortDirection': 1
509 | } // This way the request is configured to filter AND sort by UserName and apply Desc sort order and use 'StartsWith' built in filter mode
510 | ],
511 | 'SearchQuery': 'username' // This one wouldn't be applied since no property on User is marked as ReFilterProperty.UsedForSearchQuery = true
512 | }
513 | ```
514 |
515 | Finally, real life example of how it all looks inside the service:
516 |
517 | ```cs
518 | // PagedResult is always the ReFilter result but you can return anything from your method
519 | public async Task>> GetPaged(BasePagedRequest request)
520 | {
521 | try
522 | {
523 | // Create any basic query you want to build upon
524 | // NoTracking for speedy reading
525 | var query = unitOfWork.GetDbSet().AsQueryable().AsNoTracking();
526 |
527 | // Transform PagedRequest to it's final form, supply it with mapping logic for end result (in that way you don't need to handle result transformation manually)
528 | // Also, I encourage you to use ProjectTo by AutoMapper <3 becase it builds the select clause for just the stuff you need in your ViewModel
529 | // If you compare it to Test project this really hides a lot of details under the hood and requires some advanced knowledge of AutoMapper <3 features
530 | // However, it also provides the best QoL stuff
531 | var pagedRequest = request.GetPagedRequest((IQueryable x) => mapper.ProjectTo(x).ToList());
532 | var pagedResult = await reFilterActions.GetPaged(query, pagedRequest);
533 |
534 | return ActionResponse.Success(pagedResult);
535 | }
536 | catch (Exception ex)
537 | {
538 | var message = stringLocalizer.GetString(Resources.FetchError);
539 | logger.LogError(message, ex, request);
540 | return ActionResponse>.Error(Message: stringLocalizer.GetString(Resources.FetchError));
541 | }
542 | }
543 | ```
544 |
545 | The only difference concearning sorting is the fact that it's configured using `PropertyFilterConfig`. So, any config that is provided and has the `SortDirection` set will be used to sort the query.
546 | Again, it's necessary to configure `UserSortBuilder` in order for everyting to apply correctly.
547 |
548 | ```cs
549 | internal class UserSortBuilder : IReSortBuilder
550 | {
551 | private readonly IUnitOfWork unitOfWork;
552 |
553 | public UserSortBuilder(IUnitOfWork unitOfWork)
554 | {
555 | this.unitOfWork = unitOfWork;
556 | }
557 |
558 | public IQueryable BuildEntityQuery()
559 | {
560 | return unitOfWork.GetDbSet();
561 | }
562 |
563 | public IOrderedQueryable BuildSortedQuery(IQueryable query, PropertyFilterConfig propertyFilterConfig, bool isFirst = false)
564 | {
565 | var sorters = GetSorters(propertyFilterConfig);
566 |
567 | if (sorters == null || sorters.Count == 0)
568 | {
569 | return (IOrderedQueryable)query;
570 | }
571 |
572 | IOrderedQueryable orderedQuery = (IOrderedQueryable)query;
573 |
574 | for (var i = 0; i < sorters.Count; i++)
575 | {
576 | orderedQuery = sorters[i].SortQuery(orderedQuery,
577 | propertyFilterConfig.SortDirection.Value,
578 | isFirst: (i == 0 && isFirst));
579 | }
580 |
581 | return orderedQuery;
582 | }
583 |
584 | public List> GetSorters(PropertyFilterConfig propertyFilterConfig)
585 | {
586 | List> sorters = new();
587 |
588 | if (propertyFilterConfig != null)
589 | {
590 | if (propertyFilterConfig.PropertyName == nameof(UserFilterRequest.FirstName))
591 | {
592 | sorters.Add(new FirstNameSorter());
593 | }
594 |
595 | if (propertyFilterConfig.PropertyName == nameof(UserFilterRequest.MiddleName))
596 | {
597 | sorters.Add(new MiddleNameSorter());
598 | }
599 |
600 | if (propertyFilterConfig.PropertyName == nameof(UserFilterRequest.LastName))
601 | {
602 | sorters.Add(new LastNameSorter());
603 | }
604 |
605 | if (propertyFilterConfig.PropertyName == nameof(UserFilterRequest.Mobile))
606 | {
607 | sorters.Add(new MobileSorter());
608 | }
609 |
610 | if (propertyFilterConfig.PropertyName == nameof(UserFilterRequest.Phone))
611 | {
612 | sorters.Add(new PhoneSorter());
613 | }
614 |
615 | if (propertyFilterConfig.PropertyName == nameof(UserFilterRequest.IsActive))
616 | {
617 | sorters.Add(new IsActiveSorter());
618 | }
619 | }
620 |
621 | return sorters;
622 | }
623 | }
624 | ```
625 |
626 | ```cs
627 | internal class MiddleNameSorter : IReSort
628 | {
629 | public IOrderedQueryable SortQuery(IQueryable query, SortDirection sortDirection = SortDirection.ASC, bool isFirst = true)
630 | {
631 | if (isFirst)
632 | {
633 | query = sortDirection == SortDirection.ASC ?
634 | query.OrderBy(e => e.UserDetails.MiddleName) :
635 | query.OrderByDescending(e => e.UserDetails.MiddleName);
636 | }
637 | else
638 | {
639 | query = sortDirection == SortDirection.ASC ?
640 | ((IOrderedQueryable)query).ThenBy(e => e.UserDetails.MiddleName)
641 | : ((IOrderedQueryable)query).ThenByDescending(e => e.UserDetails.MiddleName);
642 | }
643 |
644 | return (IOrderedQueryable)query;
645 | }
646 | }
647 | ```
648 |
649 | ### Projections
650 |
651 | Another feature ReFilter "has" is implementing automatic projections.
652 | This is important because the query built over the database tables would only select fields needed for materialization, unlike when you would do `.ToList()`.
653 | Use of this feature is highly encouraged and made having [AutoMapper's projection feature](https://docs.automapper.org/en/stable/Queryable-Extensions.html) in mind.
654 | You can see examples of manual projection inside test project but [real life scenario can be viewed here](#real-life-projection-example).
655 |
656 | ## Notes
657 |
658 | Most of the mechanisms used are public and can be reused in your code.
659 |
660 | ## Pending Features
661 |
662 | - Special SearchQuery => Custom search provider in form of an `Expression>` => this would make search use custom implementation when desired (not only `OperatorComparer.Contains`).
663 | - SearchQuery in combination with overridable OperationComparers
664 | - recursive IReFilterRequest filtering over child or parent objects => currently achiavable via special filters but the goal is to filter related objects and link the filter to foreign keys
665 | - use Testcontainers for setup of real database and safer testing
666 | - create performance tests
667 |
668 | Additionally, check the Docs folder for examples of FE requests.
669 | If needed, I can give you the AgGrid implementation as well.
670 |
671 | Any kind of help or suggestions are welcome.
672 |
673 | ## Thank you and I hope you enjoy using ReFilter
674 |
--------------------------------------------------------------------------------
/ReFilter.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30517.126
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReFilter", "ReFilter\ReFilter.csproj", "{821B70AB-F007-4205-A1BE-8599BEFCB500}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestProject", "TestProject\TestProject.csproj", "{1DBAAC8A-BC24-440D-98F1-2C18E3DD8DCE}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {821B70AB-F007-4205-A1BE-8599BEFCB500}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {821B70AB-F007-4205-A1BE-8599BEFCB500}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {821B70AB-F007-4205-A1BE-8599BEFCB500}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {821B70AB-F007-4205-A1BE-8599BEFCB500}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {1DBAAC8A-BC24-440D-98F1-2C18E3DD8DCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {1DBAAC8A-BC24-440D-98F1-2C18E3DD8DCE}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {1DBAAC8A-BC24-440D-98F1-2C18E3DD8DCE}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {1DBAAC8A-BC24-440D-98F1-2C18E3DD8DCE}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {84C28F7B-145B-43AE-B619-0AC6D520504F}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/ReFilter/Attributes/ReFilterBuilder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ReFilter.Attributes
4 | {
5 | [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
6 | public sealed class ReFilterBuilder : Attribute
7 | {
8 | public Type FilterBuilderType { get; }
9 |
10 | public ReFilterBuilder(Type filterProviderType)
11 | {
12 | FilterBuilderType = filterProviderType;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/ReFilter/Attributes/ReFilterProperty.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ReFilter.Attributes
4 | {
5 | [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
6 | public sealed class ReFilterProperty : Attribute
7 | {
8 | public bool UsedForSearchQuery { get; set; } = true;
9 | public bool HasSpecialFilter { get; set; } = false;
10 | public bool HasSpecialSort { get; set; } = false;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/ReFilter/Attributes/ReFilterSpecialFilter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ReFilter.Attributes
4 | {
5 | [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
6 | public sealed class ReFilterSpecialFilter : Attribute
7 | {
8 | public string AttributeName { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/ReFilter/Converters/DateOnlyConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Text.RegularExpressions;
4 | using Newtonsoft.Json;
5 |
6 | namespace ReFilter.Converters
7 | {
8 | public class DateOnlyConverter : JsonConverter
9 | {
10 | private const string DateFormat = "yyyy-MM-dd";
11 |
12 | public override DateOnly ReadJson(JsonReader reader, Type objectType, DateOnly existingValue, bool hasExistingValue, JsonSerializer serializer)
13 | {
14 | if (reader.Value.GetType() == typeof(string))
15 | {
16 | Regex dateOnlyRegex = new(@"\d{4}-\d{1,2}-\d{1,2}");
17 | Match match = dateOnlyRegex.Match((string)reader.Value);
18 |
19 | if (match.Success && match.Groups.Count == 1)
20 | {
21 | return DateOnly.ParseExact(match.Groups[0].Value, DateFormat, CultureInfo.InvariantCulture);
22 | }
23 | }
24 |
25 | if (reader.Value.GetType() == typeof(DateTime))
26 | {
27 | return DateOnly.FromDateTime((DateTime)reader.Value);
28 | }
29 |
30 | return DateOnly.ParseExact((string)reader.Value, DateFormat, CultureInfo.InvariantCulture);
31 | }
32 |
33 | public override void WriteJson(JsonWriter writer, DateOnly value, JsonSerializer serializer)
34 | {
35 | writer.WriteValue(value.ToString(DateFormat, CultureInfo.InvariantCulture));
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ReFilter/Converters/DateOnlyNullableConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.Text.RegularExpressions;
4 | using Newtonsoft.Json;
5 |
6 | namespace ReFilter.Converters
7 | {
8 | public class DateOnlyNullableConverter : JsonConverter
9 | {
10 | private const string DateFormat = "yyyy-MM-dd";
11 |
12 | public override DateOnly? ReadJson(JsonReader reader, Type objectType, DateOnly? existingValue, bool hasExistingValue, JsonSerializer serializer)
13 | {
14 | if (reader.Value == null)
15 | {
16 | return null;
17 | }
18 |
19 | if (reader.Value.GetType() == typeof(string))
20 | {
21 | Regex dateOnlyRegex = new(@"\d{4}-\d{1,2}-\d{1,2}");
22 | Match match = dateOnlyRegex.Match((string)reader.Value);
23 |
24 | if (match.Success && match.Groups.Count == 1)
25 | {
26 | return DateOnly.ParseExact(match.Groups[0].Value, DateFormat, CultureInfo.InvariantCulture);
27 | }
28 | }
29 |
30 | if (reader.Value.GetType() == typeof(DateTime))
31 | {
32 | return DateOnly.FromDateTime((DateTime)reader.Value);
33 | }
34 |
35 | return null;
36 | }
37 |
38 | public override void WriteJson(JsonWriter writer, DateOnly? value, JsonSerializer serializer)
39 | {
40 | writer.WriteValue(value?.ToString(DateFormat, CultureInfo.InvariantCulture));
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/ReFilter/Converters/TimeOnlyConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using Newtonsoft.Json;
4 |
5 | namespace ReFilter.Converters
6 | {
7 | public class TimeOnlyConverter : JsonConverter
8 | {
9 | private const string TimeFormat = "HH:mm:ss.FFFFFFF";
10 |
11 | public override TimeOnly ReadJson(JsonReader reader, Type objectType, TimeOnly existingValue, bool hasExistingValue, JsonSerializer serializer)
12 | {
13 | return TimeOnly.ParseExact((string)reader.Value, TimeFormat, CultureInfo.InvariantCulture);
14 | }
15 |
16 | public override void WriteJson(JsonWriter writer, TimeOnly value, JsonSerializer serializer)
17 | {
18 | writer.WriteValue(value.ToString(TimeFormat, CultureInfo.InvariantCulture));
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ReFilter/Converters/TimeOnlyNullableConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using Newtonsoft.Json;
4 |
5 | namespace ReFilter.Converters
6 | {
7 | public class TimeOnlyNullableConverter : JsonConverter
8 | {
9 | private const string TimeFormat = "HH:mm:ss.FFFFFFF";
10 |
11 | public override TimeOnly? ReadJson(JsonReader reader, Type objectType, TimeOnly? existingValue, bool hasExistingValue, JsonSerializer serializer)
12 | {
13 | return TimeOnly.ParseExact((string)reader.Value, TimeFormat, CultureInfo.InvariantCulture);
14 | }
15 |
16 | public override void WriteJson(JsonWriter writer, TimeOnly? value, JsonSerializer serializer)
17 | {
18 | writer.WriteValue(value?.ToString(TimeFormat, CultureInfo.InvariantCulture));
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ReFilter/Enums/OperatorComparer.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 |
3 | namespace ReFilter.Enums
4 | {
5 | public enum OperatorComparer
6 | {
7 | Contains,
8 | StartsWith,
9 | EndsWith,
10 | Equals = ExpressionType.Equal,
11 | GreaterThan = ExpressionType.GreaterThan,
12 | GreaterThanOrEqual = ExpressionType.GreaterThanOrEqual,
13 | LessThan = ExpressionType.LessThan,
14 | LessThanOrEqual = ExpressionType.LessThanOrEqual,
15 | NotEqual = ExpressionType.NotEqual,
16 | Not = ExpressionType.Not,
17 | NotStartsWith = 92,
18 | NotEndsWith = 93,
19 | NotContains = 94,
20 | BetweenExclusive = 95,
21 | BetweenInclusive = 96,
22 | BetweenLowerInclusive = 97,
23 | BetweenHigherInclusive = 98,
24 | CustomFilter = 99
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ReFilter/Enums/SortDirection.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 |
3 | namespace ReFilter.Enums
4 | {
5 | public enum SortDirection
6 | {
7 | ASC = 0,
8 | DESC = 1
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/ReFilter/Extensions/BasePagedRequestExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using LinqKit;
4 | using ReFilter.Models;
5 |
6 | namespace ReFilter.Extensions
7 | {
8 | public static class BasePagedRequestExtensions
9 | {
10 | public static List GetPropertyFilterConfigs(this BasePagedRequest request, string filterKey, Dictionary filterValues)
11 | {
12 | var existingPfcs = request.PropertyFilterConfigs?
13 | .Where(pfc => pfc.PropertyName == filterKey);
14 |
15 | if (existingPfcs is not null && existingPfcs.Count() > 0)
16 | {
17 | return existingPfcs
18 | .Select(pfc =>
19 | {
20 | pfc.Value ??= filterValues[filterKey];
21 | return pfc;
22 | })
23 | .ToList();
24 | }
25 | else
26 | {
27 | return new List
28 | {
29 | new()
30 | {
31 | PropertyName = filterKey,
32 | Value = filterValues[filterKey],
33 | PredicateOperator = PredicateOperator.And
34 | }
35 | };
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/ReFilter/Extensions/IServiceCollectionExtension.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.DependencyInjection;
3 | using Newtonsoft.Json;
4 | using ReFilter.ReFilterActions;
5 | using ReFilter.ReFilterConfigBuilder;
6 | using ReFilter.ReFilterTypeMatcher;
7 |
8 | namespace ReFilter.Extensions
9 | {
10 | public static class IServiceCollectionExtension
11 | {
12 | public static IServiceCollection AddReFilter(this IServiceCollection services,
13 | Type ReFilterTypeMatcherImplementation,
14 | Type ReSortTypeMatcherImplementation)
15 | {
16 | services.AddScoped();
17 | services.AddScoped(typeof(IReFilterConfigBuilder), ReFilterTypeMatcherImplementation);
18 | services.AddScoped(typeof(IReSortConfigBuilder), ReSortTypeMatcherImplementation);
19 |
20 | return services;
21 | }
22 |
23 | public static IServiceCollection AddReFilter(this IServiceCollection services,
24 | Type ReFilterTypeMatcherImplementation,
25 | Type ReSortTypeMatcherImplementation,
26 | JsonSerializer Serializer)
27 | {
28 | services.AddScoped(x => new ReFilterActions.ReFilterActions(
29 | x.GetRequiredService(),
30 | x.GetRequiredService(),
31 | Serializer));
32 |
33 | services.AddScoped(typeof(IReFilterConfigBuilder), ReFilterTypeMatcherImplementation);
34 | services.AddScoped(typeof(IReSortConfigBuilder), ReSortTypeMatcherImplementation);
35 |
36 | return services;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/ReFilter/Extensions/ObjectExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Linq;
5 | using System.Reflection;
6 |
7 | namespace ReFilter.Extensions
8 | {
9 | public static class ObjectExtensions
10 | {
11 | public static object GetPropValue(this object entity, string propName)
12 | {
13 | string[] nameParts = propName.Split('.');
14 | if (nameParts.Length == 1)
15 | {
16 | return entity.GetType().GetProperty(propName).GetValue(entity, null);
17 | }
18 |
19 | foreach (string part in nameParts)
20 | {
21 | if (entity == null) { return null; }
22 |
23 | Type type = entity.GetType();
24 | PropertyInfo info = type.GetProperty(part);
25 | if (info == null) { return null; }
26 |
27 | entity = info.GetValue(entity, null);
28 | }
29 | return entity;
30 | }
31 |
32 | public static Dictionary GetPropertiesWithValue(this T entity)
33 | {
34 | var values = typeof(T).GetProperties()
35 | .Where(p => p.GetValue(entity) != null)
36 | .ToDictionary(pv => pv.Name, pv => pv.GetValue(entity));
37 |
38 | return values;
39 | }
40 |
41 | public static Dictionary GetObjectPropertiesWithValue(this object entity)
42 | {
43 | var values = entity.GetType().GetProperties()
44 | .Where(p => p.GetValue(entity) != null)
45 | .ToDictionary(pv => pv.Name, pv => pv.GetValue(entity));
46 |
47 | return values;
48 | }
49 |
50 | public static Dictionary GetObjectPropertiesWithValueUnsafe(this object entity)
51 | {
52 | var values = entity.GetType().GetProperties()
53 | .ToDictionary(pv => pv.Name, pv => pv.GetValue(entity));
54 |
55 | return values;
56 | }
57 |
58 | public static string GetDescription(this T value)
59 | {
60 | return value.GetType()
61 | .GetMember(value.ToString())
62 | .First()
63 | .GetCustomAttribute()?
64 | .Description ?? string.Empty;
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/ReFilter/Extensions/PagedResultExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using ReFilter.Models;
5 |
6 | namespace ReFilter.Extensions
7 | {
8 | public static class PagedResultExtensions
9 | {
10 | public static PagedResult TransformResult(this PagedResult pagedResult, Func, List> mapFunction) where T : new() where U : new()
11 | {
12 | var newPagedResult = new PagedResult
13 | {
14 | PageCount = pagedResult.PageCount,
15 | PageIndex = pagedResult.PageIndex,
16 | PageSize = pagedResult.PageSize,
17 | RowCount = pagedResult.RowCount
18 | };
19 |
20 | newPagedResult.Results = mapFunction(pagedResult.Results ?? new List());
21 |
22 | return newPagedResult;
23 | }
24 |
25 | public static PagedResult TransformResult(this PagedResult pagedResult, Func, List> mapFunction) where T : new() where U : new()
26 | {
27 | var newPagedResult = new PagedResult
28 | {
29 | PageCount = pagedResult.PageCount,
30 | PageIndex = pagedResult.PageIndex,
31 | PageSize = pagedResult.PageSize,
32 | RowCount = pagedResult.RowCount
33 | };
34 |
35 | newPagedResult.Results = mapFunction(pagedResult.ResultQuery);
36 |
37 | return newPagedResult;
38 | }
39 |
40 | public static PagedResult TransformResult(this PagedResult pagedResult, PagedRequest pagedRequest, IQueryable query) where T : class, new() where U : class, new()
41 | {
42 | var newPagedResult = new PagedResult
43 | {
44 | PageCount = pagedResult.PageCount,
45 | PageIndex = pagedResult.PageIndex,
46 | PageSize = pagedResult.PageSize,
47 | RowCount = pagedResult.RowCount
48 | };
49 |
50 | newPagedResult.Results = pagedRequest.MappingFunction != null ?
51 | pagedRequest.MappingFunction(query.ToList())
52 | : pagedRequest.MappingProjection(query);
53 |
54 | return newPagedResult;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/ReFilter/Extensions/RangeFilterExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using ReFilter.Enums;
3 | using ReFilter.Models;
4 |
5 | namespace ReFilter.Extensions
6 | {
7 | public static class RangeFilterExtensions
8 | {
9 | public static List Unpack(this RangeFilter rangeFilter, PropertyFilterConfig selectedPfc) where U : struct
10 | {
11 | var newPropertyFilterConfigs = new List();
12 |
13 | var lowValue = rangeFilter.Start;
14 | var highValue = rangeFilter.End;
15 |
16 | switch (selectedPfc.OperatorComparer)
17 | {
18 | case OperatorComparer.Equals:
19 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
20 | {
21 | OperatorComparer = OperatorComparer.LessThanOrEqual,
22 | PropertyName = selectedPfc.PropertyName,
23 | Value = lowValue ?? highValue
24 | });
25 |
26 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
27 | {
28 | OperatorComparer = OperatorComparer.GreaterThanOrEqual,
29 | PropertyName = selectedPfc.PropertyName,
30 | Value = lowValue ?? highValue
31 | });
32 | break;
33 | case OperatorComparer.BetweenExclusive:
34 | if (lowValue != null)
35 | {
36 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
37 | {
38 | OperatorComparer = OperatorComparer.GreaterThan,
39 | PropertyName = selectedPfc.PropertyName,
40 | Value = lowValue
41 | });
42 | }
43 |
44 | if (highValue != null)
45 | {
46 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
47 | {
48 | OperatorComparer = OperatorComparer.LessThan,
49 | PropertyName = selectedPfc.PropertyName,
50 | Value = highValue
51 | });
52 | }
53 | break;
54 | case OperatorComparer.BetweenInclusive:
55 | if (lowValue != null)
56 | {
57 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
58 | {
59 | OperatorComparer = OperatorComparer.GreaterThanOrEqual,
60 | PropertyName = selectedPfc.PropertyName,
61 | Value = lowValue
62 | });
63 | }
64 |
65 | if (highValue != null)
66 | {
67 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
68 | {
69 | OperatorComparer = OperatorComparer.LessThanOrEqual,
70 | PropertyName = selectedPfc.PropertyName,
71 | Value = highValue
72 | });
73 | }
74 | break;
75 | case OperatorComparer.BetweenHigherInclusive:
76 | if (lowValue != null)
77 | {
78 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
79 | {
80 | OperatorComparer = OperatorComparer.GreaterThan,
81 | PropertyName = selectedPfc.PropertyName,
82 | Value = lowValue
83 | });
84 | }
85 |
86 | if (highValue != null)
87 | {
88 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
89 | {
90 | OperatorComparer = OperatorComparer.LessThanOrEqual,
91 | PropertyName = selectedPfc.PropertyName,
92 | Value = highValue
93 | });
94 | }
95 | break;
96 | case OperatorComparer.BetweenLowerInclusive:
97 | if (lowValue != null)
98 | {
99 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
100 | {
101 | OperatorComparer = OperatorComparer.GreaterThanOrEqual,
102 | PropertyName = selectedPfc.PropertyName,
103 | Value = lowValue
104 | });
105 | }
106 |
107 | if (highValue != null)
108 | {
109 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
110 | {
111 | OperatorComparer = OperatorComparer.LessThan,
112 | PropertyName = selectedPfc.PropertyName,
113 | Value = highValue
114 | });
115 | }
116 | break;
117 | case OperatorComparer.LessThan:
118 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
119 | {
120 | OperatorComparer = selectedPfc.OperatorComparer,
121 | PropertyName = selectedPfc.PropertyName,
122 | Value = lowValue ?? highValue
123 | });
124 | break;
125 | case OperatorComparer.LessThanOrEqual:
126 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
127 | {
128 | OperatorComparer = selectedPfc.OperatorComparer,
129 | PropertyName = selectedPfc.PropertyName,
130 | Value = lowValue ?? highValue
131 | });
132 | break;
133 | case OperatorComparer.GreaterThan:
134 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
135 | {
136 | OperatorComparer = selectedPfc.OperatorComparer,
137 | PropertyName = selectedPfc.PropertyName,
138 | Value = lowValue ?? highValue
139 | });
140 | break;
141 | case OperatorComparer.GreaterThanOrEqual:
142 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
143 | {
144 | OperatorComparer = selectedPfc.OperatorComparer,
145 | PropertyName = selectedPfc.PropertyName,
146 | Value = lowValue ?? highValue
147 | });
148 | break;
149 | default:
150 | if (lowValue != null)
151 | {
152 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
153 | {
154 | OperatorComparer = OperatorComparer.GreaterThan,
155 | PropertyName = selectedPfc.PropertyName,
156 | Value = lowValue
157 | });
158 | }
159 |
160 | if (highValue != null)
161 | {
162 | newPropertyFilterConfigs.Add(new PropertyFilterConfig
163 | {
164 | OperatorComparer = OperatorComparer.LessThan,
165 | PropertyName = selectedPfc.PropertyName,
166 | Value = highValue
167 | });
168 | }
169 | break;
170 | }
171 |
172 | return newPropertyFilterConfigs;
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/ReFilter/Extensions/SortDirectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using ReFilter.Enums;
2 |
3 | namespace ReFilter.Extensions
4 | {
5 | public static class SortDirectionExtensions
6 | {
7 | public static string GetOrderByNames(this SortDirection value, bool isThen = false)
8 | {
9 | switch (value)
10 | {
11 | case SortDirection.ASC:
12 | return isThen ? "ThenBy" : "OrderBy";
13 | default:
14 | return isThen ? "ThenByDescending" : "OrderByDescending";
15 | }
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/ReFilter/Extensions/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace ReFilter.Extensions
2 | {
3 | public static class StringExtensions
4 | {
5 | public static string NullSafeToLower(this string value)
6 | {
7 | if (value == null)
8 | {
9 | value = string.Empty;
10 | }
11 | return value.ToLower();
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ReFilter/Extensions/TypeExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reflection;
5 | using ReFilter.Attributes;
6 |
7 | namespace ReFilter.Extensions
8 | {
9 | public static class TypeExtensions
10 | {
11 | public static List GetSearchableProperties(this Type type)
12 | {
13 | return type.GetProperties()
14 | .Where(p => p.GetCustomAttributes().OfType().Any(e => e.UsedForSearchQuery))
15 | .ToList();
16 | }
17 |
18 | public static List GetSpecialFilterProperties(this Type type)
19 | {
20 | return type.GetProperties()
21 | .Where(p => p.GetCustomAttributes().OfType().Any(e => e.HasSpecialFilter))
22 | .ToList();
23 | }
24 |
25 | public static List GetSpecialSortProperties(this Type type)
26 | {
27 | return type.GetProperties()
28 | .Where(p => p.GetCustomAttributes().OfType().Any(e => e.HasSpecialSort))
29 | .ToList();
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ReFilter/Models/BasePagedRequest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using LinqKit;
5 | using Newtonsoft.Json.Linq;
6 |
7 | namespace ReFilter.Models
8 | {
9 | public class BasePagedRequest : PagedBase
10 | {
11 | public PredicateOperator PredicateOperator { get; set; } = PredicateOperator.And;
12 |
13 | ///
14 | /// Object meant for mapping into query conditions.
15 | /// Only requirenment is that property names match destination
16 | ///
17 | public JObject Where { get; set; }
18 |
19 | ///
20 | /// Defines rules for sorting and filtering
21 | /// Can be left empty and in such way, the default values are used.
22 | /// Default values are no sort and Equals comparer
23 | ///
24 | public List PropertyFilterConfigs { get; set; }
25 |
26 | ///
27 | /// String SearchQuery meant for searching ANY of the tagged property
28 | ///
29 | public string SearchQuery { get; set; }
30 |
31 | ///
32 | /// If you need to filter by multiple incompatible filters, this is the easiest way to do it
33 | /// Depending on set in parent BasePagedRequest, child requests are added either as AND or OR clauses
34 | /// Predicate is being built the same way every time so you are able to chain multiple complex filters
35 | ///
36 | public List PagedRequests { get; set; }
37 |
38 | public PagedRequest GetPagedRequest(bool returnQuery = true, bool returnResults = false)
39 | {
40 | var pagedRequest = new PagedRequest(this)
41 | {
42 | ReturnQuery = returnQuery,
43 | ReturnResults = returnResults
44 | };
45 |
46 | return pagedRequest;
47 | }
48 |
49 | public PagedRequest GetPagedRequest(bool returnQuery = true, bool returnResults = false) where T : class, new() where U : class, new()
50 | {
51 | var pagedRequest = new PagedRequest(this)
52 | {
53 | ReturnQuery = returnQuery,
54 | ReturnResults = returnResults
55 | };
56 |
57 | return pagedRequest;
58 | }
59 |
60 | public PagedRequest GetPagedRequest(Func, List> mappingFunction) where T : class, new() where U : class, new()
61 | {
62 | var pagedRequest = new PagedRequest(this)
63 | {
64 | ReturnQuery = false,
65 | ReturnResults = true,
66 | MappingFunction = mappingFunction
67 | };
68 |
69 | return pagedRequest;
70 | }
71 |
72 | public PagedRequest GetPagedRequest(Func, List> mappingProjection) where T : class, new() where U : class, new()
73 | {
74 | var pagedRequest = new PagedRequest(this)
75 | {
76 | ReturnQuery = false,
77 | ReturnResults = true,
78 | MappingProjection = mappingProjection
79 | };
80 |
81 | return pagedRequest;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/ReFilter/Models/Filtering/Contracts/IReFilter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Linq.Expressions;
4 |
5 | namespace ReFilter.Models.Filtering.Contracts
6 | {
7 | public interface IReFilter where T : class, new()
8 | {
9 | ///
10 | /// Filters query using AND clause
11 | ///
12 | ///
13 | ///
14 | IQueryable FilterQuery(IQueryable query);
15 | ///
16 | /// Generates expression which can be applied later on using either clause
17 | ///
18 | ///
19 | Expression> GeneratePredicate(IQueryable query = null);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ReFilter/Models/Filtering/Contracts/IReFilterRequest.cs:
--------------------------------------------------------------------------------
1 | namespace ReFilter.Models.Filtering.Contracts
2 | {
3 | public interface IReFilterRequest
4 | {
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/ReFilter/Models/Filtering/Contracts/IReFilterable.cs:
--------------------------------------------------------------------------------
1 | namespace ReFilter.Models.Filtering.Contracts
2 | {
3 | public interface IReFilterable where T : struct
4 | {
5 | T Id { get; set; }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/ReFilter/Models/Filtering/Contracts/IReSort.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using ReFilter.Enums;
3 |
4 | namespace ReFilter.Models.Filtering.Contracts
5 | {
6 | public interface IReSort where T : class, new()
7 | {
8 | IOrderedQueryable SortQuery(IQueryable query, SortDirection sortDirection = SortDirection.ASC, bool isFirst = true);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/ReFilter/Models/PagedBase.cs:
--------------------------------------------------------------------------------
1 | namespace ReFilter.Models
2 | {
3 | public class PagedBase
4 | {
5 | public int PageSize { get; set; }
6 | public int PageIndex { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/ReFilter/Models/PagedRequest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace ReFilter.Models
6 | {
7 | public class PagedRequest : BasePagedRequest
8 | {
9 | public PagedRequest() { }
10 |
11 | public PagedRequest(BasePagedRequest pagedRequest)
12 | {
13 | PageIndex = pagedRequest.PageIndex;
14 | PageSize = pagedRequest.PageSize;
15 | PropertyFilterConfigs = pagedRequest.PropertyFilterConfigs;
16 | SearchQuery = pagedRequest.SearchQuery;
17 | Where = pagedRequest.Where;
18 | PagedRequests = pagedRequest.PagedRequests;
19 | PredicateOperator = pagedRequest.PredicateOperator;
20 | ReturnQuery = false;
21 | ReturnResults = true;
22 | }
23 |
24 | public bool ReturnQuery { get; set; } = true;
25 | public bool ReturnResults { get; set; } = false;
26 | }
27 |
28 | public class PagedRequest : PagedRequest where T : class, new() where U : class, new()
29 | {
30 | public PagedRequest(BasePagedRequest pagedRequest) : base(pagedRequest)
31 | {
32 | ReturnQuery = true;
33 | ReturnResults = false;
34 | MappingFunction = null;
35 | MappingProjection = null;
36 | }
37 |
38 | public Func, List> MappingFunction { get; set; } = null;
39 | public Func, List> MappingProjection { get; set; } = null;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ReFilter/Models/PagedResult.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 |
4 | namespace ReFilter.Models
5 | {
6 | public class PagedResult : PagedResultBase where T : new()
7 | {
8 | public List Results { get; set; }
9 | public IQueryable ResultQuery { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/ReFilter/Models/PagedResultBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ReFilter.Models
4 | {
5 | public abstract class PagedResultBase : PagedBase
6 | {
7 | public int PageCount { get; set; }
8 | public int RowCount { get; set; }
9 |
10 | public int CurrentPage => PageIndex + 1;
11 |
12 | public int FirstRowOnPage => PageIndex * PageSize + 1;
13 |
14 | public int LastRowOnPage => Math.Min(CurrentPage * PageSize, RowCount);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ReFilter/Models/PropertyFilterConfig.cs:
--------------------------------------------------------------------------------
1 | using LinqKit;
2 | using ReFilter.Enums;
3 |
4 | namespace ReFilter.Models
5 | {
6 | public class PropertyFilterConfig
7 | {
8 | public string PropertyName { get; set; }
9 | public OperatorComparer? OperatorComparer { get; set; } = Enums.OperatorComparer.Equals;
10 | public SortDirection? SortDirection { get; set; }
11 | public PredicateOperator PredicateOperator { get; set; } = PredicateOperator.And;
12 | public object Value { get; set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ReFilter/Models/RangeFilter.cs:
--------------------------------------------------------------------------------
1 | namespace ReFilter.Models
2 | {
3 | public class RangeFilter where T : struct
4 | {
5 | public T? Start { get; set; }
6 | public T? End { get; set; }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/ReFilter/ReFilter.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | true
6 | Cubelaster
7 | Cubelaster
8 | A package designed for filtering
9 | 2.0.3
10 | 0.0.1.0
11 | 0.0.1.0
12 | C#,Filter
13 | Initial version for filtering and pagination solution
14 | en
15 | MIT
16 | https://github.com/Cubelaster/ReFilter
17 | README.md
18 | https://github.com/Cubelaster/ReFilter
19 | ReFilter
20 |
21 |
22 |
23 |
24 | True
25 | \
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/ReFilter/ReFilterActions/IReFilterActions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Linq.Expressions;
5 | using System.Threading.Tasks;
6 | using ReFilter.Models;
7 |
8 | namespace ReFilter.ReFilterActions
9 | {
10 | public interface IReFilterActions
11 | {
12 | public Task> GetPaged(IQueryable query, PagedRequest pagedRequest) where T : class, new();
13 | public Task> GetPaged(IQueryable query, PagedRequest pagedRequest) where T : class, new() where U : class, new();
14 | public Task> GetFiltered(IQueryable query, PagedRequest pagedRequest) where T : class, new();
15 | public Task> GetFiltered(IQueryable query, PagedRequest pagedRequest) where T : class, new() where U : class, new();
16 | public Task> GetBySearchQuery(IQueryable query, BasePagedRequest pagedRequest,
17 | bool applyPagination = false, bool returnQueryOnly = false, bool returnResultsOnly = false) where T : class, new();
18 | public Task> GetBySearchQuery(IQueryable query, PagedRequest pagedRequest,
19 | bool applyPagination = false, bool returnQueryOnly = false, bool returnResultsOnly = false) where T : class, new() where U : class, new();
20 | public IQueryable ApplyPagination(IQueryable query, BasePagedRequest pagedRequest) where T : class, new();
21 | IQueryable SortObject(IQueryable query, List propertyFilterConfigs) where T : class, new();
22 | public IQueryable FilterObject(IQueryable query, PagedRequest request) where T : class, new();
23 | public IQueryable SearchObject(IQueryable query, BasePagedRequest request) where T : class, new();
24 | Expression> SearchObject(BasePagedRequest request) where T : class, new();
25 | Expression> FilterObject(PagedRequest request, IQueryable query) where T : class, new();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/ReFilter/ReFilterActions/ReFilterActions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Linq.Expressions;
5 | using System.Reflection;
6 | using System.Threading.Tasks;
7 | using LinqKit;
8 | using Newtonsoft.Json;
9 | using Newtonsoft.Json.Linq;
10 | using ReFilter.Converters;
11 | using ReFilter.Extensions;
12 | using ReFilter.Models;
13 | using ReFilter.Models.Filtering.Contracts;
14 | using ReFilter.ReFilterConfigBuilder;
15 | using ReFilter.ReFilterTypeMatcher;
16 |
17 | namespace ReFilter.ReFilterActions
18 | {
19 | public class ReFilterActions : IReFilterActions
20 | {
21 | #region Ctors and Members
22 |
23 | private readonly IReFilterConfigBuilder reFilterTypeMatcher;
24 | private readonly IReSortConfigBuilder reSortConfigBuilder;
25 | private readonly JsonSerializer Serializer;
26 |
27 | public ReFilterActions(IReFilterConfigBuilder reFilterTypeMatcher, IReSortConfigBuilder reSortConfigBuilder)
28 | {
29 | this.reFilterTypeMatcher = reFilterTypeMatcher;
30 | this.reSortConfigBuilder = reSortConfigBuilder;
31 |
32 | Serializer = new JsonSerializer();
33 | Serializer.Converters.Add(new DateOnlyConverter());
34 | Serializer.Converters.Add(new DateOnlyNullableConverter());
35 | Serializer.Converters.Add(new TimeOnlyConverter());
36 | Serializer.Converters.Add(new TimeOnlyNullableConverter());
37 | }
38 |
39 | public ReFilterActions(IReFilterConfigBuilder reFilterTypeMatcher, IReSortConfigBuilder reSortConfigBuilder, JsonSerializer jsonSerializer)
40 | {
41 | this.reFilterTypeMatcher = reFilterTypeMatcher;
42 | this.reSortConfigBuilder = reSortConfigBuilder;
43 |
44 | Serializer = jsonSerializer;
45 | }
46 |
47 | #endregion Ctors and Members
48 |
49 | #region Pagination
50 |
51 | ///
52 | /// This is a basic GetPaged
53 | /// It has options which enable you to select either the query or the list return types
54 | ///
55 | ///
56 | ///
57 | ///
58 | ///
59 | public async Task> GetPaged(IQueryable query, PagedRequest pagedRequest) where T : class, new()
60 | {
61 | var result = new PagedResult
62 | {
63 | PageIndex = pagedRequest.PageIndex,
64 | PageSize = pagedRequest.PageSize,
65 | };
66 |
67 | if (pagedRequest.PropertyFilterConfigs != null
68 | && pagedRequest.PropertyFilterConfigs.Any(pfc => pfc.SortDirection.HasValue))
69 | {
70 | query = SortObject(query, pagedRequest.PropertyFilterConfigs);
71 | }
72 |
73 | if (pagedRequest.Where is not null
74 | || (pagedRequest.PagedRequests is not null && pagedRequest.PagedRequests.Any())
75 | || (pagedRequest.PropertyFilterConfigs is not null && pagedRequest.PropertyFilterConfigs.Any(pfc => pfc.Value is not null)))
76 | {
77 | var predicate = FilterObject(pagedRequest, query);
78 | query = query.Where(predicate);
79 | }
80 |
81 | var resultQuery = ApplyPagination(query, pagedRequest);
82 |
83 | result.RowCount = query.Count();
84 | result.PageCount = (int)Math.Ceiling((double)result.RowCount / pagedRequest.PageSize);
85 |
86 | result.Results = pagedRequest.ReturnResults ? await Task.FromResult(resultQuery.ToList()) : new List();
87 | result.ResultQuery = pagedRequest.ReturnQuery ? resultQuery : null;
88 | return result;
89 | }
90 |
91 | ///
92 | /// Advanced option of GetPaged
93 | /// It automatically returns mapped result and it does not return the query, since it is already resolved
94 | ///
95 | ///
96 | ///
97 | ///
98 | ///
99 | ///
100 | public async Task> GetPaged(IQueryable query, PagedRequest pagedRequest) where T : class, new() where U : class, new()
101 | {
102 | var result = new PagedResult
103 | {
104 | PageIndex = pagedRequest.PageIndex,
105 | PageSize = pagedRequest.PageSize,
106 | };
107 |
108 | if (pagedRequest.PropertyFilterConfigs != null
109 | && pagedRequest.PropertyFilterConfigs.Any(pfc => pfc.SortDirection.HasValue))
110 | {
111 | query = SortObject(query, pagedRequest.PropertyFilterConfigs);
112 | }
113 |
114 | if (pagedRequest.Where is not null
115 | || (pagedRequest.PagedRequests is not null && pagedRequest.PagedRequests.Any())
116 | || (pagedRequest.PropertyFilterConfigs is not null && pagedRequest.PropertyFilterConfigs.Any(pfc => pfc.Value is not null)))
117 | {
118 | var predicate = FilterObject(pagedRequest, query);
119 | query = query.Where(predicate);
120 | }
121 |
122 | var resultQuery = ApplyPagination(query, pagedRequest);
123 |
124 | result.RowCount = query.Count();
125 | result.PageCount = (int)Math.Ceiling((double)result.RowCount / pagedRequest.PageSize);
126 |
127 | return await Task.FromResult(result.TransformResult(pagedRequest, resultQuery));
128 | }
129 |
130 | public IQueryable ApplyPagination(IQueryable query, BasePagedRequest pagedRequest) where T : class, new()
131 | {
132 | int skip = pagedRequest.PageIndex * pagedRequest.PageSize;
133 | return query.Skip(skip).Take(pagedRequest.PageSize);
134 | }
135 |
136 | #endregion Pagination
137 |
138 | #region Filtering
139 |
140 | public async Task> GetFiltered(IQueryable query, PagedRequest pagedRequest) where T : class, new()
141 | {
142 | var result = new PagedResult
143 | {
144 | PageIndex = pagedRequest.PageIndex,
145 | PageSize = pagedRequest.PageSize,
146 | };
147 |
148 | if (pagedRequest.PropertyFilterConfigs != null
149 | && pagedRequest.PropertyFilterConfigs.Any(pfc => pfc.SortDirection.HasValue))
150 | {
151 | query = SortObject(query, pagedRequest.PropertyFilterConfigs);
152 | }
153 |
154 | if (pagedRequest.Where is not null
155 | || (pagedRequest.PagedRequests is not null && pagedRequest.PagedRequests.Any())
156 | || (pagedRequest.PropertyFilterConfigs is not null && pagedRequest.PropertyFilterConfigs.Any(pfc => pfc.Value is not null)))
157 | {
158 | var predicate = FilterObject(pagedRequest, query);
159 | query = query.Where(predicate);
160 | }
161 |
162 | result.RowCount = query.Count();
163 | result.PageCount = (int)Math.Ceiling((double)result.RowCount / pagedRequest.PageSize);
164 |
165 | result.Results = pagedRequest.ReturnResults ? new List() : await Task.FromResult(query.ToList());
166 | result.ResultQuery = pagedRequest.ReturnQuery ? null : query;
167 | return result;
168 | }
169 |
170 | public async Task> GetFiltered(IQueryable query, PagedRequest pagedRequest) where T : class, new() where U : class, new()
171 | {
172 | var result = new PagedResult
173 | {
174 | PageIndex = pagedRequest.PageIndex,
175 | PageSize = pagedRequest.PageSize,
176 | };
177 |
178 | if (pagedRequest.PropertyFilterConfigs != null
179 | && pagedRequest.PropertyFilterConfigs.Any(pfc => pfc.SortDirection.HasValue))
180 | {
181 | query = SortObject(query, pagedRequest.PropertyFilterConfigs);
182 | }
183 |
184 | if (pagedRequest.Where is not null
185 | || (pagedRequest.PagedRequests is not null && pagedRequest.PagedRequests.Any())
186 | || (pagedRequest.PropertyFilterConfigs is not null && pagedRequest.PropertyFilterConfigs.Any(pfc => pfc.Value is not null)))
187 | {
188 | var predicate = FilterObject(pagedRequest, query);
189 | query = query.Where(predicate);
190 | }
191 |
192 | result.RowCount = query.Count();
193 | result.PageCount = (int)Math.Ceiling((double)result.RowCount / pagedRequest.PageSize);
194 |
195 | return await Task.FromResult(result.TransformResult(pagedRequest, query));
196 | }
197 |
198 | public IQueryable FilterObject(IQueryable query, PagedRequest request) where T : class, new()
199 | {
200 | // We generally want to return everything if we don't set filters
201 | // true essentially resolves to Where 1=1
202 | var predicate = PredicateBuilder.New(true);
203 |
204 | var filterObjectType = reFilterTypeMatcher.GetMatchingType();
205 | var filterObject = request.Where?.ToObject(filterObjectType, Serializer);
206 |
207 | var filterValues = filterObject?.GetObjectPropertiesWithValue() ?? new Dictionary();
208 | var specialFilterProperties = filterObjectType.GetSpecialFilterProperties();
209 |
210 | var filterPfcs = request.PropertyFilterConfigs?
211 | .Where(pfc => pfc.Value is not null)
212 | .Select(pfc => pfc.PropertyName)
213 | ?? new List();
214 |
215 | var filterKeys = filterValues.Keys
216 | .Concat(filterPfcs)
217 | .ToHashSet();
218 |
219 | if (filterKeys.Any())
220 | {
221 | var expressionBuilder = new ReFilterExpressionBuilder.ReFilterExpressionBuilder();
222 |
223 | foreach (var filterKey in filterKeys.Where(fk => !specialFilterProperties.Any(sfp => sfp.Name == fk)))
224 | {
225 | var propertyPredicate = PredicateBuilder.New(true);
226 |
227 | var pfcs = request.GetPropertyFilterConfigs(filterKey, filterValues);
228 |
229 | pfcs.ForEach(pfc =>
230 | {
231 | var pfcPredicate = PredicateBuilder.New(false);
232 |
233 | if (pfc.Value.GetType().Name == typeof(RangeFilter<>).Name)
234 | {
235 | // RangeFilter setup
236 | Type type = pfc.Value.GetType().GetGenericArguments()[0];
237 | var methodType = typeof(RangeFilterExtensions).GetMethod(nameof(RangeFilterExtensions.Unpack));
238 | var methodInfo = methodType.MakeGenericMethod(type);
239 |
240 | List newPropertyFilterConfigs = (List)
241 | methodInfo.Invoke(this, new object[] { pfc.Value, pfc });
242 |
243 | newPropertyFilterConfigs.ForEach(npfc =>
244 | {
245 | // false essentially resolves to Where 1=2
246 | // This should generally not happen but failing the filter is better than showing incorrect data
247 | var pfcPredicate = PredicateBuilder.New(false);
248 | var innerPredicates = expressionBuilder.BuildPredicate(npfc);
249 |
250 | innerPredicates.ForEach(newpfc =>
251 | {
252 | pfcPredicate.And(newpfc);
253 | });
254 |
255 | propertyPredicate.And(pfcPredicate);
256 | });
257 | }
258 | else if (pfc.Value.GetType() is IReFilterRequest)
259 | {
260 | // Recursive build here?
261 | // If we ever want to chain filtering via FilterRequests, here is where we should do it
262 | // And then we would use the IReFilterBuilder.GetForeignKeys here to filter by it
263 | }
264 | else
265 | {
266 | var innerPredicates = expressionBuilder.BuildPredicate(pfc);
267 |
268 | // Inner predicates are all predicates generated to filter accordingly to a pfc
269 | innerPredicates.ForEach(newpfc =>
270 | {
271 | pfcPredicate.And(newpfc);
272 | });
273 |
274 | // Different pfcs can be used as And/Or clauses
275 | if (pfc.PredicateOperator == PredicateOperator.And)
276 | {
277 | propertyPredicate.And(pfcPredicate);
278 | }
279 | else
280 | {
281 | propertyPredicate.Or(pfcPredicate);
282 | }
283 | }
284 | });
285 |
286 | if (request.PredicateOperator == PredicateOperator.And)
287 | {
288 | predicate.And(propertyPredicate);
289 | }
290 | else
291 | {
292 | predicate.Or(propertyPredicate);
293 | }
294 | }
295 |
296 | // Special properties only support IReFilterRequest, not PropertyFilterConfig
297 | foreach (var filterKey in filterKeys.Where(fk => specialFilterProperties.Any(sfp => sfp.Name == fk)))
298 | {
299 | var filterBuilder = reFilterTypeMatcher.GetMatchingFilterBuilder();
300 | var specialPredicates = filterBuilder.BuildPredicates(filterObject as IReFilterRequest, query);
301 |
302 | specialPredicates.ForEach(specialPredicate =>
303 | {
304 | if (request.PredicateOperator == PredicateOperator.And)
305 | {
306 | predicate.And(specialPredicate);
307 | }
308 | else
309 | {
310 | predicate.Or(specialPredicate);
311 | }
312 | });
313 | }
314 | }
315 |
316 | if (!string.IsNullOrEmpty(request.SearchQuery))
317 | {
318 | var searchPredicate = SearchObject