├── .gitattributes ├── .github ├── hooks │ └── commit-msg └── workflows │ ├── publish-package.yml │ └── pull-request.yml ├── .gitignore ├── .releaserc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build ├── 83317562_m.jpg ├── 83317571_m.jpg ├── all-in-one.png ├── pack.ps1 ├── push.ps1 ├── restful-icon-2.png ├── restful-icon-github.png ├── restful-icon-nuget.png └── restful-icon.png ├── src ├── AspNetCore.IQueryable.Extensions │ ├── AspNetCore.IQueryable.Extensions.csproj │ ├── Attributes │ │ └── QueryOperatorAttribute.cs │ ├── Filter │ │ ├── ExpressionFactory.cs │ │ ├── ExpressionParser.cs │ │ ├── ExpressionParserCollection.cs │ │ ├── FiltersExtensions.cs │ │ ├── WhereClause.cs │ │ └── WhereOperator.cs │ ├── ICustomQueryable.cs │ ├── Pagination │ │ ├── IQueryPaging.cs │ │ └── PagingExtensions.cs │ ├── PrimitiveExtensions.cs │ ├── QueryableExtensions.cs │ └── Sort │ │ ├── IQuerySort.cs │ │ └── SortingExtensions.cs ├── IQueryable.Extensions.sln └── RESTFul.Api │ ├── Commands │ ├── RegisterUserCommand.cs │ └── UpdateUserCommand.cs │ ├── Configuration │ ├── AutomapperConfiguration.cs │ └── MappingProfile.cs │ ├── Contexts │ └── RestfulContext.cs │ ├── Controllers │ ├── ApiBaseController.cs │ └── UserController.cs │ ├── Models │ ├── Claim.cs │ └── User.cs │ ├── Notification │ ├── DomainNotification.cs │ ├── DomainNotificationHandler.cs │ ├── DomainNotificationMediatorService.cs │ └── IDomainNotificationMediatorService.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── RESTFul.Api.csproj │ ├── Service │ ├── DummyUserService.cs │ └── Interfaces │ │ └── IDummyUserService.cs │ ├── Startup.cs │ ├── ViewModels │ ├── UserSearch.cs │ └── UserViewModel.cs │ ├── WeatherForecast.cs │ ├── appsettings.Development.json │ └── appsettings.json └── tests └── RestFulTests ├── Fakers └── UserFaker.cs ├── FilterTests.cs ├── Models ├── Claim.cs ├── PagingMax.cs ├── User.cs └── UserSearch.cs ├── PagingTests.cs ├── RestFulTests.csproj └── SortingTests.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to check the commit log message. 4 | # Called by "git commit" with one argument, the name of the file 5 | # that has the commit message. The hook should exit with non-zero 6 | # status after issuing an appropriate message if it wants to stop the 7 | # commit. The hook is allowed to edit the commit message file. 8 | # 9 | # To enable this hook, rename this file to "commit-msg". 10 | 11 | # Uncomment the below to add a Signed-off-by line to the message. 12 | # Doing this in a hook is a bad idea in general, but the prepare-commit-msg 13 | # hook is more suited to it. 14 | # 15 | # SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') 16 | # grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" 17 | 18 | # This example catches duplicate Signed-off-by lines. 19 | 20 | test "" = "$(grep '^Signed-off-by: ' "$1" | 21 | sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { 22 | echo >&2 Duplicate Signed-off-by lines. 23 | exit 1 24 | } 25 | if ! head -1 "$1" | grep -qE "^(feat|fix|ci|chore|docs|test|style|refactor|chk)(\(.+?\))?(\!)?: .{1,}$"; then 26 | echo "Aborting commit. Your commit message is invalid. See some examples below:" >&2 27 | echo "feat(logging): added logs for failed signups" >&2 28 | echo "fix(homepage): fixed image gallery" >&2 29 | echo "test(homepage): updated tests" >&2 30 | echo "docs(readme): added new logging table information" >&2 31 | echo "For more information check https://www.conventionalcommits.org/en/v1.0.0/ for more details" >&2 32 | exit 1 33 | fi 34 | if ! head -1 "$1" | grep -qE "^.{1,50}$"; then 35 | echo "Aborting commit. Your commit message is too long." >&2 36 | exit 1 37 | fi -------------------------------------------------------------------------------- /.github/workflows/publish-package.yml: -------------------------------------------------------------------------------- 1 | name: Master - Publish packages 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | env: 8 | REPOSITORY_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json 9 | CURRENT_REPO_URL: https://github.com/${{ github.repository }} 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | defaults: 15 | run: 16 | working-directory: src 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup .NET 6 23 | uses: actions/setup-dotnet@v3 24 | with: 25 | dotnet-version: 6.0.x 26 | 27 | - name: Setup .NET 7 28 | uses: actions/setup-dotnet@v3 29 | with: 30 | dotnet-version: 7.0.x 31 | 32 | - name: Setup .NET 8 33 | uses: actions/setup-dotnet@v3 34 | with: 35 | dotnet-version: 8.0.x 36 | 37 | - name: Restore dependencies 38 | run: dotnet restore 39 | 40 | - name: Build 41 | run: dotnet build --no-restore 42 | 43 | - name: Semantic Release 44 | id: semantic 45 | uses: cycjimmy/semantic-release-action@v3 46 | with: 47 | semantic_version: 19.0.5 48 | extra_plugins: | 49 | @semantic-release/changelog 50 | @semantic-release/git 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | - name: Generate Package 55 | run: dotnet pack -c Release -o out -p:PackageVersion=${{ steps.semantic.outputs.new_release_version }} -p:RepositoryUrl=${{env.CURRENT_REPO_URL}} 56 | 57 | - name: Publish the package to nuget.org 58 | run: dotnet nuget push ./out/*.nupkg -n -d -k ${{ secrets.NUGET_AUTH_TOKEN}} -s https://api.nuget.org/v3/index.json 59 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Analisys 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | defaults: 11 | run: 12 | working-directory: ./src 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup .NET 6 19 | uses: actions/setup-dotnet@v3 20 | with: 21 | dotnet-version: 6.0.x 22 | 23 | - name: Setup .NET 7 24 | uses: actions/setup-dotnet@v3 25 | with: 26 | dotnet-version: 7.0.x 27 | 28 | - name: Setup .NET 8 29 | uses: actions/setup-dotnet@v3 30 | with: 31 | dotnet-version: 8.0.x 32 | 33 | - name: Restore dependencies 34 | run: dotnet restore 35 | 36 | - name: Build 37 | run: dotnet build --no-restore 38 | 39 | - name: Test 40 | run: dotnet test --no-build 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | 11 | # User-specific files (MonoDevelop/Xamarin Studio) 12 | *.userprefs 13 | 14 | # Build results 15 | [Dd]ebug/ 16 | [Dd]ebugPublic/ 17 | [Rr]elease/ 18 | [Rr]eleases/ 19 | [Xx]64/ 20 | [Xx]86/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | [Dd]ist/ 25 | 26 | # Visual Studio 2015 cache/options directory 27 | .vs/ 28 | .sonarqube/ 29 | .nuget/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # Vs publish files 34 | *.pubxml 35 | 36 | # MSTest test Results 37 | [Tt]est[Rr]esult*/ 38 | [Bb]uild[Ll]og.* 39 | 40 | # NUNIT 41 | *.VisualState.xml 42 | TestResult.xml 43 | coverage.opencover.xml 44 | 45 | # Build Results of an ATL Project 46 | [Dd]ebugPS/ 47 | [Rr]eleasePS/ 48 | dlldata.c 49 | 50 | # DNX 51 | project.lock.json 52 | artifacts/ 53 | 54 | *_i.c 55 | *_p.c 56 | *_i.h 57 | *.ilk 58 | *.meta 59 | *.obj 60 | *.pch 61 | *.pdb 62 | *.pgc 63 | *.pgd 64 | *.rsp 65 | *.sbr 66 | *.tlb 67 | *.tli 68 | *.tlh 69 | *.tmp 70 | *.tmp_proj 71 | *.log 72 | *.vspscc 73 | *.vssscc 74 | .builds 75 | *.pidb 76 | *.svclog 77 | *.scc 78 | 79 | # Chutzpah Test files 80 | _Chutzpah* 81 | 82 | # Visual C++ cache files 83 | ipch/ 84 | *.aps 85 | *.ncb 86 | *.opendb 87 | *.opensdf 88 | *.sdf 89 | *.cachefile 90 | *.VC.db 91 | 92 | # Visual Studio profiler 93 | *.psess 94 | *.vsp 95 | *.vspx 96 | *.sap 97 | 98 | # TFS 2012 Local Workspace 99 | $tf/ 100 | 101 | # Guidance Automation Toolkit 102 | *.gpState 103 | 104 | # ReSharper is a .NET coding add-in 105 | _ReSharper*/ 106 | *.[Rr]e[Ss]harper 107 | *.DotSettings.user 108 | 109 | # JustCode is a .NET coding add-in 110 | .JustCode 111 | 112 | # TeamCity is a build add-in 113 | _TeamCity* 114 | 115 | # DotCover is a Code Coverage Tool 116 | *.dotCover 117 | 118 | # NCrunch 119 | _NCrunch_* 120 | .*crunch*.local.xml 121 | nCrunchTemp_* 122 | 123 | # MightyMoose 124 | *.mm.* 125 | AutoTest.Net/ 126 | 127 | # Web workbench (sass) 128 | .sass-cache/ 129 | 130 | # Installshield output folder 131 | [Ee]xpress/ 132 | 133 | # DocProject is a documentation generator add-in 134 | DocProject/buildhelp/ 135 | DocProject/Help/*.HxT 136 | DocProject/Help/*.HxC 137 | DocProject/Help/*.hhc 138 | DocProject/Help/*.hhk 139 | DocProject/Help/*.hhp 140 | DocProject/Help/Html2 141 | DocProject/Help/html 142 | 143 | # Click-Once directory 144 | publish/ 145 | 146 | # Publish Web Output 147 | *.[Pp]ublish.xml 148 | *.azurePubxml 149 | 150 | # TODO: Un-comment the next line if you do not want to checkin 151 | # your web deploy settings because they may include unencrypted 152 | # passwords 153 | #*.pubxml 154 | *.publishproj 155 | 156 | # NuGet Packages 157 | *.nupkg 158 | *.snupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directory 178 | AppPackages/ 179 | BundleArtifacts/ 180 | 181 | # Visual Studio cache files 182 | # files ending in .cache can be ignored 183 | *.[Cc]ache 184 | # but keep track of directories ending in .cache 185 | !*.[Cc]ache/ 186 | 187 | # Others 188 | ClientBin/ 189 | [Ss]tyle[Cc]op.* 190 | ~$* 191 | *~ 192 | *.dbmdl 193 | *.dbproj.schemaview 194 | *.pfx 195 | *.publishsettings 196 | node_modules/ 197 | orleans.codegen.cs 198 | 199 | # RIA/Silverlight projects 200 | Generated_Code/ 201 | 202 | # Backup & report files from converting an old project file 203 | # to a newer Visual Studio version. Backup files are not needed, 204 | # because we have git ;-) 205 | _UpgradeReport_Files/ 206 | Backup*/ 207 | UpgradeLog*.XML 208 | UpgradeLog*.htm 209 | 210 | # SQL Server files 211 | *.mdf 212 | *.ldf 213 | 214 | # Business Intelligence projects 215 | *.rdl.data 216 | *.bim.layout 217 | *.bim_*.settings 218 | 219 | # Microsoft Fakes 220 | FakesAssemblies/ 221 | 222 | # GhostDoc plugin setting file 223 | *.GhostDoc.xml 224 | 225 | # Node.js Tools for Visual Studio 226 | .ntvs_analysis.dat 227 | 228 | # Visual Studio 6 build log 229 | *.plg 230 | 231 | # Visual Studio 6 workspace options file 232 | *.opt 233 | 234 | # Visual Studio LightSwitch build output 235 | **/*.HTMLClient/GeneratedArtifacts 236 | **/*.DesktopClient/GeneratedArtifacts 237 | **/*.DesktopClient/ModelManifest.xml 238 | **/*.Server/GeneratedArtifacts 239 | **/*.Server/ModelManifest.xml 240 | _Pvt_Extensions 241 | 242 | # LightSwitch generated files 243 | GeneratedArtifacts/ 244 | ModelManifest.xml 245 | 246 | # Paket dependency manager 247 | .paket/paket.exe 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # Sphinx 253 | *.opt 254 | docs/_build 255 | docs/.vscode 256 | 257 | # Log files 258 | **/*log*.txt 259 | *.db 260 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["master"], 3 | 4 | "plugins": [ 5 | "@semantic-release/commit-analyzer", 6 | "@semantic-release/release-notes-generator", 7 | "@semantic-release/changelog", 8 | [ 9 | "@semantic-release/github", 10 | { 11 | "successComment": false, 12 | "failTitle": false 13 | } 14 | ], 15 | "@semantic-release/git" 16 | ] 17 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [8.0.0](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/compare/v7.2.0...v8.0.0) (2024-02-08) 2 | 3 | 4 | ### Features 5 | 6 | * .NET 8 release ([972a0cd](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/commit/972a0cdce76125cc3079a8edf85f39b3e1622637)) 7 | 8 | 9 | ### BREAKING CHANGES 10 | 11 | * .NET 8 12 | 13 | # [7.2.0](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/compare/v7.1.1...v7.2.0) (2024-02-08) 14 | 15 | 16 | ### Features 17 | 18 | * .NET 8 release ([3ffb25b](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/commit/3ffb25bb3d66858ddb246c1b87fcabda2ff2b1e9)) 19 | 20 | ## [7.1.1](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/compare/v7.1.0...v7.1.1) (2023-09-08) 21 | 22 | 23 | ### Bug Fixes 24 | 25 | * github token ([4a9b5bb](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/commit/4a9b5bb1b61006b5db39f1c6aa6bf015e30c538d)) 26 | 27 | # [7.1.0](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/compare/v7.0.0...v7.1.0) (2023-09-08) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * semantic release ([e73ff6c](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/commit/e73ff6cd570fda5e517a040714a32aeed06cf73e)) 33 | 34 | 35 | ### Features 36 | 37 | * EqualsNullable ([0fd26d9](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/commit/0fd26d9fb9cf154e6888c9e5407e4699d7d63d0e)) 38 | * fix nuget key ([5d60cf2](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/commit/5d60cf2f08420fde95e3135e9b2930aa169a6545)) 39 | * net 7 ([9283c74](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/commit/9283c748919a8bcef0eb0a75f7e59e7bf21044b4)) 40 | * net7 ([fe2dbd8](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/commit/fe2dbd8722acbdc3f5de588c5dfa5893886590b2)) 41 | 42 | # [7.0.0](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/compare/v6.0.2...v7.0.0) (2022-11-17) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * tests ([1fde8bd](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/commit/1fde8bd222ffddca0d929bdfa1d28e5f2c4e029f)) 48 | 49 | 50 | ### Features 51 | 52 | * net 7 ([993e633](https://github.com/brunobritodev/AspNetCore.IQueryable.Extensions/commit/993e63331a8f53fa31d5a4888d51f2f449c2628d)) 53 | 54 | 55 | ### BREAKING CHANGES 56 | 57 | * dotnet 7 58 | 59 | ## [6.0.2](https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions/compare/v6.0.1...v6.0.2) (2022-03-22) 60 | 61 | 62 | ### Bug Fixes 63 | 64 | * ci ([2d9f83e](https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions/commit/2d9f83ee3b64694aad7b1b2ef4c80e30be5b948c)) 65 | 66 | ## [6.0.1](https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions/compare/v6.0.0...v6.0.1) (2022-03-22) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * max operator ([2a6dcca](https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions/commit/2a6dccac5c1a7a80a657461f24c913bbbbee4672)), closes [#9](https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions/issues/9) 72 | 73 | # [6.0.0](https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions/compare/v5.0.0...v6.0.0) (2022-02-27) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * Guid bug fix ([8191807](https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions/commit/819180787ace4f06aae176db8ddeea0e6fa2b4a1)), closes [#5](https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions/issues/5) [#2](https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions/issues/2) 79 | * semantic version ([54d7a6f](https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions/commit/54d7a6fff165953391aca8f1bdc907434dbd86cd)) 80 | 81 | 82 | ### Features 83 | 84 | * net 6 ([cb6591a](https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions/commit/cb6591a5c748e877233e5b4fa9b1adfe3d98b269)) 85 | 86 | 87 | ### BREAKING CHANGES 88 | 89 | * dotnet 6 version 90 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | ![image](https://github.com/brunohbrito/JP-Project/blob/master/docs/images/logo.png?raw=true) 5 | 6 | We are open to community contributions. 7 | 8 | First, Read this: [Being a good open source 9 | citizen](https://hackernoon.com/being-a-good-open-source-citizen-9060d0ab9732). 10 | There are a couple of guidelines you should follow so we can handle this 11 | without too much effort. 12 | 13 | How to contribute? 14 | ------------------ 15 | 16 | Looking to contribute something to Jp Ploject? **Here's how you can 17 | help.** 18 | 19 | Contributing to Jp Project 20 | -------------------------- 21 | 22 | Please take a moment to review this document in order to make the 23 | contribution process easy and effective for everyone involved. 24 | 25 | Following these guidelines helps to communicate that you respect the 26 | time of the developers managing and developing this open source project. 27 | In return, we will be lovely persons that respect in addressing your 28 | issue or assessing patches and features. 29 | 30 | **The easiest way to contribute is to open an issue and start a 31 | discussion.** 32 | 33 | ### Using the issue tracker 34 | 35 | The [issue tracker](https://github.com/brunohbrito/JP-Project/issues) is 36 | the preferred channel for [bug reports](\#bug-reports), [features 37 | requests](\#feature-requests) and [submitting pull 38 | requests](\#pull-requests), but please respect the following 39 | restrictions: 40 | 41 | - Please **do not** use the issue tracker for personal support 42 | requests. 43 | - Please **do not** post comments consisting solely of "+1" or 44 | ":thumbsup:". Use [GitHub's "reactions" 45 | feature](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) 46 | instead. 47 | 48 | ### Bug reports 49 | 50 | A bug is a \_demonstrable problem\_ that is caused by the code in the 51 | repository. Good bug reports are extremely helpful! 52 | 53 | Guidelines for bug reports: 54 | 55 | 0. **Validate and lint your code** — to ensure your problem isn't 56 | caused by a simple error in your own code. 57 | 1. **Use the GitHub issue search** — check if the issue has 58 | already been reported. 59 | 2. **Check if the issue has been fixed** — try to reproduce it 60 | using the latest master or development branch in the repository. 61 | 62 | A good bug report shouldn't leave others needing to chase you up for 63 | more information. Please try to be as detailed as possible in your 64 | report. What is your environment? What steps will reproduce the issue? 65 | Did you check the logs? All these details will help people to fix any 66 | potential bugs. 67 | 68 | Example: 69 | 70 | > **Short and descriptive example bug report title** 71 | > 72 | > A summary of the issue and the OS environment in which it occurs. If 73 | > suitable, include the steps required to reproduce the bug. 74 | > 75 | > 1. This is the first step 76 | > 2. This is the second step 77 | > 3. Further steps, etc. 78 | > 79 | > Any other information you want to share that is relevant to the issue 80 | > being reported. This might include the lines of code that you have 81 | > identified as causing the bug, and potential solutions (and your 82 | > opinions on their merits). 83 | 84 | 85 | ### Feature requests 86 | 87 | Feature requests are welcome. Before opening a feature request, please 88 | take a moment to find out whether your idea fits with the scope and aims 89 | of the project. It's up to *you* to make a strong case to convince the 90 | project's developers of the merits of this feature. Please provide as 91 | much detail and context as possible. 92 | 93 | ### Pull requests 94 | 95 | **Issue First** Before even writing the first line of code raise an 96 | issue and get buy in on your proposal from the maintainers. There’s 97 | several reasons for this, people might already be working on the issue, 98 | the issue might not be an issue or by design, but mainly just letting 99 | the community know your working on something, it gets “assigned” to you 100 | and you get implementation detail feedback early. All to reduce chance 101 | of redoing work or getting your contribution rejected. 102 | 103 | Good pull requests—patches, improvements, new features—are a fantastic 104 | help. They should remain focused in scope and avoid containing unrelated 105 | commits. 106 | 107 | **Please ask first** before embarking on any significant pull request 108 | (e.g. implementing features, refactoring code, porting to a different 109 | language), otherwise you risk spending a lot of time working on 110 | something that the project's developers might not want to merge into the 111 | project. 112 | 113 | Adhering to the following process is the best way to get your work 114 | included in the project: 115 | 116 | 1. [Fork](https://help.github.com/fork-a-repo/) the project, clone your 117 | fork, and configure the remotes: 118 | 119 | ``` {.sourceCode .} 120 | # Clone your fork of the repo into the current directory 121 | git clone https://github.com//free-bootstrap-admin-template.git 122 | # Navigate to the newly cloned directory 123 | cd free-bootstrap-admin-template 124 | # Assign the original repo to a remote called "upstream" 125 | git remote add upstream https://github.com/coreui/coreui-free-bootstrap-admin-template.git 126 | ``` 127 | 128 | 2. If you cloned a while ago, get the latest changes from upstream: 129 | 130 | ``` {.sourceCode .} 131 | git checkout master 132 | git pull upstream master 133 | ``` 134 | 135 | 3. Create a new topic branch (off the main project development branch) 136 | to contain your feature, change, or fix: 137 | 138 | ``` {.sourceCode .} 139 | git checkout -b 140 | ``` 141 | 142 | 4. Commit your changes in logical chunks. Please adhere to these [git 143 | commit message 144 | guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 145 | or your code is unlikely to be merged into the main project. Use 146 | Git's [interactive 147 | rebase](https://help.github.com/articles/interactive-rebase) feature 148 | to tidy up your commits before making them public. 149 | 5. Locally merge (or rebase) the upstream development branch into your 150 | topic branch: 151 | 152 | ``` {.sourceCode .} 153 | git pull [--rebase] upstream master 154 | ``` 155 | 156 | 6. Push your topic branch up to your fork: 157 | 158 | ``` {.sourceCode .} 159 | git push origin 160 | ``` 161 | 162 | 7. [Open a Pull 163 | Request](https://help.github.com/articles/using-pull-requests/) with 164 | a clear title and description against the master branch. 165 | 166 | **IMPORTANT**: By submitting a patch, you agree to allow the project 167 | owners to license your work under the terms of the [MIT 168 | License](https://github.com/brunohbrito/JP-Project/blob/master/LICENSE). 169 | 170 | ### Platform 171 | 172 | Backend of JpProject is built against ASP.NET Core and runs on .NET 173 | Framework 4.6.1 (and higher) and .NET Core 2.1 (and higher). 174 | 175 | The Frontend SPA is built against Angular 6 and runs on Node and Angular 176 | Cli 6. 177 | 178 | General feedback and discussions? 179 | --------------------------------- 180 | 181 | Please start a discussion on the [issue 182 | tracker](https://github.com/brunohbrito/JP-Project/issues). 183 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bruno Brito 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 | 2 | # ASP.NET Core IQueryable Extensions 3 | ![Nuget](https://img.shields.io/nuget/v/AspNetCore.IQueryable.Extensions)![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/brunohbrito/AspNetCore.IQueryable.Extensions/14)[![Build Status](https://dev.azure.com/brunohbrito/AspNetCore.IQueryable.Extensions/_apis/build/status/brunohbrito.AspNetCore.IQueryable.Extensions?branchName=master)](https://dev.azure.com/brunohbrito/AspNetCore.IQueryable.Extensions/_build/latest?definitionId=16&branchName=master) 4 | 5 | 6 | Lightweight API that construct custom IQueryable LINQ Extensions to help you filter, sort and paginate your objects from a custom Class and expose it as GET parameter. 7 | 8 | 9 | ## Table of Contents ## 10 | 11 | - [ASP.NET Core IQueryable Extensions](#aspnet-core-iqueryable-extensions) 12 | - [Table of Contents](#table-of-contents) 13 | - [How](#how) 14 | - [Sort](#sort) 15 | - [Paging](#paging) 16 | - [All in One](#all-in-one) 17 | - [Criterias for filtering](#criterias-for-filtering) 18 | - [Different database fields name](#different-database-fields-name) 19 | - [Or Operator](#or-operator) 20 | - [Why](#why) 21 | - [License](#license) 22 | 23 | ------------------ 24 | 25 | # How # 26 | 27 | You should install [AspNetCore.IQueryable.Extensions with NuGet](https://www.nuget.org/packages/AspNetCore.IQueryable.Extensions): 28 | ``` 29 | Install-Package AspNetCore.IQueryable.Extensions 30 | ``` 31 | 32 | Or via the .NET Core command line interface: 33 | 34 | ``` 35 | dotnet add package AspNetCore.IQueryable.Extensions 36 | ``` 37 | 38 | Create a class with filtering properties: 39 | 40 | ``` c# 41 | public class UserSearch 42 | { 43 | public string Username { get; set; } 44 | 45 | [QueryOperator(Operator = WhereOperator.GreaterThan)] 46 | public DateTime? Birthday { get; set; } 47 | 48 | [QueryOperator(Operator = WhereOperator.Contains, HasName = "Firstname")] 49 | public string Name { get; set; } 50 | } 51 | ``` 52 | 53 | Expose this class as GET in your API and use it to Filter your collection: 54 | 55 | ``` c# 56 | [HttpGet("")] 57 | public async Task>> Get([FromQuery] UserSearch search) 58 | { 59 | var result = await context.Users.AsQueryable().Filter(search).ToListAsync(); 60 | 61 | return Ok(result); 62 | } 63 | ``` 64 | 65 | Done! 66 | 67 | You can send a request to you API like this: `https://www.myapi.com/users?username=bhdebrito@gmail.com&name=bruno` 68 | 69 | 70 | The component will construct a IQueryable. If you are using an ORM like EF Core it construct a SQL query based in IQueryable, improving performance. 71 | 72 | # Sort 73 | 74 | A comma separetd fields. E.g username,birthday,-firstname 75 | 76 | **-**(minus) for **descending** **+**(plus) or nothing for **ascending** 77 | 78 | ``` c# 79 | public class UserSearch 80 | { 81 | public string Username { get; set; } 82 | 83 | public string SortBy { get; set; } 84 | } 85 | ``` 86 | 87 | 88 | ``` c# 89 | [HttpGet("")] 90 | public async Task>> Get([FromQuery] UserSearch search) 91 | { 92 | var result = await context.Users.AsQueryable().Filter(search).Sort(search.SortBy).ToListAsync(); 93 | 94 | return Ok(result); 95 | } 96 | ``` 97 | Example GET: `https://www.myapi.com/users?username=bruno&sortby=username,-birtday` 98 | 99 | 100 | # Paging 101 | 102 | A exclusive extension for paging 103 | 104 | 105 | ``` c# 106 | public class UserSearch 107 | { 108 | public string Username { get; set; } 109 | 110 | [QueryOperator(Max = 100)] 111 | public int Limit { get; set; } = 10; 112 | 113 | public int Offset { get; set; } = 0; 114 | } 115 | ``` 116 | 117 | **Limit** is the total results in response. **Offset** is how many rows to Skip. Optionally you can set the `Max` attribute to restrict the max items of pagination. 118 | 119 | ``` c# 120 | [HttpGet("")] 121 | public async Task>> Get([FromQuery] UserSearch search) 122 | { 123 | var result = await context.Users.AsQueryable().Filter(search).Paging(search.Limit, search.Offset).ToListAsync(); 124 | 125 | return Ok(result); 126 | } 127 | ``` 128 | 129 | Example GET: `https://www.myapi.com/users?username=bruno&limit=10&offset=20` 130 | 131 | 132 | # All in One 133 | 134 | 135 | Create a search class like this 136 | 137 | ``` c# 138 | public class UserSearch : IQuerySort, IQueryPaging 139 | { 140 | public string Username { get; set; } 141 | 142 | [QueryOperator(Operator = WhereOperator.GreaterThan)] 143 | public DateTime? Birthday { get; set; } 144 | 145 | [QueryOperator(Operator = WhereOperator.Contains, HasName = "Firstname")] 146 | public string Name { get; set; } 147 | 148 | public int Offset { get; set; } 149 | public int Limit { get; set; } = 10; 150 | public string Sort { get; set; } 151 | } 152 | ``` 153 | Call Apply method, instead calling each one with custom parameters. 154 | 155 | ``` c# 156 | [HttpGet("")] 157 | public async Task>> Get([FromQuery] UserSearch search) 158 | { 159 | var result = await context.Users.AsQueryable().Apply(search).ToListAsync(); 160 | 161 | return Ok(result); 162 | } 163 | ``` 164 | 165 | `IQuerySort` and `IQueryPaging` give the ability for method `Apply` use **Sort** and **Pagination**. If don't wanna sort, just use pagination remove `IQuerySort` Interface from Class. 166 | 167 | # Criterias for filtering 168 | 169 | When creating a Search class, you can define criterias by decorating your properties: 170 | 171 | ``` c# 172 | public class CustomUserSearch 173 | { 174 | [QueryOperator(Operator = WhereOperator.Equals, UseNot = true)] 175 | public string Category { get; set; } 176 | 177 | [QueryOperator(Operator = WhereOperator.GreaterThanOrEqualTo)] 178 | public int OlderThan { get; set; } 179 | 180 | [QueryOperator(Operator = WhereOperator.StartsWith, CaseSensitive = true)] 181 | public string Username { get; set; } 182 | 183 | [QueryOperator(Operator = WhereOperator.GreaterThan)] 184 | public DateTime? Birthday { get; set; } 185 | 186 | [QueryOperator(Operator = WhereOperator.Contains)] 187 | public string Name { get; set; } 188 | } 189 | ``` 190 | 191 | # Different database fields name 192 | 193 | You can specify different property name to hide you properties original fields 194 | 195 | ``` c# 196 | public class CustomUserSearch 197 | { 198 | [QueryOperator(Operator = WhereOperator.Equals, UseNot = true, HasName = "Privilege")] 199 | public string Category { get; set; } 200 | 201 | [QueryOperator(Operator = WhereOperator.GreaterThanOrEqualTo)] 202 | public int OlderThan { get; set; } 203 | 204 | [QueryOperator(Operator = WhereOperator.StartsWith, CaseSensitive = true, HasName = "Username")] 205 | public string Email { get; set; } 206 | } 207 | ``` 208 | 209 | # Or Operator 210 | 211 | You can use Or operator for your queries. 212 | 213 | ``` c# 214 | public class CustomUserSearch 215 | { 216 | [QueryOperator(Operator = WhereOperator.Equals, UseOr = true] 217 | public string Category { get; set; } 218 | 219 | [QueryOperator(Operator = WhereOperator.GreaterThanOrEqualTo)] 220 | public int OlderThan { get; set; } 221 | 222 | [QueryOperator(Operator = WhereOperator.StartsWith, CaseSensitive = true, HasName = "Username")] 223 | public string Email { get; set; } 224 | } 225 | ``` 226 | Take care, Or replace all "AND" at query. 227 | 228 | # Why 229 | 230 | RESTFul api's are hard to create. See the example get: 231 | 232 | `https://www.myapi.com/users?name=bruno&age_lessthan=30&sortby=name,-age&limit=20&offset=20` 233 | 234 | How many code you need to perform such search? A custom filter for each Field, maybe a for and a switch for each `sortby` and after all apply pagination. 235 | How many resources your api have? 236 | 237 | This lightweight API create a custom IQueryable based in Querystring to help your ORM or LINQ to filter data. 238 | 239 | --------------- 240 | 241 | # License 242 | 243 | AspNet.Core.IQueryable.Extensions is Open Source software and is released under the MIT license. This license allow the use of AspNet.Core.IQueryable.Extensions in free and commercial applications and libraries without restrictions. 244 | -------------------------------------------------------------------------------- /build/83317562_m.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunobritodev/AspNetCore.IQueryable.Extensions/19a8b8820a3c1ff766bf33f73cd17864b75ffb85/build/83317562_m.jpg -------------------------------------------------------------------------------- /build/83317571_m.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunobritodev/AspNetCore.IQueryable.Extensions/19a8b8820a3c1ff766bf33f73cd17864b75ffb85/build/83317571_m.jpg -------------------------------------------------------------------------------- /build/all-in-one.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunobritodev/AspNetCore.IQueryable.Extensions/19a8b8820a3c1ff766bf33f73cd17864b75ffb85/build/all-in-one.png -------------------------------------------------------------------------------- /build/pack.ps1: -------------------------------------------------------------------------------- 1 | param([string] $v) 2 | 3 | if (!$v) 4 | { 5 | $version = '3.1.9-prerelease1.' + $([System.DateTime]::Now.ToString('MM-dd-HHmmss')) 6 | } 7 | else{ 8 | $version = $v 9 | } 10 | Write-Host 'Version: ' $version 11 | get-childitem * -include *.nupkg | remove-item 12 | dotnet build ..\src\IQueryable.Extensions.sln 13 | dotnet test ..\src\IQueryable.Extensions.sln 14 | dotnet pack ..\src\IQueryable.Extensions.sln -o .\ -p:PackageVersion=$version -------------------------------------------------------------------------------- /build/push.ps1: -------------------------------------------------------------------------------- 1 | param([string] $source = "../.nuget", 2 | [switch] $prod) 3 | 4 | if($prod) 5 | { 6 | $source = "https://api.nuget.org/v3/index.json" 7 | } 8 | 9 | $files = Get-ChildItem -recurse -filter *.nupkg 10 | 11 | foreach ($file in $files) { 12 | if($prod) 13 | { 14 | dotnet nuget push $file.Name -s $source 15 | } 16 | else 17 | { 18 | nuget add $file.Name -source $source 19 | } 20 | } 21 | 22 | $files = Get-ChildItem -recurse -filter *.snupkg 23 | 24 | foreach ($file in $files) { 25 | if($prod) 26 | { 27 | dotnet nuget push $file.Name -s $source 28 | } 29 | else 30 | { 31 | nuget add $file.Name -source $source 32 | } 33 | } -------------------------------------------------------------------------------- /build/restful-icon-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunobritodev/AspNetCore.IQueryable.Extensions/19a8b8820a3c1ff766bf33f73cd17864b75ffb85/build/restful-icon-2.png -------------------------------------------------------------------------------- /build/restful-icon-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunobritodev/AspNetCore.IQueryable.Extensions/19a8b8820a3c1ff766bf33f73cd17864b75ffb85/build/restful-icon-github.png -------------------------------------------------------------------------------- /build/restful-icon-nuget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunobritodev/AspNetCore.IQueryable.Extensions/19a8b8820a3c1ff766bf33f73cd17864b75ffb85/build/restful-icon-nuget.png -------------------------------------------------------------------------------- /build/restful-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunobritodev/AspNetCore.IQueryable.Extensions/19a8b8820a3c1ff766bf33f73cd17864b75ffb85/build/restful-icon.png -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/AspNetCore.IQueryable.Extensions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1;net6.0;net7.0;net8.0 5 | 3.1.0 6 | Bruno Brito 7 | https://jpproject.blob.core.windows.net/images/restful-icon-nuget.png 8 | RESTFul API Extensions IQueryable linq 9 | IQueryable Extensions .NET 10 | Extensions to help build truly RESTFul API's 11 | en 12 | MIT 13 | https://github.com/brunohbrito/AspNet.Core.RESTFul.Extensions/ 14 | git 15 | https://github.com/brunohbrito/AspNetCore.IQueryable.Extensions 16 | true 17 | snupkg 18 | AspNetCore.IQueryable.Extensions 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | <_CustomFiles Include="../../.github/hooks/commit-msg" /> 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/Attributes/QueryOperatorAttribute.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions.Filter; 2 | using System; 3 | 4 | namespace AspNetCore.IQueryable.Extensions.Attributes 5 | { 6 | [AttributeUsage(AttributeTargets.Property)] 7 | public class QueryOperatorAttribute : Attribute 8 | { 9 | public WhereOperator Operator { get; set; } = WhereOperator.Equals; 10 | public bool UseNot { get; set; } = false; 11 | public bool CaseSensitive { get; set; } = true; 12 | public string HasName { get; set; } 13 | public int Max { get; set; } 14 | public bool UseOr { get; set; } = false; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/Filter/ExpressionFactory.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions.Attributes; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | 8 | namespace AspNetCore.IQueryable.Extensions.Filter 9 | { 10 | internal static class ExpressionFactory 11 | { 12 | internal static ExpressionParserCollection GetOperators(ICustomQueryable model) 13 | { 14 | var expressions = new ExpressionParserCollection(); 15 | 16 | var type = model.GetType(); 17 | expressions.ParameterExpression = Expression.Parameter(typeof(TEntity), "model"); 18 | 19 | foreach (var propertyInfo in type.GetProperties()) 20 | { 21 | var criteria = GetCriteria(model, propertyInfo); 22 | if (criteria == null) 23 | continue; 24 | 25 | if (!typeof(TEntity).HasProperty(criteria.FieldName) && !criteria.FieldName.Contains(".")) 26 | continue; 27 | 28 | dynamic propertyValue = expressions.ParameterExpression; 29 | 30 | foreach (var part in criteria.FieldName.Split('.')) 31 | { 32 | propertyValue = Expression.PropertyOrField(propertyValue, part); 33 | } 34 | 35 | var expressionData = new ExpressionParser(); 36 | expressionData.FieldToFilter = propertyValue; 37 | expressionData.FilterBy = GetClosureOverConstant(criteria.Property.GetValue(model, null), GetNonNullable(criteria.Property.PropertyType)); 38 | expressionData.Criteria = criteria; 39 | 40 | if (criteria.Property.GetValue(model, null) != null) 41 | expressions.Add(expressionData); 42 | } 43 | 44 | return expressions; 45 | } 46 | private static Type GetNonNullable(Type propertyType) 47 | { 48 | if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) 49 | { 50 | return Nullable.GetUnderlyingType(propertyType); 51 | } 52 | 53 | return propertyType; 54 | } 55 | static bool IsNullableType(Type t) 56 | { 57 | return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>); 58 | } 59 | internal static WhereClause GetCriteria(ICustomQueryable model, PropertyInfo propertyInfo) 60 | { 61 | bool isCollection = propertyInfo.IsPropertyACollection(); 62 | //if (!isCollection && propertyInfo.IsPropertyObject(model)) 63 | // return null; 64 | 65 | var criteria = new WhereClause(); 66 | 67 | var attr = Attribute.GetCustomAttributes(propertyInfo); 68 | // Check for the AnimalType attribute. 69 | if (attr.Any(a => a.GetType() == typeof(QueryOperatorAttribute))) 70 | { 71 | var data = (QueryOperatorAttribute)attr.First(a => a.GetType() == typeof(QueryOperatorAttribute)); 72 | criteria.UpdateAttributeData(data); 73 | if (data.Operator != WhereOperator.Contains && isCollection) 74 | throw new ArgumentException($"{propertyInfo.Name} - For array the only Operator available is Contains"); 75 | } 76 | 77 | if (isCollection) 78 | criteria.Operator = WhereOperator.Contains; 79 | 80 | var customValue = propertyInfo.GetValue(model, null); 81 | if (customValue == null) 82 | return null; 83 | 84 | criteria.UpdateValues(propertyInfo); 85 | return criteria; 86 | } 87 | 88 | // Workaround to ensure that the filter value gets passed as a parameter in generated SQL from EF Core 89 | // See https://github.com/aspnet/EntityFrameworkCore/issues/3361 90 | // Expression.Constant passed the target type to allow Nullable comparison 91 | // See http://bradwilson.typepad.com/blog/2008/07/creating-nullab.html 92 | internal static Expression GetClosureOverConstant(T constant, Type targetType) 93 | { 94 | return Expression.Constant(constant, targetType); 95 | } 96 | 97 | 98 | internal static List GetCriterias(ICustomQueryable searchModel) 99 | { 100 | var type = searchModel.GetType(); 101 | var criterias = new List(); 102 | // Iterate through all the methods of the class. 103 | foreach (var propertyInfo in type.GetProperties()) 104 | { 105 | bool isCollection = propertyInfo.IsPropertyACollection(); 106 | if (!isCollection && propertyInfo.IsPropertyObject(searchModel)) 107 | continue; 108 | 109 | var criteria = new WhereClause(); 110 | 111 | var attr = Attribute.GetCustomAttributes(propertyInfo).FirstOrDefault(); 112 | // Check for the AnimalType attribute. 113 | if (attr?.GetType() == typeof(QueryOperatorAttribute)) 114 | { 115 | var data = (QueryOperatorAttribute)attr; 116 | criteria.UpdateAttributeData(data); 117 | if (data.Operator != WhereOperator.Contains && isCollection) 118 | throw new ArgumentException($"{propertyInfo.Name} - For array the only Operator available is Contains"); 119 | } 120 | 121 | if (isCollection) 122 | criteria.Operator = WhereOperator.Contains; 123 | 124 | var customValue = propertyInfo.GetValue(searchModel, null); 125 | if (customValue == null) 126 | continue; 127 | 128 | criteria.UpdateValues(propertyInfo); 129 | criterias.Add(criteria); 130 | } 131 | 132 | return criterias.OrderBy(o => o.UseOr).ToList(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/Filter/ExpressionParser.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace AspNetCore.IQueryable.Extensions.Filter 4 | { 5 | internal class ExpressionParser 6 | { 7 | public WhereClause Criteria { get; set; } 8 | public Expression FieldToFilter { get; set; } 9 | public Expression FilterBy { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/Filter/ExpressionParserCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | 5 | namespace AspNetCore.IQueryable.Extensions.Filter 6 | { 7 | internal class ExpressionParserCollection : List 8 | { 9 | public ParameterExpression ParameterExpression { get; set; } 10 | 11 | public List Ordered() 12 | { 13 | return this.OrderBy(b => b.Criteria.UseOr).ToList(); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/Filter/FiltersExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | 6 | namespace AspNetCore.IQueryable.Extensions.Filter 7 | { 8 | public static class FiltersExtensions 9 | { 10 | public static IQueryable Filter(this IQueryable result, ICustomQueryable model) 11 | { 12 | if (model == null) 13 | { 14 | return result; 15 | } 16 | 17 | var lastExpression = result.FilterExpression(model); 18 | return lastExpression == null 19 | ? result 20 | : result.Where(lastExpression); 21 | } 22 | 23 | public static Expression> FilterExpression(this IQueryable result, ICustomQueryable model) 24 | { 25 | if (model == null) 26 | { 27 | return null; 28 | } 29 | 30 | Expression lastExpression = null; 31 | 32 | var operations = ExpressionFactory.GetOperators(model); 33 | foreach (var expression in operations.Ordered()) 34 | { 35 | if (!expression.Criteria.CaseSensitive) 36 | { 37 | expression.FieldToFilter = Expression.Call(expression.FieldToFilter, 38 | typeof(string).GetMethods() 39 | .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); 40 | 41 | expression.FilterBy = Expression.Call(expression.FilterBy, 42 | typeof(string).GetMethods() 43 | .First(m => m.Name == "ToUpper" && m.GetParameters().Length == 0)); 44 | } 45 | 46 | 47 | var actualExpression = GetExpression(expression); 48 | 49 | if (expression.Criteria.UseNot) 50 | { 51 | actualExpression = Expression.Not(actualExpression); 52 | } 53 | 54 | if (lastExpression == null) 55 | { 56 | lastExpression = actualExpression; 57 | } 58 | else 59 | { 60 | if (expression.Criteria.UseOr) 61 | lastExpression = Expression.Or(lastExpression, actualExpression); 62 | else 63 | lastExpression = Expression.And(lastExpression, actualExpression); 64 | } 65 | } 66 | 67 | return lastExpression != null ? Expression.Lambda>(lastExpression, operations.ParameterExpression) : null; 68 | } 69 | 70 | 71 | 72 | private static Expression GetExpression(ExpressionParser expression) 73 | { 74 | 75 | switch (expression.Criteria.Operator) 76 | { 77 | case WhereOperator.Equals: 78 | return Expression.Equal(expression.FieldToFilter, expression.FilterBy); 79 | case WhereOperator.NotEquals: 80 | return Expression.NotEqual(expression.FieldToFilter, expression.FilterBy); 81 | case WhereOperator.GreaterThan: 82 | return Expression.GreaterThan(expression.FieldToFilter, expression.FilterBy); 83 | case WhereOperator.LessThan: 84 | return Expression.LessThan(expression.FieldToFilter, expression.FilterBy); 85 | case WhereOperator.GreaterThanOrEqualTo: 86 | return Expression.GreaterThanOrEqual(expression.FieldToFilter, expression.FilterBy); 87 | case WhereOperator.LessThanOrEqualTo: 88 | return Expression.LessThanOrEqual(expression.FieldToFilter, expression.FilterBy); 89 | case WhereOperator.Contains: 90 | return ContainsExpression(expression); 91 | case WhereOperator.GreaterThanOrEqualWhenNullable: 92 | return GreaterThanOrEqualWhenNullable(expression.FieldToFilter, expression.FilterBy); 93 | case WhereOperator.LessThanOrEqualWhenNullable: 94 | return LessThanOrEqualWhenNullable(expression.FieldToFilter, expression.FilterBy); 95 | case WhereOperator.EqualsWhenNullable: 96 | return EqualsWhenNullable(expression.FieldToFilter, expression.FilterBy); 97 | case WhereOperator.StartsWith: 98 | return Expression.Call(expression.FieldToFilter, 99 | typeof(string).GetMethods() 100 | .First(m => m.Name == "StartsWith" && m.GetParameters().Length == 1), 101 | expression.FilterBy); 102 | default: 103 | return Expression.Equal(expression.FieldToFilter, expression.FilterBy); 104 | } 105 | } 106 | 107 | private static Expression LessThanOrEqualWhenNullable(Expression e1, Expression e2) 108 | { 109 | if (IsNullableType(e1.Type) && !IsNullableType(e2.Type)) 110 | e2 = Expression.Convert(e2, e1.Type); 111 | 112 | else if (!IsNullableType(e1.Type) && IsNullableType(e2.Type)) 113 | e1 = Expression.Convert(e1, e2.Type); 114 | 115 | return Expression.LessThanOrEqual(e1, e2); 116 | } 117 | 118 | private static Expression GreaterThanOrEqualWhenNullable(Expression e1, Expression e2) 119 | { 120 | if (IsNullableType(e1.Type) && !IsNullableType(e2.Type)) 121 | e2 = Expression.Convert(e2, e1.Type); 122 | 123 | else if (!IsNullableType(e1.Type) && IsNullableType(e2.Type)) 124 | e1 = Expression.Convert(e1, e2.Type); 125 | 126 | return Expression.GreaterThanOrEqual(e1, e2); 127 | } 128 | 129 | private static Expression EqualsWhenNullable(Expression e1, Expression e2) 130 | { 131 | if (IsNullableType(e1.Type) && !IsNullableType(e2.Type)) 132 | e2 = Expression.Convert(e2, e1.Type); 133 | 134 | else if (!IsNullableType(e1.Type) && IsNullableType(e2.Type)) 135 | e1 = Expression.Convert(e1, e2.Type); 136 | 137 | return Expression.Equal(e1, e2); 138 | } 139 | 140 | private static bool IsNullableType(Type t) 141 | { 142 | return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>); 143 | } 144 | 145 | private static Expression ContainsExpression(ExpressionParser expression) 146 | { 147 | if (expression.Criteria.Property.IsPropertyACollection()) 148 | { 149 | var methodToApplyContains = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) 150 | .Single(x => x.Name == "Contains" && x.GetParameters().Length == 2) 151 | .MakeGenericMethod(expression.FieldToFilter.Type); 152 | return Expression.Call(methodToApplyContains, expression.FilterBy, expression.FieldToFilter); 153 | } 154 | else 155 | { 156 | var methodToApplyContains = expression.FieldToFilter.Type.GetMethods() 157 | .First(m => m.Name == "Contains" && m.GetParameters().Length == 1); 158 | 159 | return Expression.Call(expression.FieldToFilter, methodToApplyContains, expression.FilterBy); 160 | } 161 | 162 | } 163 | 164 | 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/Filter/WhereClause.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions.Attributes; 2 | using System.Diagnostics; 3 | using System.Reflection; 4 | 5 | namespace AspNetCore.IQueryable.Extensions.Filter 6 | { 7 | [DebuggerDisplay("{FieldName}")] 8 | public class WhereClause 9 | { 10 | private bool _customName; 11 | 12 | public WhereOperator Operator { get; set; } 13 | public bool CaseSensitive { get; set; } 14 | public bool UseNot { get; set; } 15 | public PropertyInfo Property { get; set; } 16 | public string FieldName { get; set; } 17 | public bool UseOr { get; set; } 18 | 19 | public WhereClause() 20 | { 21 | Operator = WhereOperator.Equals; 22 | UseNot = false; 23 | CaseSensitive = true; 24 | } 25 | 26 | public void UpdateAttributeData(QueryOperatorAttribute data) 27 | { 28 | Operator = data.Operator; 29 | UseNot = data.UseNot; 30 | CaseSensitive = data.CaseSensitive; 31 | FieldName = data.HasName; 32 | UseOr = data.UseOr; 33 | if (!string.IsNullOrEmpty(FieldName)) 34 | _customName = true; 35 | } 36 | 37 | 38 | public void UpdateValues(PropertyInfo propertyInfo) 39 | { 40 | Property = propertyInfo; 41 | if (!_customName) 42 | FieldName = Property.Name; 43 | } 44 | 45 | } 46 | } -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/Filter/WhereOperator.cs: -------------------------------------------------------------------------------- 1 | namespace AspNetCore.IQueryable.Extensions.Filter 2 | { 3 | public enum WhereOperator 4 | { 5 | Equals, 6 | NotEquals, 7 | GreaterThan, 8 | LessThan, 9 | GreaterThanOrEqualTo, 10 | LessThanOrEqualTo, 11 | Contains, 12 | StartsWith, 13 | LessThanOrEqualWhenNullable, 14 | GreaterThanOrEqualWhenNullable, 15 | EqualsWhenNullable 16 | } 17 | } -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/ICustomQueryable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace AspNetCore.IQueryable.Extensions 6 | { 7 | public interface ICustomQueryable 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/Pagination/IQueryPaging.cs: -------------------------------------------------------------------------------- 1 | namespace AspNetCore.IQueryable.Extensions.Pagination 2 | { 3 | public interface IQueryPaging : ICustomQueryable 4 | { 5 | int? Limit { get; set; } 6 | int? Offset { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/Pagination/PagingExtensions.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions.Attributes; 2 | using System; 3 | using System.Linq; 4 | 5 | namespace AspNetCore.IQueryable.Extensions.Pagination 6 | { 7 | public static class PagingExtensions 8 | { 9 | public static IQueryable Paginate(this 10 | IQueryable result, 11 | int limit = 10, 12 | int offset = 0) 13 | { 14 | if (limit <= 0) 15 | limit = 10; 16 | return result.Skip(offset).Take(limit); 17 | } 18 | public static IQueryable Paginate(this 19 | IQueryable result, 20 | TModel options) 21 | where TModel : class, IQueryPaging 22 | { 23 | var attr = Attribute.GetCustomAttribute(PrimitiveExtensions.GetProperty(options.GetType(), "Limit"), typeof(QueryOperatorAttribute)); 24 | 25 | if (attr?.GetType() == typeof(QueryOperatorAttribute)) 26 | { 27 | var data = (QueryOperatorAttribute)attr; 28 | if (options.Limit is null || options.Limit < 0 || options.Limit > data.Max && data.Max >= 0) 29 | options.Limit = data.Max; 30 | } 31 | 32 | if (options.Offset.HasValue) 33 | result = result.Skip(options.Offset.Value); 34 | 35 | if (options.Limit.HasValue) 36 | result = result.Take(options.Limit.Value); 37 | 38 | return result; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/PrimitiveExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace AspNetCore.IQueryable.Extensions 8 | { 9 | internal static class PrimitiveExtensions 10 | { 11 | private static readonly ConcurrentDictionary> MemoryObjects; 12 | static PrimitiveExtensions() 13 | { 14 | MemoryObjects = new ConcurrentDictionary>(); 15 | } 16 | public static bool IsPropertyACollection(this PropertyInfo property) 17 | { 18 | return IsGenericEnumerable(property.PropertyType) || property.PropertyType.IsArray; 19 | } 20 | public static bool IsPropertyObject(this PropertyInfo property, object value) 21 | { 22 | return Convert.GetTypeCode(property.GetValue(value, null)) == TypeCode.Object; 23 | } 24 | private static bool IsGenericEnumerable(Type type) 25 | { 26 | return type.IsGenericType && 27 | type.GetInterfaces().Any( 28 | ti => (ti == typeof(IEnumerable<>) || ti.Name == "IEnumerable")); 29 | } 30 | 31 | 32 | internal static List GetAllProperties(this Type type) 33 | { 34 | if (MemoryObjects.ContainsKey(type)) 35 | return MemoryObjects[type]; 36 | 37 | var properties = type.GetProperties().ToList(); 38 | MemoryObjects.TryAdd(type, properties); 39 | return properties; 40 | } 41 | internal static PropertyInfo GetProperty(Type type, string name) 42 | { 43 | return type 44 | .GetAllProperties() 45 | .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); 46 | } 47 | internal static PropertyInfo GetProperty(string name) 48 | { 49 | return typeof(TEntity) 50 | .GetAllProperties() 51 | .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); 52 | } 53 | public static bool HasProperty(this Type type, string propertyName) 54 | { 55 | return type.GetAllProperties().Any(a => a.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); 56 | } 57 | /// 58 | /// Comma separated string: a,b,c 59 | /// 60 | public static string[] Fields(this string fields) 61 | { 62 | return fields.Split(','); 63 | } 64 | 65 | public static string FieldName(this string field) 66 | { 67 | return field.StartsWith("-", "+") ? field.Substring(1).Trim() : field.Trim(); 68 | } 69 | 70 | public static bool StartsWith(this string text, params string[] with) 71 | { 72 | return with.Any(text.StartsWith); 73 | } 74 | 75 | public static bool IsDescending(this string field) 76 | { 77 | return field.StartsWith("-"); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/QueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions.Filter; 2 | using AspNetCore.IQueryable.Extensions.Pagination; 3 | using AspNetCore.IQueryable.Extensions.Sort; 4 | using System.Linq; 5 | 6 | namespace AspNetCore.IQueryable.Extensions 7 | { 8 | public static class QueryableExtensions 9 | { 10 | public static IQueryable Apply(this IQueryable result, ICustomQueryable model) 11 | { 12 | result = result.Filter(model); 13 | 14 | if (model is IQuerySort sort) 15 | result = result.Sort(sort); 16 | 17 | if (model is IQueryPaging pagination) 18 | result = result.Paginate(pagination); 19 | 20 | return result; 21 | } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/Sort/IQuerySort.cs: -------------------------------------------------------------------------------- 1 | namespace AspNetCore.IQueryable.Extensions.Sort 2 | { 3 | public interface IQuerySort : ICustomQueryable 4 | { 5 | string Sort { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /src/AspNetCore.IQueryable.Extensions/Sort/SortingExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | 5 | namespace AspNetCore.IQueryable.Extensions.Sort 6 | { 7 | public static class SortingExtensions 8 | { 9 | public static IQueryable Sort(this IQueryable result, string fields) 10 | { 11 | if (string.IsNullOrEmpty(fields)) 12 | { 13 | return result; 14 | } 15 | 16 | var useThenBy = false; 17 | foreach (var sortTerm in fields.Fields()) 18 | { 19 | var property = PrimitiveExtensions.GetProperty(sortTerm.FieldName()); 20 | 21 | if (property != null) 22 | { 23 | var command = useThenBy ? "ThenBy" : "OrderBy"; 24 | command += sortTerm.IsDescending() ? "Descending" : string.Empty; 25 | 26 | result = result.OrderBy(property, command); 27 | } 28 | 29 | useThenBy = true; 30 | } 31 | 32 | return result; 33 | } 34 | 35 | private static IQueryable OrderBy(this IQueryable source, PropertyInfo propertyInfo, string command) 36 | { 37 | var type = typeof(TEntity); 38 | var parameter = Expression.Parameter(type, "p"); 39 | 40 | dynamic propertyValue = parameter; 41 | if (propertyInfo.Name.Contains(".")) 42 | { 43 | var parts = propertyInfo.Name.Split('.'); 44 | for (var i = 0; i < parts.Length - 1; i++) 45 | { 46 | propertyValue = Expression.PropertyOrField(propertyValue, parts[i]); 47 | } 48 | } 49 | 50 | var propertyAccess = Expression.MakeMemberAccess(propertyValue, propertyInfo); 51 | var orderByExpression = Expression.Lambda(propertyAccess, parameter); 52 | var resultExpression = Expression.Call(typeof(Queryable), command, new[] { type, propertyInfo.PropertyType }, 53 | source.Expression, Expression.Quote(orderByExpression)); 54 | return source.Provider.CreateQuery(resultExpression); 55 | } 56 | 57 | public static IQueryable Sort(this IQueryable result, TModel fields) where TModel : IQuerySort 58 | { 59 | return Sort(result, fields.Sort); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/IQueryable.Extensions.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29519.181 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RESTFul.Api", "RESTFul.Api\RESTFul.Api.csproj", "{22DA1EB9-4AEC-42F5-AB8E-25B8DB3F9589}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore.IQueryable.Extensions", "AspNetCore.IQueryable.Extensions\AspNetCore.IQueryable.Extensions.csproj", "{1D0339BD-2ED5-48F2-8072-7D848363410D}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{58D30F1C-B75E-42E2-917A-4657FF203D2D}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RestFulTests", "..\tests\RestFulTests\RestFulTests.csproj", "{493EFF68-8411-4A7E-8C9E-D240A0577D78}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {22DA1EB9-4AEC-42F5-AB8E-25B8DB3F9589}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {22DA1EB9-4AEC-42F5-AB8E-25B8DB3F9589}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {22DA1EB9-4AEC-42F5-AB8E-25B8DB3F9589}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {22DA1EB9-4AEC-42F5-AB8E-25B8DB3F9589}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {1D0339BD-2ED5-48F2-8072-7D848363410D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {1D0339BD-2ED5-48F2-8072-7D848363410D}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {1D0339BD-2ED5-48F2-8072-7D848363410D}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {1D0339BD-2ED5-48F2-8072-7D848363410D}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {493EFF68-8411-4A7E-8C9E-D240A0577D78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {493EFF68-8411-4A7E-8C9E-D240A0577D78}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {493EFF68-8411-4A7E-8C9E-D240A0577D78}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {493EFF68-8411-4A7E-8C9E-D240A0577D78}.Release|Any CPU.Build.0 = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(SolutionProperties) = preSolution 34 | HideSolutionNode = FALSE 35 | EndGlobalSection 36 | GlobalSection(NestedProjects) = preSolution 37 | {493EFF68-8411-4A7E-8C9E-D240A0577D78} = {58D30F1C-B75E-42E2-917A-4657FF203D2D} 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {2B73AD5A-0D80-4BC4-99D0-0AD04CB80D1F} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Commands/RegisterUserCommand.cs: -------------------------------------------------------------------------------- 1 | using RESTFul.Api.Models; 2 | using System; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace RESTFul.Api.Commands 6 | { 7 | public class RegisterUserCommand 8 | { 9 | [Required] 10 | public string FirstName { get; set; } 11 | public string LastName { get; set; } 12 | public string Gender { get; set; } 13 | [Required] 14 | public string Username { get; set; } 15 | public DateTime? Birthday { get; set; } 16 | 17 | public User ToEntity() 18 | { 19 | return new User() 20 | { 21 | Birthday = Birthday, 22 | FirstName = FirstName, 23 | LastName = LastName, 24 | Gender = Gender, 25 | Username = Username, 26 | CustomId = Guid.NewGuid() 27 | }; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Commands/UpdateUserCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using RESTFul.Api.Models; 4 | 5 | namespace RESTFul.Api.Commands 6 | { 7 | public class UpdateUserCommand 8 | { 9 | [Required] 10 | public string FirstName { get; set; } 11 | public string LastName { get; set; } 12 | public string Gender { get; set; } 13 | public DateTime? Birthday { get; set; } 14 | 15 | public void Update(User actual) 16 | { 17 | actual.FirstName = FirstName; 18 | actual.LastName = LastName; 19 | actual.Gender = Gender; 20 | actual.Birthday = Birthday; 21 | 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/RESTFul.Api/Configuration/AutomapperConfiguration.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | namespace RESTFul.Api.Configuration 6 | { 7 | public static class AutomapperConfiguration 8 | { 9 | public static void ConfigureAutomapper(this IServiceCollection services) 10 | { 11 | // Auto Mapper Configurations 12 | var mappingConfig = new MapperConfiguration(mc => 13 | { 14 | mc.AddProfile(new MappingProfile()); 15 | }); 16 | 17 | IMapper mapper = new Mapper(mappingConfig); 18 | services.TryAddSingleton(mapper); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/RESTFul.Api/Configuration/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using RESTFul.Api.Models; 3 | using RESTFul.Api.ViewModels; 4 | 5 | namespace RESTFul.Api.Configuration 6 | { 7 | public class MappingProfile : Profile 8 | { 9 | public MappingProfile() 10 | { 11 | // Add as many of these lines as you need to map your objects 12 | CreateMap(); 13 | } 14 | 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Contexts/RestfulContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Logging; 3 | using RESTFul.Api.Models; 4 | 5 | namespace RESTFul.Api.Contexts 6 | { 7 | public class RestfulContext : DbContext 8 | { 9 | 10 | public static readonly ILoggerFactory MyLoggerFactory 11 | = LoggerFactory.Create(builder => { builder.AddConsole(); }); 12 | 13 | 14 | public RestfulContext(DbContextOptions options) 15 | : base(options) 16 | { 17 | } 18 | public DbSet Users { get; set; } 19 | public DbSet Claims { get; set; } 20 | 21 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 22 | { 23 | optionsBuilder 24 | .UseLoggerFactory(MyLoggerFactory) 25 | .EnableSensitiveDataLogging(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Controllers/ApiBaseController.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Mvc; 3 | using RESTFul.Api.Notification; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace RESTFul.Api.Controllers 8 | { 9 | [ApiController] 10 | public abstract class ApiBaseController : ControllerBase 11 | { 12 | private readonly DomainNotificationHandler _notifications; 13 | private readonly IDomainNotificationMediatorService _mediator; 14 | 15 | protected ApiBaseController(INotificationHandler notifications, 16 | IDomainNotificationMediatorService mediator) 17 | { 18 | _notifications = (DomainNotificationHandler)notifications; 19 | _mediator = mediator; 20 | } 21 | 22 | 23 | protected bool IsValidOperation() 24 | { 25 | return (!_notifications.HasNotifications()); 26 | } 27 | 28 | 29 | protected ActionResult ResponsePutPatch() 30 | { 31 | if (IsValidOperation()) 32 | { 33 | return NoContent(); 34 | } 35 | 36 | return BadRequest(new ValidationProblemDetails(_notifications.GetNotificationsByKey())); 37 | } 38 | 39 | protected ActionResult ResponseDelete() 40 | { 41 | if (IsValidOperation()) 42 | { 43 | return NoContent(); 44 | } 45 | 46 | return BadRequest(new ValidationProblemDetails(_notifications.GetNotificationsByKey())); 47 | } 48 | 49 | protected ActionResult ResponsePost(string action, object route, T result) 50 | { 51 | if (IsValidOperation()) 52 | { 53 | if (result == null) 54 | return NoContent(); 55 | 56 | return CreatedAtAction(action, route, result); 57 | } 58 | 59 | return BadRequest(new ValidationProblemDetails(_notifications.GetNotificationsByKey())); 60 | } 61 | 62 | protected ActionResult ResponsePost(string action, string controller, object route, T result) 63 | { 64 | if (IsValidOperation()) 65 | { 66 | if (result == null) 67 | return NoContent(); 68 | 69 | return CreatedAtAction(action, controller, route, result); 70 | } 71 | 72 | return BadRequest(new ValidationProblemDetails(_notifications.GetNotificationsByKey())); 73 | } 74 | protected ActionResult> ResponseGet(IEnumerable result) 75 | { 76 | 77 | if (result == null || (result != null && !result.Any())) 78 | return NoContent(); 79 | 80 | return Ok(result); 81 | } 82 | 83 | protected ActionResult ResponseGet(T result) 84 | { 85 | if (result == null) 86 | return NotFound(); 87 | 88 | return Ok(result); 89 | } 90 | 91 | protected void NotifyModelStateErrors() 92 | { 93 | var erros = ModelState.Values.SelectMany(v => v.Errors); 94 | foreach (var erro in erros) 95 | { 96 | var erroMsg = erro.Exception == null ? erro.ErrorMessage : erro.Exception.Message; 97 | NotifyError(string.Empty, erroMsg); 98 | } 99 | } 100 | 101 | protected ActionResult ModelStateErrorResponseError() 102 | { 103 | return BadRequest(new ValidationProblemDetails(ModelState)); 104 | } 105 | 106 | protected void NotifyError(string code, string message) 107 | { 108 | _mediator.Notify(new DomainNotification(code, message)); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Controllers/UserController.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions; 2 | using AutoMapper; 3 | using MediatR; 4 | using Microsoft.AspNetCore.JsonPatch; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.EntityFrameworkCore; 7 | using RESTFul.Api.Commands; 8 | using RESTFul.Api.Models; 9 | using RESTFul.Api.Notification; 10 | using RESTFul.Api.Service.Interfaces; 11 | using RESTFul.Api.ViewModels; 12 | using System.Collections.Generic; 13 | using System.Threading.Tasks; 14 | 15 | namespace RESTFul.Api.Controllers 16 | { 17 | [Route("users")] 18 | public class UserController : ApiBaseController 19 | { 20 | private readonly IDummyUserService _dummyUserService; 21 | private readonly IMapper _mapper; 22 | 23 | public UserController( 24 | INotificationHandler notifications, 25 | IDomainNotificationMediatorService mediator, 26 | IDummyUserService dummyUserService, 27 | IMapper mapper) : base(notifications, mediator) 28 | { 29 | _dummyUserService = dummyUserService; 30 | _mapper = mapper; 31 | } 32 | 33 | [HttpGet("")] 34 | public async Task>> Get([FromQuery] UserSearch search) 35 | { 36 | var result = _dummyUserService.Query().Apply(search); 37 | 38 | return ResponseGet(await _mapper.ProjectTo(result).ToListAsync()); 39 | } 40 | 41 | [HttpGet("{username}"), 42 | HttpHead("{username}"), 43 | ResponseCache(Location = ResponseCacheLocation.Any, Duration = 600)] 44 | public async Task> Get(string username) 45 | { 46 | return ResponseGet(await _dummyUserService.Find(username).ConfigureAwait(false)); 47 | } 48 | 49 | [HttpPost("")] 50 | public async Task> Post([FromBody] RegisterUserCommand command) 51 | { 52 | if (!ModelState.IsValid) 53 | { 54 | NotifyModelStateErrors(); 55 | return ModelStateErrorResponseError(); 56 | } 57 | 58 | await _dummyUserService.Save(command).ConfigureAwait(false); 59 | var newUser = await _dummyUserService.Find(command.Username).ConfigureAwait(false); 60 | return ResponsePost(nameof(Get), new { id = command.Username }, newUser); 61 | } 62 | 63 | [HttpPatch("{username}")] 64 | public async Task Patch(string username, [FromBody] JsonPatchDocument model) 65 | { 66 | if (!ModelState.IsValid) 67 | { 68 | NotifyModelStateErrors(); 69 | return ModelStateErrorResponseError(); 70 | } 71 | 72 | var actualUser = await _dummyUserService.Find(username).ConfigureAwait(false); 73 | model.ApplyTo(actualUser); 74 | await _dummyUserService.Update(actualUser).ConfigureAwait(false); 75 | return ResponsePutPatch(); 76 | } 77 | 78 | 79 | [HttpPut("{username}")] 80 | public async Task Put(string username, [FromBody] UpdateUserCommand model) 81 | { 82 | if (!ModelState.IsValid) 83 | { 84 | NotifyModelStateErrors(); 85 | return ModelStateErrorResponseError(); 86 | } 87 | 88 | var actual = await _dummyUserService.Find(username).ConfigureAwait(false); 89 | model.Update(actual); 90 | await _dummyUserService.Update(actual).ConfigureAwait(false); 91 | return ResponsePutPatch(); 92 | } 93 | 94 | 95 | [HttpDelete("{username}")] 96 | public ActionResult Delete(string username) 97 | { 98 | if (!ModelState.IsValid) 99 | { 100 | NotifyModelStateErrors(); 101 | return ModelStateErrorResponseError(); 102 | } 103 | 104 | _dummyUserService.Remove(username); 105 | return ResponseDelete(); 106 | } 107 | 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Models/Claim.cs: -------------------------------------------------------------------------------- 1 | namespace RESTFul.Api.Models 2 | { 3 | public class Claim 4 | { 5 | public string Type { get; } 6 | public string Value { get; } 7 | public int UserId { get; } 8 | public int Id { get; set; } 9 | public Claim() { } 10 | public Claim(string type, string value, int userId) 11 | { 12 | Type = type; 13 | Value = value; 14 | UserId = userId; 15 | } 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /src/RESTFul.Api/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace RESTFul.Api.Models 6 | { 7 | [DebuggerDisplay("{FullName}")] 8 | public class User 9 | { 10 | public string FirstName { get; set; } 11 | public string LastName { get; set; } 12 | public string FullName => $"{LastName}, {FirstName}"; 13 | public string Gender { get; set; } 14 | public DateTime? Birthday { get; set; } 15 | public int Id { get; set; } 16 | public Guid CustomId { get; set; } 17 | public string Username { get; set; } 18 | public bool Active { get; set; } 19 | public IEnumerable Claims { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/RESTFul.Api/Notification/DomainNotification.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using System; 3 | 4 | namespace RESTFul.Api.Notification 5 | { 6 | public class DomainNotification : INotification 7 | { 8 | public Guid DomainNotificationId { get; } 9 | public string Key { get; } 10 | public string Value { get; } 11 | 12 | public DomainNotification(string key, string value) 13 | { 14 | this.DomainNotificationId = Guid.NewGuid(); 15 | this.Key = key; 16 | this.Value = value; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Notification/DomainNotificationHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace RESTFul.Api.Notification 9 | { 10 | public class DomainNotificationHandler : INotificationHandler 11 | { 12 | private List _notifications; 13 | 14 | public DomainNotificationHandler() 15 | { 16 | this._notifications = new List(); 17 | } 18 | 19 | public Task Handle(DomainNotification message, CancellationToken cancellationToken) 20 | { 21 | this._notifications.Add(message); 22 | return Task.CompletedTask; 23 | } 24 | 25 | public virtual Dictionary GetNotificationsByKey() 26 | { 27 | var strings = this._notifications.Select(s => s.Key).Distinct(); 28 | var dictionary = new Dictionary(); 29 | foreach (var str in strings) 30 | { 31 | var key = str; 32 | dictionary[key] = this._notifications.Where(w => w.Key.Equals(key, StringComparison.Ordinal)).Select(s => s.Value).ToArray(); 33 | } 34 | return dictionary; 35 | } 36 | 37 | public virtual List GetNotifications() 38 | { 39 | return this._notifications; 40 | } 41 | 42 | public virtual bool HasNotifications() 43 | { 44 | return this.GetNotifications().Any(); 45 | } 46 | 47 | public void Dispose() 48 | { 49 | this._notifications = new List(); 50 | } 51 | 52 | public void Clear() 53 | { 54 | this._notifications = new List(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Notification/DomainNotificationMediatorService.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace RESTFul.Api.Notification 4 | { 5 | public class DomainNotificationMediatorService : IDomainNotificationMediatorService 6 | { 7 | private readonly IMediator _mediator; 8 | 9 | public DomainNotificationMediatorService(IMediator mediator) 10 | { 11 | _mediator = mediator; 12 | } 13 | 14 | public void Notify(DomainNotification notify) 15 | { 16 | _mediator.Publish(notify); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/RESTFul.Api/Notification/IDomainNotificationMediatorService.cs: -------------------------------------------------------------------------------- 1 | namespace RESTFul.Api.Notification 2 | { 3 | public interface IDomainNotificationMediatorService 4 | { 5 | void Notify(DomainNotification notify); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | using RESTFul.Api.Contexts; 5 | using System.Diagnostics; 6 | using System.Threading.Tasks; 7 | 8 | namespace RESTFul.Api 9 | { 10 | public class Program 11 | { 12 | public static void Main(string[] args) 13 | { 14 | var host = CreateHostBuilder(args).Build(); 15 | 16 | Task.WaitAll(DbMigrationHelpers.EnsureSeedData(serviceScope: host.Services.CreateScope())); 17 | 18 | host.Run(); 19 | } 20 | 21 | public static IHostBuilder CreateHostBuilder(string[] args) => 22 | Host.CreateDefaultBuilder(args) 23 | .ConfigureWebHostDefaults(webBuilder => 24 | { 25 | webBuilder.UseStartup(); 26 | }); 27 | } 28 | 29 | public static class DbMigrationHelpers 30 | { 31 | 32 | public static async Task EnsureSeedData(IServiceScope serviceScope) 33 | { 34 | Debug.Assert(serviceScope != null, nameof(serviceScope) + " != null"); 35 | 36 | var serviceProvider = serviceScope?.ServiceProvider; 37 | using var scope = serviceProvider.GetRequiredService().CreateScope(); 38 | var appContext = scope.ServiceProvider.GetRequiredService(); 39 | 40 | await appContext.Database.EnsureCreatedAsync().ConfigureAwait(false); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:55514", 8 | "sslPort": 44383 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "RESTFul.Api": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/RESTFul.Api/RESTFul.Api.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 8 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Service/DummyUserService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Polyjuice.Potions; 3 | using RESTFul.Api.Commands; 4 | using RESTFul.Api.Contexts; 5 | using RESTFul.Api.Models; 6 | using RESTFul.Api.Notification; 7 | using RESTFul.Api.Service.Interfaces; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Threading.Tasks; 12 | using static System.Linq.Enumerable; 13 | 14 | namespace RESTFul.Api.Service 15 | { 16 | public class DummyUserService : IDummyUserService 17 | { 18 | private readonly IDomainNotificationMediatorService _domainNotification; 19 | private readonly RestfulContext _context; 20 | private static Random _rnd = new Random(); 21 | 22 | public DummyUserService(IDomainNotificationMediatorService domainNotification, 23 | RestfulContext context) 24 | { 25 | _domainNotification = domainNotification; 26 | _context = context; 27 | } 28 | private async Task CheckUsers() 29 | { 30 | if (_context.Users.Any()) 31 | return; 32 | var users = Range(1, 200).Select(index => new User 33 | { 34 | FirstName = Name.FirstName, 35 | LastName = Name.LastName, 36 | Username = Internet.Email(Name.FirstName), 37 | Gender = Gender.Random, 38 | Birthday = DateAndTime.Birthday, 39 | Active = true, 40 | Claims = GenerateClaims(index + 1) 41 | }).ToList(); 42 | 43 | foreach (var user in users) 44 | { 45 | await _context.Users.AddAsync(user); 46 | } 47 | 48 | await _context.SaveChangesAsync().ConfigureAwait(false); 49 | } 50 | 51 | private IEnumerable GenerateClaims(int userId) 52 | { 53 | return Range(1, _rnd.Next(1, 7)).Select(i => new Claim(Job.Title, Lorem.Paragraph(), userId)).ToList(); 54 | } 55 | 56 | public IQueryable Query() 57 | { 58 | CheckUsers().Wait(); 59 | return _context.Users.AsQueryable(); 60 | } 61 | 62 | public async Task> All() 63 | { 64 | await CheckUsers().ConfigureAwait(false); 65 | return await _context.Users.ToListAsync().ConfigureAwait(false); 66 | } 67 | 68 | public async Task Save(RegisterUserCommand command) 69 | { 70 | var user = command.ToEntity(); 71 | if (CheckIfUserIsValid(user)) 72 | return; 73 | 74 | await _context.Users.AddAsync(user); 75 | await _context.SaveChangesAsync().ConfigureAwait(false); 76 | } 77 | 78 | public async Task Update(User user) 79 | { 80 | if (user != null && CheckIfUserIsValid(user)) 81 | return; 82 | 83 | var actua = await Find(user.Username).ConfigureAwait(false); 84 | _context.Users.Remove(actua); 85 | await _context.Users.AddAsync(user); 86 | 87 | await _context.SaveChangesAsync().ConfigureAwait(false); 88 | } 89 | 90 | public async Task Remove(string username) 91 | { 92 | var actual = await Find(username).ConfigureAwait(false); 93 | if (actual != null) 94 | _context.Users.Remove(actual); 95 | return await _context.SaveChangesAsync().ConfigureAwait(false); 96 | } 97 | 98 | 99 | private bool CheckIfUserIsValid(User command) 100 | { 101 | var valid = true; 102 | if (string.IsNullOrEmpty(command.FirstName)) 103 | { 104 | _domainNotification.Notify(new DomainNotification("User", "Invalid firstname")); 105 | valid = false; 106 | } 107 | 108 | if (string.IsNullOrEmpty(command.LastName)) 109 | { 110 | _domainNotification.Notify(new DomainNotification("User", "Invalid firstname")); 111 | valid = false; 112 | } 113 | 114 | if (Find(command.Username) != null) 115 | { 116 | _domainNotification.Notify(new DomainNotification("User", "Username already exists")); 117 | valid = false; 118 | } 119 | 120 | return valid; 121 | } 122 | 123 | public Task Find(string username) 124 | { 125 | return _context.Users.FirstOrDefaultAsync(f => f.Username == username); 126 | 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/RESTFul.Api/Service/Interfaces/IDummyUserService.cs: -------------------------------------------------------------------------------- 1 | using RESTFul.Api.Commands; 2 | using RESTFul.Api.Models; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace RESTFul.Api.Service.Interfaces 8 | { 9 | public interface IDummyUserService 10 | { 11 | IQueryable Query(); 12 | Task> All(); 13 | Task Find(string id); 14 | Task Save(RegisterUserCommand command); 15 | Task Update(User actualUser); 16 | Task Remove(string username); 17 | } 18 | } -------------------------------------------------------------------------------- /src/RESTFul.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.ResponseCompression; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.OpenApi.Models; 11 | using RESTFul.Api.Contexts; 12 | using RESTFul.Api.Notification; 13 | using RESTFul.Api.Service; 14 | using RESTFul.Api.Service.Interfaces; 15 | using System; 16 | using System.Linq; 17 | using System.Net.Mime; 18 | using System.Reflection; 19 | 20 | namespace RESTFul.Api 21 | { 22 | public class Startup 23 | { 24 | public Startup(IConfiguration configuration) 25 | { 26 | Configuration = configuration; 27 | } 28 | 29 | public IConfiguration Configuration { get; } 30 | 31 | // This method gets called by the runtime. Use this method to add services to the container. 32 | public void ConfigureServices(IServiceCollection services) 33 | { 34 | services.AddControllers(); 35 | services.AddSwaggerGen(options => 36 | { 37 | options.SwaggerDoc("v1", new OpenApiInfo() 38 | { 39 | Version = "v1", 40 | Title = "RESTFull API - To test Component", 41 | Description = "Swagger surface", 42 | Contact = new OpenApiContact() 43 | { 44 | Name = "Bruno Brito", 45 | Email = "bhdebrito@gmail.com", 46 | Url = new Uri("https://www.brunobrito.net.br") 47 | }, 48 | License = new OpenApiLicense() 49 | { 50 | Name = "MIT", 51 | Url = new Uri("https://github.com/brunohbrito/AspNet.Core.RESTFull.Extensions/blob/master/LICENSE") 52 | }, 53 | 54 | }); 55 | 56 | }); 57 | services.AddMediatR(Assembly.GetExecutingAssembly()); 58 | 59 | services.AddResponseCaching(); 60 | services.AddResponseCompression(options => 61 | { 62 | options.Providers.Add(); 63 | options.Providers.Add(); 64 | options.EnableForHttps = true; 65 | options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "image/jpeg", "image/png", "application/font-woff2", "image/svg+xml", MediaTypeNames.Application.Json }); 66 | }); 67 | services.AddTransient(); 68 | services.AddTransient(); 69 | services.AddEntityFrameworkInMemoryDatabase().AddDbContext(options => 70 | { 71 | options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); 72 | options.EnableSensitiveDataLogging(); 73 | }); 74 | services.AddAutoMapper(typeof(Startup)); 75 | } 76 | 77 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 78 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 79 | { 80 | if (env.IsDevelopment()) 81 | { 82 | app.UseDeveloperExceptionPage(); 83 | app.UseSwagger(); 84 | app.UseSwaggerUI(c => 85 | { 86 | c.SwaggerEndpoint("/swagger/v1/swagger.json", "SSO Api Management"); 87 | c.OAuthClientId("Swagger"); 88 | c.OAuthClientSecret("swagger"); 89 | c.OAuthAppName("SSO Management Api"); 90 | c.OAuthUseBasicAuthenticationWithAccessCodeGrant(); 91 | }); 92 | } 93 | 94 | app.UseHttpsRedirection(); 95 | 96 | app.UseRouting(); 97 | 98 | app.UseAuthorization(); 99 | app.UseEndpoints(endpoints => 100 | { 101 | endpoints.MapControllers(); 102 | }); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/RESTFul.Api/ViewModels/UserSearch.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions.Attributes; 2 | using AspNetCore.IQueryable.Extensions.Filter; 3 | using AspNetCore.IQueryable.Extensions.Pagination; 4 | using AspNetCore.IQueryable.Extensions.Sort; 5 | using System; 6 | 7 | namespace RESTFul.Api.ViewModels 8 | { 9 | public class UserSearch : IQuerySort, IQueryPaging 10 | { 11 | [QueryOperator(Operator = WhereOperator.Contains, UseOr = true)] 12 | public string Username { get; set; } 13 | 14 | [QueryOperator(Operator = WhereOperator.GreaterThan)] 15 | public DateTime? Birthday { get; set; } 16 | 17 | [QueryOperator(Operator = WhereOperator.Contains, HasName = "Firstname")] 18 | public string Name { get; set; } 19 | 20 | [QueryOperator(Operator = WhereOperator.Equals)] 21 | public Guid CustomId { get; set; } 22 | 23 | public int? Offset { get; set; } 24 | public int? Limit { get; set; } = 10; 25 | public string Sort { get; set; } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/RESTFul.Api/ViewModels/UserViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace RESTFul.Api.ViewModels 2 | { 3 | public class UserViewModel 4 | { 5 | public string FullName { get; set; } 6 | public string Username { get; set; } 7 | public bool Active { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/RESTFul.Api/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace RESTFul.Api 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/RESTFul.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/RESTFul.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "ConnectionStrings": { 11 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=TestQueryable;Trusted_Connection=True;MultipleActiveResultSets=true" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/RestFulTests/Fakers/UserFaker.cs: -------------------------------------------------------------------------------- 1 | using Bogus; 2 | using Bogus.Extensions.UnitedStates; 3 | using RestFulTests.Models; 4 | 5 | namespace RestFulTests.Fakers 6 | { 7 | public static class UserFaker 8 | { 9 | private static Faker _faker = new Faker(); 10 | public static Faker GenerateUserViewModel() 11 | { 12 | var claims = GenerateClaims().Generate(10); 13 | return new Faker() 14 | .RuleFor(u => u.Id, f => f.Random.Int()) 15 | .RuleFor(u => u.CustomId, f => f.Random.Guid()) 16 | .RuleFor(u => u.FirstName, f => f.Person.FirstName) 17 | .RuleFor(u => u.LastName, f => f.Person.LastName) 18 | .RuleFor(u => u.FullName, f => f.Person.FullName) 19 | .RuleFor(u => u.Gender, f => f.Person.Email) 20 | .RuleFor(u => u.Birthday, f => f.Person.DateOfBirth) 21 | .RuleFor(u => u.Username, f => f.Person.UserName) 22 | .RuleFor(u => u.Active, f => f.Random.Bool()) 23 | .RuleFor(u => u.Age, f => f.Random.Int(1, 85)) 24 | .RuleFor(u => u.SocialNumber, f => new Ssn() { Identification = f.Person.Ssn() }) 25 | .RuleFor(u => u.Claims, claims); 26 | } 27 | 28 | public static Faker GenerateClaims() 29 | { 30 | return new Faker().CustomInstantiator(f => new Claim(f.Commerce.Department(), f.Finance.Account(), f.Random.Int())); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/RestFulTests/FilterTests.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions.Filter; 2 | using FluentAssertions; 3 | using RestFulTests.Fakers; 4 | using RestFulTests.Models; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Xunit; 9 | 10 | namespace RestFulTests 11 | { 12 | public class FilterTests 13 | { 14 | private readonly List _users; 15 | 16 | public FilterTests() 17 | { 18 | _users = UserFaker.GenerateUserViewModel().Generate(50); 19 | } 20 | 21 | 22 | [Fact] 23 | public void Should_Filter_By_Has_Name_Attribute() 24 | { 25 | var userSearch = new UserSearch() 26 | { 27 | Name = _users.Last().FirstName 28 | }; 29 | 30 | var sortingByFieldName = _users.AsQueryable().Filter(userSearch); 31 | sortingByFieldName.Should().HaveCountGreaterOrEqualTo(1); 32 | } 33 | 34 | [Fact] 35 | public void Should_Filter_By_Guid() 36 | { 37 | var userSearch = new UserSearch() 38 | { 39 | CustomId = _users.Last().CustomId 40 | }; 41 | 42 | var sortingByFieldName = _users.AsQueryable().Filter(userSearch); 43 | sortingByFieldName.Should().HaveCountGreaterOrEqualTo(1); 44 | } 45 | 46 | 47 | [Fact] 48 | public void Should_Apply_Allfilter_Based_In_Class() 49 | { 50 | var userSearch = new UserSearch() 51 | { 52 | Name = _users.Last().FirstName, 53 | Username = _users.Last().Username 54 | }; 55 | var sortingByFieldName = _users.AsQueryable().Filter(userSearch); 56 | sortingByFieldName.Should().HaveCountGreaterOrEqualTo(1); 57 | } 58 | 59 | [Fact] 60 | public void Should_Apply_Allfilter_For_Two_Fields_For_Same_Attribute() 61 | { 62 | var userSearch = new UserSearch() 63 | { 64 | OlderThan = 18 65 | }; 66 | var sortingByFieldName = _users.AsQueryable().Filter(userSearch); 67 | sortingByFieldName.Should().HaveCountGreaterOrEqualTo(1); 68 | } 69 | 70 | 71 | [Fact] 72 | public void Should_Apply_Or_In_Filter() 73 | { 74 | var userSearch = new UserSearch() 75 | { 76 | Name = _users.First().FirstName, 77 | Ssn = _users.First().SocialNumber.Identification, 78 | Username = _users.Last().Username 79 | }; 80 | var sortingByFieldName = _users.AsQueryable().Filter(userSearch); 81 | sortingByFieldName.Should().HaveCountGreaterOrEqualTo(1); 82 | } 83 | 84 | 85 | 86 | [Fact] 87 | public void Should_Apply_Exclusive_Or_In_Filter() 88 | { 89 | var userSearch = new UserSearch() 90 | { 91 | Name = _users.First().FirstName, 92 | Username = _users.Last().Username 93 | }; 94 | var sortingByFieldName = _users.AsQueryable().Filter(userSearch); 95 | sortingByFieldName.Should().HaveCountGreaterOrEqualTo(1); 96 | } 97 | 98 | [Fact] 99 | public void Should_Apply_All_Filter_In_Nested_Object() 100 | { 101 | var userSearch = new UserSearch() 102 | { 103 | Ssn = _users.Last().SocialNumber.Identification 104 | }; 105 | var sortingByFieldName = _users.AsQueryable().Filter(userSearch); 106 | sortingByFieldName.Should().HaveCountGreaterOrEqualTo(1); 107 | } 108 | 109 | 110 | [Fact] 111 | public void Should_Find_User_Id_From_Array() 112 | { 113 | var ids = _users.Take(2).Select(s => s.Id); 114 | 115 | var userSearch = new UserSearch() 116 | { 117 | Id = ids 118 | }; 119 | var sortingByFieldName = _users.AsQueryable().Filter(userSearch); 120 | sortingByFieldName.Should().HaveCount(2); 121 | } 122 | 123 | [Fact] 124 | public void Should_Throw_Exception_When_Is_Array_And_Operator_Isnt_Contains() 125 | { 126 | var ids = _users.Take(2).Select(s => s.Id); 127 | 128 | var userSearch = new WrongOperator() 129 | { 130 | Id = ids 131 | }; 132 | Action act = () => _users.AsQueryable().Filter(userSearch); 133 | 134 | 135 | act.Should().Throw(); 136 | 137 | 138 | } 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /tests/RestFulTests/Models/Claim.cs: -------------------------------------------------------------------------------- 1 | namespace RestFulTests.Models 2 | { 3 | public class Claim 4 | { 5 | public string Type { get; } 6 | public string Value { get; } 7 | public int UserId { get; } 8 | public int Id { get; set; } 9 | public Claim() { } 10 | public Claim(string type, string value, int userId) 11 | { 12 | Type = type; 13 | Value = value; 14 | UserId = userId; 15 | } 16 | 17 | } 18 | } -------------------------------------------------------------------------------- /tests/RestFulTests/Models/PagingMax.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions.Attributes; 2 | using AspNetCore.IQueryable.Extensions.Pagination; 3 | 4 | namespace RestFulTests.Models 5 | { 6 | public class PagingMax : IQueryPaging 7 | { 8 | public int? Offset { get; set; } 9 | [QueryOperator(Max = 5)] 10 | public int? Limit { get; set; } 11 | } 12 | public class SinglePaging : IQueryPaging 13 | { 14 | public int? Offset { get; set; } 15 | public int? Limit { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /tests/RestFulTests/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace RestFulTests.Models 6 | { 7 | [DebuggerDisplay("{FullName}")] 8 | public class User 9 | { 10 | public int Id { get; set; } 11 | public Guid CustomId { get; set; } 12 | public string FirstName { get; set; } 13 | public string LastName { get; set; } 14 | public string FullName => $"{LastName}, {FirstName}"; 15 | public string Gender { get; set; } 16 | public DateTime? Birthday { get; set; } 17 | public string Username { get; set; } 18 | public bool Active { get; set; } 19 | public int Age { get; set; } 20 | public Ssn SocialNumber { get; set; } 21 | public IEnumerable Claims { get; set; } 22 | } 23 | 24 | public class Ssn 25 | { 26 | public string Identification { get; set; } 27 | } 28 | } -------------------------------------------------------------------------------- /tests/RestFulTests/Models/UserSearch.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions; 2 | using AspNetCore.IQueryable.Extensions.Attributes; 3 | using AspNetCore.IQueryable.Extensions.Filter; 4 | using AspNetCore.IQueryable.Extensions.Pagination; 5 | using AspNetCore.IQueryable.Extensions.Sort; 6 | using System; 7 | using System.Collections.Generic; 8 | 9 | namespace RestFulTests.Models 10 | { 11 | public class UserSearch : IQueryPaging, IQuerySort 12 | { 13 | [QueryOperator(Operator = WhereOperator.Contains, UseOr = true)] 14 | public string Username { get; set; } 15 | 16 | [QueryOperator(Operator = WhereOperator.GreaterThan)] 17 | public DateTime? Birthday { get; set; } 18 | 19 | [QueryOperator(Operator = WhereOperator.Contains, HasName = "Firstname")] 20 | public string Name { get; set; } 21 | 22 | [QueryOperator(Operator = WhereOperator.Contains, HasName = "SocialNumber.Identification")] 23 | public string Ssn { get; set; } 24 | 25 | [QueryOperator(Operator = WhereOperator.GreaterThanOrEqualTo, HasName = "Age")] 26 | public int? OlderThan { get; set; } 27 | 28 | [QueryOperator(Operator = WhereOperator.LessThanOrEqualTo, HasName = "Age")] 29 | public int? YoungerThan { get; set; } 30 | 31 | [QueryOperator(Operator = WhereOperator.Contains)] 32 | public IEnumerable Id { get; set; } 33 | 34 | [QueryOperator(Operator = WhereOperator.Equals)] 35 | public Guid? CustomId { get; set; } 36 | 37 | 38 | public int? Offset { get; set; } 39 | public int? Limit { get; set; } = 10; 40 | public string Sort { get; set; } 41 | } 42 | 43 | public class WrongOperator : ICustomQueryable 44 | { 45 | [QueryOperator(Operator = WhereOperator.Equals)] 46 | public IEnumerable Id { get; set; } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/RestFulTests/PagingTests.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions; 2 | using AspNetCore.IQueryable.Extensions.Pagination; 3 | using FluentAssertions; 4 | using RestFulTests.Fakers; 5 | using RestFulTests.Models; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Xunit; 9 | 10 | namespace RestFulTests 11 | { 12 | public class PagingTests 13 | { 14 | private readonly List _users; 15 | public PagingTests() 16 | { 17 | _users = UserFaker.GenerateUserViewModel().Generate(50); 18 | } 19 | 20 | [Fact] 21 | public void Should_Paging() 22 | { 23 | var sortingByFieldName = _users.AsQueryable().Paginate(5, 0); 24 | sortingByFieldName.Should().HaveCount(5); 25 | } 26 | 27 | [Theory] 28 | [InlineData(0)] 29 | [InlineData(-10)] 30 | [InlineData(-166)] 31 | public void Should_Get_Default_Paging_When_Limit_Is_Zero_Or_Negative(int limit) 32 | { 33 | var sortingByFieldName = _users.AsQueryable().Paginate(limit, 0); 34 | sortingByFieldName.Should().HaveCount(10); 35 | } 36 | 37 | [Fact] 38 | public void Should_Paging_From_Interface_Implementation() 39 | { 40 | var paginate = new UserSearch() 41 | { 42 | Limit = 5, 43 | Offset = 0 44 | }; 45 | var sortingByFieldName = _users.AsQueryable().Paginate(paginate); 46 | 47 | sortingByFieldName.Should().HaveCount(5); 48 | } 49 | 50 | [Fact] 51 | public void Should_Limit_Not_Bigger_Than_Atrribute_Max() 52 | { 53 | var paginate = new PagingMax() 54 | { 55 | Limit = 20, 56 | Offset = 0 57 | }; 58 | var sortingByFieldName = _users.AsQueryable().Paginate(paginate); 59 | sortingByFieldName.Should().HaveCount(5); 60 | } 61 | 62 | [Fact] 63 | public void Should_Respect_Attribute_Max_When_Limit_Is_Not_Set() 64 | { 65 | var paginate = new PagingMax() 66 | { 67 | Offset = 0 68 | }; 69 | var sortingByFieldName = _users.AsQueryable().Apply(paginate); 70 | sortingByFieldName.Should().HaveCount(5); 71 | } 72 | 73 | [Fact] 74 | public void Should_Ignore_Attribute_Max_When_Valid_Limit_Is_Given() 75 | { 76 | var paginate = new PagingMax() 77 | { 78 | Offset = 0, 79 | Limit = 2 80 | }; 81 | var sortingByFieldName = _users.AsQueryable().Apply(paginate); 82 | sortingByFieldName.Should().HaveCount(2); 83 | } 84 | 85 | [Fact] 86 | public void Should_Return_Alldata_When_Limit_Is_Not_Specified() 87 | { 88 | var paginate = new SinglePaging(); 89 | var pagingData = _users.AsQueryable().Paginate(paginate); 90 | pagingData.Should().HaveCount(50); 91 | } 92 | 93 | 94 | [Fact] 95 | public void Should_Return_Partial_Data_When_Offset_Specified() 96 | { 97 | var paginate = new SinglePaging() { Offset = 10 }; 98 | var pagingData = _users.AsQueryable().Paginate(paginate); 99 | pagingData.Should().HaveCount(40); 100 | } 101 | 102 | [Fact] 103 | public void Should_Return_Partial_Data_When_Limit_Specified() 104 | { 105 | var paginate = new SinglePaging() { Limit = 15 }; 106 | var pagingData = _users.AsQueryable().Paginate(paginate); 107 | pagingData.Should().HaveCount(15); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /tests/RestFulTests/RestFulTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net7.0;net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/RestFulTests/SortingTests.cs: -------------------------------------------------------------------------------- 1 | using AspNetCore.IQueryable.Extensions.Sort; 2 | using FluentAssertions; 3 | using RestFulTests.Fakers; 4 | using RestFulTests.Models; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using Xunit; 8 | 9 | namespace RestFulTests 10 | { 11 | public class SortingTests 12 | { 13 | private readonly List _users; 14 | 15 | public SortingTests() 16 | { 17 | _users = UserFaker.GenerateUserViewModel().Generate(50); 18 | } 19 | 20 | [Fact] 21 | public void Should_Sort_By_Username() 22 | { 23 | var sortingByFieldName = _users.AsQueryable().Sort("username").Select(s => s.Username).ToList(); 24 | var sortingByOriginal = _users.OrderBy(s => s.Username).Select(s => s.Username).ToList(); 25 | for (int i = 0; i < sortingByFieldName.Count(); i++) 26 | { 27 | sortingByFieldName[i].Should().Be(sortingByOriginal[i]); 28 | } 29 | } 30 | 31 | [Fact] 32 | public void Should_Sort_By_Username_Descending() 33 | { 34 | var sortingByFieldName = _users.AsQueryable().Sort("-username").Select(s => s.Username).ToList(); 35 | var sortingByOriginal = _users.OrderByDescending(s => s.Username).Select(s => s.Username).ToList(); 36 | for (int i = 0; i < sortingByFieldName.Count(); i++) 37 | { 38 | sortingByFieldName[i].Should().Be(sortingByOriginal[i]); 39 | } 40 | } 41 | 42 | 43 | [Fact] 44 | public void Should_Sort_By_Username__Descending_Then_By_Firstname() 45 | { 46 | var sortingByFieldName = _users.AsQueryable().Sort("-username, firstname").ToList(); 47 | var sortingByOriginal = _users.OrderByDescending(s => s.Username).ThenBy(s => s.FirstName).ToList(); 48 | for (int i = 0; i < sortingByFieldName.Count(); i++) 49 | { 50 | sortingByFieldName[i].Username.Should().Be(sortingByOriginal[i].Username); 51 | sortingByFieldName[i].FirstName.Should().Be(sortingByOriginal[i].FirstName); 52 | } 53 | } 54 | 55 | 56 | [Fact] 57 | public void Should_Sort_From_Interface_Implementation() 58 | { 59 | var sort = new UserSearch() 60 | { 61 | Sort = "username" 62 | }; 63 | var sortingByFieldName = _users.AsQueryable().Sort(sort).ToList(); 64 | var sortingByOriginal = _users.OrderBy(s => s.Username).ThenBy(s => s.FirstName).ToList(); 65 | for (int i = 0; i < sortingByFieldName.Count(); i++) 66 | { 67 | sortingByFieldName[i].Username.Should().Be(sortingByOriginal[i].Username); 68 | sortingByFieldName[i].FirstName.Should().Be(sortingByOriginal[i].FirstName); 69 | } 70 | } 71 | 72 | 73 | 74 | [Fact] 75 | public void Should_Not_Throw_Error_When_Sort_Field_Doesnt_Exist() 76 | { 77 | var sort = new UserSearch() 78 | { 79 | Sort = "usernameaa" 80 | }; 81 | var sortingByFieldName = _users.AsQueryable().Sort(sort).ToList(); 82 | var sortingByOriginal = _users.ToList(); 83 | for (int i = 0; i < sortingByFieldName.Count(); i++) 84 | { 85 | sortingByFieldName[i].Username.Should().Be(sortingByOriginal[i].Username); 86 | sortingByFieldName[i].FirstName.Should().Be(sortingByOriginal[i].FirstName); 87 | } 88 | } 89 | 90 | } 91 | } 92 | --------------------------------------------------------------------------------