├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets └── logo.png ├── scripts └── New-CoverageReport.ps1 └── src ├── QueryR.Examples.ConsoleApp ├── MenuSystem │ ├── IMenuItem.cs │ ├── IPrompt.cs │ ├── IntPrompt.cs │ ├── Menu.cs │ ├── MenuItem.cs │ ├── MultiPrompt.cs │ ├── PromptBase.cs │ └── TextPrompt.cs ├── Program.cs └── QueryR.Examples.ConsoleApp.csproj ├── QueryR.Examples.Data ├── Craft.cs ├── CraftNames.cs ├── Instances.cs ├── Kerbal.cs ├── KerbalNames.cs ├── PlanetaryBody.cs ├── PlanetaryBodyNames.cs ├── QueryR.Examples.Data.csproj └── SnackNames.cs ├── QueryR.Tests ├── JoinQueryTests.cs ├── QueryActions │ ├── FilterQueryActionTests.cs │ ├── PagingQueryActionTests.cs │ ├── SortQueryActionTests.cs │ └── SparseFieldsQueryActionTests.cs ├── QueryModels │ └── FilterOperatorsTests │ │ ├── CollectionContainsTests.cs │ │ ├── ContainsTests.cs │ │ ├── EndsWithTests.cs │ │ ├── EqualTests.cs │ │ ├── GreaterThanOrEqualTests.cs │ │ ├── GreaterThanTests.cs │ │ ├── InTests.cs │ │ ├── LessThanOrEqualTests.cs │ │ ├── LessThanTests.cs │ │ ├── NotEqualTests.cs │ │ └── StartsWithTests.cs ├── QueryR.Tests.csproj ├── Services │ └── NullMaxDepthServiceTests.cs └── TestHelpers │ ├── AutoSubDataAttribute.cs │ ├── ListExtensions.cs │ ├── MemberAutoDataAttribute.cs │ └── TestData.cs ├── QueryR.sln └── QueryR ├── Extensions ├── StringExtensions.cs ├── TruthTableExtensions.cs └── TypeExtensions.cs ├── IQueryableExtensions.cs ├── QueryActions ├── FilterQueryAction.cs ├── IQueryAction.cs ├── PagingQueryAction.cs ├── SortQueryAction.cs └── SparseFieldsQueryAction.cs ├── QueryModels ├── Filter.cs ├── FilterOperator.cs ├── FilterOperators.cs ├── IQueryPart.cs ├── PagingOptions.cs ├── Query.cs ├── QueryResult.cs ├── Sort.cs └── SparseField.cs ├── QueryR.csproj ├── QueryResultExtensions.cs └── Services ├── IMaxDepthService.cs └── NullMaxDepthService.cs /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v3 21 | with: 22 | dotnet-version: 9.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore ./src 25 | - name: Build 26 | run: dotnet build ./src --no-restore --configuration Release 27 | - name: Test 28 | run: dotnet test ./src/QueryR.Tests --verbosity quiet --nologo --configuration Release --collect:"XPlat Code Coverage" --results-directory coverage 29 | - name: Copy Coverage to Predictable Location 30 | run: cp coverage/*/coverage.cobertura.xml coverage/coverage.cobertura.xml 31 | - name: Setup .NET Core # Required to execute ReportGenerator 32 | uses: actions/setup-dotnet@v3 33 | with: 34 | dotnet-version: 9.x 35 | dotnet-quality: 'ga' 36 | - name: ReportGenerator 37 | uses: danielpalme/ReportGenerator-GitHub-Action@5.1.13 38 | with: 39 | reports: coverage/coverage.cobertura.xml 40 | targetdir: coveragereport 41 | reporttypes: MarkdownSummaryGithub 42 | - name: Adding markdown summary 43 | run: cat coveragereport/SummaryGithub.md > $GITHUB_STEP_SUMMARY 44 | -------------------------------------------------------------------------------- /.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 | /CoverageReport 352 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Craig McCauley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QueryR 2 | 3 | ![QueryR Logo](./assets/logo.png) 4 | 5 | [![.NET](https://github.com/craigmccauley/QueryR/actions/workflows/dotnet.yml/badge.svg)](https://github.com/craigmccauley/QueryR/actions/workflows/dotnet.yml) 6 | 7 | QueryR provides a simple interface for executing ad hoc queries against `IQueryable` implementations. 8 | 9 | This is useful in situations where there is a need to provide end users with the ability to create custom queries without increasing the complexity of the solution. 10 | 11 | In practice you will have your own domain query criteria object that collects what you want to query. You will perform a map to the `QueryR.Query` object and send it to the `IQueryable.Query` method. 12 | 13 | If you intend to use QueryR with EntityFrameworkCore, please use [QueryR.EntityFrameworkCore](https://github.com/craigmccauley/QueryR.EntityFrameworkCore). 14 | 15 | ## Basic Functionality Example 16 | 17 | ```CSharp 18 | var kerbals = new List 19 | { 20 | new Kerbal { FirstName = "Bob", LastName = "Kerman" }, 21 | new Kerbal { FirstName = "Bill", LastName = "Kerman" }, 22 | new Kerbal { FirstName = "Jeb", LastName = "Kerman" }, 23 | new Kerbal { FirstName = "Val", LastName = "Kerman" }, 24 | }; 25 | 26 | //Note: 27 | var queryResult = kerbals.AsQueryable().Query(new Filter 28 | { 29 | PropertyName = nameof(Kerbal.FirstName), 30 | Operator = FilterOperators.StartsWith, 31 | Value = "B" 32 | }).ToList(); 33 | 34 | Console.WriteLine($"Filter matched {queryResult.Count} Kerbal(s). They are:"); 35 | foreach(var item in queryResult) 36 | { 37 | Console.WriteLine($" - {item.FirstName}"); 38 | } 39 | 40 | // Expected Output : 41 | // Filter matched 2 Kerbal(s). They are: 42 | // - Bob 43 | // - Bill 44 | 45 | ``` 46 | 47 | ## QueryR Full Functionality Example 48 | 49 | QueryR can perform the following `IQueryAction`s. 50 | 51 | - Filter - Reduce the amount of records returned by specifying conditions. Does not support "OR", if you need "OR", run a seond query. 52 | - Paging 53 | - Sort 54 | - Sparse Fieldsets - Restrict the fields returned for a specified entity. 55 | 56 | ```CSharp 57 | var (Count, Items) = kerbals.AsQueryable().Query(new Query 58 | { 59 | Filters = new List 60 | { 61 | new Filter 62 | { 63 | PropertyName = nameof(Kerbal.FirstName), 64 | Operator = FilterOperators.Contains, 65 | Value = "l" 66 | }, 67 | }, 68 | PagingOptions = new PagingOptions 69 | { 70 | PageNumber = 2, 71 | PageSize = 1 72 | }, 73 | Sorts = new List 74 | { 75 | new Sort 76 | { 77 | IsAscending = false, 78 | PropertyName = nameof(Kerbal.FirstName) 79 | }, 80 | }, 81 | SparseFields = new List 82 | { 83 | new SparseField 84 | { 85 | EntityName = nameof(Kerbal), 86 | PropertyNames = new List { nameof(Kerbal.FirstName) } 87 | }, 88 | } 89 | }).GetCountAndList(); 90 | 91 | Console.WriteLine($"{Count} Kerbal(s) match filter, {Items.Count} Kerbal(s) returned:"); 92 | foreach(var item in queryResult) 93 | { 94 | Console.WriteLine($" - {item.FirstName} {item.LastName} was found."); 95 | } 96 | 97 | // Expected output: 98 | // 2 Kerbal(s) match filter, 1 Kerbal(s) returned: 99 | // - Bill was found. 100 | ``` 101 | 102 | ## QueryR Filters 103 | 104 | QueryR provides the following filters: 105 | 106 | - Equal 107 | - GreaterThan 108 | - GreaterThanOrEqual 109 | - LessThan 110 | - LessThanOrEqual 111 | - NotEqual 112 | - Contains 113 | - In 114 | - StartsWith 115 | - EndsWith 116 | - CollectionContains 117 | 118 | Extra filters can be defined and used as if they were a part of QueryR. 119 | For example, if a string length filter was needed. 120 | 121 | We could do 122 | 123 | ```CSharp 124 | public static class ExtendedFilterOperators 125 | { 126 | public static readonly FilterOperator LengthLessThan = new FilterOperator("llt", nameof(LengthLessThan), 127 | (property, target) => Expression.LessThan(Expression.Property(property, nameof(string.Length)), target)); 128 | } 129 | ``` 130 | 131 | and use it like so 132 | 133 | ```CSharp 134 | var queryResult = kerbals.AsQueryable().Query(new Filter 135 | { 136 | PropertyName = nameof(Kerbal.FirstName), 137 | Operator = ExtendedFilterOperators.LengthLessThan, 138 | Value = 4 139 | }).ToList(); 140 | 141 | Console.WriteLine($"Filter matched {queryResult.Count} Kerbal(s). They are:"); 142 | foreach(var item in queryResult) 143 | { 144 | Console.WriteLine($" - {item.FirstName}"); 145 | } 146 | 147 | // Expected Output : 148 | // Filter matched 3 Kerbal(s). They are: 149 | // - Bob 150 | // - Jeb 151 | // - Val 152 | ``` 153 | 154 | ## Working Example 155 | 156 | For a working example of QueryR in action, check out the [ConsoleApp](https://github.com/craigmccauley/QueryR/tree/main/src/QueryR.Examples.ConsoleApp) example. 157 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmccauley/QueryR/a12a47925a00e7d52c4620674b57e1a6d0198179/assets/logo.png -------------------------------------------------------------------------------- /scripts/New-CoverageReport.ps1: -------------------------------------------------------------------------------- 1 | $tools = dotnet tool list -g 2 | $isReportGeneratorInstalled = $tools -like '*dotnet-reportgenerator-globaltool*' 3 | if ($isReportGeneratorInstalled) { 4 | dotnet tool update dotnet-reportgenerator-globaltool -g 5 | } else { 6 | dotnet tool install dotnet-reportgenerator-globaltool -g 7 | } 8 | 9 | $currentDir = Get-Location 10 | $rootDir = "$PSScriptRoot\.." 11 | $slnDir = "$rootDir\src" 12 | $reportDir = "$rootDir\CoverageReport" 13 | $testResultDirs = "$rootDir\*\TestResults" 14 | 15 | 16 | if (Test-Path $reportDir) { 17 | Remove-Item $reportDir -Recurse 18 | } 19 | 20 | Get-ChildItem $slnDir -Recurse -File -Filter *.Tests.csproj | ForEach-Object { 21 | $csprojPath = $_.FullName 22 | Invoke-Expression "dotnet test $csprojPath --collect:""XPlat Code Coverage"" /p:CoverletOutputFormat=cobertura" | Out-Null 23 | } 24 | 25 | $reportList = (Get-ChildItem $slnDir -Recurse -File -Filter *coverage.cobertura.xml).FullName -join ";" 26 | Invoke-Expression "ReportGenerator -reporttypes:Html -classfilters: -assemblyfilters: ""-targetdir:$reportDir"" ""-reports:$reportList""" 27 | 28 | 29 | & $reportDir\index.html 30 | 31 | Set-Location $currentDir 32 | -------------------------------------------------------------------------------- /src/QueryR.Examples.ConsoleApp/MenuSystem/IMenuItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace QueryR.Examples.ConsoleApp.MenuSystem 8 | { 9 | public interface IMenuItem 10 | { 11 | string? Description { get; } 12 | string? Response { get; } 13 | Func Run { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/QueryR.Examples.ConsoleApp/MenuSystem/IPrompt.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace QueryR.Examples.ConsoleApp.MenuSystem 8 | { 9 | public interface IPrompt : IMenuItem 10 | { 11 | bool IsValid(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/QueryR.Examples.ConsoleApp/MenuSystem/IntPrompt.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.Examples.ConsoleApp.MenuSystem 2 | { 3 | internal class IntPrompt : PromptBase 4 | { 5 | public override int GetValue() => int.Parse(Response); 6 | public override bool IsValid() => int.TryParse(Response, out _); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/QueryR.Examples.ConsoleApp/MenuSystem/Menu.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.Examples.ConsoleApp.MenuSystem 2 | { 3 | public class Menu : IMenuItem 4 | { 5 | private string title = string.Empty; 6 | public required string Title 7 | { 8 | get => GetTitle?.Invoke() ?? title; 9 | set => title = value; 10 | } 11 | public Func? GetTitle { get; set; } 12 | public string? Description { get; set; } 13 | public string? Response { get; set; } 14 | 15 | public Func> GetItems { get; set; } = () => new List(); 16 | 17 | private bool isRunning = false; 18 | public Func Run => ShowMenu; 19 | 20 | 21 | private bool ShowMenu() 22 | { 23 | isRunning = true; 24 | Console.Clear(); 25 | while (isRunning) 26 | { 27 | Console.WriteLine(Title); 28 | Console.WriteLine($"Choose one of the following options"); 29 | var items = GetItems().ToList(); 30 | for (int i = 0; i < items.Count; i++) 31 | { 32 | Console.WriteLine($"{i + 1} - {items[i].Description}"); 33 | } 34 | Response = Console.ReadLine(); 35 | 36 | if (int.TryParse(Response, out var index) 37 | && index > 0 38 | && index <= items.Count) 39 | { 40 | if (!items[index - 1].Run()) 41 | { 42 | return true; 43 | } 44 | } 45 | else 46 | { 47 | Console.Clear(); 48 | Console.WriteLine("Invalid Entry"); 49 | } 50 | } 51 | return true; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/QueryR.Examples.ConsoleApp/MenuSystem/MenuItem.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.Examples.ConsoleApp.MenuSystem 2 | { 3 | public class MenuItem : IMenuItem 4 | { 5 | public required string Description { get; set; } 6 | public required virtual Func Run { get; set; } 7 | public string? Response { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/QueryR.Examples.ConsoleApp/MenuSystem/MultiPrompt.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.Examples.ConsoleApp.MenuSystem 2 | { 3 | public class MultiPrompt : IMenuItem 4 | { 5 | public required string Description { get; set; } 6 | public string? Response => null; 7 | public List Prompts { get; set; } = new(); 8 | public Func Run => ShowPrompts; 9 | 10 | public required Action> OnSuccess { get; set; } 11 | 12 | public bool ShowPrompts() 13 | { 14 | var isNext = false; 15 | var index = 0; 16 | while (index < Prompts.Count) 17 | { 18 | if(index < 0) 19 | { 20 | return true; 21 | } 22 | if (Prompts[index] is IPrompt p) 23 | { 24 | p.Run(); 25 | isNext = p.IsValid(); 26 | } 27 | else 28 | { 29 | isNext = Prompts[index].Run(); 30 | } 31 | 32 | if (isNext) 33 | { 34 | index++; 35 | } 36 | else 37 | { 38 | index--; 39 | } 40 | } 41 | 42 | OnSuccess?.Invoke(Prompts); 43 | return true; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/QueryR.Examples.ConsoleApp/MenuSystem/PromptBase.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.Examples.ConsoleApp.MenuSystem 2 | { 3 | public abstract class PromptBase: IPrompt 4 | { 5 | public required string Description { get; set; } 6 | public required string PromptText { get; set; } 7 | public string? Response { get; set; } 8 | public required Action OnSuccess { get; set; } 9 | public Func Run => () => 10 | { 11 | Console.Clear(); 12 | Console.WriteLine(PromptText); 13 | Response = Console.ReadLine(); 14 | if (IsValid()) 15 | { 16 | OnSuccess?.Invoke(GetValue()); 17 | } 18 | Console.Clear(); 19 | return true; 20 | }; 21 | 22 | public abstract bool IsValid(); 23 | public abstract T GetValue(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QueryR.Examples.ConsoleApp/MenuSystem/TextPrompt.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.Examples.ConsoleApp.MenuSystem 2 | { 3 | public class TextPrompt : PromptBase 4 | { 5 | public override string GetValue() => Response!; 6 | public override bool IsValid() => !string.IsNullOrWhiteSpace(Response); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/QueryR.Examples.ConsoleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using QueryR; 2 | using QueryR.Examples.ConsoleApp.MenuSystem; 3 | using QueryR.Examples.Data; 4 | using QueryR.QueryModels; 5 | 6 | public class Program 7 | { 8 | public static Query KerbalQuery = new(); 9 | public static Query CraftQuery = new(); 10 | 11 | public static void Main(string[] args) 12 | { 13 | var fields = new List 14 | { 15 | nameof(Kerbal.Id), 16 | nameof(Kerbal.FirstName), 17 | nameof(Kerbal.LastName), 18 | nameof(Kerbal.AssignedSpaceCraftId), 19 | $"{nameof(Kerbal.PlanetaryBodiesVisited)}.{nameof(PlanetaryBody.Id)}", 20 | $"{nameof(Kerbal.PlanetaryBodiesVisited)}.{nameof(PlanetaryBody.Name)}", 21 | $"{nameof(Kerbal.SnacksOnHand)}.{nameof(KeyValuePair.Key)}", 22 | $"{nameof(Kerbal.SnacksOnHand)}.{nameof(KeyValuePair.Value)}", 23 | $"{nameof(Kerbal.AssignedSpaceCraft)}.{nameof(Craft.Id)}", 24 | $"{nameof(Kerbal.AssignedSpaceCraft)}.{nameof(Craft.CraftName)}", 25 | $"{nameof(Kerbal.AssignedSpaceCraft)}.{nameof(Craft.AssignedKerbals)}", 26 | $"{nameof(Kerbal.AssignedSpaceCraft)}.{nameof(Craft.AssignedKerbals)}.{nameof(Kerbal.Id)}", 27 | $"{nameof(Kerbal.AssignedSpaceCraft)}.{nameof(Craft.AssignedKerbals)}.{nameof(Kerbal.FirstName)}", 28 | $"{nameof(Kerbal.AssignedSpaceCraft)}.{nameof(Craft.AssignedKerbals)}.{nameof(Kerbal.LastName)}", 29 | $"{nameof(Kerbal.AssignedSpaceCraft)}.{nameof(Craft.AssignedKerbals)}.{nameof(Kerbal.PlanetaryBodiesVisited)}.{nameof(PlanetaryBody.Id)}", 30 | $"{nameof(Kerbal.AssignedSpaceCraft)}.{nameof(Craft.AssignedKerbals)}.{nameof(Kerbal.PlanetaryBodiesVisited)}.{nameof(PlanetaryBody.Name)}", 31 | $"{nameof(Kerbal.AssignedSpaceCraft)}.{nameof(Craft.AssignedKerbals)}.{nameof(Kerbal.SnacksOnHand)}.{nameof(KeyValuePair.Key)}", 32 | $"{nameof(Kerbal.AssignedSpaceCraft)}.{nameof(Craft.AssignedKerbals)}.{nameof(Kerbal.SnacksOnHand)}.{nameof(KeyValuePair.Value)}", 33 | 34 | }; 35 | 36 | var menu = new Menu 37 | { 38 | Title = "Welcome to the Kerbal Querying Program", 39 | GetItems = () => new IMenuItem[] 40 | { 41 | new Menu 42 | { 43 | Description = "Query Kerbals", 44 | Title = "Kerbal Menu", 45 | GetItems = () => new IMenuItem[] 46 | { 47 | new MenuItem 48 | { 49 | Description = "Execute Query", 50 | Run = () => 51 | { 52 | Console.Clear(); 53 | Console.WriteLine("Listing Kerbals"); 54 | var queryResult = Instances.Kerbals.AsQueryable().Query(KerbalQuery); 55 | var (Count, Items) = queryResult.GetCountAndList(); 56 | Console.WriteLine(string.Join(Environment.NewLine, Items)); 57 | Console.WriteLine($"Total Count : {Count}"); 58 | Console.WriteLine("Press any key to continue..."); 59 | Console.ReadKey(); 60 | Console.Clear(); 61 | return true; 62 | } 63 | }, 64 | new Menu 65 | { 66 | Description = "Manage Filters", 67 | Title = "Kerbal Query Filters", 68 | GetItems = () => new IMenuItem[] 69 | { 70 | new MenuItem 71 | { 72 | Description = "List Current Filters", 73 | Run = () => 74 | { 75 | Console.Clear(); 76 | Console.WriteLine("Kerbal Query Filters"); 77 | Console.WriteLine(string.Join(Environment.NewLine, KerbalQuery.Filters.Select(f=> $"{f.PropertyName} -{f.Operator.Code} \"{f.Value}\""))); 78 | Console.WriteLine("Press any key to continue..."); 79 | Console.ReadKey(); 80 | Console.Clear(); 81 | return true; 82 | } 83 | }, 84 | new MultiPrompt 85 | { 86 | Description = "Add Query Filter", 87 | Prompts = 88 | { 89 | new Menu 90 | { 91 | Title = "Choose Field (Normally you can specify whatever you want as long as it works with the operator, this is a sample of the fields for demo purposes.)", 92 | GetItems = () => new List(fields 93 | .Select(field=> new MenuItem 94 | { 95 | Description = field, 96 | Run = () => false 97 | })) 98 | }, 99 | new Menu 100 | { 101 | Title = "Choose Operation", 102 | GetItems = () => new List(FilterOperators.Items 103 | .Select(fo=> new MenuItem 104 | { 105 | Description = fo.Name, 106 | Run = () => false 107 | })) 108 | }, 109 | new TextPrompt 110 | { 111 | PromptText = "Enter Filter Value", 112 | Description = string.Empty, 113 | OnSuccess = _ => { } 114 | } 115 | }, 116 | OnSuccess = prompts => 117 | { 118 | var propertyName = fields.ElementAt(int.Parse(prompts[0].Response) - 1); 119 | var filterOperator = FilterOperators.Items.ElementAt(int.Parse(prompts[1].Response) - 1); 120 | KerbalQuery.Filters.Add(new Filter 121 | { 122 | PropertyName = propertyName, 123 | Operator = filterOperator, 124 | Value = prompts[2].Response, 125 | }); 126 | } 127 | }, 128 | new Menu 129 | { 130 | Description = "Remove Query Filter", 131 | Title = "Select Query Filter to Remove", 132 | GetItems = () => new List(KerbalQuery.Filters.Select(f => new MenuItem 133 | { 134 | Description = $"{f.PropertyName} -{f.Operator.Code} \"{f.Value}\"", 135 | Run = () => 136 | { 137 | Console.Clear(); 138 | KerbalQuery.Filters.Remove(f); 139 | return false; 140 | } 141 | }).Append(BackMenuItem)) 142 | }, 143 | BackMenuItem 144 | } 145 | }, 146 | new Menu 147 | { 148 | Description = "Manage Paging", 149 | Title = string.Empty, 150 | GetTitle = () => $"Kerbal Query Paging - Current Settings: { (KerbalQuery.PagingOptions == null ? "Unset" : $"Size { KerbalQuery.PagingOptions.PageSize }, Number { KerbalQuery.PagingOptions.PageNumber }") }", 151 | GetItems = () => new IMenuItem[] 152 | { 153 | new IntPrompt 154 | { 155 | Description = "Set Page Size", 156 | PromptText = "Enter Page Size", 157 | OnSuccess = response => 158 | { 159 | KerbalQuery.PagingOptions ??= new PagingOptions(); 160 | KerbalQuery.PagingOptions.PageSize = response; 161 | } 162 | }, 163 | new IntPrompt 164 | { 165 | Description = "Set Page Number", 166 | PromptText = "Enter Page Number", 167 | OnSuccess = response => 168 | { 169 | KerbalQuery.PagingOptions ??= new PagingOptions(); 170 | KerbalQuery.PagingOptions.PageNumber = response; 171 | } 172 | }, 173 | new MenuItem 174 | { 175 | Description = "Clear Paging", 176 | Run = () => 177 | { 178 | KerbalQuery.PagingOptions = null; 179 | Console.Clear(); 180 | return true; 181 | } 182 | }, 183 | BackMenuItem 184 | } 185 | }, 186 | new Menu 187 | { 188 | Description = "Manage Sorts", 189 | Title = "Kerbal Query Sorts", 190 | GetItems = () => new IMenuItem[] 191 | { 192 | new MenuItem 193 | { 194 | Description = "List Current Sorts", 195 | Run = () => 196 | { 197 | Console.Clear(); 198 | Console.WriteLine("Kerbal Query Sorts"); 199 | Console.WriteLine(string.Join(Environment.NewLine, KerbalQuery.Sorts.Select(f=> $"{f.PropertyName} {(f.IsAscending ? "Ascending" : "Descending")}"))); 200 | Console.WriteLine("Press any key to continue..."); 201 | Console.ReadKey(); 202 | Console.Clear(); 203 | return true; 204 | } 205 | }, 206 | new MultiPrompt 207 | { 208 | Description = "Add Sort", 209 | Prompts = 210 | { 211 | new Menu 212 | { 213 | Title = "Choose Field (Normally you can specify whatever you want as long as it works with the operator, this is a sample of the fields for demo purposes.)", 214 | GetItems = () => new List(fields 215 | .Select(field => new MenuItem 216 | { 217 | Description = field, 218 | Run = () => false 219 | })) 220 | }, 221 | new Menu 222 | { 223 | Title = "Choose Order", 224 | GetItems = () => new List(new[] { "Ascending", "Descending" } 225 | .Select(a => new MenuItem 226 | { 227 | Description = a, 228 | Run = () => false 229 | })) 230 | }, 231 | }, 232 | OnSuccess = prompts => 233 | { 234 | var propertyName = typeof(Kerbal).GetProperties().Where(mi => mi.Name != nameof(Kerbal.AssignedSpaceCraft)).ElementAt(int.Parse(prompts[0].Response) - 1).Name; 235 | var isAscending = int.Parse(prompts[1].Response) == 1; 236 | KerbalQuery.Sorts.Add(new Sort 237 | { 238 | PropertyName = propertyName, 239 | IsAscending = isAscending 240 | }); 241 | } 242 | }, 243 | new Menu 244 | { 245 | Description = "Remove Sort", 246 | Title = "Select Sort to Remove", 247 | GetItems = () => new List(KerbalQuery.Sorts.Select(s => new MenuItem 248 | { 249 | Description = $"{s.PropertyName} {(s.IsAscending ? "Ascending" : "Descending")}", 250 | Run = () => 251 | { 252 | KerbalQuery.Sorts.Remove(s); 253 | return false; 254 | } 255 | }).Append(BackMenuItem)) 256 | }, 257 | BackMenuItem 258 | } 259 | }, 260 | new Menu 261 | { 262 | Description = "Manage Sparse Fields", 263 | Title = "Kerbal Query Sparse Fields", 264 | GetItems = () => new IMenuItem[] 265 | { 266 | new MenuItem 267 | { 268 | Description = "List Current Sparse Fields", 269 | Run = () => 270 | { 271 | Console.Clear(); 272 | Console.WriteLine("Kerbal Query Sparse Fields"); 273 | Console.WriteLine(string.Join(Environment.NewLine, KerbalQuery.SparseFields.Select(sf=> $"{sf.EntityName} {string.Join(", ", sf.PropertyNames)}"))); 274 | Console.WriteLine("Press any key to continue..."); 275 | Console.ReadKey(); 276 | Console.Clear(); 277 | return true; 278 | } 279 | }, 280 | new Menu 281 | { 282 | Description = "Add Kerbal Sparse Field", 283 | Title = "Choose Field", 284 | GetItems = () => new List(typeof(Kerbal).GetProperties() 285 | .Where(mi => !KerbalQuery.SparseFields.Any(sf=> sf.EntityName == nameof(Kerbal) && sf.PropertyNames.Contains(mi.Name))) 286 | .Select(mi=> new MenuItem 287 | { 288 | Description = mi.Name, 289 | Run = () => 290 | { 291 | Console.Clear(); 292 | var sparseField = KerbalQuery.SparseFields.FirstOrDefault(sf=> sf.EntityName == nameof(Kerbal)); 293 | if(sparseField == null) 294 | { 295 | sparseField = new SparseField{ EntityName = nameof(Kerbal) }; 296 | KerbalQuery.SparseFields.Add(sparseField); 297 | } 298 | sparseField.PropertyNames.Add(mi.Name); 299 | return true; 300 | } 301 | }).Append(BackMenuItem)), 302 | }, 303 | new Menu 304 | { 305 | Description = "Remove Kerbal Sparse Field", 306 | Title = "Choose Field", 307 | GetItems = () => new List(KerbalQuery.SparseFields.FirstOrDefault(sf=> sf.EntityName == nameof(Kerbal))?.PropertyNames 308 | .Select(pn => new MenuItem 309 | { 310 | Description = pn, 311 | Run = () => 312 | { 313 | Console.Clear(); 314 | var kerbalSparse = KerbalQuery.SparseFields.FirstOrDefault(sf=> sf.EntityName == nameof(Kerbal)); 315 | kerbalSparse?.PropertyNames.Remove(pn); 316 | if(!kerbalSparse.PropertyNames.Any()) 317 | { 318 | KerbalQuery.SparseFields.Remove(kerbalSparse); 319 | } 320 | return true; 321 | } 322 | }).Append(BackMenuItem) ?? Enumerable.Empty().Append(BackMenuItem)), 323 | }, 324 | new Menu 325 | { 326 | Description = "Add Craft Sparse Field", 327 | Title = "Choose Field", 328 | GetItems = () => new List(typeof(Craft).GetProperties() 329 | .Where(mi => !KerbalQuery.SparseFields.Any(sf=> sf.EntityName == nameof(Craft) && sf.PropertyNames.Contains(mi.Name))) 330 | .Select(mi=> new MenuItem 331 | { 332 | Description = mi.Name, 333 | Run = () => 334 | { 335 | Console.Clear(); 336 | var sparseField = KerbalQuery.SparseFields.FirstOrDefault(sf=> sf.EntityName == nameof(Craft)); 337 | if(sparseField == null) 338 | { 339 | sparseField = new SparseField{ EntityName = nameof(Craft) }; 340 | KerbalQuery.SparseFields.Add(sparseField); 341 | } 342 | sparseField.PropertyNames.Add(mi.Name); 343 | return true; 344 | } 345 | }).Append(BackMenuItem)), 346 | }, 347 | new Menu 348 | { 349 | Description = "Remove Craft Sparse Field", 350 | Title = "Choose Field", 351 | GetItems = () => new List(KerbalQuery.SparseFields.FirstOrDefault(sf=> sf.EntityName == nameof(Craft))?.PropertyNames 352 | .Select(pn => new MenuItem 353 | { 354 | Description = pn, 355 | Run = () => 356 | { 357 | Console.Clear(); 358 | var craftSparse = KerbalQuery.SparseFields.FirstOrDefault(sf=> sf.EntityName == nameof(Craft)); 359 | craftSparse?.PropertyNames.Remove(pn); 360 | if(!craftSparse.PropertyNames.Any()) 361 | { 362 | KerbalQuery.SparseFields.Remove(craftSparse); 363 | } 364 | return true; 365 | } 366 | }).Append(BackMenuItem) ?? Enumerable.Empty().Append(BackMenuItem)), 367 | }, 368 | new Menu 369 | { 370 | Description = "Add Planetary Body Sparse Field", 371 | Title = "Choose Field", 372 | GetItems = () => new List(typeof(PlanetaryBody).GetProperties() 373 | .Where(mi => !KerbalQuery.SparseFields.Any(sf=> sf.EntityName == nameof(PlanetaryBody) && sf.PropertyNames.Contains(mi.Name))) 374 | .Select(mi=> new MenuItem 375 | { 376 | Description = mi.Name, 377 | Run = () => 378 | { 379 | Console.Clear(); 380 | var sparseField = KerbalQuery.SparseFields.FirstOrDefault(sf=> sf.EntityName == nameof(PlanetaryBody)); 381 | if(sparseField == null) 382 | { 383 | sparseField = new SparseField{ EntityName = nameof(PlanetaryBody) }; 384 | KerbalQuery.SparseFields.Add(sparseField); 385 | } 386 | sparseField.PropertyNames.Add(mi.Name); 387 | return true; 388 | } 389 | }).Append(BackMenuItem)), 390 | }, 391 | new Menu 392 | { 393 | Description = "Remove Planetary Body Sparse Field", 394 | Title = "Choose Field", 395 | GetItems = () => new List(KerbalQuery.SparseFields.FirstOrDefault(sf=> sf.EntityName == nameof(PlanetaryBody))?.PropertyNames 396 | .Select(pn => new MenuItem 397 | { 398 | Description = pn, 399 | Run = () => 400 | { 401 | Console.Clear(); 402 | var sparseField = KerbalQuery.SparseFields.FirstOrDefault(sf=> sf.EntityName == nameof(PlanetaryBody)); 403 | sparseField?.PropertyNames.Remove(pn); 404 | if(!sparseField.PropertyNames.Any()) 405 | { 406 | KerbalQuery.SparseFields.Remove(sparseField); 407 | } 408 | return true; 409 | } 410 | }).Append(BackMenuItem) ?? Enumerable.Empty().Append(BackMenuItem)), 411 | }, 412 | BackMenuItem 413 | } 414 | }, 415 | BackMenuItem 416 | } 417 | }, 418 | new MenuItem 419 | { 420 | Description = "Quit", 421 | Run = () => false 422 | } 423 | } 424 | }; 425 | 426 | menu.Run(); 427 | } 428 | 429 | 430 | public static MenuItem BackMenuItem => new MenuItem 431 | { 432 | Description = "Back", 433 | Run = () => 434 | { 435 | Console.Clear(); 436 | return false; 437 | } 438 | }; 439 | } 440 | 441 | 442 | -------------------------------------------------------------------------------- /src/QueryR.Examples.ConsoleApp/QueryR.Examples.ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/QueryR.Examples.Data/Craft.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace QueryR.Examples.Data 4 | { 5 | public class Craft 6 | { 7 | public int Id { get; set; } 8 | public string CraftName { get; set; } 9 | public List AssignedKerbals { get; set; } = new List(); 10 | 11 | public override string ToString() => $"{Id} - {CraftName} - Complement: {(AssignedKerbals == null ? "unknown" : AssignedKerbals.Count.ToString())}"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/QueryR.Examples.Data/CraftNames.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace QueryR.Examples.Data 5 | { 6 | internal static class CraftNames 7 | { 8 | public static readonly IReadOnlyList OtherNames = new List 9 | { 10 | "Bebop", 11 | "Discovery One", 12 | "Lewis & Clark", 13 | "Event Horizon", 14 | "USS Enterprise D", 15 | "Stargazer", 16 | "Yamato", 17 | "Artemis", 18 | "USG Ishimura", 19 | "Evangelion Unit-01", 20 | }; 21 | public static readonly IReadOnlyList HeinleinShips = new List 22 | { 23 | "Gay Deceiver", 24 | "Rocinante", 25 | "Little Prince", 26 | "Goliath", 27 | "Jason Smith", 28 | "Lying Bastard", 29 | "Mayflower II", 30 | "New Frontiers", 31 | "Vanguard", 32 | "Winston", 33 | "Penguin", 34 | "Betsy", 35 | "Mothership", 36 | "Envoy", 37 | "Swiftsure", 38 | "Betsy Ross", 39 | "H.M.S. Rodney", 40 | "Bonny Sandy", 41 | "Dora", 42 | "Sisu", 43 | "Queen of Sheba", 44 | "Challenger", 45 | "New Frontiers", 46 | "Lunar Queen", 47 | "Arachne", 48 | "Dolphin", 49 | "Belle of Boskone", 50 | "Lazy Eight III", 51 | }; 52 | public static readonly IReadOnlyList Names = HeinleinShips.Concat(OtherNames).ToList(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/QueryR.Examples.Data/Instances.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace QueryR.Examples.Data 6 | { 7 | public static class Instances 8 | { 9 | private static List kerbals = null; 10 | private static List crafts = null; 11 | private static List planetaryBodies = null; 12 | 13 | public static List Kerbals 14 | { 15 | get 16 | { 17 | Initialize(); 18 | return kerbals; 19 | } 20 | } 21 | 22 | private static Random random = new Random(0); 23 | 24 | private static void Initialize() 25 | { 26 | if (kerbals != null) 27 | { 28 | return; 29 | } 30 | 31 | planetaryBodies = new List(PlanetaryBodyNames.Names.OrderBy(_ => random.Next()).Select((name, index) => new PlanetaryBody 32 | { 33 | Id = index + 1, 34 | Name = name, 35 | })); 36 | 37 | kerbals = new List(KerbalNames.Names.OrderBy(_ => random.Next()).Select((name, index) => new Kerbal 38 | { 39 | Id = index + 1, 40 | FirstName = name, 41 | LastName = "Kerman", 42 | PlanetaryBodiesVisited = planetaryBodies.OrderBy(_ => random.Next()).Take(random.Next(0, 5)).ToList(), 43 | SnacksOnHand = SnackNames.Names.OrderBy(_ => random.Next()).Take(random.Next(0, 5)).ToDictionary(snackName => snackName, _ => random.Next(1, 3)), 44 | })); 45 | 46 | crafts = new List(CraftNames.Names.OrderBy(_ => random.Next()).Select((name, index) => new Craft 47 | { 48 | Id = index + 1, 49 | CraftName = name, 50 | })); 51 | 52 | foreach (var kerbal in kerbals.OrderBy(_ => random.Next()).Take(kerbals.Count * 4 / 5)) 53 | { 54 | var craft = crafts.OrderBy(_ => random.Next()).Take(1).First(); 55 | kerbal.AssignedSpaceCraft = craft; 56 | kerbal.AssignedSpaceCraftId = craft.Id; 57 | craft.AssignedKerbals.Add(kerbal); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/QueryR.Examples.Data/Kerbal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace QueryR.Examples.Data 6 | { 7 | public class Kerbal 8 | { 9 | public int Id { get; set; } 10 | public string FirstName { get; set; } 11 | public string LastName { get; set; } 12 | public int AssignedSpaceCraftId { get; set; } 13 | public Craft AssignedSpaceCraft { get; set; } 14 | 15 | public List PlanetaryBodiesVisited { get; set; } = new List(); 16 | public Dictionary SnacksOnHand { get; set; } = new Dictionary(); 17 | 18 | public override string ToString() => $@"{FirstName} {LastName} 19 | ID: {Id} 20 | Craft ID: {AssignedSpaceCraftId} 21 | Craft Details: {(AssignedSpaceCraft == null ? "unknown" : AssignedSpaceCraft.ToString())} 22 | Visited Planetary Bodies 23 | {(PlanetaryBodiesVisited == null ? "unknown" : !PlanetaryBodiesVisited.Any() ? "none" : string.Join($"{Environment.NewLine}\t", PlanetaryBodiesVisited.Select(pb => pb.Id + ") " + pb.Name)))} 24 | Snacks on Hand 25 | {(SnacksOnHand == null ? "unknown" : !SnacksOnHand.Any() ? "none" : string.Join($"{Environment.NewLine}\t", SnacksOnHand.Select(snack => $"{snack.Value} x {snack.Key}")))} 26 | "; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/QueryR.Examples.Data/KerbalNames.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace QueryR.Examples.Data 5 | { 6 | internal static class KerbalNames 7 | { 8 | public static IReadOnlyList Prefixes = new List 9 | { 10 | "Ad", 11 | "Al", 12 | "Ald", 13 | "An", 14 | "Bar", 15 | "Bart", 16 | "Bil", 17 | "Billy-Bob", 18 | "Bob", 19 | "Bur", 20 | "Cal", 21 | "Cam", 22 | "Chad", 23 | "Cor", 24 | "Dan", 25 | "Der", 26 | "Des", 27 | "Dil", 28 | "Do", 29 | "Don", 30 | "Dood", 31 | "Dud", 32 | "Dun", 33 | "Ed", 34 | "El", 35 | "En", 36 | "Er", 37 | "Fer", 38 | "Fred", 39 | "Gene", 40 | "Geof", 41 | "Ger", 42 | "Gil", 43 | "Greg", 44 | "Gus", 45 | "Had", 46 | "Hal", 47 | "Han", 48 | "Har", 49 | "Hen", 50 | "Her", 51 | "Hud", 52 | "Jed", 53 | "Jen", 54 | "Jer", 55 | "Joe", 56 | "John", 57 | "Jon", 58 | "Jor", 59 | "Kel", 60 | "Ken", 61 | "Ker", 62 | "Kir", 63 | "Lan", 64 | "Lem", 65 | "Len", 66 | "Lo", 67 | "Lod", 68 | "Lu", 69 | "Lud", 70 | "Mac", 71 | "Mal", 72 | "Mat", 73 | "Mel", 74 | "Mer", 75 | "Mil", 76 | "Mit", 77 | "Mun", 78 | "Ned", 79 | "Neil", 80 | "Nel", 81 | "New", 82 | "Ob", 83 | "Or", 84 | "Pat", 85 | "Phil", 86 | "Ray", 87 | "Rib", 88 | "Rich", 89 | "Ro", 90 | "Rod", 91 | "Ron", 92 | "Sam", 93 | "Sean", 94 | "See", 95 | "Shel", 96 | "Shep", 97 | "Sher", 98 | "Sid", 99 | "Sig", 100 | "Son", 101 | "Thom", 102 | "Thomp", 103 | "Tom", 104 | "Wehr", 105 | "Wil", 106 | }; 107 | public static IReadOnlyList Suffixes = new List 108 | { 109 | "ald", 110 | "bal", 111 | "bald", 112 | "bart", 113 | "bas", 114 | "berry", 115 | "bert", 116 | "bin", 117 | "ble", 118 | "bles", 119 | "bo", 120 | "bree", 121 | "brett", 122 | "bro", 123 | "bur", 124 | "burry", 125 | "bus", 126 | "by", 127 | "cal", 128 | "can", 129 | "cas", 130 | "cott", 131 | "dan", 132 | "das", 133 | "den", 134 | "din", 135 | "do", 136 | "don", 137 | "dorf", 138 | "dos", 139 | "dous", 140 | "dred", 141 | "drin", 142 | "dun", 143 | "ely", 144 | "emone", 145 | "emy", 146 | "eny", 147 | "fal", 148 | "fel", 149 | "fen", 150 | "field", 151 | "ford", 152 | "fred", 153 | "frey", 154 | "frey", 155 | "frid", 156 | "frod", 157 | "fry", 158 | "furt", 159 | "gan", 160 | "gard", 161 | "gas", 162 | "gee", 163 | "gel", 164 | "ger", 165 | "gun", 166 | "hat", 167 | "ing", 168 | "ke", 169 | "kin", 170 | "lan", 171 | "las", 172 | "ler", 173 | "ley", 174 | "lie", 175 | "lin", 176 | "lin", 177 | "lo", 178 | "lock", 179 | "long", 180 | "lorf", 181 | "ly", 182 | "mal", 183 | "man", 184 | "min", 185 | "ming", 186 | "mon", 187 | "more", 188 | "mund", 189 | "my", 190 | "nand", 191 | "nard", 192 | "ner", 193 | "ney", 194 | "nie", 195 | "ny", 196 | "oly", 197 | "ory", 198 | "rey", 199 | "rick", 200 | "rie", 201 | "righ", 202 | "rim", 203 | "rod", 204 | "ry", 205 | "sby", 206 | "sel", 207 | "sen", 208 | "sey", 209 | "ski", 210 | "son", 211 | "sted", 212 | "ster", 213 | "sy", 214 | "ton", 215 | "top", 216 | "trey", 217 | "van", 218 | "vey", 219 | "vin", 220 | "vis", 221 | "well", 222 | "wig", 223 | "win", 224 | "wise", 225 | "zer", 226 | "zon", 227 | "zor", 228 | }; 229 | public static IReadOnlyList Names = new[] { "Bob", "Bill", "Jeb", "Val" }.Concat(Prefixes.SelectMany(p => Suffixes.Select(s => p + s))).ToList(); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/QueryR.Examples.Data/PlanetaryBody.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.Examples.Data 2 | { 3 | public class PlanetaryBody 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/QueryR.Examples.Data/PlanetaryBodyNames.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace QueryR.Examples.Data 4 | { 5 | public static class PlanetaryBodyNames 6 | { 7 | public static List Names = new List 8 | { 9 | "Kerbol", 10 | "Moho", 11 | "Eve", 12 | "Gilly", 13 | "Kerbin", 14 | "Mun", 15 | "Minimus", 16 | "Duna", 17 | "Ike", 18 | "Dres", 19 | "Jool", 20 | "Laythe", 21 | "Vall", 22 | "Tylo", 23 | "Bop", 24 | "Pol", 25 | "Eeloo", 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/QueryR.Examples.Data/QueryR.Examples.Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/QueryR.Examples.Data/SnackNames.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace QueryR.Examples.Data 6 | { 7 | public class SnackNames 8 | { 9 | public static List Names = new List 10 | { 11 | "Alien Aisle Snacks", 12 | "Black Hole Brownies", 13 | "Celestial Cereal", 14 | "Celestial Snacks", 15 | "Cosmic Confections", 16 | "Cosmic Crisps", 17 | "Cosmic Cupcakes", 18 | "Galaxy Glaze Donuts", 19 | "Galaxy Gummies", 20 | "Intergalactic Ice Cream", 21 | "Interstellar Treats", 22 | "Jupiter Jellies", 23 | "Lunar Lattes", 24 | "Mars Munchies", 25 | "Meteor Munchies", 26 | "Meteorite Melts", 27 | "Milky Way Muffins", 28 | "Moonbeam Muffins", 29 | "Nebula Nachos", 30 | "Neptune Nuggets", 31 | "Nova Nuts", 32 | "Orion Oreos", 33 | "Outer Space Trail Mix", 34 | "Pluto Pops", 35 | "Rocket Pops", 36 | "Saturn's Snacks", 37 | "Solar Snacks", 38 | "Space Odyssey Snacks", 39 | "Space Station Snacks", 40 | "Starburst Bites", 41 | "Stardust Snack Mix", 42 | "Starlight Snacks", 43 | "Starry Night Snack Mix", 44 | "Starry Sky Snack Mix", 45 | "Starry Snack Bites", 46 | "Starry Starry Bites", 47 | "Supernova Snacks", 48 | "Uranus Uncrustables", 49 | "Venus Vanilla Wafers", 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/QueryR.Tests/JoinQueryTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System.Linq; 5 | using Xunit; 6 | 7 | namespace QueryR.Tests 8 | { 9 | public class JoinQueryTests 10 | { 11 | [Fact] 12 | internal void Query_WhenQuerableIsJoinedObject_ShouldWorkAsExpected() 13 | { 14 | //arrange 15 | var testData = new TestData(); 16 | 17 | var joinedData = 18 | from person in testData.Persons 19 | join pet in testData.Pets 20 | on person equals pet.Owner 21 | select new 22 | { 23 | OwnerName = person.Name, 24 | PetName = pet.Name 25 | }; 26 | 27 | var filter = new Filter 28 | { 29 | PropertyName = "OwnerName", 30 | Operator = FilterOperators.Equal, 31 | Value = "Craig" 32 | }; 33 | 34 | //act 35 | var result = joinedData.AsQueryable().Query(filter); 36 | 37 | //assert 38 | var (Count, Items) = result.GetCountAndList(); 39 | 40 | Count.ShouldBe(3); 41 | foreach(var item in Items) 42 | { 43 | item.OwnerName.ShouldBe("Craig"); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryActions/FilterQueryActionTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryActions; 2 | using QueryR.QueryModels; 3 | using QueryR.Tests.TestHelpers; 4 | using Shouldly; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Xunit; 9 | using static QueryR.Tests.TestHelpers.TestData; 10 | 11 | namespace QueryR.Tests.QueryActions 12 | { 13 | public class FilterQueryActionTests 14 | { 15 | [Theory, AutoSubData] 16 | internal void Filter_ShouldWorkAsExpected( 17 | FilterQueryAction sut) 18 | { 19 | //arrange 20 | var testData = new TestData(); 21 | var queryResult = new QueryResult 22 | { 23 | CountQuery = testData.Persons.AsQueryable(), 24 | PagedQuery = testData.Persons.AsQueryable(), 25 | }; 26 | 27 | var query = new Query 28 | { 29 | Filters = new List 30 | { 31 | new Filter 32 | { 33 | PropertyName = nameof(Person.Name), 34 | Operator = FilterOperators.Equal, 35 | Value = "Craig", 36 | } 37 | } 38 | }; 39 | 40 | 41 | //act 42 | var result = sut.Execute(query, queryResult); 43 | 44 | //assert 45 | var (count, list) = result.GetCountAndList(); 46 | count.ShouldBe(1); 47 | list.First().ShouldBe(testData.Craig); 48 | } 49 | 50 | [Theory, AutoSubData] 51 | internal void Filter_WhenItemIsOnNavigationPropertyPath_ShouldWorkAsExpected( 52 | FilterQueryAction sut) 53 | { 54 | //arrange 55 | var testData = new TestData(); 56 | var queryResult = new QueryResult 57 | { 58 | CountQuery = testData.Persons.AsQueryable(), 59 | PagedQuery = testData.Persons.AsQueryable(), 60 | }; 61 | 62 | var query = new Query 63 | { 64 | Filters = new List 65 | { 66 | new Filter 67 | { 68 | PropertyName = $"{nameof(Person.Pets)}.{nameof(Pet.PetType)}.{nameof(PetType.Name)}", 69 | Operator = FilterOperators.Equal, 70 | Value = "Bird", 71 | } 72 | } 73 | }; 74 | 75 | 76 | //act 77 | var result = sut.Execute(query, queryResult); 78 | 79 | //assert 80 | var (count, list) = result.GetCountAndList(); 81 | count.ShouldBe(1); 82 | list.First().ShouldBe(testData.Craig); 83 | } 84 | 85 | [Theory, AutoSubData] 86 | internal void Filter_WhenNavigationPropertyPathIsCollectionChild_ShouldWorkAsExpected( 87 | FilterQueryAction sut) 88 | { 89 | //arrange 90 | var testData = new TestData(); 91 | var queryResult = new QueryResult 92 | { 93 | CountQuery = testData.Persons.AsQueryable(), 94 | PagedQuery = testData.Persons.AsQueryable(), 95 | }; 96 | 97 | var query = new Query 98 | { 99 | Filters = new List 100 | { 101 | new Filter 102 | { 103 | PropertyName = $"{nameof(Person.Pets)}.{nameof(Pet.Name)}", 104 | Operator = FilterOperators.Equal, 105 | Value = "Titan" 106 | } 107 | } 108 | }; 109 | 110 | //act 111 | var result = sut.Execute(query, queryResult); 112 | 113 | //assert 114 | var (count, list) = result.GetCountAndList(); 115 | count.ShouldBe(1); 116 | list.First().ShouldBe(testData.Craig); 117 | } 118 | 119 | 120 | [Theory, AutoSubData] 121 | internal void Filter_WhenNavigationPropertyPathIsCollection_ShouldWorkAsExpected( 122 | FilterQueryAction sut) 123 | { 124 | //arrange 125 | var testData = new TestData(); 126 | var queryResult = new QueryResult 127 | { 128 | CountQuery = testData.Persons.AsQueryable(), 129 | PagedQuery = testData.Persons.AsQueryable(), 130 | }; 131 | 132 | var query = new Query 133 | { 134 | Filters = new List 135 | { 136 | new Filter 137 | { 138 | PropertyName = $"{nameof(Person.AltNames)}", 139 | Operator = FilterOperators.CollectionContains, 140 | Value = "Robbie" 141 | } 142 | } 143 | }; 144 | 145 | //act 146 | var result = sut.Execute(query, queryResult); 147 | 148 | //assert 149 | var (count, list) = result.GetCountAndList(); 150 | count.ShouldBe(1); 151 | list.First().ShouldBe(testData.Bob); 152 | } 153 | [Theory, AutoSubData] 154 | internal void Filter_WhenParentNavigationPropertyPathIsCollection_ShouldWorkAsExpected( 155 | FilterQueryAction sut) 156 | { 157 | //arrange 158 | var testData = new TestData(); 159 | var queryResult = new QueryResult 160 | { 161 | CountQuery = testData.Persons.AsQueryable(), 162 | PagedQuery = testData.Persons.AsQueryable(), 163 | }; 164 | 165 | var query = new Query 166 | { 167 | Filters = new List 168 | { 169 | new Filter 170 | { 171 | PropertyName = $"{nameof(Person.Pets)}.{nameof(Pet.AltNames)}", 172 | Operator = FilterOperators.CollectionContains, 173 | Value = "Stinky" 174 | } 175 | } 176 | }; 177 | 178 | //act 179 | var result = sut.Execute(query, queryResult); 180 | 181 | //assert 182 | var (count, list) = result.GetCountAndList(); 183 | count.ShouldBe(2); 184 | list.ShouldContain(testData.Craig); 185 | list.ShouldContain(testData.Bob); 186 | } 187 | 188 | [Theory, AutoSubData] 189 | internal void Filter_WhenCollectionWithTypeMismatch_ShouldNotFilter( 190 | FilterQueryAction sut) 191 | { 192 | //arrange 193 | var testData = new TestData(); 194 | var queryResult = new QueryResult 195 | { 196 | CountQuery = testData.Persons.AsQueryable(), 197 | PagedQuery = testData.Persons.AsQueryable(), 198 | }; 199 | 200 | var query = new Query 201 | { 202 | Filters = new List 203 | { 204 | new Filter 205 | { 206 | PropertyName = $"{nameof(Person.Pets)}.{nameof(Pet.AltNames)}", 207 | Operator = FilterOperators.Equal, 208 | Value = "Stinky" 209 | } 210 | } 211 | }; 212 | 213 | //act 214 | QueryResult result = null; 215 | var exception = Record.Exception(() => result = sut.Execute(query, queryResult)); 216 | 217 | //assert 218 | exception.ShouldBeNull(); 219 | var (count, items) = result.GetCountAndList(); 220 | items.ShouldBeEquivalentTo(testData.Persons); 221 | } 222 | 223 | [Theory, AutoSubData] 224 | internal void Filter_WhenObjectWithTypeMismatch_ShouldNotFilter( 225 | FilterQueryAction sut) 226 | { 227 | //arrange 228 | var testData = new TestData(); 229 | var queryResult = new QueryResult 230 | { 231 | CountQuery = testData.Persons.AsQueryable(), 232 | PagedQuery = testData.Persons.AsQueryable(), 233 | }; 234 | 235 | var query = new Query 236 | { 237 | Filters = new List 238 | { 239 | new Filter 240 | { 241 | PropertyName = $"{nameof(Person.Age)}", 242 | Operator = FilterOperators.CollectionContains, 243 | Value = "4" 244 | } 245 | } 246 | }; 247 | 248 | //act 249 | QueryResult result = null; 250 | var exception = Record.Exception(() => result = sut.Execute(query, queryResult)); 251 | 252 | //assert 253 | exception.ShouldBeNull(); 254 | var (count, items) = result.GetCountAndList(); 255 | items.ShouldBeEquivalentTo(testData.Persons); 256 | } 257 | 258 | [Theory, AutoSubData] 259 | internal void Filter_WhenQueryResultIsNull_ShouldThrowArgumentNullException( 260 | FilterQueryAction sut) 261 | { 262 | //arrange 263 | var query = new Query(); 264 | 265 | //act 266 | var result = Record.Exception(() => sut.Execute(query, null)); 267 | 268 | //assert 269 | var ex = result.ShouldBeOfType(); 270 | ex.ParamName.ShouldBe("queryResult"); 271 | ex.Message.ShouldStartWith("QueryResult cannot be null."); 272 | } 273 | 274 | [Theory, AutoSubData] 275 | internal void Filter_WhenPagedQueryIsNull_ShouldThrowArgumentNullException( 276 | FilterQueryAction sut) 277 | { 278 | // Arrange 279 | var query = new Query(); 280 | var queryResult = new QueryResult 281 | { 282 | CountQuery = new List().AsQueryable(), 283 | PagedQuery = null 284 | }; 285 | 286 | // Act 287 | var result = Record.Exception(() => sut.Execute(query, queryResult)); 288 | 289 | // Assert 290 | var ex = result.ShouldBeOfType(); 291 | ex.ParamName.ShouldBe("queryResult.PagedQuery"); 292 | ex.Message.ShouldStartWith("PagedQuery within QueryResult cannot be null."); 293 | } 294 | 295 | [Theory, AutoSubData] 296 | internal void Filter_WhenCountQueryIsNull_ShouldThrowArgumentNullException( 297 | FilterQueryAction sut) 298 | { 299 | // Arrange 300 | var query = new Query(); 301 | var queryResult = new QueryResult 302 | { 303 | CountQuery = null, 304 | PagedQuery = new List().AsQueryable() 305 | }; 306 | 307 | // Act 308 | var result = Record.Exception(() => sut.Execute(query, queryResult)); 309 | 310 | // Assert 311 | var ex = result.ShouldBeOfType(); 312 | ex.ParamName.ShouldBe("queryResult.CountQuery"); 313 | ex.Message.ShouldStartWith("CountQuery within QueryResult cannot be null."); 314 | } 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryActions/PagingQueryActionTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryActions; 2 | using QueryR.QueryModels; 3 | using QueryR.Tests.TestHelpers; 4 | using Shouldly; 5 | using System.Linq; 6 | using Xunit; 7 | using static QueryR.Tests.TestHelpers.TestData; 8 | 9 | namespace QueryR.Tests.QueryActions 10 | { 11 | public class PagingQueryActionTests 12 | { 13 | 14 | [Theory, AutoSubData] 15 | internal void Paging_ShouldWorkAsExpected( 16 | PagingQueryAction sut) 17 | { 18 | //arrange 19 | var testData = new TestData(); 20 | var queryResult = new QueryResult 21 | { 22 | CountQuery = testData.Persons.AsQueryable(), 23 | PagedQuery = testData.Persons.AsQueryable(), 24 | }; 25 | 26 | var query = new Query 27 | { 28 | PagingOptions = new PagingOptions 29 | { 30 | PageNumber = 1, 31 | PageSize = 1 32 | } 33 | }; 34 | 35 | //act 36 | var result = sut.Execute(query, queryResult); 37 | 38 | //assert 39 | var (count, list) = result.GetCountAndList(); 40 | count.ShouldBe(2); 41 | list.First().ShouldBe(testData.Craig); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryActions/SortQueryActionTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryActions; 2 | using QueryR.QueryModels; 3 | using QueryR.Tests.TestHelpers; 4 | using Shouldly; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Xunit; 8 | using static QueryR.Tests.TestHelpers.TestData; 9 | 10 | namespace QueryR.Tests.QueryActions 11 | { 12 | public class SortQueryActionTests 13 | { 14 | [Theory, AutoSubData] 15 | internal void Sort_ShouldWorkAsExpected( 16 | SortQueryAction sut) 17 | { 18 | //arrange 19 | var testData = new TestData(); 20 | var baseQuery = testData.Persons.AsQueryable(); 21 | var queryResult = new QueryResult 22 | { 23 | CountQuery = baseQuery, 24 | PagedQuery = baseQuery, 25 | }; 26 | 27 | var query = new Query 28 | { 29 | Sorts = new List 30 | { 31 | new Sort 32 | { 33 | IsAscending = false, 34 | PropertyName = nameof(Person.Name) 35 | } 36 | } 37 | }; 38 | 39 | 40 | //act 41 | var result = sut.Execute(query, queryResult); 42 | 43 | //assert 44 | var (count, list) = result.GetCountAndList(); 45 | count.ShouldBe(2); 46 | list.First().ShouldBe(testData.Craig); 47 | list.Skip(1).First().ShouldBe(testData.Bob); 48 | } 49 | 50 | [Theory, AutoSubData] 51 | internal void Sort_WhenItemIsOnNavigationPropertyPath_ShouldWorkAsExpected( 52 | SortQueryAction sut) 53 | { 54 | //arrange 55 | var testData = new TestData(); 56 | var baseQuery = testData.Pets.AsQueryable(); 57 | var queryResult = new QueryResult 58 | { 59 | CountQuery = baseQuery, 60 | PagedQuery = baseQuery, 61 | }; 62 | 63 | var query = new Query 64 | { 65 | Sorts = new List 66 | { 67 | new Sort 68 | { 69 | IsAscending = false, 70 | PropertyName = $"{nameof(Pet.PetType)}.{nameof(PetType.Name)}" 71 | }, 72 | new Sort 73 | { 74 | IsAscending = true, 75 | PropertyName = nameof(Pet.Name) 76 | } 77 | } 78 | }; 79 | 80 | //act 81 | var result = sut.Execute(query, queryResult).ToList(); 82 | 83 | //assert 84 | result[0].ShouldBeSameAs(testData.Rufus); 85 | result[1].ShouldBeSameAs(testData.Titan); 86 | result[2].ShouldBeSameAs(testData.Kitty); 87 | result[3].ShouldBeSameAs(testData.Meowswers); 88 | result[4].ShouldBeSameAs(testData.Tweeter); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryActions/SparseFieldsQueryActionTests.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture.Xunit2; 2 | using NSubstitute; 3 | using QueryR.QueryActions; 4 | using QueryR.QueryModels; 5 | using QueryR.Services; 6 | using QueryR.Tests.TestHelpers; 7 | using Shouldly; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using Xunit; 11 | using static QueryR.Tests.TestHelpers.TestData; 12 | 13 | namespace QueryR.Tests.QueryActions 14 | { 15 | public class SparseFieldsQueryActionTests 16 | { 17 | [Theory, AutoSubData] 18 | internal void SparseField_ShouldWorkAsExpected( 19 | [Frozen] IMaxDepthService maxDepthServiceMock, 20 | SparseFieldsQueryAction sut) 21 | { 22 | //arrange 23 | maxDepthServiceMock 24 | .GetMaxDepth(Arg.Any()) 25 | .Returns(new int?()); 26 | var testData = new TestData(); 27 | var queryResult = new QueryResult 28 | { 29 | CountQuery = testData.Persons.AsQueryable(), 30 | PagedQuery = testData.Persons.AsQueryable(), 31 | }; 32 | 33 | var query = new Query 34 | { 35 | SparseFields = new List 36 | { 37 | new SparseField 38 | { 39 | EntityName = nameof(Person), 40 | PropertyNames = new List 41 | { 42 | nameof(Person.Name) 43 | } 44 | } 45 | } 46 | }; 47 | 48 | 49 | //act 50 | var result = sut.Execute(query, queryResult); 51 | 52 | //assert 53 | var (count, list) = result.GetCountAndList(); 54 | count.ShouldBe(2); 55 | list.First().Id.ShouldBe(default); 56 | list.First().Name.ShouldBe(testData.Craig.Name); 57 | list.First().Pets.ShouldBeNull(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryModels/FilterOperatorsTests/CollectionContainsTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using Xunit; 9 | 10 | namespace QueryR.Tests.QueryModels.FilterOperatorsTests; 11 | public class CollectionContainsTests 12 | { 13 | [Theory, AutoSubData] 14 | public void CollectionContains_ShouldWorkOnList( 15 | List> values) 16 | { 17 | //arrange 18 | var valueToFind = values.PickRandom().PickRandom(); 19 | 20 | var parameter = Expression.Parameter(typeof(List), "value"); 21 | var constant = Expression.Constant(valueToFind); 22 | var whereExpression = Expression.Lambda, bool>>(FilterOperators.CollectionContains.ExpressionMethod(parameter, constant), parameter); 23 | 24 | //act 25 | var result = values.AsQueryable().Where(whereExpression).ToList(); 26 | 27 | //assert 28 | result.ShouldContain(list => list.Contains(valueToFind)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryModels/FilterOperatorsTests/ContainsTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using Xunit; 9 | 10 | namespace QueryR.Tests.QueryModels.FilterOperatorsTests; 11 | public class ContainsTests 12 | { 13 | [Theory, AutoSubData] 14 | public void Contains_ShouldWorkOnString( 15 | List values) 16 | { 17 | //arrange 18 | var valueToContain = values.PickRandom(); 19 | 20 | var parameter = Expression.Parameter(typeof(string), "value"); 21 | var constant = Expression.Constant(valueToContain); 22 | var whereExpression = Expression.Lambda>(FilterOperators.Contains.ExpressionMethod(parameter, constant), parameter); 23 | 24 | //act 25 | var result = values.AsQueryable().Where(whereExpression).ToList(); 26 | 27 | //assert 28 | result.ShouldAllBe(value => value.Contains(valueToContain)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryModels/FilterOperatorsTests/EndsWithTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using Xunit; 9 | 10 | namespace QueryR.Tests.QueryModels.FilterOperatorsTests; 11 | public class EndsWithTests 12 | { 13 | [Theory, AutoSubData] 14 | public void EndsWith_ShouldWorkOnString( 15 | List values) 16 | { 17 | //arrange 18 | var valueToEndWith = values.PickRandom()[^5..]; 19 | 20 | var parameter = Expression.Parameter(typeof(string), "value"); 21 | var constant = Expression.Constant(valueToEndWith); 22 | var whereExpression = Expression.Lambda>(FilterOperators.EndsWith.ExpressionMethod(parameter, constant), parameter); 23 | 24 | //act 25 | var result = values.AsQueryable().Where(whereExpression).ToList(); 26 | 27 | //assert 28 | result.ShouldAllBe(value => value.EndsWith(valueToEndWith)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryModels/FilterOperatorsTests/EqualTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using Xunit; 9 | 10 | namespace QueryR.Tests.QueryModels.FilterOperatorsTests; 11 | public class EqualTests 12 | { 13 | [Theory, AutoSubData] 14 | public void Equal_ShouldWorkOnValueTypes( 15 | List values) 16 | { 17 | //arrange 18 | var valueToEqual = values.PickRandom(); 19 | 20 | var parameter = Expression.Parameter(typeof(int), "value"); 21 | var constant = Expression.Constant(valueToEqual); 22 | var whereExpression = Expression.Lambda>(FilterOperators.Equal.ExpressionMethod(parameter, constant), parameter); 23 | 24 | //act 25 | var result = values.AsQueryable().Where(whereExpression).ToList(); 26 | 27 | //assert 28 | result.ShouldHaveSingleItem(); 29 | result.First().ShouldBe(valueToEqual); 30 | } 31 | 32 | [Theory, AutoSubData] 33 | public void Equal_ShouldWorkOnString( 34 | List values) 35 | { 36 | //arrange 37 | var valueToEqual = values.PickRandom(); 38 | 39 | var parameter = Expression.Parameter(typeof(string), "value"); 40 | var constant = Expression.Constant(valueToEqual); 41 | var whereExpression = Expression.Lambda>(FilterOperators.Equal.ExpressionMethod(parameter, constant), parameter); 42 | 43 | //act 44 | var result = values.AsQueryable().Where(whereExpression).ToList(); 45 | 46 | //assert 47 | result.ShouldHaveSingleItem(); 48 | result.First().ShouldBe(valueToEqual); 49 | } 50 | [Theory, AutoSubData] 51 | public void Equal_ShouldWorkOnGuid( 52 | List values) 53 | { 54 | //arrange 55 | var valueToEqual = values.PickRandom(); 56 | 57 | var parameter = Expression.Parameter(typeof(Guid), "value"); 58 | var constant = Expression.Constant(valueToEqual); 59 | var whereExpression = Expression.Lambda>(FilterOperators.Equal.ExpressionMethod(parameter, constant), parameter); 60 | 61 | //act 62 | var result = values.AsQueryable().Where(whereExpression).ToList(); 63 | 64 | //assert 65 | result.ShouldHaveSingleItem(); 66 | result.First().ShouldBe(valueToEqual); 67 | } 68 | [Theory, AutoSubData] 69 | public void Equal_ShouldWorkOnDateTime( 70 | List values) 71 | { 72 | //arrange 73 | var valueToEqual = values.PickRandom(); 74 | 75 | var parameter = Expression.Parameter(typeof(DateTime), "value"); 76 | var constant = Expression.Constant(valueToEqual); 77 | var whereExpression = Expression.Lambda>(FilterOperators.Equal.ExpressionMethod(parameter, constant), parameter); 78 | 79 | //act 80 | var result = values.AsQueryable().Where(whereExpression).ToList(); 81 | 82 | //assert 83 | result.ShouldHaveSingleItem(); 84 | result.First().ShouldBe(valueToEqual); 85 | } 86 | public class TestDummy { } 87 | [Theory, AutoSubData] 88 | public void Equal_ShouldWorkOnReferenceType( 89 | List values) 90 | { 91 | //arrange 92 | var valueToEqual = values.PickRandom(); 93 | 94 | var parameter = Expression.Parameter(typeof(TestDummy), "value"); 95 | var constant = Expression.Constant(valueToEqual); 96 | var whereExpression = Expression.Lambda>(FilterOperators.Equal.ExpressionMethod(parameter, constant), parameter); 97 | 98 | //act 99 | var result = values.AsQueryable().Where(whereExpression).ToList(); 100 | 101 | //assert 102 | result.ShouldHaveSingleItem(); 103 | result.First().ShouldBe(valueToEqual); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryModels/FilterOperatorsTests/GreaterThanOrEqualTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using Xunit; 9 | 10 | namespace QueryR.Tests.QueryModels.FilterOperatorsTests; 11 | public class GreaterThanOrEqualTests 12 | { 13 | [Theory, AutoSubData] 14 | public void GreaterThanOrEqual_ShouldWorkOnValueTypes( 15 | List values) 16 | { 17 | //arrange 18 | var valueToCompare = values.PickRandom(); 19 | 20 | var parameter = Expression.Parameter(typeof(int), "value"); 21 | var constant = Expression.Constant(valueToCompare); 22 | var whereExpression = Expression.Lambda>(FilterOperators.GreaterThanOrEqual.ExpressionMethod(parameter, constant), parameter); 23 | 24 | //act 25 | var result = values.AsQueryable().Where(whereExpression).ToList(); 26 | 27 | //assert 28 | result.ShouldAllBe(value => value >= valueToCompare); 29 | } 30 | 31 | [Theory, AutoSubData] 32 | public void GreaterThanOrEqual_ShouldWorkOnDateTime( 33 | List values) 34 | { 35 | //arrange 36 | var valueToCompare = values.PickRandom(); 37 | 38 | var parameter = Expression.Parameter(typeof(DateTime), "value"); 39 | var constant = Expression.Constant(valueToCompare); 40 | var whereExpression = Expression.Lambda>(FilterOperators.GreaterThanOrEqual.ExpressionMethod(parameter, constant), parameter); 41 | 42 | //act 43 | var result = values.AsQueryable().Where(whereExpression).ToList(); 44 | 45 | //assert 46 | result.ShouldAllBe(value => value >= valueToCompare); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryModels/FilterOperatorsTests/GreaterThanTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using Xunit; 9 | 10 | namespace QueryR.Tests.QueryModels.FilterOperatorsTests; 11 | public class GreaterThanTests 12 | { 13 | [Theory, AutoSubData] 14 | public void GreaterThan_ShouldWorkOnValueTypes( 15 | List values) 16 | { 17 | //arrange 18 | var valueToCompare = values.OrderBy(v => v).Skip(1).First(); 19 | 20 | var parameter = Expression.Parameter(typeof(int), "value"); 21 | var constant = Expression.Constant(valueToCompare); 22 | var whereExpression = Expression.Lambda>(FilterOperators.GreaterThan.ExpressionMethod(parameter, constant), parameter); 23 | 24 | //act 25 | var result = values.AsQueryable().Where(whereExpression).ToList(); 26 | 27 | //assert 28 | result.ShouldAllBe(value => value > valueToCompare); 29 | } 30 | 31 | [Theory, AutoSubData] 32 | public void GreaterThan_ShouldWorkOnDateTime( 33 | List values) 34 | { 35 | //arrange 36 | var valueToCompare = values.OrderBy(v => v).Skip(1).First(); 37 | 38 | var parameter = Expression.Parameter(typeof(DateTime), "value"); 39 | var constant = Expression.Constant(valueToCompare); 40 | var whereExpression = Expression.Lambda>(FilterOperators.GreaterThan.ExpressionMethod(parameter, constant), parameter); 41 | 42 | //act 43 | var result = values.AsQueryable().Where(whereExpression).ToList(); 44 | 45 | //assert 46 | result.ShouldAllBe(value => value > valueToCompare); 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryModels/FilterOperatorsTests/InTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using Xunit; 9 | 10 | namespace QueryR.Tests.QueryModels.FilterOperatorsTests; 11 | public class InTests 12 | { 13 | [Theory, AutoSubData] 14 | public void In_ShouldWorkOnValueTypes( 15 | List values) 16 | { 17 | //arrange 18 | var valueToFind = values.PickRandom(); 19 | var listToSearchIn = new List { valueToFind }; 20 | 21 | var parameter = Expression.Parameter(typeof(int), "value"); 22 | var constant = Expression.Constant(listToSearchIn); 23 | var whereExpression = Expression.Lambda>(FilterOperators.In.ExpressionMethod(parameter, constant), parameter); 24 | 25 | //act 26 | var result = values.AsQueryable().Where(whereExpression).ToList(); 27 | 28 | //assert 29 | result.ShouldAllBe(value => value == valueToFind); 30 | } 31 | 32 | [Theory, AutoSubData] 33 | public void In_ShouldWorkOnString( 34 | List values) 35 | { 36 | //arrange 37 | var valueToFind = values.PickRandom(); 38 | var listToSearchIn = new List { valueToFind }; 39 | 40 | var parameter = Expression.Parameter(typeof(string), "value"); 41 | var constant = Expression.Constant(listToSearchIn); 42 | var whereExpression = Expression.Lambda>(FilterOperators.In.ExpressionMethod(parameter, constant), parameter); 43 | 44 | //act 45 | var result = values.AsQueryable().Where(whereExpression).ToList(); 46 | 47 | //assert 48 | result.ShouldAllBe(value => value == valueToFind); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryModels/FilterOperatorsTests/LessThanOrEqualTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using Xunit; 9 | 10 | namespace QueryR.Tests.QueryModels.FilterOperatorsTests; 11 | public class LessThanOrEqualTests 12 | { 13 | [Theory, AutoSubData] 14 | public void LessThanOrEqual_ShouldWorkOnValueTypes( 15 | List values) 16 | { 17 | //arrange 18 | var valueToCompare = values.PickRandom(); 19 | 20 | var parameter = Expression.Parameter(typeof(int), "value"); 21 | var constant = Expression.Constant(valueToCompare); 22 | var whereExpression = Expression.Lambda>(FilterOperators.LessThanOrEqual.ExpressionMethod(parameter, constant), parameter); 23 | 24 | //act 25 | var result = values.AsQueryable().Where(whereExpression).ToList(); 26 | 27 | //assert 28 | result.ShouldAllBe(value => value <= valueToCompare); 29 | } 30 | 31 | [Theory, AutoSubData] 32 | public void LessThanOrEqual_ShouldWorkOnDateTime( 33 | List values) 34 | { 35 | //arrange 36 | var valueToCompare = values.PickRandom(); 37 | 38 | var parameter = Expression.Parameter(typeof(DateTime), "value"); 39 | var constant = Expression.Constant(valueToCompare); 40 | var whereExpression = Expression.Lambda>(FilterOperators.LessThanOrEqual.ExpressionMethod(parameter, constant), parameter); 41 | 42 | //act 43 | var result = values.AsQueryable().Where(whereExpression).ToList(); 44 | 45 | //assert 46 | result.ShouldAllBe(value => value <= valueToCompare); 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryModels/FilterOperatorsTests/LessThanTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using Xunit; 9 | 10 | namespace QueryR.Tests.QueryModels.FilterOperatorsTests; 11 | public class LessThanTests 12 | { 13 | [Theory, AutoSubData] 14 | public void LessThan_ShouldWorkOnValueTypes( 15 | List values) 16 | { 17 | //arrange 18 | var valueToCompare = values.OrderBy(v => v).Skip(1).First(); 19 | 20 | var parameter = Expression.Parameter(typeof(int), "value"); 21 | var constant = Expression.Constant(valueToCompare); 22 | var whereExpression = Expression.Lambda>(FilterOperators.LessThan.ExpressionMethod(parameter, constant), parameter); 23 | 24 | //act 25 | var result = values.AsQueryable().Where(whereExpression).ToList(); 26 | 27 | //assert 28 | result.ShouldAllBe(value => value < valueToCompare); 29 | } 30 | 31 | [Theory, AutoSubData] 32 | public void LessThan_ShouldWorkOnDateTime( 33 | List values) 34 | { 35 | //arrange 36 | var valueToCompare = values.OrderBy(v => v).Skip(1).First(); 37 | 38 | var parameter = Expression.Parameter(typeof(DateTime), "value"); 39 | var constant = Expression.Constant(valueToCompare); 40 | var whereExpression = Expression.Lambda>(FilterOperators.LessThan.ExpressionMethod(parameter, constant), parameter); 41 | 42 | //act 43 | var result = values.AsQueryable().Where(whereExpression).ToList(); 44 | 45 | //assert 46 | result.ShouldAllBe(value => value < valueToCompare); 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryModels/FilterOperatorsTests/NotEqualTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using Xunit; 9 | 10 | namespace QueryR.Tests.QueryModels.FilterOperatorsTests; 11 | public class NotEqualTests 12 | { 13 | [Theory, AutoSubData] 14 | public void NotEqual_ShouldWorkOnValueTypes( 15 | List values) 16 | { 17 | //arrange 18 | var valueToCompare = values.PickRandom(); 19 | 20 | var parameter = Expression.Parameter(typeof(int), "value"); 21 | var constant = Expression.Constant(valueToCompare); 22 | var whereExpression = Expression.Lambda>(FilterOperators.NotEqual.ExpressionMethod(parameter, constant), parameter); 23 | 24 | //act 25 | var result = values.AsQueryable().Where(whereExpression).ToList(); 26 | 27 | //assert 28 | result.ShouldAllBe(value => value != valueToCompare); 29 | } 30 | 31 | [Theory, AutoSubData] 32 | public void NotEqual_ShouldWorkOnString( 33 | List values) 34 | { 35 | //arrange 36 | var valueToCompare = values.PickRandom(); 37 | 38 | var parameter = Expression.Parameter(typeof(string), "value"); 39 | var constant = Expression.Constant(valueToCompare); 40 | var whereExpression = Expression.Lambda>(FilterOperators.NotEqual.ExpressionMethod(parameter, constant), parameter); 41 | 42 | //act 43 | var result = values.AsQueryable().Where(whereExpression).ToList(); 44 | 45 | //assert 46 | result.ShouldAllBe(value => value != valueToCompare); 47 | } 48 | 49 | [Theory, AutoSubData] 50 | public void NotEqual_ShouldWorkOnGuid( 51 | List values) 52 | { 53 | //arrange 54 | var valueToCompare = values.PickRandom(); 55 | 56 | var parameter = Expression.Parameter(typeof(Guid), "value"); 57 | var constant = Expression.Constant(valueToCompare); 58 | var whereExpression = Expression.Lambda>(FilterOperators.NotEqual.ExpressionMethod(parameter, constant), parameter); 59 | 60 | //act 61 | var result = values.AsQueryable().Where(whereExpression).ToList(); 62 | 63 | //assert 64 | result.ShouldAllBe(value => value != valueToCompare); 65 | } 66 | 67 | [Theory, AutoSubData] 68 | public void NotEqual_ShouldWorkOnDateTime( 69 | List values) 70 | { 71 | //arrange 72 | var valueToCompare = values.PickRandom(); 73 | 74 | var parameter = Expression.Parameter(typeof(DateTime), "value"); 75 | var constant = Expression.Constant(valueToCompare); 76 | var whereExpression = Expression.Lambda>(FilterOperators.NotEqual.ExpressionMethod(parameter, constant), parameter); 77 | 78 | //act 79 | var result = values.AsQueryable().Where(whereExpression).ToList(); 80 | 81 | //assert 82 | result.ShouldAllBe(value => value != valueToCompare); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryModels/FilterOperatorsTests/StartsWithTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Tests.TestHelpers; 3 | using Shouldly; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using Xunit; 9 | 10 | namespace QueryR.Tests.QueryModels.FilterOperatorsTests; 11 | public class StartsWithTests 12 | { 13 | [Theory, AutoSubData] 14 | public void StartsWith_ShouldWorkOnString( 15 | List values) 16 | { 17 | //arrange 18 | var valueToStartWith = values.PickRandom().Substring(0, 1); 19 | 20 | var parameter = Expression.Parameter(typeof(string), "value"); 21 | var constant = Expression.Constant(valueToStartWith); 22 | var whereExpression = Expression.Lambda>(FilterOperators.StartsWith.ExpressionMethod(parameter, constant), parameter); 23 | 24 | //act 25 | var result = values.AsQueryable().Where(whereExpression).ToList(); 26 | 27 | //assert 28 | result.ShouldAllBe(value => value.StartsWith(valueToStartWith)); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/QueryR.Tests/QueryR.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | 6 | false 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | all 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/QueryR.Tests/Services/NullMaxDepthServiceTests.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using QueryR.Services; 3 | using QueryR.Tests.TestHelpers; 4 | using Shouldly; 5 | using Xunit; 6 | 7 | namespace QueryR.Tests.Services; 8 | public class NullMaxDepthServiceTests 9 | { 10 | [Theory, AutoSubData] 11 | internal void GetMaxDepth_ReturnsNull( 12 | Query query, 13 | NullMaxDepthService sut) 14 | { 15 | // Act 16 | var result = sut.GetMaxDepth(query); 17 | 18 | // Assert 19 | result.ShouldBeNull(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/QueryR.Tests/TestHelpers/AutoSubDataAttribute.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture; 2 | using AutoFixture.AutoNSubstitute; 3 | using AutoFixture.Xunit2; 4 | 5 | namespace QueryR.Tests.TestHelpers 6 | { 7 | internal class AutoSubDataAttribute : AutoDataAttribute 8 | { 9 | public AutoSubDataAttribute() 10 | : base(() => new Fixture().Customize(new AutoNSubstituteCustomization())) 11 | { 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/QueryR.Tests/TestHelpers/ListExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace QueryR.Tests.TestHelpers 5 | { 6 | internal static class ListExtensions 7 | { 8 | private static readonly Random rng = new(); 9 | 10 | public static void Shuffle(this IList list) 11 | { 12 | int n = list.Count; 13 | while (n > 1) 14 | { 15 | n--; 16 | int k = rng.Next(n + 1); 17 | (list[n], list[k]) = (list[k], list[n]); 18 | } 19 | } 20 | public static T PickRandom(this IList list) 21 | { 22 | return list[rng.Next(list.Count)]; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QueryR.Tests/TestHelpers/MemberAutoDataAttribute.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture; 2 | using AutoFixture.AutoNSubstitute; 3 | using AutoFixture.Kernel; 4 | using AutoFixture.Xunit2; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Threading; 10 | using Xunit; 11 | using Xunit.Sdk; 12 | 13 | namespace QueryR.Tests.TestHelpers 14 | { 15 | //Work around for 16 | //MemberAutoData only uses first entry from the supplied enumerable #1142 17 | //https://github.com/AutoFixture/AutoFixture/issues/1142 18 | //Code taken from here 19 | //https://github.com/AutoFixture/AutoFixture/issues/1142#issuecomment-545579385 20 | internal class MemberAutoDataAttribute : DataAttribute 21 | { 22 | private readonly Lazy fixture; 23 | private readonly MemberDataAttribute memberDataAttribute; 24 | 25 | public MemberAutoDataAttribute(string memberName, params object[] parameters) 26 | : this(memberName, parameters, () => new Fixture().Customize(new AutoNSubstituteCustomization())) 27 | { 28 | } 29 | 30 | protected MemberAutoDataAttribute(string memberName, object[] parameters, Func fixtureFactory) 31 | { 32 | if (fixtureFactory == null) 33 | { 34 | throw new ArgumentNullException(nameof(fixtureFactory)); 35 | } 36 | 37 | memberDataAttribute = new MemberDataAttribute(memberName, parameters); 38 | fixture = new Lazy(fixtureFactory, LazyThreadSafetyMode.PublicationOnly); 39 | } 40 | 41 | public override IEnumerable GetData(MethodInfo testMethod) 42 | { 43 | if (testMethod == null) 44 | { 45 | throw new ArgumentNullException(nameof(testMethod)); 46 | } 47 | 48 | var memberData = memberDataAttribute.GetData(testMethod); 49 | 50 | using var enumerator = memberData.GetEnumerator(); 51 | if (enumerator.MoveNext()) 52 | { 53 | var specimens = GetSpecimens(testMethod.GetParameters(), enumerator.Current.Length).ToArray(); 54 | 55 | do 56 | { 57 | yield return enumerator.Current.Concat(specimens).ToArray(); 58 | } while (enumerator.MoveNext()); 59 | } 60 | } 61 | 62 | private IEnumerable GetSpecimens(IEnumerable parameters, int skip) 63 | { 64 | foreach (var parameter in parameters.Skip(skip)) 65 | { 66 | CustomizeFixture(parameter); 67 | 68 | yield return Resolve(parameter); 69 | } 70 | } 71 | 72 | private void CustomizeFixture(ParameterInfo p) 73 | { 74 | var customizeAttributes = p.GetCustomAttributes() 75 | .OfType() 76 | .OrderBy(x => x, new CustomizeAttributeComparer()); 77 | 78 | foreach (var ca in customizeAttributes) 79 | { 80 | var c = ca.GetCustomization(p); 81 | fixture.Value.Customize(c); 82 | } 83 | } 84 | 85 | private object Resolve(ParameterInfo p) 86 | { 87 | var context = new SpecimenContext(fixture.Value); 88 | 89 | return context.Resolve(p); 90 | } 91 | 92 | private class CustomizeAttributeComparer : Comparer 93 | { 94 | public override int Compare(IParameterCustomizationSource x, IParameterCustomizationSource y) 95 | { 96 | var xfrozen = x is FrozenAttribute; 97 | var yfrozen = y is FrozenAttribute; 98 | 99 | if (xfrozen && !yfrozen) 100 | { 101 | return 1; 102 | } 103 | 104 | if (yfrozen && !xfrozen) 105 | { 106 | return -1; 107 | } 108 | 109 | return 0; 110 | } 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /src/QueryR.Tests/TestHelpers/TestData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace QueryR.Tests.TestHelpers 5 | { 6 | internal class TestData 7 | { 8 | public class Person 9 | { 10 | public int Id { get; set; } 11 | public string Name { get; set; } 12 | public int Age { get; set; } 13 | public List Pets { get; set; } 14 | public List AltNames { get; set; } 15 | } 16 | public class Pet 17 | { 18 | public int Id { get; set; } 19 | public string Name { get; set; } 20 | public int OwnerId { get; set; } 21 | public Person Owner { get; set; } 22 | public int PetTypeId { get; set; } 23 | public PetType PetType { get; set; } 24 | public List AltNames { get; set; } 25 | } 26 | public class PetType 27 | { 28 | public int Id { get; set; } 29 | public string Name { get; set; } 30 | } 31 | 32 | public PetType Cat { get; init; } 33 | public PetType Dog { get; init; } 34 | public PetType Bird { get; init; } 35 | public List PetTypes { get; init; } 36 | 37 | public Person Craig { get; init; } 38 | public Person Bob { get; init; } 39 | public List Persons { get; init; } 40 | 41 | public Pet Titan { get; init; } 42 | public Pet Rufus { get; init; } 43 | public Pet Meowswers { get; init; } 44 | public Pet Kitty { get; init; } 45 | public Pet Tweeter { get; init; } 46 | public List Pets { get; init; } 47 | 48 | public TestData() 49 | { 50 | Cat = new PetType { Id = 1, Name = "Cat" }; 51 | Dog = new PetType { Id = 2, Name = "Dog" }; 52 | Bird = new PetType { Id = 3, Name = "Bird" }; 53 | 54 | Craig = new Person { Id = 1, Name = "Craig", Age = 20, AltNames = ["Greg"] }; 55 | Bob = new Person { Id = 2, Name = "Bob", Age = 25, AltNames = ["Robert", "Robbie"] }; 56 | 57 | Titan = new Pet { Id = 1, Name = "Titan", OwnerId = 1, PetTypeId = 2, AltNames = ["Mr Slobber", "Stinky"] }; 58 | Rufus = new Pet { Id = 2, Name = "Rufus", OwnerId = 2, PetTypeId = 2, AltNames = ["Stinky", "Sir Barks-a-lot"] }; 59 | Meowswers = new Pet { Id = 3, Name = "Meowswers", OwnerId = 1, PetTypeId = 1, AltNames = ["BAD CAT!"] }; 60 | Kitty = new Pet { Id = 4, Name = "Kitty", OwnerId = 2, PetTypeId = 1, AltNames = ["Puddin'"] }; 61 | Tweeter = new Pet { Id = 5, Name = "Tweeter", OwnerId = 1, PetTypeId = 3, AltNames = ["Chirp Chirp"] }; 62 | 63 | PetTypes = new List { Cat, Dog, Bird }; 64 | Persons = new List { Craig, Bob }; 65 | Pets = new List { Titan, Rufus, Meowswers, Kitty, Tweeter }; 66 | 67 | foreach (var person in Persons) 68 | { 69 | person.Pets = Pets.Where(p => p.OwnerId == person.Id).ToList(); 70 | } 71 | 72 | foreach (var pet in Pets) 73 | { 74 | pet.Owner = Persons.First(p => p.Id == pet.OwnerId); 75 | pet.PetType = PetTypes.First(p => p.Id == pet.PetTypeId); 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/QueryR.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33103.184 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryR", "QueryR\QueryR.csproj", "{F919B6DD-8549-4882-86D3-D520003F2721}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryR.Tests", "QueryR.Tests\QueryR.Tests.csproj", "{62A06A7B-D472-435F-97A6-C5D0AAC6EBA0}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{88254E9C-239F-457C-95BA-BB246A355387}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryR.Examples.Data", "QueryR.Examples.Data\QueryR.Examples.Data.csproj", "{C58D30DC-9C86-4713-B687-8DF628EE3860}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QueryR.Examples.ConsoleApp", "QueryR.Examples.ConsoleApp\QueryR.Examples.ConsoleApp.csproj", "{D4FDDCE9-F01A-4578-901F-B9A85A0D26DA}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {F919B6DD-8549-4882-86D3-D520003F2721}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {F919B6DD-8549-4882-86D3-D520003F2721}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {F919B6DD-8549-4882-86D3-D520003F2721}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {F919B6DD-8549-4882-86D3-D520003F2721}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {62A06A7B-D472-435F-97A6-C5D0AAC6EBA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {62A06A7B-D472-435F-97A6-C5D0AAC6EBA0}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {62A06A7B-D472-435F-97A6-C5D0AAC6EBA0}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {62A06A7B-D472-435F-97A6-C5D0AAC6EBA0}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {C58D30DC-9C86-4713-B687-8DF628EE3860}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {C58D30DC-9C86-4713-B687-8DF628EE3860}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {C58D30DC-9C86-4713-B687-8DF628EE3860}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {C58D30DC-9C86-4713-B687-8DF628EE3860}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {D4FDDCE9-F01A-4578-901F-B9A85A0D26DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {D4FDDCE9-F01A-4578-901F-B9A85A0D26DA}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {D4FDDCE9-F01A-4578-901F-B9A85A0D26DA}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {D4FDDCE9-F01A-4578-901F-B9A85A0D26DA}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(NestedProjects) = preSolution 43 | {C58D30DC-9C86-4713-B687-8DF628EE3860} = {88254E9C-239F-457C-95BA-BB246A355387} 44 | {D4FDDCE9-F01A-4578-901F-B9A85A0D26DA} = {88254E9C-239F-457C-95BA-BB246A355387} 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {B9BDC915-45D6-4807-8279-FE43BEF0A60D} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /src/QueryR/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | 4 | namespace QueryR.Extensions 5 | { 6 | internal static class StringExtensions 7 | { 8 | public static object Convert(this string input, Type type) 9 | { 10 | try 11 | { 12 | var converter = TypeDescriptor.GetConverter(type); 13 | if (converter != null) 14 | { 15 | return converter.ConvertFromString(input); 16 | } 17 | return null; 18 | } 19 | catch (NotSupportedException) 20 | { 21 | return null; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QueryR/Extensions/TruthTableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.Extensions 2 | { 3 | internal static class TruthTableExtensions 4 | { 5 | public static T TruthTable(this (bool A, bool B) inputs, T notANotB, T notAB, T ANotB, T AB) 6 | { 7 | return inputs switch 8 | { 9 | (true, true) => AB, 10 | (true, false) => ANotB, 11 | (false, true) => notAB, 12 | _ => notANotB 13 | }; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/QueryR/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace QueryR.Extensions 7 | { 8 | internal static class TypeExtensions 9 | { 10 | //TODO: having issues with Queryable.Select 2 different methods 11 | public static MethodInfo GetGenericMethod(this Type type, string methodName, int genericArgumentCount, int parameterCount, params Type[] typeArguments) 12 | { 13 | return type.GetMethods().First( 14 | method => method.Name == methodName 15 | && method.IsGenericMethodDefinition 16 | && method.GetGenericArguments().Length == genericArgumentCount 17 | && method.GetParameters().Length == parameterCount) 18 | .MakeGenericMethod(typeArguments); 19 | } 20 | 21 | 22 | /// 23 | /// Finds the type of the element of a type. Returns null if this type does not enumerate. 24 | /// From https://stackoverflow.com/a/55244482/102526 25 | /// 26 | /// The type to check. 27 | /// The element type, if found; otherwise, . 28 | public static Type FindElementType(this Type type) 29 | { 30 | if (type.IsArray) 31 | return type.GetElementType(); 32 | 33 | // type is IEnumerable; 34 | if (ImplIEnumT(type)) 35 | return type.GetGenericArguments().First(); 36 | 37 | // type implements/extends IEnumerable; 38 | var enumType = type.GetInterfaces().Where(ImplIEnumT).Select(t => t.GetGenericArguments().First()).FirstOrDefault(); 39 | if (enumType != null) 40 | return enumType; 41 | 42 | // type is IEnumerable 43 | if (IsIEnum(type) || type.GetInterfaces().Any(IsIEnum)) 44 | return typeof(object); 45 | 46 | return null; 47 | 48 | bool IsIEnum(Type t) => t == typeof(System.Collections.IEnumerable); 49 | bool ImplIEnumT(Type t) => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/QueryR/IQueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryActions; 2 | using QueryR.QueryModels; 3 | using QueryR.Services; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace QueryR 8 | { 9 | public static class IQueryableExtensions 10 | { 11 | private static List QueryActions { get; set; } 12 | 13 | private static void InitializeQueryActions() 14 | { 15 | if (QueryActions == null) 16 | { 17 | QueryActions = new List() 18 | { 19 | new SparseFieldsQueryAction(new NullMaxDepthService()), 20 | new FilterQueryAction(), 21 | new SortQueryAction(), 22 | new PagingQueryAction(), 23 | }; 24 | } 25 | } 26 | 27 | /// 28 | /// Performs the on the source. 29 | /// 30 | /// 31 | /// 32 | /// 33 | /// 34 | public static QueryResult Query(this IQueryable source, Query query) 35 | { 36 | var result = new QueryResult 37 | { 38 | CountQuery = source, 39 | PagedQuery = source 40 | }; 41 | 42 | InitializeQueryActions(); 43 | 44 | foreach (var action in QueryActions) 45 | { 46 | action.Execute(query, result); 47 | } 48 | 49 | return result; 50 | } 51 | 52 | /// 53 | /// Convienience method, adds all the query parts to a and executes the extension method. 54 | /// 55 | /// 56 | /// 57 | /// 58 | /// 59 | /// 60 | public static QueryResult Query(this IQueryable source, IQueryPart firstQueryPart, params IQueryPart[] queryParts) 61 | { 62 | return source.Query(new[] { firstQueryPart }.Concat(queryParts)); 63 | } 64 | 65 | /// 66 | /// Convienience method, adds all the query parts to a and executes the extension method. 67 | /// 68 | /// 69 | /// 70 | /// 71 | /// 72 | public static QueryResult Query(this IQueryable source, IEnumerable queryParts) 73 | { 74 | var query = new Query 75 | { 76 | Filters = new List(), 77 | Sorts = new List(), 78 | SparseFields = new List() 79 | }; 80 | 81 | foreach (var item in queryParts) 82 | { 83 | if (item is Filter filter) 84 | { 85 | query.Filters.Add(filter); 86 | } 87 | else if (item is PagingOptions pagingOptions) 88 | { 89 | query.PagingOptions = pagingOptions; 90 | } 91 | else if (item is Sort sort) 92 | { 93 | query.Sorts.Add(sort); 94 | } 95 | else if (item is SparseField sparseField) 96 | { 97 | query.SparseFields.Add(sparseField); 98 | } 99 | } 100 | 101 | return source.Query(query); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/QueryR/QueryActions/FilterQueryAction.cs: -------------------------------------------------------------------------------- 1 | using QueryR.Extensions; 2 | using QueryR.QueryModels; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | 9 | namespace QueryR.QueryActions 10 | { 11 | internal class FilterQueryAction : IQueryAction 12 | { 13 | public QueryResult Execute(Query query, QueryResult queryResult) 14 | { 15 | ValidateQueryResult(queryResult); 16 | 17 | foreach (var filter in query?.Filters ?? Enumerable.Empty()) 18 | { 19 | var parameter = Expression.Parameter(typeof(T), "t"); 20 | var propertyNames = new Queue(filter.PropertyName.Split('.')); 21 | var filterExpression = FilterExpression(typeof(T), parameter, propertyNames, filter); 22 | 23 | var lambda = Expression.Lambda>(filterExpression, parameter); 24 | queryResult.PagedQuery = queryResult.PagedQuery.Where(lambda); 25 | queryResult.CountQuery = queryResult.CountQuery.Where(lambda); 26 | } 27 | 28 | return queryResult; 29 | } 30 | 31 | private void ValidateQueryResult(QueryResult queryResult) 32 | { 33 | if (queryResult == null) 34 | { 35 | throw new ArgumentNullException(nameof(queryResult), "QueryResult cannot be null."); 36 | } 37 | 38 | if (queryResult.PagedQuery == null) 39 | { 40 | throw new ArgumentNullException($"{nameof(queryResult)}.{nameof(QueryResult.PagedQuery)}", "PagedQuery within QueryResult cannot be null."); 41 | } 42 | 43 | if (queryResult.CountQuery == null) 44 | { 45 | throw new ArgumentNullException($"{nameof(queryResult)}.{nameof(QueryResult.CountQuery)}", "CountQuery within QueryResult cannot be null."); 46 | } 47 | } 48 | 49 | /// 50 | /// Recursive method to build Property chain with .Any() on collections. 51 | /// 52 | private Expression FilterExpression( 53 | Type type, 54 | Expression parentExpression, 55 | Queue propertyNames, 56 | Filter filter) 57 | { 58 | var propertyName = propertyNames.Dequeue(); 59 | var enumerableType = TypeExtensions.FindElementType(parentExpression.Type); 60 | 61 | var isStillNavigating = propertyNames.Any(); 62 | var isObject = enumerableType == null; 63 | 64 | return (isStillNavigating, isObject).TruthTable( 65 | //end of navigation, is collection 66 | () => CollectionFilter(enumerableType, parentExpression, propertyName, filter), 67 | //end of navigation, is object 68 | () => ObjectFilter(enumerableType, parentExpression, propertyName, filter), 69 | //still navigating, is collection 70 | () => ChainedCollectionExpression(enumerableType, parentExpression, propertyName, propertyNames, filter), 71 | //still navigating, is object 72 | () => ChainedObjectExpression(parentExpression, propertyName, propertyNames, filter) 73 | )(); 74 | } 75 | 76 | /// 77 | /// eg. t.Items.Any(item => [ObjectFilter]) 78 | /// 79 | private Expression CollectionFilter(Type elementType, Expression parentExpression, string propertyName, Filter filter) 80 | { 81 | var itemParameter = Expression.Parameter(elementType, "item"); 82 | var filterExpression = ObjectFilter(elementType, itemParameter, propertyName, filter); 83 | return Any(elementType, parentExpression, itemParameter, filterExpression); 84 | } 85 | 86 | /// 87 | /// If collection, t.ListOfNames.Contains("MyName") 88 | /// If object, t.Name == "MyItem" 89 | /// 90 | private Expression ObjectFilter(Type elementType, Expression parentExpression, string propertyName, Filter filter) 91 | { 92 | var memberExpression = Expression.Property(parentExpression, propertyName); 93 | //eg. t.ListOfNames.Contains("MyName") 94 | if (typeof(IEnumerable).IsAssignableFrom(memberExpression.Type) 95 | && memberExpression.Type != typeof(string)) 96 | { 97 | var type = TypeExtensions.FindElementType(memberExpression.Type); 98 | var target = Expression.Constant(filter.Value.Convert(type), type); 99 | 100 | try 101 | { 102 | return filter.Operator.ExpressionMethod(memberExpression, target); 103 | } 104 | //If we get an expression we can't handle, silently do not filter. 105 | //TODO: In future perhaps we should configure silent fail and/or a "CanExecute" on FilterOperator class. 106 | catch 107 | { 108 | return Expression.Constant(true); 109 | } 110 | 111 | 112 | } 113 | //eg. t.Name == "MyItem" 114 | else 115 | { 116 | var target = Expression.Constant(filter.Value.Convert(memberExpression.Type), memberExpression.Type); 117 | try 118 | { 119 | return filter.Operator.ExpressionMethod(memberExpression, target); 120 | } 121 | 122 | //If we get an expression we can't handle, silently do not filter. 123 | //TODO: In future perhaps we should configure silent fail and/or a "CanExecute" on FilterOperator class. 124 | catch 125 | { 126 | return Expression.Constant(true); 127 | } 128 | } 129 | } 130 | 131 | /// 132 | /// eg. t.Items.Any(item => ... 133 | /// 134 | private Expression ChainedCollectionExpression( 135 | Type elementType, 136 | Expression parentExpression, 137 | string propertyName, 138 | Queue propertyNames, 139 | Filter filter) 140 | { 141 | var itemParameter = Expression.Parameter(elementType, "item"); 142 | var itemProperty = Expression.Property(itemParameter, propertyName); 143 | var subExpression = FilterExpression(elementType, itemProperty, propertyNames, filter); 144 | 145 | return Any(elementType, parentExpression, itemParameter, subExpression); 146 | } 147 | 148 | /// 149 | /// eg. IIF(t.Items == null, False, ... 150 | /// 151 | private Expression ChainedObjectExpression( 152 | Expression parentExpression, 153 | string propertyName, 154 | Queue propertyNames, 155 | Filter filter) 156 | { 157 | var memberExpression = Expression.Property(parentExpression, propertyName); 158 | var nullCheck = Expression.Equal(memberExpression, Expression.Constant(null)); 159 | var nestedExpression = FilterExpression(memberExpression.Type, memberExpression, propertyNames, filter); 160 | 161 | return Expression.Condition(nullCheck, Expression.Constant(false), nestedExpression); 162 | } 163 | 164 | private Expression Any( 165 | Type elementType, 166 | Expression parentExpression, 167 | ParameterExpression itemParameter, 168 | Expression subExpression) 169 | { 170 | return Expression.Call( 171 | typeof(Enumerable).GetGenericMethod(nameof(Enumerable.Any), 1, 2, elementType), 172 | parentExpression, 173 | Expression.Lambda(subExpression, itemParameter)); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/QueryR/QueryActions/IQueryAction.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | 3 | namespace QueryR.QueryActions 4 | { 5 | internal interface IQueryAction 6 | { 7 | QueryResult Execute(Query query, QueryResult queryResult); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/QueryR/QueryActions/PagingQueryAction.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | using System.Linq; 3 | 4 | namespace QueryR.QueryActions 5 | { 6 | internal class PagingQueryAction : IQueryAction 7 | { 8 | public QueryResult Execute(Query query, QueryResult queryResult) 9 | { 10 | if (query.PagingOptions != null) 11 | { 12 | //one-based indexing for pages 13 | //TODO: Configuration for 0-based indexing? 14 | var skipCount = (query.PagingOptions.PageNumber - 1) * query.PagingOptions.PageSize; 15 | queryResult.PagedQuery = queryResult.PagedQuery 16 | .Skip(skipCount) 17 | .Take(query.PagingOptions.PageSize); 18 | } 19 | 20 | return queryResult; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/QueryR/QueryActions/SortQueryAction.cs: -------------------------------------------------------------------------------- 1 | using QueryR.Extensions; 2 | using QueryR.QueryModels; 3 | using System; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | 7 | namespace QueryR.QueryActions 8 | { 9 | internal class SortQueryAction : IQueryAction 10 | { 11 | public QueryResult Execute(Query query, QueryResult queryResult) 12 | { 13 | var type = typeof(T); 14 | bool isFirst = true; 15 | 16 | foreach (var sort in query.Sorts ?? Enumerable.Empty()) 17 | { 18 | var parameter = Expression.Parameter(type, "t"); 19 | 20 | Expression memberExpression = parameter; 21 | foreach (var propertyName in sort.PropertyName.Split(new[] { "." }, StringSplitOptions.None)) 22 | { 23 | memberExpression = Expression.Property(memberExpression, propertyName); 24 | } 25 | 26 | var lambda = Expression.Lambda(memberExpression, parameter); 27 | var methodName = GetOrderMethodName(isFirst, sort.IsAscending); 28 | var method = typeof(Queryable).GetGenericMethod(methodName, 2, 2, type, memberExpression.Type); 29 | queryResult.PagedQuery = (IQueryable)method.Invoke(null, new object[] { queryResult.PagedQuery, lambda }); 30 | 31 | isFirst = false; 32 | } 33 | 34 | return queryResult; 35 | } 36 | private static string GetOrderMethodName(bool isFirst, bool isAscending) => isFirst ? 37 | isAscending ? nameof(Queryable.OrderBy) : nameof(Queryable.OrderByDescending) : 38 | isAscending ? nameof(Queryable.ThenBy) : nameof(Queryable.ThenByDescending); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/QueryR/QueryActions/SparseFieldsQueryAction.cs: -------------------------------------------------------------------------------- 1 | using QueryR.Extensions; 2 | using QueryR.QueryModels; 3 | using QueryR.Services; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using System.Reflection; 9 | 10 | namespace QueryR.QueryActions 11 | { 12 | internal class SparseFieldsQueryAction : IQueryAction 13 | { 14 | private readonly IMaxDepthService maxDepthService; 15 | 16 | public SparseFieldsQueryAction( 17 | IMaxDepthService maxDepthService) 18 | { 19 | this.maxDepthService = maxDepthService; 20 | } 21 | 22 | public QueryResult Execute(Query query, QueryResult queryResult) 23 | { 24 | 25 | if (query.SparseFields != null && query.SparseFields.Any()) 26 | { 27 | var type = typeof(T); 28 | var parameter = Expression.Parameter(type, "input"); 29 | var maxDepth = maxDepthService.GetMaxDepth(query); 30 | 31 | var init = GetSparseMemberInitExpression(type, query.SparseFields, parameter, new List(), maxDepth); 32 | var expression = Expression.Lambda>(init, parameter); 33 | 34 | queryResult.PagedQuery = queryResult.PagedQuery 35 | .Select(expression); 36 | } 37 | return queryResult; 38 | } 39 | 40 | private Expression GetSparseMemberInitExpression(Type sparseType, List sparseFields, ParameterExpression parameter, List memberExpressions, int? maxDepth, int depth = 0) 41 | { 42 | var properties = sparseType.GetProperties().ToList(); 43 | 44 | var sparseFieldsForThisType = sparseFields.FirstOrDefault(sf => sf.EntityName == sparseType.Name); 45 | if (sparseFieldsForThisType != null) 46 | { 47 | properties = properties.Where(p => sparseFieldsForThisType.PropertyNames.Contains(p.Name)).ToList(); 48 | } 49 | 50 | List memberBindings = new List(); 51 | foreach (var prop in properties) 52 | { 53 | var propExpression = memberExpressions.Any() ? (Expression) memberExpressions.Last() : parameter; 54 | var currentMemberExpression = Expression.Property(propExpression, prop.Name); 55 | memberExpressions.Add(currentMemberExpression); 56 | 57 | var property = sparseType.GetProperty(prop.Name); 58 | 59 | if (sparseFields.Any(sf => sf.EntityName == prop.PropertyType.Name)) 60 | { 61 | if (!maxDepth.HasValue || depth < maxDepth) 62 | { 63 | memberBindings.Add(GetNestedMemberBinding(property, sparseFields, parameter, memberExpressions, maxDepth, depth, currentMemberExpression)); 64 | } 65 | } 66 | else if (sparseFields.Any(sf => prop.PropertyType.GenericTypeArguments.Any(gta => gta.Name == sf.EntityName))) 67 | { 68 | if (maxDepth.HasValue && depth < maxDepth) 69 | { 70 | memberBindings.Add(GetListMemberBinding(prop, sparseFields, currentMemberExpression, maxDepth, depth)); 71 | } 72 | } 73 | else 74 | { 75 | memberBindings.Add(Expression.Bind(property, currentMemberExpression)); 76 | } 77 | memberExpressions.Remove(currentMemberExpression); 78 | } 79 | return Expression.MemberInit(Expression.New(sparseType), memberBindings); 80 | } 81 | 82 | 83 | 84 | private MemberBinding GetNestedMemberBinding(PropertyInfo property, List sparseFields, ParameterExpression parameter, List memberExpressions, int? maxDepth, int depth, MemberExpression currentMemberExpression) 85 | { 86 | var defaultValue = Expression.Constant(property.PropertyType.IsValueType ? Activator.CreateInstance(property.PropertyType) : null); 87 | var castDefaultValue = Expression.Convert(defaultValue, property.PropertyType); 88 | var nullCheck = Expression.Equal(Expression.Property(parameter, property.Name), Expression.Constant(null)); 89 | var nestedMemberExpression = GetSparseMemberInitExpression(property.PropertyType, sparseFields, parameter, memberExpressions, maxDepth, depth + 1); 90 | var nestedWithNullCheck = Expression.Condition(nullCheck, castDefaultValue, nestedMemberExpression); 91 | return Expression.Bind(property, nestedWithNullCheck); 92 | } 93 | 94 | private MemberBinding GetListMemberBinding(PropertyInfo prop, List sparseFields, MemberExpression currentMemberExpression, int? maxDepth, int depth) 95 | { 96 | var listType = prop.PropertyType.GenericTypeArguments.First(gta => sparseFields.Any(sf => gta.Name == sf.EntityName)); 97 | var selectMethodExpression = typeof(Enumerable).GetGenericMethod(nameof(Enumerable.Select), 2, 2, listType, listType); 98 | 99 | var listParameter = Expression.Parameter(listType, "listInput"); 100 | var listInit = GetSparseMemberInitExpression(listType, sparseFields, listParameter, new List(), maxDepth, depth + 1); 101 | 102 | var lambda = typeof(Expression).GetGenericMethod(nameof(Expression.Lambda), 1, 2, listType); 103 | 104 | var listExpression = Expression.Lambda(listInit, listParameter); 105 | 106 | var callSelectExpression = Expression.Call(selectMethodExpression, currentMemberExpression, listExpression); 107 | 108 | var toListMethodExpression = typeof(Enumerable).GetGenericMethod(nameof(Enumerable.ToList), 1, 1, listType); 109 | 110 | var callToListExpression = Expression.Call(toListMethodExpression, callSelectExpression); 111 | 112 | return Expression.Bind(prop, callToListExpression); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/QueryR/QueryModels/Filter.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.QueryModels 2 | { 3 | public class Filter: IQueryPart 4 | { 5 | public string PropertyName { get; set; } 6 | public FilterOperator Operator { get; set; } 7 | public string Value { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/QueryR/QueryModels/FilterOperator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace QueryR.QueryModels 5 | { 6 | public class FilterOperator 7 | { 8 | public string Code { get; } 9 | public string Name { get; } 10 | public Func ExpressionMethod { get; } 11 | public FilterOperator(string code, string name, Func expressionMethod) 12 | { 13 | Code = code; 14 | Name = name; 15 | ExpressionMethod = expressionMethod; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/QueryR/QueryModels/FilterOperators.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | 6 | namespace QueryR.QueryModels 7 | { 8 | public static class FilterOperators 9 | { 10 | public static readonly FilterOperator Equal = new FilterOperator( 11 | "eq", 12 | nameof(Equal), 13 | (property, target) => Expression.Equal(property, target) 14 | ); 15 | public static readonly FilterOperator GreaterThan = new FilterOperator( 16 | "gt", 17 | nameof(GreaterThan), 18 | (property, target) => Expression.GreaterThan(property, target) 19 | ); 20 | public static readonly FilterOperator GreaterThanOrEqual = new FilterOperator( 21 | "gte", 22 | nameof(GreaterThanOrEqual), 23 | (property, target) => Expression.GreaterThanOrEqual(property, target) 24 | ); 25 | public static readonly FilterOperator LessThan = new FilterOperator( 26 | "lt", 27 | nameof(LessThan), 28 | (property, target) => Expression.LessThan(property, target) 29 | ); 30 | public static readonly FilterOperator LessThanOrEqual = new FilterOperator( 31 | "lte", 32 | nameof(LessThanOrEqual), 33 | (property, target) => Expression.LessThanOrEqual(property, target) 34 | ); 35 | public static readonly FilterOperator NotEqual = new FilterOperator( 36 | "ne", 37 | nameof(NotEqual), 38 | (property, target) => Expression.NotEqual(property, target) 39 | ); 40 | public static readonly FilterOperator Contains = new FilterOperator( 41 | "ct", 42 | nameof(Contains), 43 | (property, target) => Expression.Call( 44 | property, 45 | typeof(string).GetMethod(nameof(string.Contains), new Type[] { typeof(string) }), 46 | target) 47 | ); 48 | public static readonly FilterOperator In = new FilterOperator( 49 | "in", 50 | nameof(In), 51 | (property, target) => Expression.Call( 52 | target, 53 | target.Type.GetMethod("Contains", new Type[] { property.Type }), 54 | property) 55 | ); 56 | public static readonly FilterOperator StartsWith = new FilterOperator( 57 | "sw", 58 | nameof(StartsWith), 59 | (property, target) => Expression.Call( 60 | property, 61 | typeof(string).GetMethod(nameof(string.StartsWith), new Type[] { typeof(string) }), 62 | target) 63 | ); 64 | public static readonly FilterOperator EndsWith = new FilterOperator( 65 | "ew", 66 | nameof(EndsWith), 67 | (property, target) => Expression.Call( 68 | property, 69 | typeof(string).GetMethod(nameof(string.EndsWith), new Type[] { typeof(string) }), 70 | target) 71 | ); 72 | public static readonly FilterOperator CollectionContains = new FilterOperator( 73 | "cc", 74 | nameof(CollectionContains), 75 | (property, target) => Expression.Call( 76 | property, 77 | property.Type.GetMethod("Contains", new Type[] { target.Type }), 78 | target) 79 | ); 80 | 81 | public static readonly IReadOnlyList Items = new List 82 | { 83 | Equal, 84 | GreaterThan, 85 | GreaterThanOrEqual, 86 | LessThan, 87 | LessThanOrEqual, 88 | NotEqual, 89 | Contains, 90 | In, 91 | StartsWith, 92 | EndsWith, 93 | CollectionContains, 94 | }; 95 | 96 | public static readonly Dictionary ToItem = Items.ToDictionary(item => item.Code, item => item); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/QueryR/QueryModels/IQueryPart.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.QueryModels 2 | { 3 | public interface IQueryPart { } 4 | } 5 | -------------------------------------------------------------------------------- /src/QueryR/QueryModels/PagingOptions.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.QueryModels 2 | { 3 | public class PagingOptions : IQueryPart 4 | { 5 | public int PageNumber { get; set; } 6 | public int PageSize { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/QueryR/QueryModels/Query.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace QueryR.QueryModels 4 | { 5 | public class Query 6 | { 7 | public List Filters { get; set; } = new List(); 8 | public PagingOptions PagingOptions { get; set; } 9 | public List Sorts { get; set; } = new List(); 10 | public List SparseFields { get; set; } = new List(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/QueryR/QueryModels/QueryResult.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace QueryR.QueryModels 4 | { 5 | /// 6 | /// Call to get the count and requested records. 7 | /// 8 | /// 9 | public class QueryResult 10 | { 11 | /// 12 | /// Call to get the number of total records matching the filters. 13 | /// 14 | public IQueryable CountQuery { get; set; } 15 | /// 16 | /// Call to get the requested records. 17 | /// 18 | public IQueryable PagedQuery { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/QueryR/QueryModels/Sort.cs: -------------------------------------------------------------------------------- 1 | namespace QueryR.QueryModels 2 | { 3 | public class Sort : IQueryPart 4 | { 5 | public bool IsAscending { get; set; } 6 | public string PropertyName { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/QueryR/QueryModels/SparseField.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace QueryR.QueryModels 4 | { 5 | public class SparseField : IQueryPart 6 | { 7 | public string EntityName { get; set; } 8 | public List PropertyNames { get; set; } = new List(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/QueryR/QueryR.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | QueryR 6 | 0.1.0 7 | Craig McCauley 8 | This library provides a simplified interface for performing ad-hoc queries against IQueryable objects. 9 | Copyright (c) Craig McCauley 2023 10 | https://github.com/craigmccauley/QueryR 11 | logo.png 12 | https://github.com/craigmccauley/QueryR 13 | README.md 14 | https://github.com/craigmccauley/QueryR.git 15 | git 16 | IQueryable;Query;Queries;CraigMcCauley;QueryR;adhoc;ad-hoc 17 | Maintenance required for QueryR.EntityFrameworkCore 18 | LICENSE 19 | latest 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/QueryR/QueryResultExtensions.cs: -------------------------------------------------------------------------------- 1 | using QueryR; 2 | using QueryR.QueryModels; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace QueryR 7 | { 8 | public static class QueryResultExtensions 9 | { 10 | public static int Count(this QueryResult queries) => queries.CountQuery.Count(); 11 | public static List ToList(this QueryResult queries) => queries.PagedQuery.ToList(); 12 | public static (int Count, List Items) GetCountAndList(this QueryResult queries) => (queries.Count(), queries.ToList()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/QueryR/Services/IMaxDepthService.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | 3 | namespace QueryR.Services 4 | { 5 | internal interface IMaxDepthService 6 | { 7 | int? GetMaxDepth(Query query); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/QueryR/Services/NullMaxDepthService.cs: -------------------------------------------------------------------------------- 1 | using QueryR.QueryModels; 2 | 3 | namespace QueryR.Services 4 | { 5 | internal class NullMaxDepthService : IMaxDepthService 6 | { 7 | public int? GetMaxDepth(Query query) => null; 8 | } 9 | } 10 | --------------------------------------------------------------------------------