├── .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(request); 319 | 320 | if (request.PredicateOperator == PredicateOperator.And) 321 | { 322 | predicate.And(searchPredicate); 323 | } 324 | else 325 | { 326 | predicate.Or(searchPredicate); 327 | } 328 | } 329 | 330 | if (request.PagedRequests is not null && request.PagedRequests.Count > 0) 331 | { 332 | request.PagedRequests.ForEach(pagedRequest => 333 | { 334 | var subPredicate = FilterObject(pagedRequest, query); 335 | 336 | if (request.PredicateOperator == PredicateOperator.And) 337 | { 338 | predicate.And(subPredicate); 339 | } 340 | else 341 | { 342 | predicate.Or(subPredicate); 343 | } 344 | }); 345 | } 346 | 347 | query = query.Where(predicate); 348 | 349 | return query; 350 | } 351 | 352 | public Expression> FilterObject(PagedRequest request, IQueryable query) where T : class, new() 353 | { 354 | // We generally want to return everything if we don't set filters 355 | // true essentially resolves to Where 1=1 356 | var predicate = PredicateBuilder.New(true); 357 | 358 | var filterObjectType = reFilterTypeMatcher.GetMatchingType(); 359 | var filterObject = request.Where?.ToObject(filterObjectType, Serializer); 360 | 361 | var filterValues = filterObject?.GetObjectPropertiesWithValue() ?? new Dictionary(); 362 | var specialFilterProperties = filterObjectType.GetSpecialFilterProperties(); 363 | 364 | var filterPfcs = request.PropertyFilterConfigs? 365 | .Where(pfc => pfc.Value is not null) 366 | .Select(pfc => pfc.PropertyName) 367 | ?? new List(); 368 | 369 | var filterKeys = filterValues.Keys 370 | .Concat(filterPfcs) 371 | .ToHashSet(); 372 | 373 | if (filterKeys.Any()) 374 | { 375 | var expressionBuilder = new ReFilterExpressionBuilder.ReFilterExpressionBuilder(); 376 | 377 | foreach (var filterKey in filterKeys.Where(fk => !specialFilterProperties.Any(sfp => sfp.Name == fk))) 378 | { 379 | var propertyPredicate = PredicateBuilder.New(true); 380 | 381 | var pfcs = request.GetPropertyFilterConfigs(filterKey, filterValues); 382 | 383 | pfcs.ForEach(pfc => 384 | { 385 | var pfcPredicate = PredicateBuilder.New(false); 386 | 387 | if (pfc.Value.GetType().Name == typeof(RangeFilter<>).Name) 388 | { 389 | // RangeFilter setup 390 | Type type = pfc.Value.GetType().GetGenericArguments()[0]; 391 | var methodType = typeof(RangeFilterExtensions).GetMethod(nameof(RangeFilterExtensions.Unpack)); 392 | var methodInfo = methodType.MakeGenericMethod(type); 393 | 394 | List newPropertyFilterConfigs = (List) 395 | methodInfo.Invoke(this, new object[] { pfc.Value, pfc }); 396 | 397 | newPropertyFilterConfigs.ForEach(npfc => 398 | { 399 | // false essentially resolves to Where 1=2 400 | // This should generally not happen but failing the filter is better than showing incorrect data 401 | var pfcPredicate = PredicateBuilder.New(false); 402 | var innerPredicates = expressionBuilder.BuildPredicate(npfc); 403 | 404 | innerPredicates.ForEach(newpfc => 405 | { 406 | pfcPredicate.And(newpfc); 407 | }); 408 | 409 | propertyPredicate.And(pfcPredicate); 410 | }); 411 | } 412 | else if (pfc.Value.GetType() is IReFilterRequest) 413 | { 414 | // Recursive build here? 415 | // If we ever want to chain filtering via FilterRequests, here is where we should do it 416 | // And then we would use the IReFilterBuilder.GetForeignKeys here to filter by it 417 | } 418 | else 419 | { 420 | var innerPredicates = expressionBuilder.BuildPredicate(pfc); 421 | 422 | // Inner predicates are all predicates generated to filter accordingly to a pfc 423 | innerPredicates.ForEach(newpfc => 424 | { 425 | pfcPredicate.And(newpfc); 426 | }); 427 | 428 | // Different pfcs can be used as And/Or clauses 429 | if (pfc.PredicateOperator == PredicateOperator.And) 430 | { 431 | propertyPredicate.And(pfcPredicate); 432 | } 433 | else 434 | { 435 | propertyPredicate.Or(pfcPredicate); 436 | } 437 | } 438 | }); 439 | 440 | if (request.PredicateOperator == PredicateOperator.And) 441 | { 442 | predicate.And(propertyPredicate); 443 | } 444 | else 445 | { 446 | predicate.Or(propertyPredicate); 447 | } 448 | } 449 | 450 | // Special properties only support IReFilterRequest, not PropertyFilterConfig 451 | foreach (var filterKey in filterKeys.Where(fk => specialFilterProperties.Any(sfp => sfp.Name == fk))) 452 | { 453 | var filterBuilder = reFilterTypeMatcher.GetMatchingFilterBuilder(); 454 | var specialPredicates = filterBuilder.BuildPredicates(filterObject as IReFilterRequest, query); 455 | 456 | specialPredicates.ForEach(specialPredicate => 457 | { 458 | if (request.PredicateOperator == PredicateOperator.And) 459 | { 460 | predicate.And(specialPredicate); 461 | } 462 | else 463 | { 464 | predicate.Or(specialPredicate); 465 | } 466 | }); 467 | } 468 | } 469 | 470 | if (!string.IsNullOrEmpty(request.SearchQuery)) 471 | { 472 | var searchPredicate = SearchObject(request); 473 | 474 | if (request.PredicateOperator == PredicateOperator.And) 475 | { 476 | predicate.And(searchPredicate); 477 | } 478 | else 479 | { 480 | predicate.Or(searchPredicate); 481 | } 482 | } 483 | 484 | if (request.PagedRequests is not null && request.PagedRequests.Count > 0) 485 | { 486 | request.PagedRequests.ForEach(pagedRequest => 487 | { 488 | var subPredicate = FilterObject(pagedRequest, query); 489 | 490 | if (request.PredicateOperator == PredicateOperator.And) 491 | { 492 | predicate.And(subPredicate); 493 | } 494 | else 495 | { 496 | predicate.Or(subPredicate); 497 | } 498 | }); 499 | } 500 | 501 | return predicate; 502 | } 503 | 504 | #endregion Filtering 505 | 506 | #region SearchQueries 507 | 508 | public IQueryable SearchObject(IQueryable query, BasePagedRequest request) where T : class, new() 509 | { 510 | var objectType = query.ElementType; 511 | 512 | var predicate = PredicateBuilder.New(query); 513 | 514 | List searchableProperties; 515 | if (!string.IsNullOrEmpty(request.SearchQuery)) 516 | { 517 | searchableProperties = objectType.GetSearchableProperties(); 518 | 519 | if (searchableProperties.Any()) 520 | { 521 | var expressionBuilder = new ReFilterExpressionBuilder.ReFilterExpressionBuilder(); 522 | foreach (var property in searchableProperties) 523 | { 524 | var propertyFilterConfig = expressionBuilder.BuildSearchPropertyFilterConfig(property, request.SearchQuery); 525 | var searchExpressions = expressionBuilder.BuildPredicate(propertyFilterConfig); 526 | 527 | searchExpressions.ForEach(searchExpression => predicate.Or(searchExpression)); 528 | } 529 | } 530 | 531 | return query.Where(predicate); 532 | } 533 | 534 | return query; 535 | } 536 | 537 | public Expression> SearchObject(BasePagedRequest request) where T : class, new() 538 | { 539 | var predicate = PredicateBuilder.New(true); 540 | 541 | List searchableProperties; 542 | if (!string.IsNullOrEmpty(request.SearchQuery)) 543 | { 544 | searchableProperties = typeof(T).GetSearchableProperties(); 545 | 546 | if (searchableProperties.Any()) 547 | { 548 | var expressionBuilder = new ReFilterExpressionBuilder.ReFilterExpressionBuilder(); 549 | foreach (var property in searchableProperties) 550 | { 551 | var propertyFilterConfig = expressionBuilder.BuildSearchPropertyFilterConfig(property, request.SearchQuery); 552 | var searchExpressions = expressionBuilder.BuildPredicate(propertyFilterConfig); 553 | 554 | searchExpressions.ForEach(searchExpression => predicate.Or(searchExpression)); 555 | } 556 | } 557 | } 558 | 559 | return predicate; 560 | } 561 | 562 | public async Task> GetBySearchQuery(IQueryable query, BasePagedRequest pagedRequest, 563 | bool applyPagination = false, bool returnQueryOnly = false, bool returnResultsOnly = false) where T : class, new() 564 | { 565 | Type objectType = query.FirstOrDefault()?.GetType(); 566 | if (objectType != null) 567 | { 568 | var result = new PagedResult 569 | { 570 | PageIndex = applyPagination ? pagedRequest.PageIndex : default, 571 | PageSize = pagedRequest.PageSize, 572 | }; 573 | 574 | if (pagedRequest.PropertyFilterConfigs != null 575 | && pagedRequest.PropertyFilterConfigs.Any(pfc => pfc.SortDirection.HasValue)) 576 | { 577 | query = SortObject(query, pagedRequest.PropertyFilterConfigs); 578 | } 579 | 580 | if (!string.IsNullOrEmpty(pagedRequest.SearchQuery)) 581 | { 582 | query = SearchObject(query, pagedRequest); 583 | } 584 | 585 | result.RowCount = query.Count(); 586 | result.PageCount = (int)Math.Ceiling((double)result.RowCount / pagedRequest.PageSize); 587 | 588 | if (applyPagination) 589 | { 590 | query = ApplyPagination(query, pagedRequest); 591 | } 592 | 593 | result.Results = returnQueryOnly ? new List() : await Task.FromResult(query.ToList()); 594 | result.ResultQuery = returnResultsOnly ? null : query; 595 | return result; 596 | } 597 | 598 | return new PagedResult 599 | { 600 | Results = new List() 601 | }; 602 | } 603 | 604 | public async Task> GetBySearchQuery(IQueryable query, PagedRequest pagedRequest, 605 | bool applyPagination = false, bool returnQueryOnly = false, bool returnResultsOnly = false) where T : class, new() where U : class, new() 606 | { 607 | Type objectType = query.FirstOrDefault()?.GetType(); 608 | if (objectType != null) 609 | { 610 | var result = new PagedResult 611 | { 612 | PageIndex = applyPagination ? pagedRequest.PageIndex : default, 613 | PageSize = pagedRequest.PageSize, 614 | }; 615 | 616 | if (pagedRequest.PropertyFilterConfigs != null 617 | && pagedRequest.PropertyFilterConfigs.Any(pfc => pfc.SortDirection.HasValue)) 618 | { 619 | query = SortObject(query, pagedRequest.PropertyFilterConfigs); 620 | } 621 | 622 | if (!string.IsNullOrEmpty(pagedRequest.SearchQuery)) 623 | { 624 | query = SearchObject(query, pagedRequest); 625 | } 626 | 627 | result.RowCount = query.Count(); 628 | result.PageCount = (int)Math.Ceiling((double)result.RowCount / pagedRequest.PageSize); 629 | 630 | if (applyPagination) 631 | { 632 | query = ApplyPagination(query, pagedRequest); 633 | } 634 | 635 | result.Results = returnQueryOnly ? new List() : await Task.FromResult(query.ToList()); 636 | result.ResultQuery = returnResultsOnly ? null : query; 637 | return result.TransformResult(pagedRequest, query); 638 | } 639 | 640 | return new PagedResult 641 | { 642 | Results = new List() 643 | }; 644 | } 645 | 646 | #endregion SearchQueries 647 | 648 | #region Sorts 649 | 650 | private IOrderedQueryable OrderBy(IQueryable source, string propertyName, string methodName) where T : class, new() 651 | { 652 | // LAMBDA: x => x.[PropertyName] 653 | var parameter = Expression.Parameter(typeof(T), "x"); 654 | Expression property = Expression.Property(parameter, propertyName); 655 | var lambda = Expression.Lambda(property, parameter); 656 | 657 | // REFLECTION: source.OrderBy(x => x.Property) 658 | var orderByMethod = typeof(Queryable).GetMethods().First(x => x.Name == methodName && x.GetParameters().Length == 2); 659 | var orderByGeneric = orderByMethod.MakeGenericMethod(typeof(T), property.Type); 660 | var result = orderByGeneric.Invoke(null, new object[] { source, lambda }); 661 | 662 | return (IOrderedQueryable)result; 663 | } 664 | 665 | private IOrderedQueryable ThenOrderBy(IOrderedQueryable source, string propertyName, string methodName) where T : class, new() 666 | { 667 | // LAMBDA: x => x.[PropertyName] 668 | var parameter = Expression.Parameter(typeof(T), "x"); 669 | Expression property = Expression.Property(parameter, propertyName); 670 | var lambda = Expression.Lambda(property, parameter); 671 | 672 | // REFLECTION: source.OrderBy(x => x.Property) 673 | var orderByMethod = typeof(Queryable).GetMethods().First(x => x.Name == methodName && x.GetParameters().Length == 2); 674 | var orderByGeneric = orderByMethod.MakeGenericMethod(typeof(T), property.Type); 675 | var result = orderByGeneric.Invoke(null, new object[] { source, lambda }); 676 | 677 | return (IOrderedQueryable)result; 678 | } 679 | 680 | public IQueryable SortObject(IQueryable query, List propertyFilterConfigs) where T : class, new() 681 | { 682 | var sortObjectType = reSortConfigBuilder.GetMatchingType(); 683 | var specialSortProperties = sortObjectType.GetSpecialSortProperties(); 684 | 685 | var realSorts = propertyFilterConfigs 686 | .Where(pfc => pfc.SortDirection.HasValue) 687 | .ToList(); 688 | 689 | var firstSort = realSorts.First(); 690 | 691 | IOrderedQueryable orderedQuery; 692 | string methodName = ""; 693 | 694 | if (specialSortProperties.Any(ssp => ssp.Name.Equals(firstSort.PropertyName))) 695 | { 696 | var sortBuilder = reSortConfigBuilder.GetMatchingSortBuilder(); 697 | orderedQuery = sortBuilder.BuildSortedQuery(query, firstSort, true); 698 | } 699 | else 700 | { 701 | methodName = firstSort.SortDirection.Value.GetOrderByNames(); 702 | orderedQuery = OrderBy(query, firstSort.PropertyName, methodName); 703 | } 704 | 705 | foreach (var sort in realSorts.Skip(1)) 706 | { 707 | if (specialSortProperties.Any(ssp => ssp.Name.Equals(sort.PropertyName))) 708 | { 709 | var sortBuilder = reSortConfigBuilder.GetMatchingSortBuilder(); 710 | orderedQuery = sortBuilder.BuildSortedQuery(orderedQuery, sort); 711 | } 712 | else 713 | { 714 | methodName = sort.SortDirection.Value.GetOrderByNames(true); 715 | orderedQuery = ThenOrderBy(orderedQuery, sort.PropertyName, methodName); 716 | } 717 | } 718 | 719 | return orderedQuery; 720 | } 721 | 722 | #endregion Sorts 723 | } 724 | } 725 | -------------------------------------------------------------------------------- /ReFilter/ReFilterBuilder/IReFilterBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using ReFilter.Models.Filtering.Contracts; 6 | 7 | namespace ReFilter.ReFilterProvider 8 | { 9 | public interface IReFilterBuilder where T : class, new() 10 | { 11 | /// 12 | /// Gets all the custom filters and matches them to properties. 13 | /// 14 | /// 15 | /// 16 | IEnumerable> GetFilters(IReFilterRequest filterRequest); 17 | /// 18 | /// Entry point of filter builder. Builds default query for that entity. 19 | /// 20 | /// 21 | /// 22 | IQueryable BuildEntityQuery(IReFilterRequest filterRequest); 23 | /// 24 | /// First uses GetFilters and then applies them to the provided query 25 | /// This builds query using AND clauses 26 | /// It is essentially not used anymore and is replaced by BuildPredicates 27 | /// 28 | /// 29 | /// 30 | /// 31 | IQueryable BuildFilteredQuery(IQueryable query, IReFilterRequest filterRequest); 32 | /// 33 | /// Builds predicates one by one 34 | /// Predicates can later on be used as And/Or clauses 35 | /// This is the intended way and is used under the hood to build and apply filters 36 | /// 37 | /// 38 | /// 39 | List>> BuildPredicates(IReFilterRequest filterRequest, IQueryable query = null); 40 | /// 41 | /// Gets the list of Ids for the provided filter parameters in order to use it as an "IN ()" clause. 42 | /// 43 | /// 44 | /// 45 | List GetForeignKeys(IReFilterRequest filterRequest); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ReFilter/ReFilterConfigBuilder/IReFilterConfigBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ReFilter.ReFilterProvider; 3 | 4 | namespace ReFilter.ReFilterTypeMatcher 5 | { 6 | public interface IReFilterConfigBuilder 7 | { 8 | Type GetMatchingType() where T : class, new(); 9 | IReFilterBuilder GetMatchingFilterBuilder() where T : class, new(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ReFilter/ReFilterConfigBuilder/IReSearchConfigBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ReFilter.ReFilterProvider; 3 | 4 | namespace ReFilter.ReFilterConfigBuilder 5 | { 6 | internal interface IReSearchConfigBuilder 7 | { 8 | Type GetMatchingType() where T : class, new(); 9 | IReFilterBuilder GetMatchingFilterBuilder() where T : class, new(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ReFilter/ReFilterConfigBuilder/IReSortConfigBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ReFilter.ReSortBuilder; 3 | 4 | namespace ReFilter.ReFilterConfigBuilder 5 | { 6 | public interface IReSortConfigBuilder 7 | { 8 | Type GetMatchingType() where T : class, new(); 9 | IReSortBuilder GetMatchingSortBuilder() where T : class, new(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ReFilter/ReFilterExpressionBuilder/ReFilterExpressionBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Reflection; 8 | using ReFilter.Enums; 9 | using ReFilter.Extensions; 10 | using ReFilter.Models; 11 | 12 | namespace ReFilter.ReFilterExpressionBuilder 13 | { 14 | // https://stackoverflow.com/questions/23718054/dynamic-linq-building-expression 15 | // https://stackoverflow.com/questions/22672050/dynamic-expression-tree-to-filter-on-nested-collection-properties/22685407#22685407 16 | // https://stackoverflow.com/questions/536932/how-to-create-expression-tree-lambda-for-a-deep-property-from-a-string 17 | public class ReFilterExpressionBuilder 18 | { 19 | public List>> BuildPredicate(PropertyFilterConfig propertyFilterConfig) 20 | { 21 | var parameterExpression = Expression.Parameter(typeof(T), typeof(T).Name); 22 | return BuildNavigationExpression(parameterExpression, propertyFilterConfig) 23 | .Cast>>() 24 | .ToList(); 25 | } 26 | 27 | public PropertyFilterConfig BuildSearchPropertyFilterConfig(PropertyInfo property, string searchQuery) 28 | { 29 | return new PropertyFilterConfig 30 | { 31 | OperatorComparer = OperatorComparer.Contains, 32 | PropertyName = property.Name, 33 | Value = searchQuery 34 | }; 35 | } 36 | 37 | public PropertyInfo GetChildProperty(Expression parameter, PropertyFilterConfig propertyFilterConfig) 38 | { 39 | var sameNameProperties = parameter.Type.GetProperties() 40 | .Where(p => p.Name == propertyFilterConfig.PropertyName) 41 | .ToList(); 42 | 43 | if (sameNameProperties.Any() && sameNameProperties.Count > 1) 44 | { 45 | var declaringTypeName = parameter.Type.Name; 46 | var childProperties = parameter.Type.GetProperties() 47 | .Where(e => e.DeclaringType.Name == declaringTypeName) 48 | .ToList(); 49 | 50 | return childProperties.FirstOrDefault(); 51 | } 52 | else 53 | { 54 | return sameNameProperties.FirstOrDefault(); 55 | } 56 | } 57 | 58 | private List BuildNavigationExpression(Expression parameter, PropertyFilterConfig propertyFilterConfig) 59 | { 60 | PropertyInfo childProperty = GetChildProperty(parameter, propertyFilterConfig); 61 | 62 | if ((!childProperty.PropertyType.IsByRef && !childProperty.PropertyType.IsClass) 63 | || childProperty.PropertyType.IsValueType || childProperty.PropertyType.Name == "String") 64 | { 65 | // Meant to handle all strings and similar simple stuff 66 | return new List { BuildCondition(parameter, propertyFilterConfig) }; 67 | } 68 | else if (childProperty.PropertyType.IsClass && !typeof(IEnumerable).IsAssignableFrom(childProperty.PropertyType)) 69 | { 70 | // Meant to handle Recursive Search 71 | List searchableProperties = childProperty.PropertyType.GetSearchableProperties(); 72 | if (searchableProperties.Any()) 73 | { 74 | // This is key for recursion 75 | var childParameter = Expression.Property(parameter, childProperty); 76 | var expressions = new List(); 77 | searchableProperties.ForEach(e => 78 | { 79 | var newPropertyFilterConfig = BuildSearchPropertyFilterConfig(e, (string)propertyFilterConfig.Value); 80 | expressions.AddRange(BuildNavigationExpression(childParameter, newPropertyFilterConfig)); 81 | }); 82 | 83 | return expressions; 84 | } 85 | else 86 | { 87 | return new List(); 88 | } 89 | } 90 | else if (typeof(IEnumerable).IsAssignableFrom(childProperty.PropertyType)) 91 | { 92 | // if it´s a collection we later need to use the predicate in the methodexpressioncall 93 | var childType = childProperty.PropertyType.GenericTypeArguments[0]; 94 | List searchableProperties = childType.GetSearchableProperties(); 95 | 96 | if (searchableProperties.Any()) 97 | { 98 | // This is key for recursion 99 | var childParameterStandalone = Expression.Parameter(childType, childType.Name); 100 | var childParameter = Expression.Property(parameter, childProperty); 101 | var subExpressions = new List(); 102 | 103 | var childBuilderMethod = typeof(ReFilterExpressionBuilder).GetMethod(nameof(BuildNavigationExpression), BindingFlags.NonPublic | BindingFlags.Instance); 104 | var childNavigationExpressionBuilder = childBuilderMethod.MakeGenericMethod(childType); 105 | var newInstance = Expression.New(typeof(ReFilterExpressionBuilder)); 106 | 107 | searchableProperties.ForEach(e => 108 | { 109 | var newPropertyFilterConfig = BuildSearchPropertyFilterConfig(e, (string)propertyFilterConfig.Value); 110 | var methodCallExpression = Expression.Call(newInstance, childNavigationExpressionBuilder, 111 | Expression.Constant(childParameterStandalone), Expression.Constant(newPropertyFilterConfig)); 112 | subExpressions.AddRange(Expression.Lambda>>(methodCallExpression).Compile()()); 113 | }); 114 | 115 | var childExpressions = new List(); 116 | var childSubQueryBuilderMethod = typeof(ReFilterExpressionBuilder).GetMethod(nameof(BuildSubQuery), BindingFlags.NonPublic | BindingFlags.Instance); 117 | var childSubQueryBuilder = childSubQueryBuilderMethod.MakeGenericMethod(childType); 118 | 119 | subExpressions.ForEach(subExpression => 120 | { 121 | childExpressions.AddRange(BuildSubQuery(childParameter, childType, subExpression)); 122 | }); 123 | 124 | return childExpressions; 125 | } 126 | else 127 | { 128 | return new List(); 129 | } 130 | } 131 | else 132 | { 133 | return new List { BuildCondition(parameter, propertyFilterConfig) }; 134 | } 135 | } 136 | 137 | private List BuildSubQuery(Expression parameter, Type childType, Expression predicate) 138 | { 139 | var anyMethod = typeof(Enumerable).GetMethods().Single(m => m.Name == "Any" && m.GetParameters().Length == 2); 140 | anyMethod = anyMethod.MakeGenericMethod(childType); 141 | predicate = Expression.Call(anyMethod, parameter, predicate); 142 | return new List { MakeLambda(parameter, predicate) }; 143 | } 144 | 145 | private Expression BuildCondition(Expression parameter, PropertyFilterConfig propertyFilterConfig) 146 | { 147 | PropertyInfo childProperty = GetChildProperty(parameter, propertyFilterConfig); 148 | 149 | var left = Expression.Property(parameter, childProperty); 150 | var right = Expression.Constant(propertyFilterConfig.Value); 151 | var predicate = BuildComparsion(left, propertyFilterConfig.OperatorComparer.Value, right); 152 | return MakeLambda(parameter, predicate); 153 | } 154 | 155 | private Expression BuildComparsion(Expression left, OperatorComparer comparer, Expression right) 156 | { 157 | var mask = new List{ 158 | OperatorComparer.Contains, 159 | OperatorComparer.NotContains, 160 | OperatorComparer.StartsWith, 161 | OperatorComparer.NotStartsWith, 162 | OperatorComparer.EndsWith, 163 | OperatorComparer.NotEndsWith 164 | }; 165 | 166 | var rangeMask = new List 167 | { 168 | OperatorComparer.BetweenExclusive, 169 | OperatorComparer.BetweenInclusive, 170 | OperatorComparer.BetweenHigherInclusive, 171 | OperatorComparer.BetweenLowerInclusive 172 | }; 173 | 174 | if (mask.Contains(comparer) && left.Type != typeof(string)) 175 | { 176 | comparer = OperatorComparer.Equals; 177 | } 178 | 179 | if (rangeMask.Contains(comparer)) 180 | { 181 | 182 | } 183 | else if (!mask.Contains(comparer)) 184 | { 185 | return Expression.MakeBinary((ExpressionType)comparer, left, Expression.Convert(right, left.Type)); 186 | } 187 | 188 | return BuildStringCondition(left, comparer, right); 189 | } 190 | 191 | private Expression BuildStringCondition(Expression left, OperatorComparer comparer, Expression right) 192 | { 193 | var isNot = false; 194 | var operatorName = Enum.GetName(typeof(OperatorComparer), comparer); 195 | if (operatorName.Contains("Not")) 196 | { 197 | isNot = true; 198 | operatorName = operatorName.Replace("Not", ""); 199 | } 200 | 201 | // Single or first, we'll need to debug 202 | var compareMethod = typeof(string).GetMethods() 203 | .Single(m => m.GetParameters().Any(p => p.ParameterType == typeof(string)) 204 | && m.Name.Equals(operatorName) && m.GetParameters().Count() == 1); 205 | //we assume ignoreCase, so call ToLower on paramter and memberexpression 206 | var toLowerMethod = typeof(string).GetMethods() 207 | .Single(m => m.Name.Equals("ToLower") && m.GetParameters().Count() == 0); 208 | 209 | left = Expression.Call(Expression.Coalesce(left, Expression.Constant(string.Empty)), toLowerMethod); 210 | right = Expression.Call(right, toLowerMethod); 211 | 212 | if (isNot) 213 | { 214 | return Expression.Not(Expression.Call(left, compareMethod, right)); 215 | } 216 | else 217 | { 218 | return Expression.Call(left, compareMethod, right); 219 | } 220 | } 221 | 222 | private Expression NullCheck(Expression toCheck) 223 | { 224 | return Expression.Not(Expression.Equal(toCheck, Expression.Constant(null, toCheck.Type))); 225 | } 226 | 227 | private Expression MakeLambda(Expression parameter, Expression predicate) 228 | { 229 | var resultParameterVisitor = new ParameterVisitor(); 230 | resultParameterVisitor.Visit(parameter); 231 | var resultParameter = resultParameterVisitor.Parameter; 232 | return Expression.Lambda(predicate, (ParameterExpression)resultParameter); 233 | } 234 | 235 | private class ParameterVisitor : ExpressionVisitor 236 | { 237 | public Expression Parameter 238 | { 239 | get; 240 | private set; 241 | } 242 | 243 | protected override Expression VisitParameter(ParameterExpression node) 244 | { 245 | Parameter = node; 246 | return node; 247 | } 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /ReFilter/ReSearchBuilder/IReSearchBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using ReFilter.Models.Filtering.Contracts; 3 | 4 | namespace ReFilter.ReSearchBuilder 5 | { 6 | internal interface IReSearchBuilder where T : class, new() 7 | { 8 | IQueryable BuildSearchQuery(IReFilterRequest filterRequest); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ReFilter/ReSortBuilder/IReSortBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using ReFilter.Models; 4 | using ReFilter.Models.Filtering.Contracts; 5 | 6 | namespace ReFilter.ReSortBuilder 7 | { 8 | public interface IReSortBuilder where T : class, new() 9 | { 10 | List> GetSorters(PropertyFilterConfig propertyFilterConfig); 11 | IQueryable BuildEntityQuery(); 12 | IOrderedQueryable BuildSortedQuery(IQueryable query, PropertyFilterConfig propertyFilterConfig, bool isFirst = false); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ReFilter/Utilities/FilterHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ReFilter.Utilities 4 | { 5 | public static class FilterHelper 6 | { 7 | public static Type GetMatchingType() where T : class, new() 8 | { 9 | switch (typeof(T)) 10 | { 11 | default: 12 | return typeof(T); 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /TestProject/Enums/Gender.cs: -------------------------------------------------------------------------------- 1 | namespace TestProject.Enums 2 | { 3 | enum Gender 4 | { 5 | Male, 6 | Female, 7 | Undisclosed 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /TestProject/FilterBuilders/SchoolFilterBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using ReFilter.Models.Filtering.Contracts; 6 | using ReFilter.ReFilterProvider; 7 | using TestProject.FilterBuilders.SchoolFilters; 8 | using TestProject.Models; 9 | using TestProject.Models.FilterRequests; 10 | using TestProject.TestData; 11 | 12 | namespace TestProject.FilterBuilders 13 | { 14 | class SchoolFilterBuilder : IReFilterBuilder 15 | { 16 | public IQueryable BuildEntityQuery(IReFilterRequest filterRequest) 17 | { 18 | var query = SchoolServiceTestData.Schools.AsQueryable(); 19 | 20 | query = BuildFilteredQuery(query, filterRequest); 21 | 22 | return query; 23 | } 24 | 25 | public IQueryable BuildFilteredQuery(IQueryable query, IReFilterRequest filterRequest) 26 | { 27 | var filters = GetFilters(filterRequest).ToList(); 28 | 29 | filters.ForEach(filter => 30 | { 31 | query = filter.FilterQuery(query); 32 | }); 33 | 34 | return query; 35 | } 36 | 37 | public List>> BuildPredicates(IReFilterRequest filterRequest, IQueryable query = null) 38 | { 39 | var filters = GetFilters(filterRequest).ToList(); 40 | 41 | List>> expressions = new(); 42 | 43 | filters.ForEach(filter => 44 | { 45 | expressions.Add(filter.GeneratePredicate(query)); 46 | }); 47 | 48 | return expressions; 49 | } 50 | 51 | public IEnumerable> GetFilters(IReFilterRequest filterRequest) 52 | { 53 | List> filters = new(); 54 | 55 | if (filterRequest == null) 56 | { 57 | return filters; 58 | } 59 | 60 | var realFilter = (SchoolFilterRequest)filterRequest; 61 | 62 | if (realFilter.StudentNames is not null && realFilter.StudentNames.Any()) 63 | { 64 | filters.Add(new StudentNamesFilter(realFilter.StudentNames)); 65 | } 66 | 67 | return filters; 68 | } 69 | 70 | public List GetForeignKeys(IReFilterRequest filterRequest) 71 | { 72 | var query = SchoolServiceTestData.Schools.AsQueryable(); 73 | 74 | query = BuildFilteredQuery(query, filterRequest); 75 | 76 | return query.Select(e => e.Id) 77 | .Distinct() 78 | .ToList(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /TestProject/FilterBuilders/SchoolFilters/StudentNamesFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using LinqKit; 6 | using ReFilter.Models.Filtering.Contracts; 7 | using TestProject.Models; 8 | 9 | namespace TestProject.FilterBuilders.SchoolFilters 10 | { 11 | internal class StudentNamesFilter : IReFilter 12 | { 13 | private readonly List studentNames; 14 | 15 | public StudentNamesFilter(List studentNames) 16 | { 17 | this.studentNames = studentNames; 18 | } 19 | 20 | public IQueryable FilterQuery(IQueryable query) 21 | { 22 | return query.Where(s => studentNames.Any(sn => s.Name.Contains(sn))); 23 | } 24 | 25 | public Expression> GeneratePredicate(IQueryable query) 26 | { 27 | var basePredicate = PredicateBuilder.New(); 28 | studentNames.ForEach(sn => 29 | { 30 | basePredicate.Or(s => s.Name.Contains(sn)); 31 | }); 32 | 33 | return basePredicate; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /TestProject/Mappers/SchoolMapper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using TestProject.Models; 3 | 4 | namespace TestProject.Mappers 5 | { 6 | static class SchoolMapper 7 | { 8 | static SchoolViewModel MapToViewModel(School school) 9 | { 10 | return new SchoolViewModel 11 | { 12 | Id = school.Id, 13 | Name = school.Name, 14 | Address = school.Address, 15 | Country = school.Country != null ? $"{school.Country.Alpha2Code} {school.Country.Name}" : null, 16 | Contacts = school.Contacts, 17 | Students = StudentMapper.MapListToViewModel(school.Students), 18 | IsActive = school.IsActive 19 | }; 20 | } 21 | 22 | static CollegeViewModel MapToViewModel(College school) 23 | { 24 | return new CollegeViewModel 25 | { 26 | Id = school.Id, 27 | Name = school.Name, 28 | Age = school.Age.ToString(), 29 | Address = school.Address, 30 | Country = school.Country != null ? $"{school.Country.Alpha2Code} {school.Country.Name}" : null, 31 | Contacts = school.Contacts, 32 | Students = StudentMapper.MapListToViewModel(school.Students), 33 | IsActive = school.IsActive 34 | }; 35 | } 36 | static College MapToCollege(School school) 37 | { 38 | return new College 39 | { 40 | Id = school.Id, 41 | Name = school.Name, 42 | Age = school.Age.ToString(), 43 | Address = school.Address, 44 | Country = school.Country, 45 | Contacts = school.Contacts, 46 | Students = school.Students, 47 | IsActive = school.IsActive, 48 | FoundingDate = school.FoundingDate, 49 | IdRange = school.IdRange, 50 | ValidOn = school.ValidOn, 51 | ValidOnSingle = school.ValidOnSingle, 52 | Building = school.Building, 53 | Certificates = school.Certificates 54 | }; 55 | } 56 | 57 | public static List MapListToViewModel(List schools) 58 | { 59 | var schoolViewModelList = new List(); 60 | 61 | schools.ForEach(school => 62 | { 63 | var mappedSchool = MapToViewModel(school); 64 | schoolViewModelList.Add(mappedSchool); 65 | }); 66 | 67 | return schoolViewModelList; 68 | } 69 | 70 | public static List MapListToViewModel(List schools) 71 | { 72 | var schoolViewModelList = new List(); 73 | 74 | schools.ForEach(school => 75 | { 76 | var mappedSchool = MapToViewModel(school); 77 | schoolViewModelList.Add(mappedSchool); 78 | }); 79 | 80 | return schoolViewModelList; 81 | } 82 | 83 | public static List MapListToCollege(List schools) 84 | { 85 | var schoolViewModelList = new List(); 86 | 87 | schools.ForEach(school => 88 | { 89 | var mappedSchool = MapToCollege(school); 90 | schoolViewModelList.Add(mappedSchool); 91 | }); 92 | 93 | return schoolViewModelList; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /TestProject/Mappers/StudentMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using TestProject.Models; 6 | 7 | namespace TestProject.Mappers 8 | { 9 | /// 10 | /// Just a showcase of a mapping funcion 11 | /// Can be anything you want 12 | /// Personally I use AutoMapper 13 | /// 14 | static class StudentMapper 15 | { 16 | static StudentViewModel MapToViewModel(Student student) 17 | { 18 | return new StudentViewModel 19 | { 20 | Id = student.Id, 21 | Age = student.Age, 22 | FirstName = student.FirstName, 23 | LastName = student.LastName, 24 | FullName = $"{student.FirstName} {student.LastName}", 25 | Gender = student.Gender 26 | }; 27 | } 28 | 29 | public static List MapListToViewModel(List students) 30 | { 31 | var studentViewModelList = new List(); 32 | 33 | students.ForEach(student => 34 | { 35 | var mappedStudent = MapToViewModel(student); 36 | studentViewModelList.Add(mappedStudent); 37 | }); 38 | 39 | return studentViewModelList; 40 | } 41 | 42 | public static Expression> MappingExpression() 43 | { 44 | Expression> expression = student => new StudentViewModel 45 | { 46 | Id = student.Id, 47 | Age = student.Age, 48 | FirstName = student.FirstName, 49 | LastName = student.LastName, 50 | FullName = $"{student.FirstName} {student.LastName}", 51 | Gender = student.Gender 52 | }; 53 | return expression; 54 | } 55 | 56 | public static List MapIQueryableToViewModel(IQueryable students) 57 | { 58 | return students.Select(MappingExpression()).ToList(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /TestProject/Models/Building.cs: -------------------------------------------------------------------------------- 1 | using ReFilter.Attributes; 2 | 3 | namespace TestProject.Models 4 | { 5 | internal class Building 6 | { 7 | public int Id { get; set; } 8 | [ReFilterProperty] 9 | public string Name { get; set; } 10 | public int Year { get; set; } 11 | public double Length { get; set; } 12 | public double Width { get; set; } 13 | [ReFilterProperty] 14 | public string Orientation { get; set; } 15 | [ReFilterProperty] 16 | public string BuiltBy { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TestProject/Models/Certificate.cs: -------------------------------------------------------------------------------- 1 | using ReFilter.Attributes; 2 | 3 | namespace TestProject.Models 4 | { 5 | internal class Certificate 6 | { 7 | public int Id { get; set; } 8 | [ReFilterProperty] 9 | public string Name { get; set; } 10 | [ReFilterProperty] 11 | public string Publisher { get; set; } 12 | [ReFilterProperty] 13 | public string Mark { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /TestProject/Models/College.cs: -------------------------------------------------------------------------------- 1 | using ReFilter.Attributes; 2 | 3 | namespace TestProject.Models 4 | { 5 | internal class College : School 6 | { 7 | [ReFilterProperty] 8 | public new string Age { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /TestProject/Models/CollegeViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace TestProject.Models 2 | { 3 | internal class CollegeViewModel : SchoolViewModel 4 | { 5 | public new string Age { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /TestProject/Models/Country.cs: -------------------------------------------------------------------------------- 1 | namespace TestProject.Models 2 | { 3 | public class Country 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | public string Description { get; set; } 8 | public string Alpha2Code { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /TestProject/Models/CountryFilterRequest.cs: -------------------------------------------------------------------------------- 1 | namespace TestProject.Models 2 | { 3 | public class CountryFilterRequest 4 | { 5 | public int? Id { get; set; } 6 | public string Name { get; set; } 7 | public string Description { get; set; } 8 | public string Alpha2Code { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /TestProject/Models/FilterRequests/CollegeFilterRequest.cs: -------------------------------------------------------------------------------- 1 | namespace TestProject.Models.FilterRequests 2 | { 3 | internal class CollegeFilterRequest : SchoolFilterRequest 4 | { 5 | public new string Age { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /TestProject/Models/FilterRequests/SchoolFilterRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ReFilter.Attributes; 4 | using ReFilter.Models; 5 | using ReFilter.Models.Filtering.Contracts; 6 | 7 | namespace TestProject.Models.FilterRequests 8 | { 9 | class SchoolFilterRequest : IReFilterRequest 10 | { 11 | public int? Id { get; set; } 12 | public RangeFilter IdRange { get; set; } 13 | public string Name { get; set; } 14 | public string Address { get; set; } 15 | 16 | [ReFilterProperty(HasSpecialSort = true)] 17 | public CountryFilterRequest Country { get; set; } 18 | 19 | public List Contacts { get; set; } 20 | 21 | [ReFilterProperty(HasSpecialFilter = true)] 22 | public List StudentNames { get; set; } 23 | 24 | public RangeFilter Age { get; set; } 25 | 26 | public RangeFilter FoundingDate { get; set; } 27 | public RangeFilter ValidOn { get; set; } 28 | public DateOnly? ValidOnSingle { get; set; } 29 | 30 | public bool? IsActive { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /TestProject/Models/School.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ReFilter.Attributes; 4 | using TestProject.FilterBuilders; 5 | 6 | namespace TestProject.Models 7 | { 8 | [ReFilterBuilder(typeof(SchoolFilterBuilder))] 9 | class School 10 | { 11 | public int Id { get; set; } 12 | public int IdRange { get; set; } 13 | [ReFilterProperty] 14 | public string Name { get; set; } 15 | [ReFilterProperty] 16 | public string Address { get; set; } 17 | 18 | public Country Country { get; set; } 19 | 20 | public List Contacts { get; set; } 21 | public List Students { get; set; } 22 | 23 | public double Age { get; set; } 24 | public DateTime FoundingDate { get; set; } 25 | public DateOnly ValidOn { get; set; } 26 | public DateOnly ValidOnSingle { get; set; } 27 | 28 | public bool IsActive { get; set; } 29 | 30 | [ReFilterProperty] 31 | public Building Building { get; set; } 32 | 33 | [ReFilterProperty] 34 | public List Certificates { get; set; } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /TestProject/Models/SchoolViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace TestProject.Models 4 | { 5 | class SchoolViewModel 6 | { 7 | public int Id { get; set; } 8 | public string Name { get; set; } 9 | public string Address { get; set; } 10 | public string Country { get; set; } 11 | 12 | public List Contacts { get; set; } 13 | public List Students { get; set; } 14 | 15 | public double Age { get; set; } 16 | 17 | public bool IsActive { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /TestProject/Models/Student.cs: -------------------------------------------------------------------------------- 1 | using TestProject.Enums; 2 | 3 | namespace TestProject.Models 4 | { 5 | class Student 6 | { 7 | public int Id { get; set; } 8 | public string FirstName { get; set; } 9 | public string LastName { get; set; } 10 | public int? Age { get; set; } 11 | public Gender Gender { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /TestProject/Models/StudentViewModel.cs: -------------------------------------------------------------------------------- 1 | using TestProject.Enums; 2 | 3 | namespace TestProject.Models 4 | { 5 | class StudentViewModel 6 | { 7 | public int Id { get; set; } 8 | public string FirstName { get; set; } 9 | public string LastName { get; set; } 10 | public int? Age { get; set; } 11 | public Gender Gender { get; set; } 12 | 13 | public string FullName { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /TestProject/RequiredImplementations/ReFilterConfigBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ReFilter.ReFilterProvider; 3 | using ReFilter.ReFilterTypeMatcher; 4 | using TestProject.FilterBuilders; 5 | using TestProject.Models; 6 | using TestProject.Models.FilterRequests; 7 | 8 | namespace TestProject.RequiredImplementations 9 | { 10 | /// 11 | /// This one is required because it's used in Startup 12 | /// Minimum implementation is shown for Student => it's the Default case 13 | /// Semi Real-Life is shown for School 14 | /// 15 | class ReFilterConfigBuilder : IReFilterConfigBuilder 16 | { 17 | public Type GetMatchingType() where T : class, new() 18 | { 19 | switch (typeof(T)) 20 | { 21 | case Type schoolType when schoolType == typeof(School): 22 | return typeof(SchoolFilterRequest); 23 | default: 24 | return typeof(T); 25 | } 26 | } 27 | 28 | public IReFilterBuilder GetMatchingFilterBuilder() where T : class, new() 29 | { 30 | switch (typeof(T)) 31 | { 32 | case Type schoolType when schoolType == typeof(School): 33 | return (IReFilterBuilder)new SchoolFilterBuilder(); 34 | default: 35 | return null; 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /TestProject/RequiredImplementations/ReSortConfigBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ReFilter.ReFilterConfigBuilder; 3 | using ReFilter.ReSortBuilder; 4 | using TestProject.Models; 5 | using TestProject.Models.FilterRequests; 6 | using TestProject.SortBuilders; 7 | 8 | namespace TestProject.RequiredImplementations 9 | { 10 | internal class ReSortConfigBuilder : IReSortConfigBuilder 11 | { 12 | public IReSortBuilder GetMatchingSortBuilder() where T : class, new() 13 | { 14 | switch (typeof(T)) 15 | { 16 | case Type schoolType when schoolType == typeof(School): 17 | return (IReSortBuilder)new SchoolSortBuilder(); 18 | default: 19 | return null; 20 | } 21 | } 22 | 23 | public Type GetMatchingType() where T : class, new() 24 | { 25 | switch (typeof(T)) 26 | { 27 | case Type schoolType when schoolType == typeof(School): 28 | return typeof(SchoolFilterRequest); 29 | default: 30 | return typeof(T); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /TestProject/SortBuilders/CountrySorter.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using ReFilter.Enums; 3 | using ReFilter.Models.Filtering.Contracts; 4 | using TestProject.Models; 5 | 6 | namespace TestProject.SortBuilders 7 | { 8 | internal class CountrySorter : IReSort 9 | { 10 | public IOrderedQueryable SortQuery(IQueryable query, SortDirection sortDirection, bool isFirst = true) 11 | { 12 | if (sortDirection == SortDirection.ASC) 13 | { 14 | return query.OrderBy(e => (e.Country == null ? null : e.Country.Alpha2Code)).ThenBy(e => e.Address); 15 | } 16 | else 17 | { 18 | return query.OrderByDescending(e => (e.Country == null ? null : e.Country.Alpha2Code)).ThenByDescending(e => e.Address); 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /TestProject/SortBuilders/SchoolSortBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using ReFilter.Models; 4 | using ReFilter.Models.Filtering.Contracts; 5 | using ReFilter.ReSortBuilder; 6 | using TestProject.Models; 7 | using TestProject.Models.FilterRequests; 8 | using TestProject.TestData; 9 | 10 | namespace TestProject.SortBuilders 11 | { 12 | internal class SchoolSortBuilder : IReSortBuilder 13 | { 14 | public IQueryable BuildEntityQuery() 15 | { 16 | return SchoolServiceTestData.Schools.AsQueryable(); 17 | } 18 | 19 | public IOrderedQueryable BuildSortedQuery(IQueryable query, PropertyFilterConfig propertyFilterConfig, 20 | bool isFirst = false) 21 | { 22 | var sorters = GetSorters(propertyFilterConfig); 23 | 24 | if (sorters == null || sorters.Count == 0) 25 | { 26 | return (IOrderedQueryable)query; 27 | } 28 | 29 | IOrderedQueryable orderedQuery = (IOrderedQueryable)query; 30 | 31 | for (var i = 0; i < sorters.Count; i++) 32 | { 33 | orderedQuery = sorters[i].SortQuery(orderedQuery, 34 | propertyFilterConfig.SortDirection.Value, 35 | isFirst: (i == 0 && isFirst)); 36 | } 37 | 38 | return orderedQuery; 39 | } 40 | 41 | public List> GetSorters(PropertyFilterConfig propertyFilterConfig) 42 | { 43 | List> sorters = new List>(); 44 | 45 | if (propertyFilterConfig != null) 46 | { 47 | if (propertyFilterConfig.PropertyName == nameof(SchoolFilterRequest.Country)) 48 | { 49 | sorters.Add(new CountrySorter()); 50 | } 51 | } 52 | 53 | return sorters; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /TestProject/TestData/SchoolServiceTestData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using TestProject.Models; 4 | 5 | namespace TestProject.TestData 6 | { 7 | static class SchoolServiceTestData 8 | { 9 | public static List Schools 10 | { 11 | get 12 | { 13 | var schoolList = new List(); 14 | for (int i = 1; i <= 100; i++) 15 | { 16 | var newSchool = new School 17 | { 18 | Id = i, 19 | IdRange = i, 20 | Name = $"School Name {i}", 21 | Address = $"School Address {i}", 22 | Contacts = new List { $"Contact {i}", $"Contact {i + 1}" }, 23 | Students = new List 24 | { 25 | new Student 26 | { 27 | Id = i, 28 | FirstName = $"Student Name School Name {i}", 29 | LastName = $"Student LastName School Name {i}", 30 | Age = i, 31 | Gender = Enums.Gender.Male, 32 | } 33 | }, 34 | Country = new Country 35 | { 36 | Id = i, 37 | Name = $"Country Name {i}", 38 | Alpha2Code = $"Alpha2Code {i}", 39 | Description = $"Description {i}", 40 | }, 41 | IsActive = Convert.ToBoolean(i % 2), 42 | Age = i * 10 / 1.7, 43 | FoundingDate = new DateTime(1900 + (i * 100) / 24, i % 12 + 1, i % 12 + 1), 44 | ValidOn = new DateOnly(1900 + (i * 100) / 24, i % 12 + 1, i % 12 + 1), 45 | ValidOnSingle = new DateOnly(1900 + (i * 100) / 24, i % 12 + 1, i % 12 + 1), 46 | Building = new Building 47 | { 48 | Id = i, 49 | Name = $"Building Name {i}", 50 | BuiltBy = $"Builder {i}", 51 | Year = 1900 + i 52 | }, 53 | Certificates = new List 54 | { 55 | new Certificate 56 | { 57 | Id = i, 58 | Mark = $"C1{i}", 59 | Name = $"Certificate1 {i}", 60 | Publisher = $"Publisher1 {i}" 61 | }, 62 | new Certificate 63 | { 64 | Id = i, 65 | Mark = $"C2{i}", 66 | Name = $"Certificate2 {i}", 67 | Publisher = $"Publisher2 {i}" 68 | } 69 | } 70 | }; 71 | 72 | schoolList.Add(newSchool); 73 | } 74 | 75 | return schoolList; 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /TestProject/TestData/StudentServiceTestData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using TestProject.Models; 4 | 5 | namespace TestProject.TestData 6 | { 7 | static class StudentServiceTestData 8 | { 9 | public static List Students 10 | { 11 | get 12 | { 13 | var studentList = new List(); 14 | for (int i = 1; i <= 100; i++) 15 | { 16 | var newStudent = new Student 17 | { 18 | Id = i, 19 | Age = new Random().Next(0, 100), 20 | FirstName = $"FirstName {i}", 21 | LastName = $"LastName {i}", 22 | Gender = (Enums.Gender)(i%3) 23 | }; 24 | 25 | studentList.Add(newStudent); 26 | } 27 | 28 | return studentList; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /TestProject/TestProject.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /TestProject/TestServices/SchoolService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using ReFilter.Models; 6 | using ReFilter.ReFilterActions; 7 | using ReFilter.ReFilterConfigBuilder; 8 | using ReFilter.ReFilterTypeMatcher; 9 | using TestProject.Mappers; 10 | using TestProject.Models; 11 | using TestProject.RequiredImplementations; 12 | 13 | namespace TestProject.TestServices 14 | { 15 | class SchoolService 16 | { 17 | #region Ctors and Members 18 | 19 | private readonly List testList; 20 | private readonly ReFilterActions testReFilterActions; 21 | 22 | /// 23 | /// Usually you will use DI for initialization 24 | /// 25 | /// 26 | public SchoolService(List testList) 27 | { 28 | this.testList = testList; 29 | 30 | testReFilterActions = InitializeTestFilterActions(new ReFilterConfigBuilder(), new ReSortConfigBuilder()); 31 | } 32 | 33 | /// 34 | /// Just a helper instead of DI 35 | /// 36 | /// 37 | /// 38 | private ReFilterActions InitializeTestFilterActions(IReFilterConfigBuilder reFilterConfigBuilder, IReSortConfigBuilder reSortConfigBuilder) 39 | { 40 | return new ReFilterActions(reFilterConfigBuilder, reSortConfigBuilder); 41 | } 42 | 43 | #endregion Ctors and Members 44 | 45 | public async Task> GetPaged(BasePagedRequest request) 46 | { 47 | var testQueryable = testList.AsQueryable(); // Any kind of queryable 48 | 49 | var pagedRequest = request.GetPagedRequest(returnResults: true); 50 | 51 | var result = await testReFilterActions.GetPaged(testQueryable, pagedRequest); 52 | 53 | return result; 54 | } 55 | 56 | public async Task> GetPagedMapped(BasePagedRequest request) 57 | { 58 | var testQueryable = testList.AsQueryable(); 59 | 60 | List mappingFunction(List x) => SchoolMapper.MapListToViewModel(x); 61 | var pagedRequest = request.GetPagedRequest((Func, List>)mappingFunction); 62 | 63 | var result = await testReFilterActions.GetPaged(testQueryable, pagedRequest); 64 | 65 | return result; 66 | } 67 | 68 | public async Task> GetPagedSearchQuery(BasePagedRequest request) 69 | { 70 | var testQueryable = testList.AsQueryable(); 71 | 72 | List mappingFunction(List x) => SchoolMapper.MapListToViewModel(x); 73 | var pagedRequest = request.GetPagedRequest((Func, List>)mappingFunction); 74 | 75 | var result = await testReFilterActions.GetBySearchQuery(testQueryable, pagedRequest); 76 | 77 | return result; 78 | } 79 | 80 | public async Task> GetCollegePagedSearchQuery(BasePagedRequest request) 81 | { 82 | var testQueryable = SchoolMapper.MapListToCollege(testList).AsQueryable(); 83 | 84 | List mappingFunction(List x) => SchoolMapper.MapListToViewModel(x); 85 | var pagedRequest = request.GetPagedRequest((Func, List>)mappingFunction); 86 | 87 | var result = await testReFilterActions.GetBySearchQuery(testQueryable, pagedRequest); 88 | 89 | return result; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /TestProject/TestServices/StudentService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using ReFilter.Models; 6 | using ReFilter.ReFilterActions; 7 | using ReFilter.ReFilterConfigBuilder; 8 | using ReFilter.ReFilterTypeMatcher; 9 | using TestProject.Mappers; 10 | using TestProject.Models; 11 | using TestProject.RequiredImplementations; 12 | 13 | namespace TestProject.TestServices 14 | { 15 | /// 16 | /// A simple test class showcasing 2 main principles 17 | /// 18 | class StudentService 19 | { 20 | #region Ctors and Members 21 | 22 | private readonly List testList; 23 | private readonly ReFilterActions testReFilterActions; 24 | 25 | /// 26 | /// Usually you will use DI for initialization 27 | /// 28 | /// 29 | public StudentService(List testList) 30 | { 31 | this.testList = testList; 32 | 33 | testReFilterActions = InitializeTestFilterActions(new ReFilterConfigBuilder(), new ReSortConfigBuilder()); 34 | } 35 | 36 | /// 37 | /// Just a helper instead of DI 38 | /// 39 | /// 40 | /// 41 | private ReFilterActions InitializeTestFilterActions(IReFilterConfigBuilder reFilterConfigBuilder, IReSortConfigBuilder reSortConfigBuilder) 42 | { 43 | return new ReFilterActions(reFilterConfigBuilder, reSortConfigBuilder); 44 | } 45 | 46 | #endregion Ctors and Members 47 | 48 | public async Task> GetPaged(BasePagedRequest request) 49 | { 50 | var testQueryable = testList.AsQueryable(); 51 | 52 | var pagedRequest = request.GetPagedRequest(returnResults: true); 53 | 54 | var result = await testReFilterActions.GetPaged(testQueryable, pagedRequest); 55 | 56 | return result; 57 | } 58 | 59 | public async Task> GetPagedMapped(BasePagedRequest request) 60 | { 61 | var testQueryable = testList.AsQueryable(); 62 | 63 | List mappingFunction(List x) => StudentMapper.MapListToViewModel(x); 64 | var pagedRequest = request.GetPagedRequest((Func, List>)mappingFunction); 65 | 66 | var result = await testReFilterActions.GetPaged(testQueryable, pagedRequest); 67 | 68 | return result; 69 | } 70 | 71 | public async Task> GetPagedMappedProjection(BasePagedRequest request) 72 | { 73 | var testQueryable = testList.AsQueryable(); 74 | 75 | List mappingFunction(IQueryable x) => StudentMapper.MapIQueryableToViewModel(x); 76 | var pagedRequest = request.GetPagedRequest((Func, List>)mappingFunction); 77 | 78 | var result = await testReFilterActions.GetPaged(testQueryable, pagedRequest); 79 | 80 | return result; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /TestProject/Tests/SchoolServiceTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | using System.Threading.Tasks; 7 | using Newtonsoft.Json.Linq; 8 | using NUnit.Framework; 9 | using ReFilter.Enums; 10 | using ReFilter.Models; 11 | using TestProject.Models; 12 | using TestProject.TestData; 13 | using TestProject.TestServices; 14 | 15 | namespace TestProject.Tests 16 | { 17 | [TestFixture] 18 | class SchoolServiceTest 19 | { 20 | public static IEnumerable TestCases 21 | { 22 | get 23 | { 24 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10 }).Returns(SchoolServiceTestData.Schools.Count).SetName("Mapped: No Filters"); 25 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, Where = JObject.Parse("{Address: \"School Address 1\"}") }).Returns(1).SetName("Mapped: Filter by Address with no Property Filter Config"); 26 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, Where = JObject.Parse("{Name: 10}") }).Returns(0).SetName("Mapped: Filter by Name(Equals)"); 27 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, Where = JObject.Parse("{Name: 10, Address: 10}") }).Returns(0).SetName("Mapped: Filter by Name(Equals) and Address(Equals)"); 28 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, Where = JObject.Parse("{IsActive: true}") }).Returns(50).SetName("Mapped: Filter by IsActive"); 29 | yield return new TestCaseData(new BasePagedRequest 30 | { 31 | PageIndex = 0, 32 | PageSize = 10, 33 | Where = JObject.Parse("{Name: 10, Address: 10}"), 34 | PropertyFilterConfigs = new List 35 | { 36 | new PropertyFilterConfig 37 | { 38 | OperatorComparer = OperatorComparer.Contains, 39 | PropertyName = "Name" 40 | }, 41 | new PropertyFilterConfig 42 | { 43 | OperatorComparer = OperatorComparer.Contains, 44 | PropertyName = "Address" 45 | } 46 | } 47 | }).Returns(2).SetName("Mapped: Filter by Name(Contains) and Address(Contains)"); 48 | 49 | yield return new TestCaseData(new BasePagedRequest 50 | { 51 | PageIndex = 0, 52 | PageSize = 10, 53 | Where = JObject.Parse("{Name: 10}"), 54 | PropertyFilterConfigs = new List 55 | { 56 | new PropertyFilterConfig 57 | { 58 | OperatorComparer = OperatorComparer.NotEqual, 59 | PropertyName = "Name" 60 | } 61 | } 62 | }).Returns(100).SetName("Mapped: Filter by Name(Not Equal)"); 63 | 64 | yield return new TestCaseData(new BasePagedRequest 65 | { 66 | PageIndex = 0, 67 | PageSize = 10, 68 | Where = JObject.Parse("{Name: 10}"), 69 | PropertyFilterConfigs = new List 70 | { 71 | new PropertyFilterConfig 72 | { 73 | OperatorComparer = OperatorComparer.NotContains, 74 | PropertyName = "Name" 75 | } 76 | } 77 | }).Returns(98).SetName("Mapped: Filter by Name(Not Contains)"); 78 | 79 | yield return new TestCaseData(new BasePagedRequest 80 | { 81 | PageIndex = 0, 82 | PageSize = 10, 83 | Where = JObject.Parse("{Name: 10}"), 84 | PropertyFilterConfigs = new List 85 | { 86 | new PropertyFilterConfig 87 | { 88 | OperatorComparer = OperatorComparer.NotStartsWith, 89 | PropertyName = "Name" 90 | } 91 | } 92 | }).Returns(100).SetName("Mapped: Filter by Name(Not Starts With 10)"); 93 | 94 | yield return new TestCaseData(new BasePagedRequest 95 | { 96 | PageIndex = 0, 97 | PageSize = 10, 98 | Where = JObject.Parse("{Name: 10}"), 99 | PropertyFilterConfigs = new List 100 | { 101 | new PropertyFilterConfig 102 | { 103 | OperatorComparer = OperatorComparer.NotEndsWith, 104 | PropertyName = "Name" 105 | } 106 | } 107 | }).Returns(99).SetName("Mapped: Filter by Name(Not Ends With 10)"); 108 | 109 | yield return new TestCaseData(new BasePagedRequest 110 | { 111 | PageIndex = 0, 112 | PageSize = 10, 113 | Where = JObject.Parse("{Id: 10}"), 114 | PropertyFilterConfigs = new List 115 | { 116 | new PropertyFilterConfig 117 | { 118 | OperatorComparer = OperatorComparer.GreaterThan, 119 | PropertyName = "Id" 120 | } 121 | } 122 | }).Returns(90).SetName("Mapped: Filter by Id(Greater Than)"); 123 | 124 | yield return new TestCaseData(new BasePagedRequest 125 | { 126 | PageIndex = 0, 127 | PageSize = 10, 128 | Where = JObject.Parse("{Id: 10}"), 129 | PropertyFilterConfigs = new List 130 | { 131 | new PropertyFilterConfig 132 | { 133 | OperatorComparer = OperatorComparer.LessThan, 134 | PropertyName = "Id" 135 | } 136 | } 137 | }).Returns(9).SetName("Mapped: Filter by Id(Less Than)"); 138 | 139 | yield return new TestCaseData(new BasePagedRequest 140 | { 141 | PageIndex = 0, 142 | PageSize = 10, 143 | Where = JObject.Parse("{IdRange: { Start: 1, End: 1 }}"), 144 | PropertyFilterConfigs = new List 145 | { 146 | new PropertyFilterConfig 147 | { 148 | OperatorComparer = OperatorComparer.BetweenInclusive, 149 | PropertyName = "IdRange" 150 | } 151 | } 152 | }).Returns(1).SetName("Mapped: Range Filter by IdRange(BetweenInclusive)"); 153 | 154 | yield return new TestCaseData(new BasePagedRequest 155 | { 156 | PageIndex = 0, 157 | PageSize = 10, 158 | Where = JObject.Parse("{Age: { Start: 200, End: 400 }}"), 159 | PropertyFilterConfigs = new List 160 | { 161 | new PropertyFilterConfig 162 | { 163 | OperatorComparer = OperatorComparer.BetweenExclusive, 164 | PropertyName = "Age" 165 | } 166 | } 167 | }).Returns(33).SetName("Mapped: Range Filter by Age(Between Exclusive)"); 168 | 169 | yield return new TestCaseData(new BasePagedRequest 170 | { 171 | PageIndex = 0, 172 | PageSize = 10, 173 | Where = JObject.Parse("{Age: { End: 400 }}"), 174 | PropertyFilterConfigs = new List 175 | { 176 | new PropertyFilterConfig 177 | { 178 | OperatorComparer = OperatorComparer.BetweenExclusive, 179 | PropertyName = "Age" 180 | } 181 | } 182 | }).Returns(67).SetName("Mapped: Range Filter by Age(Between Exclusive No Low)"); 183 | 184 | yield return new TestCaseData(new BasePagedRequest 185 | { 186 | PageIndex = 0, 187 | PageSize = 10, 188 | Where = JObject.Parse("{Age: { Start: 400 }}"), 189 | PropertyFilterConfigs = new List 190 | { 191 | new PropertyFilterConfig 192 | { 193 | OperatorComparer = OperatorComparer.GreaterThan, 194 | PropertyName = "Age" 195 | } 196 | } 197 | }).Returns(32).SetName("Mapped: Range Filter by Age(GreaterThen Low Only)"); 198 | 199 | yield return new TestCaseData(new BasePagedRequest 200 | { 201 | PageIndex = 0, 202 | PageSize = 10, 203 | Where = JObject.Parse("{Age: { Start: 200 }}"), 204 | PropertyFilterConfigs = new List 205 | { 206 | new PropertyFilterConfig 207 | { 208 | OperatorComparer = OperatorComparer.LessThan, 209 | PropertyName = "Age" 210 | } 211 | } 212 | }).Returns(33).SetName("Mapped: Range Filter by Age(LessThan Low Only)"); 213 | 214 | yield return new TestCaseData(new BasePagedRequest 215 | { 216 | PageIndex = 0, 217 | PageSize = 10, 218 | Where = JObject.Parse("{FoundingDate: { Start: \"1904-02-02\" }}"), 219 | PropertyFilterConfigs = new List 220 | { 221 | new PropertyFilterConfig 222 | { 223 | OperatorComparer = OperatorComparer.GreaterThan, 224 | PropertyName = "FoundingDate" 225 | } 226 | } 227 | }).Returns(99).SetName("Mapped: Range Filter by FoundingDate(GreaterThan Low Only)"); 228 | 229 | yield return new TestCaseData(new BasePagedRequest 230 | { 231 | PageIndex = 0, 232 | PageSize = 10, 233 | Where = JObject.Parse("{FoundingDate: { Start: \"1904-02-02\" }}"), 234 | PropertyFilterConfigs = new List 235 | { 236 | new PropertyFilterConfig 237 | { 238 | OperatorComparer = OperatorComparer.Equals, 239 | PropertyName = "FoundingDate" 240 | } 241 | } 242 | }).Returns(1).SetName("Mapped: Range Filter by FoundingDate(Equals Low Only)"); 243 | 244 | yield return new TestCaseData(new BasePagedRequest 245 | { 246 | PageIndex = 0, 247 | PageSize = 10, 248 | Where = JObject.Parse("{FoundingDate: { Start: \"1904-02-03\" }}"), 249 | PropertyFilterConfigs = new List 250 | { 251 | new PropertyFilterConfig 252 | { 253 | OperatorComparer = OperatorComparer.LessThan, 254 | PropertyName = "FoundingDate" 255 | } 256 | } 257 | }).Returns(1).SetName("Mapped: Range Filter by FoundingDate(LessThan Low Only)"); 258 | 259 | yield return new TestCaseData(new BasePagedRequest 260 | { 261 | PageIndex = 0, 262 | PageSize = 10, 263 | Where = JObject.Parse("{ValidOn: { Start: \"1904-02-03 00:00:00\" }}"), 264 | PropertyFilterConfigs = new List 265 | { 266 | new PropertyFilterConfig 267 | { 268 | OperatorComparer = OperatorComparer.LessThan, 269 | PropertyName = "ValidOn" 270 | } 271 | } 272 | }).Returns(1).SetName("Mapped: Range Filter by ValidOn(LessThan Low Only)"); 273 | 274 | yield return new TestCaseData(new BasePagedRequest 275 | { 276 | PageIndex = 0, 277 | PageSize = 10, 278 | Where = JObject.Parse("{ValidOnSingle: \"1916-05-05T00:00:00Z\" }"), 279 | PropertyFilterConfigs = new List 280 | { 281 | new PropertyFilterConfig 282 | { 283 | OperatorComparer = OperatorComparer.Equals, 284 | PropertyName = "ValidOnSingle" 285 | } 286 | } 287 | }).Returns(1).SetName("Mapped: Range Filter by ValidOnSingle(Exact)"); 288 | 289 | yield return new TestCaseData(new BasePagedRequest 290 | { 291 | PageIndex = 0, 292 | PageSize = 10, 293 | Where = JObject.Parse("{ValidOnSingle: \"1916-05-05T00:00:00Z\" }"), 294 | PropertyFilterConfigs = new List 295 | { 296 | new PropertyFilterConfig 297 | { 298 | OperatorComparer = OperatorComparer.LessThan, 299 | PropertyName = "ValidOnSingle" 300 | } 301 | } 302 | }).Returns(3).SetName("Mapped: Range Filter by ValidOnSingle(LessThan)"); 303 | 304 | yield return new TestCaseData(new BasePagedRequest 305 | { 306 | PageIndex = 0, 307 | PageSize = 10, 308 | Where = JObject.Parse("{ValidOnSingle: \"1916-05-05T00:00:00Z\" }"), 309 | PropertyFilterConfigs = new List 310 | { 311 | new PropertyFilterConfig 312 | { 313 | OperatorComparer = OperatorComparer.GreaterThan, 314 | PropertyName = "ValidOnSingle" 315 | } 316 | } 317 | }).Returns(96).SetName("Mapped: Range Filter by ValidOnSingle(GreaterThan)"); 318 | 319 | yield return new TestCaseData(new BasePagedRequest 320 | { 321 | PageIndex = 0, 322 | PageSize = 10, 323 | Where = JObject.Parse("{ValidOnSingle: \"1916-05-05T00:00:00Z\" }"), 324 | PropertyFilterConfigs = new List 325 | { 326 | new PropertyFilterConfig 327 | { 328 | OperatorComparer = OperatorComparer.GreaterThan, 329 | PropertyName = "ValidOnSingle" 330 | }, 331 | new PropertyFilterConfig 332 | { 333 | OperatorComparer = OperatorComparer.LessThan, 334 | PredicateOperator = LinqKit.PredicateOperator.Or, 335 | PropertyName = "ValidOnSingle" 336 | } 337 | } 338 | }).Returns(99).SetName("Mapped: Range Filter by ValidOnSingle(Or => LessThan + GreaterThan)"); 339 | 340 | yield return new TestCaseData(new BasePagedRequest 341 | { 342 | PageIndex = 0, 343 | PageSize = 10, 344 | Where = JObject.Parse("{ValidOnSingle: \"1916-05-05T00:00:00Z\" }"), 345 | PropertyFilterConfigs = new List 346 | { 347 | new PropertyFilterConfig 348 | { 349 | OperatorComparer = OperatorComparer.GreaterThan, 350 | PropertyName = "ValidOnSingle" 351 | }, 352 | new PropertyFilterConfig 353 | { 354 | OperatorComparer = OperatorComparer.LessThan, 355 | PropertyName = "ValidOnSingle" 356 | } 357 | } 358 | }).Returns(0).SetName("Mapped: Range Filter by ValidOnSingle(And => LessThan + GreaterThan)"); 359 | 360 | yield return new TestCaseData(new BasePagedRequest 361 | { 362 | PageIndex = 0, 363 | PageSize = 10, 364 | Where = JObject.Parse("{StudentNames: [ \"SchoolName\" ] }"), 365 | }).Returns(0).SetName("Mapped: Special Filter on StudentName, No Result (And clause)"); 366 | 367 | yield return new TestCaseData(new BasePagedRequest 368 | { 369 | PageIndex = 0, 370 | PageSize = 10, 371 | Where = JObject.Parse("{StudentNames: [ \"School Name\" ] }"), 372 | }).Returns(100).SetName("Mapped: Special Filter on StudentName, Match All (And clause)"); 373 | 374 | yield return new TestCaseData(new BasePagedRequest 375 | { 376 | PageIndex = 0, 377 | PageSize = 10, 378 | Where = JObject.Parse("{StudentNames: [ \"School Name\" ], ValidOnSingle: \"1916-05-05T00:00:00Z\" }"), 379 | PropertyFilterConfigs = new List 380 | { 381 | new PropertyFilterConfig 382 | { 383 | OperatorComparer = OperatorComparer.Equals, 384 | PropertyName = "ValidOnSingle" 385 | } 386 | } 387 | }).Returns(1).SetName("Mapped: Special Filter on StudentName, regular on ValidOnSingle (And clause)"); 388 | 389 | yield return new TestCaseData(new BasePagedRequest 390 | { 391 | PageIndex = 0, 392 | PageSize = 10, 393 | PredicateOperator = LinqKit.PredicateOperator.Or, 394 | Where = JObject.Parse("{StudentNames: [ \"School Name\" ], ValidOnSingle: \"1916-05-05T00:00:00Z\" }"), 395 | PropertyFilterConfigs = new List 396 | { 397 | new PropertyFilterConfig 398 | { 399 | OperatorComparer = OperatorComparer.Equals, 400 | PropertyName = "ValidOnSingle" 401 | } 402 | } 403 | }).Returns(100).SetName("Mapped: Special Filter on StudentName, regular on ValidOnSingle (Or clause)"); 404 | } 405 | } 406 | 407 | public static IEnumerable TestCasesSearch 408 | { 409 | get 410 | { 411 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, SearchQuery = "10" }).Returns(2).SetName("Search Query"); 412 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, SearchQuery = "100" }).Returns(1).SetName("Search Query 100"); 413 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, SearchQuery = "Builder 100" }).Returns(1).SetName("Search SubQuery 1"); 414 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, SearchQuery = "Builder 10" }).Returns(2).SetName("Search SubQuery 2"); 415 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, SearchQuery = "C1100" }).Returns(1).SetName("Search SubQuery Certificates Mark 1"); 416 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, SearchQuery = "C110" }).Returns(2).SetName("Search SubQuery Certificates Mark 2"); 417 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, SearchQuery = "Certificate2 10" }).Returns(2).SetName("Search SubQuery Certificates Name 2"); 418 | } 419 | } 420 | 421 | public static IEnumerable TestCasesSort 422 | { 423 | get 424 | { 425 | yield return new TestCaseData(new BasePagedRequest 426 | { 427 | PageIndex = 0, 428 | PageSize = 10, 429 | PropertyFilterConfigs = new List 430 | { 431 | new PropertyFilterConfig 432 | { 433 | PropertyName = "Name", 434 | SortDirection = SortDirection.ASC 435 | } 436 | } 437 | }) 438 | .Returns(100) 439 | .SetName("Sort by Name"); 440 | 441 | yield return new TestCaseData(new BasePagedRequest 442 | { 443 | PageIndex = 0, 444 | PageSize = 10, 445 | PropertyFilterConfigs = new List 446 | { 447 | new PropertyFilterConfig 448 | { 449 | PropertyName = "Country", 450 | SortDirection = SortDirection.DESC 451 | } 452 | } 453 | }) 454 | .Returns(100) 455 | .SetName("Sort by Special Desc"); 456 | 457 | yield return new TestCaseData(new BasePagedRequest 458 | { 459 | PageIndex = 0, 460 | PageSize = 10, 461 | PropertyFilterConfigs = new List 462 | { 463 | new PropertyFilterConfig 464 | { 465 | PropertyName = "Country", 466 | SortDirection = SortDirection.ASC 467 | } 468 | } 469 | }) 470 | .Returns(100) 471 | .SetName("Sort by Special Asc"); 472 | 473 | yield return new TestCaseData(new BasePagedRequest 474 | { 475 | PageIndex = 0, 476 | PageSize = 10, 477 | PropertyFilterConfigs = new List 478 | { 479 | new PropertyFilterConfig 480 | { 481 | PropertyName = "Name", 482 | SortDirection = SortDirection.DESC 483 | }, 484 | new PropertyFilterConfig 485 | { 486 | PropertyName = "Address", 487 | SortDirection = SortDirection.ASC 488 | } 489 | } 490 | }) 491 | .Returns(100) 492 | .SetName("Sort by Multiple"); 493 | } 494 | } 495 | 496 | public static IEnumerable TestCasesFilter 497 | { 498 | get 499 | { 500 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, Where = JObject.Parse("{Address: \"School Address 1\"}") }).Returns(1).SetName("Mapped: Filter by Address with no Property Filter Config"); 501 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, PredicateOperator = LinqKit.PredicateOperator.Or, Where = JObject.Parse("{Address: \"School Address 1\", ValidOnSingle: \"1916-05-05T00:00:00Z\"}") }).Returns(2).SetName("Mapped: Filter by Address with no Property Filter Config (OR)"); 502 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, Where = JObject.Parse("{Address: \"School Address 1\", ValidOnSingle: \"1916-05-05T00:00:00Z\"}") }).Returns(0).SetName("Mapped: Filter by Address with no Property Filter Config (AND)"); 503 | yield return new TestCaseData( 504 | new BasePagedRequest 505 | { 506 | PageIndex = 0, 507 | PageSize = 10, 508 | Where = JObject.Parse("{Address: \"School Address 1\"}"), 509 | PredicateOperator = LinqKit.PredicateOperator.Or, 510 | PagedRequests = new List 511 | { 512 | new PagedRequest 513 | { 514 | Where = JObject.Parse("{ValidOnSingle: \"1916-05-05T00:00:00Z\"}") 515 | } 516 | } 517 | }) 518 | .Returns(2) 519 | .SetName("TestCasesFilter: List of PagedRequests using OR and different properties"); 520 | } 521 | } 522 | 523 | public static IEnumerable AdvancedTestCases 524 | { 525 | get 526 | { 527 | yield return new TestCaseData(new BasePagedRequest 528 | { 529 | PageIndex = 0, 530 | PageSize = 10, 531 | PropertyFilterConfigs = new List 532 | { 533 | new PropertyFilterConfig 534 | { 535 | OperatorComparer = OperatorComparer.Contains, 536 | PropertyName = "Name", 537 | Value = "10" 538 | } 539 | } 540 | }).Returns(2).SetName("Advanced: Filter by PFC Name(Contains 10)"); 541 | 542 | } 543 | } 544 | 545 | [Test] 546 | [TestCaseSource(nameof(TestCases))] 547 | [Parallelizable(ParallelScope.All)] 548 | public async Task MappingTests(BasePagedRequest request) 549 | { 550 | var unitUnderTest = new SchoolService(SchoolServiceTestData.Schools); 551 | var result = await unitUnderTest.GetPagedMapped(request); 552 | 553 | Type type = result.Results.GetType().GetGenericArguments()[0]; 554 | 555 | Assert.IsTrue(type == typeof(SchoolViewModel)); 556 | //Assert.IsTrue(result.Results.TrueForAll(s => s.Name == $"{s.FirstName} {s.LastName}")); 557 | 558 | return result.RowCount; 559 | } 560 | 561 | [Test] 562 | public async Task MappingTests_Empty() 563 | { 564 | var request = new BasePagedRequest { PageIndex = 0, PageSize = 10, SearchQuery = "10" }; 565 | var unitUnderTest = new SchoolService(new List()); 566 | var result = await unitUnderTest.GetPagedMapped(request); 567 | 568 | Type type = result.Results.GetType().GetGenericArguments()[0]; 569 | 570 | Assert.IsTrue(type == typeof(SchoolViewModel)); 571 | Assert.IsTrue(result.Results.Count == 0); 572 | //Assert.IsTrue(result.Results.TrueForAll(s => s.Name == $"{s.FirstName} {s.LastName}")); 573 | } 574 | 575 | [Test] 576 | [TestCaseSource(nameof(TestCasesSearch))] 577 | [Parallelizable(ParallelScope.All)] 578 | public async Task SearchMappingTests(BasePagedRequest request) 579 | { 580 | var unitUnderTest = new SchoolService(SchoolServiceTestData.Schools); 581 | var result = await unitUnderTest.GetPagedSearchQuery(request); 582 | 583 | Type type = result.Results.GetType().GetGenericArguments()[0]; 584 | 585 | Assert.IsTrue(type == typeof(SchoolViewModel)); 586 | //Assert.IsTrue(result.Results.TrueForAll(s => s.Name == $"{s.FirstName} {s.LastName}")); 587 | 588 | return result.RowCount; 589 | } 590 | 591 | [Test] 592 | [TestCaseSource(nameof(TestCasesSort))] 593 | [Parallelizable(ParallelScope.All)] 594 | public async Task MappingTestsSort(BasePagedRequest request) 595 | { 596 | var unitUnderTest = new SchoolService(SchoolServiceTestData.Schools); 597 | var result = await unitUnderTest.GetPagedMapped(request); 598 | 599 | Type type = result.Results.GetType().GetGenericArguments()[0]; 600 | 601 | Assert.IsTrue(type == typeof(SchoolViewModel)); 602 | if (request.PropertyFilterConfigs.Any(pfc => pfc.SortDirection == SortDirection.DESC)) 603 | { 604 | Assert.IsTrue(result.Results.First().Name.Contains("99")); 605 | } 606 | else 607 | { 608 | Assert.IsTrue(result.Results.First().Name.Contains("1")); 609 | } 610 | 611 | return result.RowCount; 612 | } 613 | 614 | [Test] 615 | [TestCaseSource(nameof(TestCasesFilter))] 616 | [Parallelizable(ParallelScope.All)] 617 | public async Task MappingTestsFilter(BasePagedRequest request) 618 | { 619 | var unitUnderTest = new SchoolService(SchoolServiceTestData.Schools); 620 | var result = await unitUnderTest.GetPaged(request); 621 | 622 | return result.RowCount; 623 | } 624 | 625 | [Test] 626 | [TestCaseSource(nameof(AdvancedTestCases))] 627 | [Parallelizable(ParallelScope.All)] 628 | public async Task MappingTestsAdvanced(BasePagedRequest request) 629 | { 630 | var unitUnderTest = new SchoolService(SchoolServiceTestData.Schools); 631 | var result = await unitUnderTest.GetPaged(request); 632 | 633 | return result.RowCount; 634 | } 635 | 636 | [Test] 637 | public async Task MappingTestsStringSearchInherited() 638 | { 639 | var request = new BasePagedRequest { PageIndex = 0, PageSize = 10, SearchQuery = "10" }; 640 | var unitUnderTest = new SchoolService(SchoolServiceTestData.Schools); 641 | var result = await unitUnderTest.GetCollegePagedSearchQuery(request); 642 | 643 | Type type = result.Results.GetType().GetGenericArguments()[0]; 644 | 645 | Assert.IsTrue(type == typeof(CollegeViewModel)); 646 | Assert.IsTrue(result.Results.Count == 4); 647 | } 648 | } 649 | } 650 | -------------------------------------------------------------------------------- /TestProject/Tests/StudentServiceTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json.Linq; 5 | using NUnit.Framework; 6 | using ReFilter.Models; 7 | using TestProject.Models; 8 | using TestProject.TestData; 9 | using TestProject.TestServices; 10 | 11 | namespace TestProject.Tests 12 | { 13 | [TestFixture] 14 | public class StudentServiceTest 15 | { 16 | public static IEnumerable TestCasesNoMapping 17 | { 18 | get 19 | { 20 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10 }).Returns(StudentServiceTestData.Students.Count).SetName("No Filters"); 21 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, Where = JObject.Parse("{FirstName: 10}") }).Returns(0).SetName("Syntax correct but default values included so expect 0"); 22 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, Where = JObject.Parse("{Id: 10, Gender: 1}") }).Returns(1).SetName("Override default value so filter can work correctly"); 23 | } 24 | } 25 | 26 | public static IEnumerable TestCases 27 | { 28 | get 29 | { 30 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10 }).Returns(StudentServiceTestData.Students.Count).SetName("Mapped: No Filters"); 31 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, Where = JObject.Parse("{FirstName: 10}") }).Returns(0).SetName("Mapped: Syntax correct but default values included so expect 0"); 32 | yield return new TestCaseData(new BasePagedRequest { PageIndex = 0, PageSize = 10, Where = JObject.Parse("{Id: 10, Gender: 1}") }).Returns(1).SetName("Mapped: Override default value so filter can work correctly"); 33 | } 34 | } 35 | 36 | [Test] 37 | [TestCaseSource(nameof(TestCasesNoMapping))] 38 | [Parallelizable(ParallelScope.All)] 39 | public async Task NoMappingTest(BasePagedRequest request) 40 | { 41 | var unitUnderTest = new StudentService(StudentServiceTestData.Students); 42 | var result = await unitUnderTest.GetPaged(request); 43 | 44 | return result.RowCount; 45 | } 46 | 47 | [Test] 48 | [TestCaseSource(nameof(TestCases))] 49 | [Parallelizable(ParallelScope.All)] 50 | public async Task MappingTests(BasePagedRequest request) 51 | { 52 | var unitUnderTest = new StudentService(StudentServiceTestData.Students); 53 | var result = await unitUnderTest.GetPagedMapped(request); 54 | 55 | Type type = result.Results.GetType().GetGenericArguments()[0]; 56 | 57 | Assert.IsTrue(type == typeof(StudentViewModel)); 58 | Assert.IsTrue(result.Results.TrueForAll(s => s.FullName == $"{s.FirstName} {s.LastName}")); 59 | 60 | return result.RowCount; 61 | } 62 | 63 | [Test] 64 | [TestCaseSource(nameof(TestCases))] 65 | [Parallelizable(ParallelScope.All)] 66 | public async Task MappingTestsProjection(BasePagedRequest request) 67 | { 68 | var unitUnderTest = new StudentService(StudentServiceTestData.Students); 69 | var result = await unitUnderTest.GetPagedMappedProjection(request); 70 | 71 | Type type = result.Results.GetType().GetGenericArguments()[0]; 72 | 73 | Assert.IsTrue(type == typeof(StudentViewModel)); 74 | Assert.IsTrue(result.Results.TrueForAll(s => s.FullName == $"{s.FirstName} {s.LastName}")); 75 | 76 | return result.RowCount; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/Enums/Gender.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Enums 2 | { 3 | enum Gender 4 | { 5 | Male, 6 | Female, 7 | Undisclosed 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/Mappers/StudentMapper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Tests.Models; 3 | 4 | namespace Tests.Mappers 5 | { 6 | /// 7 | /// Just a showcase of a mapping funcion 8 | /// Can be anything you want 9 | /// Personally I use AutoMapper 10 | /// 11 | static class StudentMapper 12 | { 13 | static StudentViewModel MapToViewModel(Student student) 14 | { 15 | return new StudentViewModel 16 | { 17 | Id = student.Id, 18 | Age = student.Age, 19 | FirstName = student.FirstName, 20 | LastName = student.LastName, 21 | FullName = $"{student.FirstName} {student.LastName}", 22 | Gender = student.Gender 23 | }; 24 | } 25 | 26 | public static List MapListToViewModel(List students) 27 | { 28 | var studentViewModelList = new List(); 29 | 30 | students.ForEach(student => 31 | { 32 | var mappedStudent = MapToViewModel(student); 33 | studentViewModelList.Add(mappedStudent); 34 | }); 35 | 36 | return studentViewModelList; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/Models/Student.cs: -------------------------------------------------------------------------------- 1 | using Tests.Enums; 2 | 3 | namespace Tests.Models 4 | { 5 | class Student 6 | { 7 | public int Id { get; set; } 8 | public string FirstName { get; set; } 9 | public string LastName { get; set; } 10 | public int? Age { get; set; } 11 | public Gender Gender { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/Models/StudentViewModel.cs: -------------------------------------------------------------------------------- 1 | using Tests.Enums; 2 | 3 | namespace Tests.Models 4 | { 5 | class StudentViewModel 6 | { 7 | public int Id { get; set; } 8 | public string FirstName { get; set; } 9 | public string LastName { get; set; } 10 | public int? Age { get; set; } 11 | public Gender Gender { get; set; } 12 | 13 | public string FullName { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/RequiredImplementations/ReFilterConfigBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ReFilter.ReFilterProvider; 3 | using ReFilter.ReFilterTypeMatcher; 4 | 5 | namespace Tests.RequiredImplementations 6 | { 7 | /// 8 | /// This one is required because it's used in Startup 9 | /// Minimum implementation is shown 10 | /// 11 | class ReFilterConfigBuilder : IReFilterConfigBuilder 12 | { 13 | public Type GetMatchingType() where T : class, new() 14 | { 15 | switch (typeof(T)) 16 | { 17 | default: 18 | return typeof(T); 19 | } 20 | } 21 | 22 | public IReFilterBuilder GetMatchingFilterBuilder() where T : class, new() 23 | { 24 | switch (typeof(T)) 25 | { 26 | default: 27 | return null; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/TestData/StudentServiceTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using ReFilter.Models; 4 | using Tests.Models; 5 | 6 | namespace Tests.TestData 7 | { 8 | public class StudentServiceTestData : IEnumerable 9 | { 10 | static List Students = new List 11 | { 12 | new Student 13 | { 14 | Id = 1, 15 | Age = 2, 16 | FirstName = "First", 17 | LastName = "First LastName", 18 | Gender = Enums.Gender.Undisclosed 19 | }, 20 | new Student 21 | { 22 | Id = 2, 23 | Age = 34, 24 | FirstName = "Second", 25 | LastName = "Second LastName", 26 | Gender = Enums.Gender.Male 27 | }, 28 | new Student 29 | { 30 | Id = 3, 31 | Age = 42, 32 | FirstName = "Third", 33 | LastName = "Third LastName", 34 | Gender = Enums.Gender.Female 35 | } 36 | }; 37 | 38 | public IEnumerator GetEnumerator() 39 | { 40 | yield return new object[] 41 | { 42 | new BasePagedRequest 43 | { 44 | PageIndex = 0, 45 | PageSize = 2 46 | }, 47 | Students.Count 48 | }; 49 | } 50 | 51 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/TestServices/StudentService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using ReFilter.Models; 6 | using ReFilter.ReFilterActions; 7 | using ReFilter.ReFilterTypeMatcher; 8 | using Tests.Mappers; 9 | using Tests.Models; 10 | using Tests.RequiredImplementations; 11 | 12 | namespace Tests.TestServices 13 | { 14 | /// 15 | /// A simple test class showcasing 2 main principles 16 | /// 17 | class StudentService 18 | { 19 | #region Ctors and Members 20 | 21 | private readonly List testList; 22 | private readonly ReFilterActions testReFilterActions; 23 | 24 | /// 25 | /// Usually you will use DI for initialization 26 | /// 27 | /// 28 | public StudentService(List testList) 29 | { 30 | this.testList = testList; 31 | 32 | testReFilterActions = InitializeTestFilterActions(new ReFilterConfigBuilder()); 33 | } 34 | 35 | /// 36 | /// Just a helper instead of DI 37 | /// 38 | /// 39 | /// 40 | private ReFilterActions InitializeTestFilterActions(IReFilterConfigBuilder reFilterConfigBuilder) 41 | { 42 | return new ReFilterActions(reFilterConfigBuilder); 43 | } 44 | 45 | #endregion Ctors and Members 46 | 47 | public async Task> GetPaged(BasePagedRequest request) 48 | { 49 | var testQueryable = testList.AsQueryable(); 50 | 51 | var pagedRequest = request.GetPagedRequest(returnResultsOnly: true); 52 | 53 | var result = await testReFilterActions.GetPaged(testQueryable, pagedRequest); 54 | 55 | return result; 56 | } 57 | 58 | public async Task> GetPagedMapped(BasePagedRequest request) 59 | { 60 | var testQueryable = testList.AsQueryable(); 61 | 62 | List mappingFunction(List x) => StudentMapper.MapListToViewModel(x); 63 | var pagedRequest = request.GetPagedRequest((Func, List>)mappingFunction); 64 | 65 | var result = await testReFilterActions.GetPaged(testQueryable, pagedRequest); 66 | 67 | return result; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Tests/Tests/StudentServiceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using ReFilter.Models; 3 | using Tests.TestData; 4 | using Xunit; 5 | 6 | namespace Tests.Tests 7 | { 8 | public class StudentServiceTests 9 | { 10 | [Fact] 11 | public void PassingTest() 12 | { 13 | Assert.True(true); 14 | } 15 | 16 | [Theory] 17 | [ClassData(typeof(StudentServiceTestData))] 18 | public async Task FilterData(BasePagedRequest request, int count) 19 | { 20 | 21 | } 22 | } 23 | } 24 | --------------------------------------------------------------------------------