├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Directory.Packages.props ├── LICENSE.txt ├── QuerySpecification.sln ├── README.md ├── clean.sh ├── exclusion.dic ├── pozitronicon.png ├── pozitronlogo.png ├── readme-nuget.md ├── run-tests.sh ├── setup-sqllocaldb.ps1 ├── src ├── Directory.Build.props ├── QuerySpecification.EntityFrameworkCore │ ├── Evaluators │ │ ├── AsNoTrackingEvaluator.cs │ │ ├── AsNoTrackingWithIdentityResolutionEvaluator.cs │ │ ├── AsSplitQueryEvaluator.cs │ │ ├── AsTrackingEvaluator.cs │ │ ├── IgnoreAutoIncludesEvaluator.cs │ │ ├── IgnoreQueryFiltersEvaluator.cs │ │ ├── IncludeEvaluator.cs │ │ ├── IncludeStringEvaluator.cs │ │ ├── LikeEvaluator.cs │ │ ├── LikeExtension.cs │ │ ├── QueryTagEvaluator.cs │ │ └── SpecificationEvaluator.cs │ ├── Extensions │ │ └── IQueryableExtensions.cs │ ├── GlobalUsings.cs │ ├── QuerySpecification.EntityFrameworkCore.csproj │ ├── RepositoryBase.cs │ └── RepositoryWithMapper.cs └── QuerySpecification │ ├── Builders │ ├── Builder_Cache.cs │ ├── Builder_Flags.cs │ ├── Builder_Include.cs │ ├── Builder_Like.cs │ ├── Builder_Order.cs │ ├── Builder_Paging.cs │ ├── Builder_Select.cs │ ├── Builder_TagWith.cs │ ├── Builder_Where.cs │ ├── IncludableSpecificationBuilder.cs │ └── SpecificationBuilder.cs │ ├── DiscoveryAttribute.cs │ ├── Evaluators │ ├── IEvaluator.cs │ ├── IMemoryEvaluator.cs │ ├── LikeExtension.cs │ ├── LikeMemoryEvaluator.cs │ ├── OrderEvaluator.cs │ ├── PaginationExtensions.cs │ ├── SpecificationMemoryEvaluator.cs │ └── WhereEvaluator.cs │ ├── Exceptions │ ├── ConcurrentSelectorsException.cs │ ├── EntityNotFoundException.cs │ ├── InvalidLikePatternException.cs │ └── SelectorNotFoundException.cs │ ├── Expressions │ ├── IncludeExpression.cs │ ├── IncludeType.cs │ ├── LikeExpression.cs │ ├── OrderExpression.cs │ ├── OrderType.cs │ ├── SelectType.cs │ └── WhereExpression.cs │ ├── GlobalUsings.cs │ ├── IProjectionRepository.cs │ ├── IReadRepositoryBase.cs │ ├── IRepositoryBase.cs │ ├── Internals │ ├── ItemType.cs │ ├── Iterator.cs │ ├── SpecFlags.cs │ ├── SpecItem.cs │ ├── SpecIterator.cs │ ├── SpecLike.cs │ ├── SpecPaging.cs │ ├── SpecSelectIterator.cs │ └── TypeDiscovery.cs │ ├── Paging │ ├── PagedResult.cs │ ├── Pagination.cs │ ├── PaginationSettings.cs │ └── PagingFilter.cs │ ├── QuerySpecification.csproj │ ├── Specification.cs │ ├── SpecificationExtensions.cs │ ├── Validators │ ├── IValidator.cs │ ├── LikeValidator.cs │ ├── SpecificationValidator.cs │ └── WhereValidator.cs │ └── build │ └── Pozitron.QuerySpecification.targets └── tests ├── Directory.Build.props ├── QuerySpecification.AutoDiscovery.Tests ├── GlobalUsings.cs ├── QuerySpecification.AutoDiscovery.Tests.csproj ├── SpecificationEvaluatorTests.cs ├── SpecificationMemoryEvaluatorTests.cs ├── SpecificationValidatorTests.cs └── TypeDiscoveryTests.cs ├── QuerySpecification.Benchmarks ├── Benchmarks │ ├── Benchmark0_SpecSize.cs │ ├── Benchmark1_IQueryable.cs │ ├── Benchmark2_ToQueryString.cs │ ├── Benchmark3_DbQuery.cs │ ├── Benchmark4_Like.cs │ ├── Benchmark5_Include.cs │ ├── Benchmark6_IncludeEvaluator.cs │ ├── Benchmark7_LikeMemoryEvaluator.cs │ └── Benchmark8_LikeMemoryValidator.cs ├── Data │ ├── BenchmarkDbContext.cs │ ├── Company.cs │ ├── Country.cs │ ├── Product.cs │ └── Store.cs ├── Directory.Build.props ├── GloblUsings.cs ├── Program.cs └── QuerySpecification.Benchmarks.csproj ├── QuerySpecification.EntityFrameworkCore.Tests ├── Evaluators │ ├── AsNoTrackingEvaluatorTests.cs │ ├── AsNoTrackingWithIdentityResolutionEvaluatorTests.cs │ ├── AsSplitQueryEvaluatorTests.cs │ ├── AsTrackingEvaluatorTests.cs │ ├── IgnoreAutoIncludesEvaluatorTests.cs │ ├── IgnoreQueryFiltersEvaluatorTests.cs │ ├── IncludeEvaluatorTests.cs │ ├── IncludeStringEvaluatorTests.cs │ ├── LikeEvaluatorTests.cs │ ├── LikeExtensionTests.cs │ ├── OrderEvaluatorTests.cs │ ├── ParameterReplacerVisitorTests.cs │ ├── QueryTagEvaluatorTests.cs │ ├── SpecificationEvaluatorTests.cs │ └── WhereEvaluatorTests.cs ├── Extensions │ ├── Extensions_ToPagedResult.cs │ └── Extensions_WithSpecification.cs ├── Fixture │ ├── Data │ │ ├── Address.cs │ │ ├── Bar.cs │ │ ├── Company.cs │ │ ├── Country.cs │ │ ├── Foo.cs │ │ ├── Product.cs │ │ ├── ProductImage.cs │ │ └── Store.cs │ ├── IntegrationTest.cs │ ├── Repository.cs │ ├── SharedCollection.cs │ ├── TestDbContext.cs │ └── TestFactory.cs ├── GlobalUsings.cs ├── QuerySpecification.EntityFrameworkCore.Tests.csproj ├── QueryTests.cs └── Repositories │ ├── RepositoryTests.cs │ ├── Repository_AnyTests.cs │ ├── Repository_CountTests.cs │ ├── Repository_FirstTests.cs │ ├── Repository_ListTests.cs │ ├── Repository_ProjectToTests.cs │ └── Repository_WriteTests.cs └── QuerySpecification.Tests ├── Builders ├── Builder_AsNoTracking.cs ├── Builder_AsNoTrackingWithIdentityResolution.cs ├── Builder_AsSplitQuery.cs ├── Builder_AsTracking.cs ├── Builder_IgnoreAutoIncludes.cs ├── Builder_IgnoreQueryFilters.cs ├── Builder_Include.cs ├── Builder_IncludeString.cs ├── Builder_Like.cs ├── Builder_OrderBy.cs ├── Builder_OrderByDescending.cs ├── Builder_OrderThenBy.cs ├── Builder_OrderThenByDescending.cs ├── Builder_Select.cs ├── Builder_SelectMany.cs ├── Builder_Skip.cs ├── Builder_TagWith.cs ├── Builder_Take.cs ├── Builder_ThenInclude.cs ├── Builder_Where.cs ├── Builder_WithCacheKey.cs └── SpecificationBuilderTests.cs ├── Evaluators ├── LikeExtensionTests.cs ├── LikeMemoryEvaluatorTests.cs ├── OrderEvaluatorTests.cs ├── PaginationExtensionsTests.cs ├── SpecificationMemoryEvaluatorTests.cs └── WhereEvaluatorTests.cs ├── Exceptions ├── ConcurrentSelectorsExceptionTests.cs ├── EntityNotFoundExceptionTests.cs ├── InvalidLikePatternExceptionTests.cs └── SelectorNotFoundExceptionTests.cs ├── GlobalUsings.cs ├── Internals └── TypeDiscoveryTests.cs ├── Paging ├── PagedResultTests.cs ├── PaginationSettingsTests.cs └── PaginationTests.cs ├── QuerySpecification.Tests.csproj ├── SpecificationExtensionsTests.cs ├── SpecificationInternalsTests.cs ├── SpecificationTests.cs └── Validators ├── LikeValidatorTests.cs ├── SpecificationValidatorTests.cs └── WhereValidatorTests.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/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Full Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Setup dotnet 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: 9.x 20 | dotnet-quality: 'preview' 21 | - name: Build 22 | run: dotnet build --configuration Release 23 | - name: Test 24 | run: dotnet test --configuration Release --no-build --no-restore --collect:"XPlat Code Coverage;Format=opencover" 25 | - name: ReportGenerator 26 | uses: danielpalme/ReportGenerator-GitHub-Action@5.3.9 27 | with: 28 | reports: tests/**/coverage.opencover.xml 29 | targetdir: ${{ runner.temp }}/coveragereport 30 | reporttypes: 'Html;Badges;MarkdownSummaryGithub' 31 | assemblyfilters: -*Tests* 32 | - name: Publish coverage report in build summary 33 | run: cat '${{ runner.temp }}'/coveragereport/SummaryGithub.md >> $GITHUB_STEP_SUMMARY 34 | shell: bash 35 | - name: Create coverage-reports branch and push content 36 | run: | 37 | git fetch 38 | git checkout coverage-reports || git checkout --orphan coverage-reports 39 | git reset --hard 40 | git clean -fd 41 | cp -rp '${{ runner.temp }}'/coveragereport/* ./ 42 | echo "queryspecification.fiseni.com" > CNAME 43 | git config user.name github-actions 44 | git config user.email github-actions@github.com 45 | git add . 46 | git commit -m "Update coverage reports [skip ci]" || echo "No changes to commit" 47 | git push origin coverage-reports --force 48 | shell: bash 49 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Setup dotnet 17 | uses: actions/setup-dotnet@v4 18 | with: 19 | dotnet-version: 9.x 20 | dotnet-quality: 'preview' 21 | - name: Build 22 | run: dotnet build --configuration Release 23 | - name: Test 24 | run: dotnet test --configuration Release --no-build --no-restore 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release to Nuget 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | - name: Setup dotnet 15 | uses: actions/setup-dotnet@v4 16 | with: 17 | dotnet-version: 9.x 18 | dotnet-quality: 'preview' 19 | - name: Build 20 | run: dotnet build --configuration Release 21 | - name: Test 22 | run: dotnet test --configuration Release --no-build --no-restore 23 | - name: Pack 24 | run: dotnet pack --configuration Release --no-build --no-restore --output . 25 | - name: Push to NuGet 26 | run: dotnet nuget push "*.nupkg" --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json 27 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Fati Iseni 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 | -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Fati Iseni 3 | 4 | WorkingDir="$(pwd)" 5 | 6 | ########## Make sure you're not on a root path :) 7 | safetyCheck() 8 | { 9 | declare -a arr=("" "/" "/c" "/d" "c:\\" "d:\\" "C:\\" "D:\\") 10 | for i in "${arr[@]}" 11 | do 12 | if [ "$WorkingDir" = "$i" ]; then 13 | echo ""; 14 | echo "You are on a root path. Please run the script from a given directory."; 15 | exit 1; 16 | fi 17 | done 18 | } 19 | 20 | deleteBinObj() 21 | { 22 | echo "Deleting bin and obj directories..."; 23 | find "$WorkingDir/" -type d -name "bin" -exec rm -rf {} \; > /dev/null 2>&1; 24 | find "$WorkingDir/" -type d -name "obj" -exec rm -rf {} \; > /dev/null 2>&1; 25 | } 26 | 27 | deleteVSDir() 28 | { 29 | echo "Deleting .vs directories..."; 30 | find "$WorkingDir/" -type d -name ".vs" -exec rm -rf {} \; > /dev/null 2>&1; 31 | } 32 | 33 | deleteLogs() 34 | { 35 | echo "Deleting Logs directories..."; 36 | find "$WorkingDir/" -type d -name "Logs" -exec rm -rf {} \; > /dev/null 2>&1; 37 | } 38 | 39 | deleteUserCsprojFiles() 40 | { 41 | echo "Deleting *.csproj.user files..."; 42 | find "$WorkingDir/" -type f -name "*.csproj.user" -exec rm -rf {} \; > /dev/null 2>&1; 43 | } 44 | 45 | deleteTestResults() 46 | { 47 | echo "Deleting test and coverage artifacts..."; 48 | find "$WorkingDir/" -type d -name "TestResults" -exec rm -rf {} \; > /dev/null 2>&1; 49 | } 50 | 51 | deleteLocalGitBranches() 52 | { 53 | echo "Deleting local unused git branches (e.g. no corresponding remote branch)..."; 54 | git fetch -p && git branch -vv | awk '/: gone\]/{print $1}' | xargs -I {} git branch -D {} 55 | } 56 | 57 | safetyCheck; 58 | echo ""; 59 | 60 | if [ "$1" = "help" ]; then 61 | echo "Usage:"; 62 | echo ""; 63 | echo -e "clean.sh [obj | vs | logs | user | coverages | branches | all]"; 64 | echo ""; 65 | echo -e "obj (Default)\t-\tDeletes bin and obj directories."; 66 | echo -e "vs\t\t-\tDeletes .vs directories."; 67 | echo -e "logs\t\t-\tDeletes Logs directories."; 68 | echo -e "user\t\t-\tDeletes *.csproj.user files."; 69 | echo -e "coverages\t-\tDeletes test and coverage artifacts."; 70 | echo -e "branches\t-\tDeletes local unused git branches (e.g. no corresponding remote branch)."; 71 | echo -e "all\t\t-\tApply all options"; 72 | 73 | elif [ "$1" = "obj" ]; then 74 | deleteBinObj; 75 | elif [ "$1" = "vs" ]; then 76 | deleteVSDir; 77 | elif [ "$1" = "logs" ]; then 78 | deleteLogs; 79 | elif [ "$1" = "user" ]; then 80 | deleteUserCsprojFiles; 81 | elif [ "$1" = "coverages" ]; then 82 | deleteTestResults; 83 | elif [ "$1" = "branches" ]; then 84 | deleteLocalGitBranches; 85 | elif [ "$1" = "all" ]; then 86 | deleteBinObj; 87 | deleteVSDir; 88 | deleteLogs; 89 | deleteUserCsprojFiles; 90 | deleteTestResults; 91 | deleteLocalGitBranches; 92 | else 93 | deleteBinObj; 94 | fi 95 | -------------------------------------------------------------------------------- /exclusion.dic: -------------------------------------------------------------------------------- 1 | Pozitron 2 | pozitron 3 | microsoft 4 | mssql 5 | mssqllocaldb 6 | localdb 7 | _respawner 8 | respawner 9 | criterias 10 | Unescape 11 | #Dummy test data 12 | Foos 13 | axxa 14 | axya 15 | aaaa 16 | vvvv 17 | irst 18 | irstt 19 | asdf 20 | asdfa 21 | aaab 22 | aaaab 23 | aaaaab 24 | axza 25 | Compilable 26 | netstandard 27 | -------------------------------------------------------------------------------- /pozitronicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiseni/QuerySpecification/29d63784f6d4630d7aaf187e4798c8936ee14dd8/pozitronicon.png -------------------------------------------------------------------------------- /pozitronlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiseni/QuerySpecification/29d63784f6d4630d7aaf187e4798c8936ee14dd8/pozitronlogo.png -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | dotnet tool list -g dotnet-reportgenerator-globaltool > /dev/null 2>&1 4 | exists=$(echo $?) 5 | if [ $exists -ne 0 ]; then 6 | echo "Installing ReportGenerator..." 7 | dotnet tool install -g dotnet-reportgenerator-globaltool 8 | echo "ReportGenerator installed" 9 | fi 10 | 11 | find . -type d -name TestResults -exec rm -rf {} \; > /dev/null 2>&1 12 | 13 | testtarget="$1" 14 | 15 | if [ "$testtarget" = "" ]; then 16 | testtarget="*.sln" 17 | fi 18 | 19 | dotnet build $testtarget --configuration Release 20 | dotnet test $testtarget --configuration Release --no-build --no-restore --collect:"XPlat Code Coverage;Format=opencover" 21 | 22 | reportgenerator \ 23 | -reports:tests/**/coverage.opencover.xml \ 24 | -targetdir:TestResults \ 25 | -reporttypes:"Html;Badges;MarkdownSummaryGithub" \ 26 | -assemblyfilters:-*Tests* 27 | -------------------------------------------------------------------------------- /setup-sqllocaldb.ps1: -------------------------------------------------------------------------------- 1 | # Taken from psake https://github.com/psake/psake 2 | 3 | <# 4 | .SYNOPSIS 5 | This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode 6 | to see if an error occcured. If an error is detected then an exception is thrown. 7 | This function allows you to run command-line programs without having to 8 | explicitly check the $lastexitcode variable. 9 | .EXAMPLE 10 | exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" 11 | #> 12 | function Exec 13 | { 14 | [CmdletBinding()] 15 | param( 16 | [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, 17 | [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) 18 | ) 19 | & $cmd 20 | if ($lastexitcode -ne 0) { 21 | throw ("Exec: " + $errorMessage) 22 | } 23 | } 24 | 25 | Write-Host "Downloading" 26 | Import-Module BitsTransfer 27 | Start-BitsTransfer -Source https://download.microsoft.com/download/7/c/1/7c14e92e-bdcb-4f89-b7cf-93543e7112d1/SqlLocalDB.msi -Destination SqlLocalDB.msi 28 | Write-Host "Installing" 29 | Start-Process -FilePath "SqlLocalDB.msi" -Wait -ArgumentList "/qn", "/norestart", "/l*v SqlLocalDBInstall.log", "IACCEPTSQLLOCALDBLICENSETERMS=YES"; 30 | <# 31 | Write-Host "Checking" 32 | sqlcmd -l 60 -S "(localdb)\MSSQLLocalDB" -Q "SELECT @@VERSION;" 33 | #> -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pozitron.QuerySpecification 5 | net8.0;net9.0 6 | enable 7 | enable 8 | latest 9 | 10 | true 11 | false 12 | true 13 | 14 | 15 | 16 | true 17 | snupkg 18 | true 19 | true 20 | 21 | 22 | 23 | 24 | 25 | Fati Iseni 26 | Pozitron Group 27 | Copyright © 2024 Pozitron Group 28 | Pozitron QuerySpecification 29 | 30 | https://github.com/fiseni/QuerySpecification 31 | https://github.com/fiseni/QuerySpecification 32 | true 33 | git 34 | MIT 35 | readme-nuget.md 36 | https://pozitrongroup.com/PozitronLogo.png 37 | pozitronicon.png 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Evaluator to apply AsNoTracking to the query if the specification has AsNoTracking set to true. 5 | /// 6 | [EvaluatorDiscovery(Order = 100)] 7 | public sealed class AsNoTrackingEvaluator : IEvaluator 8 | { 9 | /// 10 | /// Gets the singleton instance of the class. 11 | /// 12 | public static AsNoTrackingEvaluator Instance = new(); 13 | private AsNoTrackingEvaluator() { } 14 | 15 | /// 16 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 17 | { 18 | if (specification.AsNoTracking) 19 | { 20 | source = source.AsNoTracking(); 21 | } 22 | 23 | return source; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QuerySpecification.EntityFrameworkCore/Evaluators/AsNoTrackingWithIdentityResolutionEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Evaluator to apply AsNoTracking to the query if the specification has AsNoTracking set to true. 5 | /// 6 | [EvaluatorDiscovery(Order = 110)] 7 | public sealed class AsNoTrackingWithIdentityResolutionEvaluator : IEvaluator 8 | { 9 | /// 10 | /// Gets the singleton instance of the class. 11 | /// 12 | public static AsNoTrackingWithIdentityResolutionEvaluator Instance = new(); 13 | private AsNoTrackingWithIdentityResolutionEvaluator() { } 14 | 15 | /// 16 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 17 | { 18 | if (specification.AsNoTrackingWithIdentityResolution) 19 | { 20 | source = source.AsNoTrackingWithIdentityResolution(); 21 | } 22 | 23 | return source; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QuerySpecification.EntityFrameworkCore/Evaluators/AsSplitQueryEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Evaluator to apply AsSplitQuery to the query if the specification has AsSplitQuery set to true. 5 | /// 6 | [EvaluatorDiscovery(Order = 90)] 7 | public sealed class AsSplitQueryEvaluator : IEvaluator 8 | { 9 | /// 10 | /// Gets the singleton instance of the class. 11 | /// 12 | public static AsSplitQueryEvaluator Instance = new(); 13 | private AsSplitQueryEvaluator() { } 14 | 15 | /// 16 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 17 | { 18 | if (specification.AsSplitQuery) 19 | { 20 | source = source.AsSplitQuery(); 21 | } 22 | 23 | return source; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QuerySpecification.EntityFrameworkCore/Evaluators/AsTrackingEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Evaluator to apply AsTracking to the query if the specification has AsTracking set to true. 5 | /// 6 | [EvaluatorDiscovery(Order = 120)] 7 | public sealed class AsTrackingEvaluator : IEvaluator 8 | { 9 | /// 10 | /// Gets the singleton instance of the class. 11 | /// 12 | public static AsTrackingEvaluator Instance = new(); 13 | private AsTrackingEvaluator() { } 14 | 15 | /// 16 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 17 | { 18 | if (specification.AsTracking) 19 | { 20 | source = source.AsTracking(); 21 | } 22 | 23 | return source; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreAutoIncludesEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Evaluator to apply IgnoreAutoIncludes to the query if the specification has IgnoreAutoIncludes set to true. 5 | /// 6 | [EvaluatorDiscovery(Order = 70)] 7 | public sealed class IgnoreAutoIncludesEvaluator : IEvaluator 8 | { 9 | 10 | /// 11 | /// Gets the singleton instance of the class. 12 | /// 13 | public static IgnoreAutoIncludesEvaluator Instance = new(); 14 | private IgnoreAutoIncludesEvaluator() { } 15 | 16 | /// 17 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 18 | { 19 | if (specification.IgnoreAutoIncludes) 20 | { 21 | source = source.IgnoreAutoIncludes(); 22 | } 23 | 24 | return source; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/QuerySpecification.EntityFrameworkCore/Evaluators/IgnoreQueryFiltersEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Evaluator to apply IgnoreQueryFilters to the query if the specification has IgnoreQueryFilters set to true. 5 | /// 6 | [EvaluatorDiscovery(Order = 80)] 7 | public sealed class IgnoreQueryFiltersEvaluator : IEvaluator 8 | { 9 | 10 | /// 11 | /// Gets the singleton instance of the class. 12 | /// 13 | public static IgnoreQueryFiltersEvaluator Instance = new(); 14 | private IgnoreQueryFiltersEvaluator() { } 15 | 16 | /// 17 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 18 | { 19 | if (specification.IgnoreQueryFilters) 20 | { 21 | source = source.IgnoreQueryFilters(); 22 | } 23 | 24 | return source; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/QuerySpecification.EntityFrameworkCore/Evaluators/IncludeStringEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Evaluates a specification to include navigation properties specified by string paths. 5 | /// 6 | [EvaluatorDiscovery(Order = 30)] 7 | public sealed class IncludeStringEvaluator : IEvaluator 8 | { 9 | 10 | /// 11 | /// Gets the singleton instance of the class. 12 | /// 13 | public static IncludeStringEvaluator Instance = new(); 14 | private IncludeStringEvaluator() { } 15 | 16 | /// 17 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 18 | { 19 | foreach (var item in specification.Items) 20 | { 21 | if (item.Type == ItemType.IncludeString && item.Reference is string includeString) 22 | { 23 | source = source.Include(includeString); 24 | } 25 | } 26 | 27 | return source; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/QuerySpecification.EntityFrameworkCore/Evaluators/LikeExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Diagnostics; 3 | using System.Reflection; 4 | 5 | namespace Pozitron.QuerySpecification; 6 | 7 | internal static class LikeExtension 8 | { 9 | private static readonly MethodInfo _likeMethodInfo = typeof(DbFunctionsExtensions) 10 | .GetMethod(nameof(DbFunctionsExtensions.Like), [typeof(DbFunctions), typeof(string), typeof(string)])!; 11 | 12 | private static readonly MemberExpression _functions = Expression.Property(null, typeof(EF).GetProperty(nameof(EF.Functions))!); 13 | 14 | // It's required so EF can generate parameterized query. 15 | // In the past I've been creating closures for this, e.g. var patternAsExpression = ((Expression>)(() => pattern)).Body; 16 | // But, that allocates 168 bytes. So, this is more efficient way. 17 | private static MemberExpression StringAsExpression(string value) => Expression.Property( 18 | Expression.Constant(new StringVar(value)), 19 | typeof(StringVar).GetProperty(nameof(StringVar.Format))!); 20 | 21 | // We'll name the property Format just so we match the produced SQL query parameter name (in case of interpolated strings). 22 | private record StringVar(string Format); 23 | 24 | public static IQueryable ApplyLikesAsOrGroup(this IQueryable source, ReadOnlySpan likeItems) 25 | { 26 | Debug.Assert(_likeMethodInfo is not null); 27 | 28 | Expression? combinedExpr = null; 29 | ParameterExpression? mainParam = null; 30 | ParameterReplacerVisitor? visitor = null; 31 | 32 | foreach (var item in likeItems) 33 | { 34 | if (item.Reference is not SpecLike specLike) continue; 35 | 36 | mainParam ??= specLike.KeySelector.Parameters[0]; 37 | 38 | var selectorExpr = specLike.KeySelector.Body; 39 | if (mainParam != specLike.KeySelector.Parameters[0]) 40 | { 41 | visitor ??= new ParameterReplacerVisitor(specLike.KeySelector.Parameters[0], mainParam); 42 | 43 | // If there are more than 2 likes, we want to avoid creating a new visitor instance (saving 32 bytes per instance). 44 | // We're in a sequential loop, no concurrency issues. 45 | visitor.Update(specLike.KeySelector.Parameters[0], mainParam); 46 | selectorExpr = visitor.Visit(selectorExpr); 47 | } 48 | 49 | var patternExpr = StringAsExpression(specLike.Pattern); 50 | 51 | var likeExpr = Expression.Call( 52 | null, 53 | _likeMethodInfo, 54 | _functions, 55 | selectorExpr, 56 | patternExpr); 57 | 58 | combinedExpr = combinedExpr is null 59 | ? likeExpr 60 | : Expression.OrElse(combinedExpr, likeExpr); 61 | } 62 | 63 | return combinedExpr is null || mainParam is null 64 | ? source 65 | : source.Where(Expression.Lambda>(combinedExpr, mainParam)); 66 | } 67 | } 68 | 69 | internal sealed class ParameterReplacerVisitor : ExpressionVisitor 70 | { 71 | private ParameterExpression _oldParameter; 72 | private Expression _newExpression; 73 | 74 | internal ParameterReplacerVisitor(ParameterExpression oldParameter, Expression newExpression) => 75 | (_oldParameter, _newExpression) = (oldParameter, newExpression); 76 | 77 | internal void Update(ParameterExpression oldParameter, Expression newExpression) => 78 | (_oldParameter, _newExpression) = (oldParameter, newExpression); 79 | 80 | protected override Expression VisitParameter(ParameterExpression node) => 81 | node == _oldParameter ? _newExpression : node; 82 | } 83 | -------------------------------------------------------------------------------- /src/QuerySpecification.EntityFrameworkCore/Evaluators/QueryTagEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Evaluator to apply the query tags to the query. 5 | /// 6 | [EvaluatorDiscovery(Order = 60)] 7 | public sealed class QueryTagEvaluator : IEvaluator 8 | { 9 | 10 | /// 11 | /// Gets the singleton instance of the class. 12 | /// 13 | public static QueryTagEvaluator Instance = new(); 14 | private QueryTagEvaluator() { } 15 | 16 | /// 17 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 18 | { 19 | foreach (var item in specification.Items) 20 | { 21 | if (item.Type == ItemType.QueryTag && item.Reference is string tag) 22 | { 23 | source = source.TagWith(tag); 24 | } 25 | } 26 | 27 | return source; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/QuerySpecification.EntityFrameworkCore/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.EntityFrameworkCore; 2 | global using System.Linq.Expressions; 3 | -------------------------------------------------------------------------------- /src/QuerySpecification.EntityFrameworkCore/QuerySpecification.EntityFrameworkCore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Pozitron.QuerySpecification.EntityFrameworkCore 5 | Pozitron.QuerySpecification.EntityFrameworkCore 6 | Pozitron.QuerySpecification.EntityFrameworkCore 7 | EntityFrameworkCore plugin to Pozitron.QuerySpecification containing EF evaluators. 8 | EntityFrameworkCore plugin to Pozitron.QuerySpecification containing EF evaluators. 9 | 10 | 11.2.0 11 | fiseni pozitron query specification efcore 12 | 13 | Refer to Releases page for details. 14 | https://github.com/fiseni/QuerySpecification/releases 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/QuerySpecification/Builders/Builder_Cache.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | public static partial class SpecificationBuilderExtensions 4 | { 5 | /// 6 | /// Sets the cache key for the specification. 7 | /// 8 | /// The type of the entity. 9 | /// The type of the result. 10 | /// The specification builder. 11 | /// The cache key to be used. 12 | /// The updated specification builder. 13 | public static ISpecificationBuilder WithCacheKey( 14 | this ISpecificationBuilder builder, 15 | string cacheKey) where T : class 16 | { 17 | WithCacheKey(builder, cacheKey, true); 18 | return (SpecificationBuilder)builder; 19 | } 20 | 21 | /// 22 | /// Sets the cache key for the specification. 23 | /// 24 | /// The type of the entity. 25 | /// The type of the result. 26 | /// The specification builder. 27 | /// The cache key to be used. 28 | /// The condition to evaluate. 29 | /// The updated specification builder. 30 | public static ISpecificationBuilder WithCacheKey( 31 | this ISpecificationBuilder builder, 32 | string cacheKey, 33 | bool condition) where T : class 34 | { 35 | if (condition) 36 | { 37 | builder.Specification.AddOrUpdateInternal(ItemType.CacheKey, cacheKey); 38 | } 39 | 40 | return builder; 41 | } 42 | 43 | /// 44 | /// Sets the cache key for the specification. 45 | /// 46 | /// The type of the entity. 47 | /// The specification builder. 48 | /// The cache key to be used. 49 | /// The updated specification builder. 50 | public static ISpecificationBuilder WithCacheKey( 51 | this ISpecificationBuilder builder, 52 | string cacheKey) where T : class 53 | => WithCacheKey(builder, cacheKey, true); 54 | 55 | /// 56 | /// Sets the cache key for the specification. 57 | /// 58 | /// The type of the entity. 59 | /// The specification builder. 60 | /// The cache key to be used. 61 | /// The condition to evaluate. 62 | /// The updated specification builder. 63 | public static ISpecificationBuilder WithCacheKey( 64 | this ISpecificationBuilder builder, 65 | string cacheKey, 66 | bool condition) where T : class 67 | { 68 | if (condition) 69 | { 70 | builder.Specification.AddOrUpdateInternal(ItemType.CacheKey, cacheKey); 71 | } 72 | 73 | return builder; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/QuerySpecification/Builders/Builder_Select.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | public static partial class SpecificationBuilderExtensions 4 | { 5 | /// 6 | /// Adds a Select clause to the specification. 7 | /// 8 | /// The type of the entity. 9 | /// The type of the result. 10 | /// The specification builder. 11 | /// The selector expression. 12 | public static void Select( 13 | this ISpecificationBuilder builder, 14 | Expression> selector) 15 | { 16 | builder.Specification.AddOrUpdateInternal(ItemType.Select, selector, (int)SelectType.Select); 17 | } 18 | 19 | /// 20 | /// Adds a SelectMany clause to the specification. 21 | /// 22 | /// The type of the entity. 23 | /// The type of the result. 24 | /// The specification builder. 25 | /// The selector expression. 26 | public static void SelectMany( 27 | this ISpecificationBuilder builder, 28 | Expression>> selector) 29 | { 30 | builder.Specification.AddOrUpdateInternal(ItemType.Select, selector, (int)SelectType.SelectMany); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/QuerySpecification/Builders/Builder_TagWith.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | public static partial class SpecificationBuilderExtensions 4 | { 5 | /// 6 | /// Adds a query tag to the specification. 7 | /// 8 | /// The type of the entity. 9 | /// The type of the result. 10 | /// The specification builder. 11 | /// The query tag. 12 | /// The updated specification builder. 13 | public static ISpecificationBuilder TagWith( 14 | this ISpecificationBuilder builder, 15 | string tag) 16 | { 17 | TagWith(builder, tag, true); 18 | return builder; 19 | } 20 | 21 | /// 22 | /// Adds a query tag to the specification if the condition is true. 23 | /// 24 | /// The type of the entity. 25 | /// The type of the result. 26 | /// The specification builder. 27 | /// The query tag. 28 | /// The condition to evaluate. 29 | /// The updated specification builder. 30 | public static ISpecificationBuilder TagWith( 31 | this ISpecificationBuilder builder, 32 | string tag, 33 | bool condition) 34 | { 35 | if (condition) 36 | { 37 | builder.Specification.AddInternal(ItemType.QueryTag, tag); 38 | } 39 | 40 | return builder; 41 | } 42 | 43 | /// 44 | /// Adds a query tag to the specification. 45 | /// 46 | /// The type of the entity. 47 | /// The specification builder. 48 | /// The query tag. 49 | /// The updated specification builder. 50 | public static ISpecificationBuilder TagWith( 51 | this ISpecificationBuilder builder, 52 | string tag) 53 | => TagWith(builder, tag, true); 54 | 55 | /// 56 | /// Adds a query tag to the specification if the condition is true. 57 | /// 58 | /// The type of the entity. 59 | /// The specification builder. 60 | /// The query tag. 61 | /// The condition to evaluate. 62 | /// The updated specification builder. 63 | public static ISpecificationBuilder TagWith( 64 | this ISpecificationBuilder builder, 65 | string tag, 66 | bool condition) 67 | { 68 | if (condition) 69 | { 70 | builder.Specification.AddInternal(ItemType.QueryTag, tag); 71 | } 72 | 73 | return builder; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/QuerySpecification/Builders/Builder_Where.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | public static partial class SpecificationBuilderExtensions 4 | { 5 | /// 6 | /// Adds a Where clause to the specification. 7 | /// 8 | /// The type of the entity. 9 | /// The type of the result. 10 | /// The specification builder. 11 | /// The predicate expression. 12 | /// The updated specification builder. 13 | public static ISpecificationBuilder Where( 14 | this ISpecificationBuilder builder, 15 | Expression> predicate) 16 | => Where(builder, predicate, true); 17 | 18 | /// 19 | /// Adds a Where clause to the specification if the condition is true. 20 | /// 21 | /// The type of the entity. 22 | /// The type of the result. 23 | /// The specification builder. 24 | /// The predicate expression. 25 | /// The condition to evaluate. 26 | /// The updated specification builder. 27 | public static ISpecificationBuilder Where( 28 | this ISpecificationBuilder builder, 29 | Expression> predicate, 30 | bool condition) 31 | { 32 | if (condition) 33 | { 34 | builder.Specification.AddInternal(ItemType.Where, predicate); 35 | } 36 | return builder; 37 | } 38 | 39 | /// 40 | /// Adds a Where clause to the specification. 41 | /// 42 | /// The type of the entity. 43 | /// The specification builder. 44 | /// The predicate expression. 45 | /// The updated specification builder. 46 | public static ISpecificationBuilder Where( 47 | this ISpecificationBuilder builder, 48 | Expression> predicate) 49 | => Where(builder, predicate, true); 50 | 51 | /// 52 | /// Adds a Where clause to the specification if the condition is true. 53 | /// 54 | /// The type of the entity. 55 | /// The specification builder. 56 | /// The predicate expression. 57 | /// The condition to evaluate. 58 | /// The updated specification builder. 59 | public static ISpecificationBuilder Where( 60 | this ISpecificationBuilder builder, 61 | Expression> predicate, 62 | bool condition) 63 | { 64 | if (condition) 65 | { 66 | builder.Specification.AddInternal(ItemType.Where, predicate); 67 | } 68 | return builder; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/QuerySpecification/Builders/IncludableSpecificationBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents a specification builder that supports include operations. 5 | /// 6 | /// The type of the entity. 7 | /// The type of the result. 8 | /// The type of the property. 9 | public interface IIncludableSpecificationBuilder : ISpecificationBuilder where T : class 10 | { 11 | } 12 | 13 | /// 14 | /// Represents a specification builder that supports include operations. 15 | /// 16 | /// The type of the entity. 17 | /// The type of the property. 18 | public interface IIncludableSpecificationBuilder : ISpecificationBuilder where T : class 19 | { 20 | } 21 | 22 | internal class IncludableSpecificationBuilder(Specification specification) 23 | : SpecificationBuilder(specification), IIncludableSpecificationBuilder where T : class 24 | { 25 | } 26 | 27 | internal class IncludableSpecificationBuilder(Specification specification) 28 | : SpecificationBuilder(specification), IIncludableSpecificationBuilder where T : class 29 | { 30 | } 31 | -------------------------------------------------------------------------------- /src/QuerySpecification/Builders/SpecificationBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents a specification builder that supports order operations. 5 | /// 6 | /// The type of the entity. 7 | /// The type of the result. 8 | public interface IOrderedSpecificationBuilder 9 | : ISpecificationBuilder, IOrderedSpecificationBuilder 10 | { 11 | } 12 | 13 | /// 14 | /// Represents a specification builder that supports order operations. 15 | /// 16 | /// The type of the entity. 17 | public interface IOrderedSpecificationBuilder 18 | : ISpecificationBuilder 19 | { 20 | } 21 | 22 | /// 23 | /// Represents a specification builder. 24 | /// 25 | /// The type of the entity. 26 | /// The type of the result. 27 | public interface ISpecificationBuilder 28 | : ISpecificationBuilder 29 | { 30 | new internal Specification Specification { get; } 31 | } 32 | 33 | /// 34 | /// Represents a specification builder. 35 | /// 36 | /// The type of the entity. 37 | public interface ISpecificationBuilder 38 | { 39 | internal Specification Specification { get; } 40 | 41 | /// 42 | /// Adds an item to the specification. 43 | /// 44 | /// The type of the item. 45 | /// The object to be stored in the item. 46 | /// Thrown if value is null 47 | /// Thrown if type is zero or negative. 48 | void Add(int type, object value); 49 | 50 | /// 51 | /// Adds or updates an item in the specification. 52 | /// 53 | /// The type of the item. 54 | /// The object to be stored in the item. 55 | /// Thrown if value is null 56 | /// Thrown if type is zero or negative. 57 | void AddOrUpdate(int type, object value); 58 | } 59 | 60 | internal class SpecificationBuilder(Specification specification) 61 | : SpecificationBuilder(specification), IOrderedSpecificationBuilder, ISpecificationBuilder 62 | { 63 | new public Specification Specification { get; } = specification; 64 | } 65 | 66 | internal class SpecificationBuilder(Specification specification) 67 | : IOrderedSpecificationBuilder, ISpecificationBuilder 68 | { 69 | public Specification Specification { get; } = specification; 70 | public void Add(int type, object value) => Specification.Add(type, value); 71 | public void AddOrUpdate(int type, object value) => Specification.AddOrUpdate(type, value); 72 | } 73 | -------------------------------------------------------------------------------- /src/QuerySpecification/DiscoveryAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Specifies whether auto discovery for evaluators and validators is enabled. 5 | /// 6 | [AttributeUsage(AttributeTargets.Assembly)] 7 | public sealed class SpecAutoDiscoveryAttribute : Attribute 8 | { 9 | } 10 | 11 | /// 12 | /// Specifies discovery options for evaluators and validators, such as the order and whether discovery is enabled. 13 | /// 14 | [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] 15 | public class DiscoveryAttribute : Attribute 16 | { 17 | /// 18 | /// Gets the order in which the evaluator/validator should be applied. Lower values are applied first. 19 | /// 20 | public int Order { get; set; } = int.MaxValue; 21 | 22 | /// 23 | /// Gets a value indicating whether the evaluator/validator is discoverable. 24 | /// 25 | public bool Enable { get; set; } = true; 26 | } 27 | 28 | /// 29 | /// Specifies discovery options for evaluators, such as the order and whether discovery is enabled. 30 | /// 31 | public sealed class EvaluatorDiscoveryAttribute : DiscoveryAttribute 32 | { 33 | } 34 | 35 | /// 36 | /// Specifies discovery options for validators, such as the order and whether discovery is enabled. 37 | /// 38 | public sealed class ValidatorDiscoveryAttribute : DiscoveryAttribute 39 | { 40 | } 41 | -------------------------------------------------------------------------------- /src/QuerySpecification/Evaluators/IEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents an evaluator that processes a specification. 5 | /// 6 | public interface IEvaluator 7 | { 8 | /// 9 | /// Evaluates the given specification on the provided queryable source. 10 | /// 11 | /// The type of the entity. 12 | /// The queryable source. 13 | /// The specification to evaluate. 14 | /// The evaluated queryable source. 15 | IQueryable Evaluate(IQueryable source, Specification specification) where T : class; 16 | } 17 | -------------------------------------------------------------------------------- /src/QuerySpecification/Evaluators/IMemoryEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents an in-memory evaluator that processes a specification. 5 | /// 6 | public interface IMemoryEvaluator 7 | { 8 | /// 9 | /// Evaluates the given specification on the provided enumerable source. 10 | /// 11 | /// The type of the entity. 12 | /// The enumerable source. 13 | /// The specification to evaluate. 14 | /// The evaluated enumerable source. 15 | IEnumerable Evaluate(IEnumerable source, Specification specification); 16 | } 17 | -------------------------------------------------------------------------------- /src/QuerySpecification/Evaluators/OrderEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents an evaluator for order expressions. 5 | /// 6 | [EvaluatorDiscovery(Order = 50)] 7 | public sealed class OrderEvaluator : IEvaluator, IMemoryEvaluator 8 | { 9 | /// 10 | /// Gets the singleton instance of the class. 11 | /// 12 | public static OrderEvaluator Instance = new(); 13 | private OrderEvaluator() { } 14 | 15 | /// 16 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 17 | { 18 | IOrderedQueryable? orderedQuery = null; 19 | 20 | foreach (var item in specification.Items) 21 | { 22 | if (item.Type == ItemType.Order && item.Reference is Expression> expr) 23 | { 24 | if (item.Bag == (int)OrderType.OrderBy) 25 | { 26 | orderedQuery = source.OrderBy(expr); 27 | } 28 | else if (item.Bag == (int)OrderType.OrderByDescending) 29 | { 30 | orderedQuery = source.OrderByDescending(expr); 31 | } 32 | else if (item.Bag == (int)OrderType.ThenBy) 33 | { 34 | orderedQuery = orderedQuery!.ThenBy(expr); 35 | } 36 | else if (item.Bag == (int)OrderType.ThenByDescending) 37 | { 38 | orderedQuery = orderedQuery!.ThenByDescending(expr); 39 | } 40 | } 41 | } 42 | 43 | if (orderedQuery is not null) 44 | { 45 | source = orderedQuery; 46 | } 47 | 48 | return source; 49 | } 50 | 51 | /// 52 | public IEnumerable Evaluate(IEnumerable source, Specification specification) 53 | { 54 | var compiledItems = specification.GetCompiledItems(); 55 | IOrderedEnumerable? orderedQuery = null; 56 | 57 | foreach (var item in compiledItems) 58 | { 59 | if (item.Type == ItemType.Order && item.Reference is Func compiledExpr) 60 | { 61 | if (item.Bag == (int)OrderType.OrderBy) 62 | { 63 | orderedQuery = source.OrderBy(compiledExpr); 64 | } 65 | else if (item.Bag == (int)OrderType.OrderByDescending) 66 | { 67 | orderedQuery = source.OrderByDescending(compiledExpr); 68 | } 69 | else if (item.Bag == (int)OrderType.ThenBy) 70 | { 71 | orderedQuery = orderedQuery!.ThenBy(compiledExpr); 72 | } 73 | else if (item.Bag == (int)OrderType.ThenByDescending) 74 | { 75 | orderedQuery = orderedQuery!.ThenByDescending(compiledExpr); 76 | } 77 | } 78 | } 79 | 80 | if (orderedQuery is not null) 81 | { 82 | source = orderedQuery; 83 | } 84 | 85 | return source; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/QuerySpecification/Evaluators/WhereEvaluator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents an evaluator for where expressions. 5 | /// 6 | [EvaluatorDiscovery(Order = 10)] 7 | public sealed class WhereEvaluator : IEvaluator, IMemoryEvaluator 8 | { 9 | /// 10 | /// Gets the singleton instance of the class. 11 | /// 12 | public static WhereEvaluator Instance = new(); 13 | private WhereEvaluator() { } 14 | 15 | /// 16 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 17 | { 18 | foreach (var item in specification.Items) 19 | { 20 | if (item.Type == ItemType.Where && item.Reference is Expression> expr) 21 | { 22 | source = source.Where(expr); 23 | } 24 | } 25 | 26 | return source; 27 | } 28 | 29 | /// 30 | public IEnumerable Evaluate(IEnumerable source, Specification specification) 31 | { 32 | var compiledItems = specification.GetCompiledItems(); 33 | 34 | foreach (var item in compiledItems) 35 | { 36 | if (item.Type == ItemType.Where && item.Reference is Func compiledExpr) 37 | { 38 | source = source.Where(compiledExpr); 39 | } 40 | } 41 | 42 | return source; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/QuerySpecification/Exceptions/ConcurrentSelectorsException.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Exception thrown when concurrent selectors are defined in the specification. 5 | /// 6 | public class ConcurrentSelectorsException : Exception 7 | { 8 | private const string _message = "Concurrent specification selector transforms defined. Ensure only one of the Select() or SelectMany() transforms is used in the same specification!"; 9 | 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | public ConcurrentSelectorsException() 14 | : base(_message) 15 | { 16 | } 17 | 18 | /// 19 | /// Initializes a new instance of the class with a specified inner exception. 20 | /// 21 | /// The exception that is the cause of this exception. 22 | public ConcurrentSelectorsException(Exception innerException) 23 | : base(_message, innerException) 24 | { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/QuerySpecification/Exceptions/EntityNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Exception thrown when an entity is not found. 5 | /// 6 | public class EntityNotFoundException : Exception 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | public EntityNotFoundException() 12 | : base($"The queried entity was not found!") 13 | { 14 | } 15 | 16 | /// 17 | /// Initializes a new instance of the class with a specified entity name. 18 | /// 19 | /// The name of the entity that was not found. 20 | public EntityNotFoundException(string entityName) 21 | : base($"The queried entity: {entityName} was not found!") 22 | { 23 | } 24 | 25 | /// 26 | /// Initializes a new instance of the class with a specified entity name and inner exception. 27 | /// 28 | /// The name of the entity that was not found. 29 | /// The exception that is the cause of this exception. 30 | public EntityNotFoundException(string entityName, Exception innerException) 31 | : base($"The queried entity: {entityName} was not found!", innerException) 32 | { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/QuerySpecification/Exceptions/InvalidLikePatternException.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Exception thrown when an invalid like pattern is encountered. 5 | /// 6 | public class InvalidLikePatternException : Exception 7 | { 8 | private const string _message = "Invalid like pattern: "; 9 | 10 | /// 11 | /// Initializes a new instance of the class with a specified pattern. 12 | /// 13 | /// The invalid like pattern. 14 | public InvalidLikePatternException(string pattern) 15 | : base($"{_message}{pattern}") 16 | { 17 | } 18 | 19 | /// 20 | /// Initializes a new instance of the class with a specified pattern and inner exception. 21 | /// 22 | /// The invalid like pattern. 23 | /// The exception that is the cause of this exception. 24 | public InvalidLikePatternException(string pattern, Exception innerException) 25 | : base($"{_message}{pattern}", innerException) 26 | { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/QuerySpecification/Exceptions/SelectorNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Exception thrown when a selector is not found in the specification. 5 | /// 6 | public class SelectorNotFoundException : Exception 7 | { 8 | private const string _message = "The specification must have a selector transform defined. Ensure either Select() or SelectMany() is used in the specification!"; 9 | 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | public SelectorNotFoundException() 14 | : base(_message) 15 | { 16 | } 17 | 18 | /// 19 | /// Initializes a new instance of the class with a specified inner exception. 20 | /// 21 | /// The exception that is the cause of this exception. 22 | public SelectorNotFoundException(Exception innerException) 23 | : base(_message, innerException) 24 | { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/QuerySpecification/Expressions/IncludeExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Pozitron.QuerySpecification; 4 | 5 | /// 6 | /// Represents an include expression used in a specification. 7 | /// 8 | /// The type of the entity. 9 | public sealed class IncludeExpression 10 | { 11 | /// 12 | /// Gets the lambda expression for the include. 13 | /// 14 | public LambdaExpression LambdaExpression { get; } 15 | 16 | /// 17 | /// Gets the type of the include. 18 | /// 19 | public IncludeType Type { get; } 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | /// The lambda expression for the include. 25 | /// The type of the include. 26 | public IncludeExpression(LambdaExpression expression, IncludeType type) 27 | { 28 | Debug.Assert(expression is not null); 29 | LambdaExpression = expression; 30 | Type = type; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/QuerySpecification/Expressions/IncludeType.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Specifies the type of include operation in a specification. 5 | /// 6 | public enum IncludeType 7 | { 8 | /// 9 | /// Represents an Include operation. 10 | /// 11 | Include = 1, 12 | 13 | /// 14 | /// Represents a ThenInclude operation after reference include. 15 | /// 16 | ThenIncludeAfterReference = 2, 17 | 18 | /// 19 | /// Represents a ThenInclude operation after collection include. 20 | /// 21 | ThenIncludeAfterCollection = 3 22 | } 23 | -------------------------------------------------------------------------------- /src/QuerySpecification/Expressions/LikeExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Pozitron.QuerySpecification; 4 | 5 | /// 6 | /// Represents a like expression used in a specification. 7 | /// 8 | /// The type of the entity. 9 | public sealed class LikeExpression 10 | { 11 | /// 12 | /// Gets the key selector expression. 13 | /// 14 | public Expression> KeySelector { get; } 15 | 16 | /// 17 | /// Gets the pattern to match. 18 | /// 19 | public string Pattern { get; } 20 | 21 | /// 22 | /// Gets the group number. 23 | /// 24 | public int Group { get; } 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// 29 | /// The key selector expression. 30 | /// The pattern to match. 31 | /// The group number. 32 | public LikeExpression(Expression> keySelector, string pattern, int group = 1) 33 | { 34 | Debug.Assert(keySelector is not null); 35 | Debug.Assert(!string.IsNullOrEmpty(pattern)); 36 | KeySelector = keySelector; 37 | Pattern = pattern; 38 | Group = group; 39 | } 40 | } 41 | 42 | /// 43 | /// Represents a compiled like expression used in a specification. 44 | /// 45 | /// The type of the entity. 46 | public sealed class LikeExpressionCompiled 47 | { 48 | /// 49 | /// Gets the compiled key selector function. 50 | /// 51 | public Func KeySelector { get; } 52 | 53 | /// 54 | /// Gets the pattern to match. 55 | /// 56 | public string Pattern { get; } 57 | 58 | /// 59 | /// Gets the group number. 60 | /// 61 | public int Group { get; } 62 | 63 | /// 64 | /// Initializes a new instance of the class. 65 | /// 66 | /// The compiled key selector function. 67 | /// The pattern to match. 68 | /// The group number. 69 | public LikeExpressionCompiled(Func keySelector, string pattern, int group = 1) 70 | { 71 | Debug.Assert(keySelector is not null); 72 | Debug.Assert(!string.IsNullOrEmpty(pattern)); 73 | KeySelector = keySelector; 74 | Pattern = pattern; 75 | Group = group; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/QuerySpecification/Expressions/OrderExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Pozitron.QuerySpecification; 4 | 5 | /// 6 | /// Represents an order expression used in a specification. 7 | /// 8 | /// The type of the entity. 9 | public sealed class OrderExpression 10 | { 11 | /// 12 | /// Gets the key selector expression. 13 | /// 14 | public Expression> KeySelector { get; } 15 | 16 | /// 17 | /// Gets the type of the order. 18 | /// 19 | public OrderType Type { get; } 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | /// The key selector expression. 25 | /// The type of the order. 26 | public OrderExpression(Expression> keySelector, OrderType type) 27 | { 28 | Debug.Assert(keySelector is not null); 29 | KeySelector = keySelector; 30 | Type = type; 31 | } 32 | } 33 | 34 | /// 35 | /// Represents a compiled order expression used in a specification. 36 | /// 37 | /// The type of the entity. 38 | public sealed class OrderExpressionCompiled 39 | { 40 | /// 41 | /// Gets the compiled key selector function. 42 | /// 43 | public Func KeySelector { get; } 44 | 45 | /// 46 | /// Gets the type of the order. 47 | /// 48 | public OrderType Type { get; } 49 | 50 | /// 51 | /// Initializes a new instance of the class. 52 | /// 53 | /// The compiled key selector function. 54 | /// The type of the order. 55 | public OrderExpressionCompiled(Func keySelector, OrderType type) 56 | { 57 | Debug.Assert(keySelector is not null); 58 | KeySelector = keySelector; 59 | Type = type; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/QuerySpecification/Expressions/OrderType.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Specifies the type of order operation in a specification. 5 | /// 6 | public enum OrderType 7 | { 8 | /// 9 | /// Represents an order by operation. 10 | /// 11 | OrderBy = 1, 12 | 13 | /// 14 | /// Represents an order by descending operation. 15 | /// 16 | OrderByDescending = 2, 17 | 18 | /// 19 | /// Represents a then by operation. 20 | /// 21 | ThenBy = 3, 22 | 23 | /// 24 | /// Represents a then by descending operation. 25 | /// 26 | ThenByDescending = 4 27 | } 28 | -------------------------------------------------------------------------------- /src/QuerySpecification/Expressions/SelectType.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Specifies the type of select operation in a specification. 5 | /// 6 | public enum SelectType 7 | { 8 | /// 9 | /// Represents a select operation. 10 | /// 11 | Select = 1, 12 | 13 | /// 14 | /// Represents a select many operation. 15 | /// 16 | SelectMany = 2 17 | } 18 | -------------------------------------------------------------------------------- /src/QuerySpecification/Expressions/WhereExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Pozitron.QuerySpecification; 4 | 5 | /// 6 | /// Represents a where expression used in a specification. 7 | /// 8 | /// The type of the entity. 9 | public sealed class WhereExpression 10 | { 11 | /// 12 | /// Gets the filter expression. 13 | /// 14 | public Expression> Filter { get; } 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The filter expression. 20 | public WhereExpression(Expression> filter) 21 | { 22 | Debug.Assert(filter is not null); 23 | Filter = filter; 24 | } 25 | } 26 | 27 | /// 28 | /// Represents a compiled where expression used in a specification. 29 | /// 30 | /// The type of the entity. 31 | public sealed class WhereExpressionCompiled 32 | { 33 | /// 34 | /// Gets the compiled filter function. 35 | /// 36 | public Func Filter { get; } 37 | 38 | /// 39 | /// Initializes a new instance of the class. 40 | /// 41 | /// The compiled filter function. 42 | public WhereExpressionCompiled(Func filter) 43 | { 44 | Debug.Assert(filter is not null); 45 | Filter = filter; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/QuerySpecification/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Linq.Expressions; 2 | -------------------------------------------------------------------------------- /src/QuerySpecification/IProjectionRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents a repository for projecting entities to different result types. 5 | /// 6 | /// The type of the entity. 7 | public interface IProjectionRepository where T : class 8 | { 9 | /// 10 | /// Projects the first entity that matches the specification to a result. It throws an exception if no entity is found. 11 | /// It ignores the selector in the specification, and projects the entity to the result type using the Map method. 12 | /// 13 | /// The type of the result. 14 | /// The specification to evaluate. 15 | /// The cancellation token. 16 | /// A task that represents the asynchronous operation. The task result contains the projected result. 17 | /// 18 | Task ProjectToFirstAsync(Specification specification, CancellationToken cancellationToken = default); 19 | 20 | /// 21 | /// Projects the first entity that matches the specification to a result or null if no entity is found. 22 | /// It ignores the selector in the specification, and projects the entity to the result type using the Map method. 23 | /// 24 | /// The type of the result. 25 | /// The specification to evaluate. 26 | /// The cancellation token. 27 | /// A task that represents the asynchronous operation. The task result contains the projected result or null if no entity is found. 28 | Task ProjectToFirstOrDefaultAsync(Specification specification, CancellationToken cancellationToken = default); 29 | 30 | /// 31 | /// Projects the entities that match the specification to a list of results. 32 | /// It ignores the selector in the specification. It projects the entities to the result type using the Map method. 33 | /// 34 | /// The type of the result. 35 | /// The specification to evaluate. 36 | /// The cancellation token. 37 | /// A task that represents the asynchronous operation. The task result contains the list of projected results. 38 | Task> ProjectToListAsync(Specification specification, CancellationToken cancellationToken = default); 39 | 40 | /// 41 | /// Projects the entities that match the specification to a paged list of results. 42 | /// It ignores the selector in the specification, and projects the entities to the result type using the Map method. 43 | /// It ignores the paging filter in the specification, and applies pagination based on the provided paging filter. 44 | /// 45 | /// The type of the result. 46 | /// The specification to evaluate. 47 | /// The paging filter. 48 | /// The cancellation token. 49 | /// A task that represents the asynchronous operation. The task result contains the paged list of projected results. 50 | Task> ProjectToListAsync(Specification specification, PagingFilter filter, CancellationToken cancellationToken = default); 51 | } 52 | -------------------------------------------------------------------------------- /src/QuerySpecification/IRepositoryBase.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents a repository for accessing and modifying entities. 5 | /// 6 | /// The type of the entity. 7 | public interface IRepositoryBase : IReadRepositoryBase where T : class 8 | { 9 | /// 10 | /// Adds a new entity to the repository. 11 | /// 12 | /// The entity to add. 13 | /// The cancellation token. 14 | /// A task that represents the asynchronous operation. The task result contains the added entity. 15 | Task AddAsync(T entity, CancellationToken cancellationToken = default); 16 | 17 | /// 18 | /// Adds a range of new entities to the repository. 19 | /// 20 | /// The entities to add. 21 | /// The cancellation token. 22 | /// A task that represents the asynchronous operation. The task result contains the added entities. 23 | Task> AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default); 24 | 25 | /// 26 | /// Updates an existing entity in the repository. 27 | /// 28 | /// The entity to update. 29 | /// The cancellation token. 30 | /// A task that represents the asynchronous operation. 31 | Task UpdateAsync(T entity, CancellationToken cancellationToken = default); 32 | 33 | /// 34 | /// Deletes an existing entity from the repository. 35 | /// 36 | /// The entity to delete. 37 | /// The cancellation token. 38 | /// A task that represents the asynchronous operation. 39 | Task DeleteAsync(T entity, CancellationToken cancellationToken = default); 40 | 41 | /// 42 | /// Deletes a range of existing entities from the repository. 43 | /// 44 | /// The entities to delete. 45 | /// The cancellation token. 46 | /// A task that represents the asynchronous operation. 47 | Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default); 48 | 49 | /// 50 | /// Saves all changes made in the repository. 51 | /// 52 | /// The cancellation token. 53 | /// A task that represents the asynchronous operation. The task result contains the number of state entries written to the database. 54 | Task SaveChangesAsync(CancellationToken cancellationToken = default); 55 | } 56 | -------------------------------------------------------------------------------- /src/QuerySpecification/Internals/ItemType.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | internal static class ItemType 4 | { 5 | public const int Where = -1; 6 | public const int Order = -2; 7 | public const int Include = -3; 8 | public const int IncludeString = -4; 9 | public const int Like = -5; 10 | public const int Select = -6; 11 | public const int Compiled = -7; 12 | 13 | // We can save 16 bytes (on x64) by storing both Flags and Paging in the same item. 14 | public const int Paging = -8; // Stored in the reference 15 | public const int Flags = -8; // Stored in the bag 16 | 17 | public const int QueryTag = -9; 18 | public const int CacheKey = -10; 19 | } 20 | -------------------------------------------------------------------------------- /src/QuerySpecification/Internals/Iterator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Pozitron.QuerySpecification; 5 | 6 | internal abstract class Iterator : IEnumerable, IEnumerator 7 | { 8 | private readonly int _threadId = Environment.CurrentManagedThreadId; 9 | 10 | private protected int _state; 11 | private protected TSource _current = default!; 12 | 13 | public Iterator GetEnumerator() 14 | { 15 | var enumerator = _state == 0 && _threadId == Environment.CurrentManagedThreadId ? this : Clone(); 16 | enumerator._state = 1; 17 | return enumerator; 18 | } 19 | 20 | public abstract Iterator Clone(); 21 | public abstract bool MoveNext(); 22 | 23 | public TSource Current => _current; 24 | object? IEnumerator.Current => Current; 25 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 26 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 27 | 28 | [ExcludeFromCodeCoverage] 29 | void IEnumerator.Reset() => throw new NotSupportedException(); 30 | 31 | public virtual void Dispose() 32 | { 33 | _current = default!; 34 | _state = -1; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/QuerySpecification/Internals/SpecFlags.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | [Flags] 4 | internal enum SpecFlags 5 | { 6 | IgnoreQueryFilters = 1, 7 | AsNoTracking = 2, 8 | AsNoTrackingWithIdentityResolution = 4, 9 | AsTracking = 8, 10 | AsSplitQuery = 16, 11 | IgnoreAutoIncludes = 32, 12 | } 13 | -------------------------------------------------------------------------------- /src/QuerySpecification/Internals/SpecItem.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | internal struct SpecItem 4 | { 5 | public int Type; // 0-4 bytes 6 | public int Bag; // 4-8 bytes 7 | public object? Reference; // 8-16 bytes (on x64) 8 | } 9 | -------------------------------------------------------------------------------- /src/QuerySpecification/Internals/SpecIterator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Pozitron.QuerySpecification; 4 | 5 | internal sealed class SpecIterator : Iterator 6 | { 7 | private readonly SpecItem[] _source; 8 | private readonly int _type; 9 | 10 | public SpecIterator(SpecItem[] source, int type) 11 | { 12 | Debug.Assert(source != null && source.Length > 0); 13 | _type = type; 14 | _source = source; 15 | } 16 | 17 | public override Iterator Clone() => 18 | new SpecIterator(_source, _type); 19 | 20 | public override bool MoveNext() 21 | { 22 | var index = _state - 1; 23 | var source = _source; 24 | var type = _type; 25 | 26 | while (unchecked((uint)index < (uint)source.Length)) 27 | { 28 | var item = source[index]; 29 | index = _state++; 30 | 31 | if (item.Type == type && item.Reference is TObject reference) 32 | { 33 | _current = reference; 34 | return true; 35 | } 36 | } 37 | 38 | Dispose(); 39 | return false; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/QuerySpecification/Internals/SpecLike.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Pozitron.QuerySpecification; 4 | 5 | internal sealed class SpecLike 6 | { 7 | public Expression> KeySelector { get; } 8 | public string Pattern { get; } 9 | 10 | public SpecLike(Expression> keySelector, string pattern) 11 | { 12 | Debug.Assert(keySelector is not null); 13 | Debug.Assert(!string.IsNullOrEmpty(pattern)); 14 | KeySelector = keySelector; 15 | Pattern = pattern; 16 | } 17 | } 18 | 19 | internal sealed class SpecLikeCompiled 20 | { 21 | public Func KeySelector { get; } 22 | public string Pattern { get; } 23 | 24 | public SpecLikeCompiled(Func keySelector, string pattern) 25 | { 26 | Debug.Assert(keySelector is not null); 27 | Debug.Assert(!string.IsNullOrEmpty(pattern)); 28 | KeySelector = keySelector; 29 | Pattern = pattern; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/QuerySpecification/Internals/SpecPaging.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | internal sealed class SpecPaging 4 | { 5 | public int Take = -1; 6 | public int Skip = -1; 7 | } 8 | -------------------------------------------------------------------------------- /src/QuerySpecification/Internals/SpecSelectIterator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Pozitron.QuerySpecification; 4 | 5 | internal sealed class SpecSelectIterator : Iterator 6 | { 7 | private readonly SpecItem[] _source; 8 | private readonly Func _selector; 9 | private readonly int _type; 10 | 11 | public SpecSelectIterator(SpecItem[] source, int type, Func selector) 12 | { 13 | Debug.Assert(source != null && source.Length > 0); 14 | Debug.Assert(selector != null); 15 | _type = type; 16 | _source = source; 17 | _selector = selector; 18 | } 19 | 20 | public override Iterator Clone() => 21 | new SpecSelectIterator(_source, _type, _selector); 22 | 23 | public override bool MoveNext() 24 | { 25 | var index = _state - 1; 26 | var source = _source; 27 | var type = _type; 28 | 29 | while (unchecked((uint)index < (uint)source.Length)) 30 | { 31 | var item = source[index]; 32 | index = _state++; 33 | if (item.Type == type && item.Reference is TObject reference) 34 | { 35 | _current = _selector(reference, item.Bag); 36 | return true; 37 | } 38 | } 39 | 40 | Dispose(); 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/QuerySpecification/Paging/PagedResult.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents a paged result with data and pagination information. 5 | /// 6 | /// The type of the data. 7 | public record PagedResult 8 | { 9 | /// 10 | /// Gets the pagination information. 11 | /// 12 | public Pagination Pagination { get; } 13 | 14 | /// 15 | /// Gets the data. 16 | /// 17 | public List Data { get; } 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The data. 23 | /// The pagination information. 24 | public PagedResult(List data, Pagination pagination) 25 | { 26 | Data = data; 27 | Pagination = pagination; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/QuerySpecification/Paging/PaginationSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents pagination settings. 5 | /// 6 | public record PaginationSettings 7 | { 8 | /// 9 | /// Gets the default page number. 10 | /// 11 | public int DefaultPage { get; } = 1; 12 | 13 | /// 14 | /// Gets the default page size. 15 | /// 16 | public int DefaultPageSize { get; } = 10; 17 | 18 | /// 19 | /// Gets the default page size limit. 20 | /// 21 | public int DefaultPageSizeLimit { get; } = 50; 22 | 23 | /// 24 | /// Gets the default pagination settings. 25 | /// 26 | public static PaginationSettings Default { get; } = new(); 27 | 28 | private PaginationSettings() { } 29 | 30 | /// 31 | /// Initializes a new instance of the class with the specified default page size and page size limit. 32 | /// 33 | /// The default page size. 34 | /// The default page size limit. 35 | public PaginationSettings(int defaultPageSize, int defaultPageSizeLimit) 36 | { 37 | DefaultPageSize = defaultPageSize; 38 | DefaultPageSizeLimit = defaultPageSizeLimit; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/QuerySpecification/Paging/PagingFilter.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents a filter for paging. 5 | /// 6 | public record PagingFilter 7 | { 8 | /// 9 | /// Gets or sets the page number. 10 | /// 11 | public int? Page { get; init; } 12 | 13 | /// 14 | /// Gets or sets the page size. 15 | /// 16 | public int? PageSize { get; init; } 17 | } 18 | -------------------------------------------------------------------------------- /src/QuerySpecification/QuerySpecification.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Pozitron.QuerySpecification 5 | Pozitron.QuerySpecification 6 | Pozitron.QuerySpecification 7 | Abstract package for building query specifications. 8 | Abstract package for building query specifications. 9 | 10 | 11.2.0 11 | fiseni pozitron query specification 12 | 13 | Refer to Releases page for details. 14 | https://github.com/fiseni/QuerySpecification/releases 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/QuerySpecification/SpecificationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Extension methods for specifications. 5 | /// 6 | public static class SpecificationExtensions 7 | { 8 | /// 9 | /// Creates a new specification by applying a projection specification to the current specification. 10 | /// 11 | /// This method clones the source specification and applies the projection specification's select 12 | /// statements to create a new specification. The input specifications remain unchanged. 13 | /// The type of the entity. 14 | /// The type of the result. 15 | /// The source specification to which the projection will be applied. Cannot be . 16 | /// The projection specification that defines the transformation to apply to the source specification. Cannot be 17 | /// . 18 | /// A new that represents the result of applying the projection to the 19 | /// source specification. 20 | public static Specification WithProjectionOf(this Specification source, Specification projectionSpec) 21 | { 22 | var newSpec = source.Clone(); 23 | 24 | foreach (var item in projectionSpec.Items) 25 | { 26 | if (item.Type == ItemType.Select && item.Reference is not null) 27 | { 28 | newSpec.AddOrUpdateInternal(item.Type, item.Reference, item.Bag); 29 | return newSpec; 30 | } 31 | } 32 | 33 | return newSpec; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/QuerySpecification/Validators/IValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents a validator for specifications. 5 | /// 6 | public interface IValidator 7 | { 8 | /// 9 | /// Determines whether the specified entity is valid according to the given specification. 10 | /// 11 | /// The type of the entity. 12 | /// The entity to validate. 13 | /// The specification to evaluate. 14 | /// true if the entity is valid; otherwise, false. 15 | bool IsValid(T entity, Specification specification); 16 | } 17 | -------------------------------------------------------------------------------- /src/QuerySpecification/Validators/LikeValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /* 4 | public bool IsValid(T entity, Specification specification) 5 | { 6 | foreach (var likeGroup in specification.LikeExpressions.GroupBy(x => x.Group)) 7 | { 8 | if (likeGroup.Any(c => c.KeySelectorFunc(entity)?.Like(c.Pattern) ?? false) == false) return false; 9 | } 10 | return true; 11 | } 12 | This was the previous implementation.We're trying to avoid allocations of LikeExpressions, GroupBy and LINQ. 13 | Instead of GroupBy, we have a single array sorted by group, and we slice it to get the groups. 14 | The new implementation preserves the behavior and reduces allocations drastically. 15 | For 1000 entities, the allocations are reduced from 651.160 bytes to ZERO bytes. Refer to LikeValidatorBenchmark results. 16 | */ 17 | 18 | /// 19 | /// Represents a validator for "like" expressions. 20 | /// 21 | [ValidatorDiscovery(Order = 20)] 22 | public sealed class LikeValidator : IValidator 23 | { 24 | /// 25 | /// Gets the singleton instance of the class. 26 | /// 27 | public static LikeValidator Instance = new(); 28 | private LikeValidator() { } 29 | 30 | /// 31 | public bool IsValid(T entity, Specification specification) 32 | { 33 | var compiledItems = specification.GetCompiledItems(); 34 | if (compiledItems.Length == 0) return true; 35 | 36 | var startIndexLikeItems = Array.FindIndex(compiledItems, item => item.Type == ItemType.Like); 37 | if (startIndexLikeItems == -1) return true; 38 | 39 | // The like items are contiguously placed as a last segment in the array and are already sorted by group. 40 | return IsValid(entity, compiledItems.AsSpan()[startIndexLikeItems..compiledItems.Length]); 41 | } 42 | 43 | private static bool IsValid(T entity, ReadOnlySpan span) 44 | { 45 | var groupStart = 0; 46 | for (var i = 1; i <= span.Length; i++) 47 | { 48 | // If we reached the end of the span or the group has changed, we slice and process the group. 49 | if (i == span.Length || span[i].Bag != span[groupStart].Bag) 50 | { 51 | if (IsValidInOrGroup(entity, span[groupStart..i]) is false) 52 | { 53 | return false; 54 | } 55 | groupStart = i; 56 | } 57 | } 58 | return true; 59 | 60 | static bool IsValidInOrGroup(T entity, ReadOnlySpan span) 61 | { 62 | var validOrGroup = false; 63 | foreach (var specItem in span) 64 | { 65 | if (specItem.Reference is not SpecLikeCompiled specLike) continue; 66 | 67 | if (specLike.KeySelector(entity)?.Like(specLike.Pattern) ?? false) 68 | { 69 | validOrGroup = true; 70 | break; 71 | } 72 | } 73 | return validOrGroup; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/QuerySpecification/Validators/SpecificationValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Validates specifications. 5 | /// 6 | public class SpecificationValidator 7 | { 8 | /// 9 | /// Gets the default instance of the class. 10 | /// 11 | public static SpecificationValidator Default = new(); 12 | 13 | /// 14 | /// Gets the list of validators. 15 | /// 16 | protected List Validators { get; } 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | public SpecificationValidator() 22 | { 23 | Validators = TypeDiscovery.IsAutoDiscoveryEnabled 24 | ? TypeDiscovery.GetValidators() 25 | : 26 | [ 27 | WhereValidator.Instance, 28 | LikeValidator.Instance, 29 | ]; 30 | } 31 | 32 | /// 33 | /// Initializes a new instance of the class with the specified validators. 34 | /// 35 | /// The validators to use. 36 | public SpecificationValidator(IEnumerable validators) 37 | { 38 | Validators = validators.ToList(); 39 | } 40 | 41 | /// 42 | /// Determines whether the specified entity is valid according to the given specification. 43 | /// 44 | /// The type of the entity. 45 | /// The entity to validate. 46 | /// The specification to evaluate. 47 | /// true if the entity is valid; otherwise, false. 48 | public virtual bool IsValid(T entity, Specification specification) 49 | { 50 | if (specification.IsEmpty) return true; 51 | 52 | foreach (var validator in Validators) 53 | { 54 | if (validator.IsValid(entity, specification) == false) 55 | return false; 56 | } 57 | 58 | return true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/QuerySpecification/Validators/WhereValidator.cs: -------------------------------------------------------------------------------- 1 | namespace Pozitron.QuerySpecification; 2 | 3 | /// 4 | /// Represents a validator for where expressions. 5 | /// 6 | [ValidatorDiscovery(Order = 10)] 7 | public sealed class WhereValidator : IValidator 8 | { 9 | /// 10 | /// Gets the singleton instance of the class. 11 | /// 12 | public static WhereValidator Instance = new(); 13 | private WhereValidator() { } 14 | 15 | /// 16 | public bool IsValid(T entity, Specification specification) 17 | { 18 | var compiledItems = specification.GetCompiledItems(); 19 | 20 | foreach (var item in compiledItems) 21 | { 22 | if (item.Type == ItemType.Where && item.Reference is Func compiledExpr) 23 | { 24 | if (compiledExpr(entity) == false) 25 | return false; 26 | } 27 | } 28 | 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/QuerySpecification/build/Pozitron.QuerySpecification.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <_SpecAutoDiscoveryLower>$([System.String]::Copy('$(SpecAutoDiscovery)').ToLowerInvariant()) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tests 5 | net9.0 6 | enable 7 | enable 8 | 9 | false 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/QuerySpecification.AutoDiscovery.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using FluentAssertions; 2 | global using Pozitron.QuerySpecification; 3 | global using System.Linq.Expressions; 4 | global using Xunit; 5 | -------------------------------------------------------------------------------- /tests/QuerySpecification.AutoDiscovery.Tests/QuerySpecification.AutoDiscovery.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/QuerySpecification.AutoDiscovery.Tests/SpecificationEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Tests; 4 | 5 | public class SpecificationEvaluatorTests 6 | { 7 | [Fact] 8 | public void DefaultSingleton_ScansEvaluators_GivenAutoDiscoveryEnabled() 9 | { 10 | var evaluator = SpecificationEvaluator.Default; 11 | 12 | var result = EvaluatorsOf(evaluator); 13 | 14 | result.Should().HaveCountGreaterThan(1); 15 | result.Should().ContainSingle(x => x is TestEvaluator); 16 | } 17 | 18 | [Fact] 19 | public void Constructor_ScansEvaluators_GivenAutoDiscoveryEnabled() 20 | { 21 | var evaluator = new SpecificationEvaluator(); 22 | 23 | var result = EvaluatorsOf(evaluator); 24 | 25 | result.Should().HaveCountGreaterThan(1); 26 | result.Should().ContainSingle(x => x is TestEvaluator); 27 | } 28 | 29 | [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] 30 | public static extern ref List EvaluatorsOf(SpecificationEvaluator @this); 31 | 32 | public class TestEvaluator : IEvaluator 33 | { 34 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 35 | { 36 | return source; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/QuerySpecification.AutoDiscovery.Tests/SpecificationMemoryEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Tests; 4 | 5 | public class SpecificationMemoryEvaluatorTests 6 | { 7 | [Fact] 8 | public void DefaultSingleton_ScansEvaluators_GivenAutoDiscoveryEnabled() 9 | { 10 | var evaluator = SpecificationMemoryEvaluator.Default; 11 | 12 | var result = EvaluatorsOf(evaluator); 13 | 14 | result.Should().HaveCountGreaterThan(1); 15 | result.Should().ContainSingle(x => x is TestMemoryEvaluator); 16 | } 17 | 18 | [Fact] 19 | public void Constructor_ScansEvaluators_GivenAutoDiscoveryEnabled() 20 | { 21 | var evaluator = new SpecificationMemoryEvaluator(); 22 | 23 | var result = EvaluatorsOf(evaluator); 24 | 25 | result.Should().HaveCountGreaterThan(1); 26 | result.Should().ContainSingle(x => x is TestMemoryEvaluator); 27 | } 28 | 29 | [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] 30 | public static extern ref List EvaluatorsOf(SpecificationMemoryEvaluator @this); 31 | 32 | public class TestMemoryEvaluator : IMemoryEvaluator 33 | { 34 | public IEnumerable Evaluate(IEnumerable source, Specification specification) 35 | { 36 | return source; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/QuerySpecification.AutoDiscovery.Tests/SpecificationValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Tests; 4 | 5 | public class SpecificationValidatorTests 6 | { 7 | [Fact] 8 | public void DefaultSingleton_ScansValidators_GivenAutoDiscoveryEnabled() 9 | { 10 | var validators = SpecificationValidator.Default; 11 | 12 | var result = ValidatorsOf(validators); 13 | 14 | result.Should().HaveCountGreaterThan(1); 15 | result.Should().ContainSingle(x => x is TestValidator); 16 | } 17 | 18 | [Fact] 19 | public void Constructor_ScansValidators_GivenAutoDiscoveryEnabled() 20 | { 21 | var validators = new SpecificationValidator(); 22 | 23 | var result = ValidatorsOf(validators); 24 | 25 | result.Should().HaveCountGreaterThan(1); 26 | result.Should().ContainSingle(x => x is TestValidator); 27 | } 28 | 29 | [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] 30 | public static extern ref List ValidatorsOf(SpecificationValidator @this); 31 | 32 | public class TestValidator : IValidator 33 | { 34 | public bool IsValid(T entity, Specification specification) 35 | { 36 | return true; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/QuerySpecification.AutoDiscovery.Tests/TypeDiscoveryTests.cs: -------------------------------------------------------------------------------- 1 | [assembly: SpecAutoDiscovery] 2 | 3 | namespace Tests; 4 | 5 | public class TypeDiscoveryTests 6 | { 7 | [Fact] 8 | public void GetMemoryEvaluators_IncludesCustom() 9 | { 10 | var allEvaluators = TypeDiscovery.GetMemoryEvaluators(); 11 | allEvaluators.Should().ContainSingle(x => x is TestMemoryEvaluator); 12 | } 13 | 14 | [Fact] 15 | public void GetEvaluators_IncludesCustom() 16 | { 17 | var allEvaluators = TypeDiscovery.GetEvaluators(); 18 | allEvaluators.Should().ContainSingle(x => x is TestEvaluator); 19 | } 20 | 21 | [Fact] 22 | public void GetValidators_IncludesCustom() 23 | { 24 | var allValidators = TypeDiscovery.GetValidators(); 25 | allValidators.Should().ContainSingle(x => x is TestValidator); 26 | } 27 | 28 | // Custom user evaluators and validators 29 | public class TestEvaluator : IEvaluator 30 | { 31 | public IQueryable Evaluate(IQueryable source, Specification specification) where T : class 32 | { 33 | return source; 34 | } 35 | } 36 | 37 | public class TestMemoryEvaluator : IMemoryEvaluator 38 | { 39 | public IEnumerable Evaluate(IEnumerable source, Specification specification) 40 | { 41 | return source; 42 | } 43 | } 44 | 45 | public class TestValidator : IValidator 46 | { 47 | public bool IsValid(T entity, Specification specification) 48 | { 49 | return true; 50 | } 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark4_Like.cs: -------------------------------------------------------------------------------- 1 | namespace QuerySpecification.Benchmarks; 2 | 3 | [MemoryDiagnoser] 4 | public class Benchmark4_Like 5 | { 6 | private DbSet _queryable = default!; 7 | 8 | [GlobalSetup] 9 | public void Setup() 10 | { 11 | _queryable = new BenchmarkDbContext().Stores; 12 | } 13 | 14 | [Params(0, 1, 2, 6)] 15 | public int Count { get; set; } 16 | 17 | [Benchmark(Baseline = true)] 18 | public object EFCore() 19 | { 20 | var nameSearchTerm = "%tore%"; 21 | if (Count == 0) 22 | { 23 | return _queryable; 24 | } 25 | else if (Count == 1) 26 | { 27 | return _queryable 28 | .Where(x => EF.Functions.Like(x.Name, nameSearchTerm)); 29 | } 30 | else if (Count == 2) 31 | { 32 | return _queryable 33 | .Where(x => EF.Functions.Like(x.Name, nameSearchTerm)) 34 | .Where(x => EF.Functions.Like(x.Name, nameSearchTerm)); 35 | } 36 | else 37 | { 38 | return _queryable 39 | .Where(x => EF.Functions.Like(x.Name, nameSearchTerm) || EF.Functions.Like(x.Name, nameSearchTerm)) 40 | .Where(x => EF.Functions.Like(x.Name, nameSearchTerm) || EF.Functions.Like(x.Name, nameSearchTerm)) 41 | .Where(x => EF.Functions.Like(x.Name, nameSearchTerm) || EF.Functions.Like(x.Name, nameSearchTerm)); 42 | } 43 | } 44 | 45 | 46 | [Benchmark] 47 | public object Spec() 48 | { 49 | var nameSearchTerm = "%tore%"; 50 | if (Count == 0) 51 | { 52 | var spec = new Specification(); 53 | return _queryable.WithSpecification(spec); 54 | } 55 | else if (Count == 1) 56 | { 57 | var spec = new Specification(); 58 | spec.Query 59 | .Like(x => x.Name, nameSearchTerm); 60 | return _queryable.WithSpecification(spec); 61 | } 62 | else if (Count == 2) 63 | { 64 | var spec = new Specification(); 65 | spec.Query 66 | .Like(x => x.Name, nameSearchTerm, 2) 67 | .Like(x => x.Name, nameSearchTerm, 1); 68 | return _queryable.WithSpecification(spec); 69 | } 70 | else 71 | { 72 | var spec = new Specification(6); 73 | spec.Query 74 | .Like(x => x.Name, nameSearchTerm, 2) 75 | .Like(x => x.Name, nameSearchTerm, 3) 76 | .Like(x => x.Name, nameSearchTerm, 1) 77 | .Like(x => x.Name, nameSearchTerm, 3) 78 | .Like(x => x.Name, nameSearchTerm, 2) 79 | .Like(x => x.Name, nameSearchTerm, 1); 80 | return _queryable.WithSpecification(spec); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark5_Include.cs: -------------------------------------------------------------------------------- 1 | namespace QuerySpecification.Benchmarks; 2 | 3 | [MemoryDiagnoser] 4 | public class Benchmark5_Include 5 | { 6 | private DbSet _queryable = default!; 7 | 8 | [GlobalSetup] 9 | public void Setup() 10 | { 11 | _queryable = new BenchmarkDbContext().Stores; 12 | } 13 | 14 | [Benchmark(Baseline = true)] 15 | public object EFCore() 16 | { 17 | var result = _queryable 18 | .Include(x => x.Company) 19 | .ThenInclude(x => x.Country); 20 | 21 | return result; 22 | } 23 | 24 | [Benchmark] 25 | public object Spec() 26 | { 27 | var spec = new Specification(); 28 | spec.Query 29 | .Include(x => x.Company) 30 | .ThenInclude(x => x.Country); 31 | 32 | return _queryable.WithSpecification(spec); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/Benchmarks/Benchmark6_IncludeEvaluator.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace QuerySpecification.Benchmarks; 4 | 5 | [MemoryDiagnoser] 6 | public class Benchmark6_IncludeEvaluator 7 | { 8 | /* 9 | * This benchmark only measures applying Include to IQueryable. 10 | * It tends to measure the pure overhead of the reflection calls. 11 | */ 12 | 13 | private static readonly Expression> _includeCompany = x => x.Company; 14 | private static readonly Expression> _includeCountry = x => x.Country; 15 | 16 | private DbSet _queryable = default!; 17 | private Specification _spec = default!; 18 | 19 | [GlobalSetup] 20 | public void Setup() 21 | { 22 | _queryable = new BenchmarkDbContext().Stores; 23 | _spec = new Specification(); 24 | _spec.Query 25 | .Include(_includeCompany) 26 | .ThenInclude(_includeCountry); 27 | } 28 | 29 | [Benchmark(Baseline = true)] 30 | public object EFCore() 31 | { 32 | var result = _queryable 33 | .Include(_includeCompany) 34 | .ThenInclude(_includeCountry); 35 | 36 | return result; 37 | } 38 | 39 | [Benchmark] 40 | public object Spec() 41 | { 42 | var evaluator = IncludeEvaluator.Instance; 43 | var result = evaluator.Evaluate(_queryable, _spec); 44 | return result; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/Data/BenchmarkDbContext.cs: -------------------------------------------------------------------------------- 1 | namespace QuerySpecification.Benchmarks; 2 | 3 | public class BenchmarkDbContext : DbContext 4 | { 5 | public DbSet Stores { get; set; } 6 | 7 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 8 | => optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=QuerySpecificationBenchmark;ConnectRetryCount=0"); 9 | 10 | public static async Task SeedAsync() 11 | { 12 | using var context = new BenchmarkDbContext(); 13 | var created = await context.Database.EnsureCreatedAsync(); 14 | 15 | if (!created) return; 16 | 17 | var company = new Company() 18 | { 19 | Name = "Company 1", 20 | Country = new() 21 | { 22 | Name = "Country 1" 23 | } 24 | }; 25 | var store1 = new Store 26 | { 27 | Name = "Store 1", 28 | Company = company, 29 | Products = 30 | [ 31 | new() { Name = "Product 1" } 32 | ] 33 | }; 34 | var store2 = new Store 35 | { 36 | Name = "Store 2", 37 | Company = company, 38 | Products = 39 | [ 40 | new() { Name = "Product 2" } 41 | ] 42 | }; 43 | 44 | context.AddRange(store1, store2); 45 | await context.SaveChangesAsync(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/Data/Company.cs: -------------------------------------------------------------------------------- 1 | namespace QuerySpecification.Benchmarks; 2 | 3 | public class Company 4 | { 5 | public int Id { get; set; } 6 | public string? Name { get; set; } 7 | public Country Country { get; set; } = default!; 8 | public List Stores { get; set; } = []; 9 | } 10 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/Data/Country.cs: -------------------------------------------------------------------------------- 1 | namespace QuerySpecification.Benchmarks; 2 | 3 | public class Country 4 | { 5 | public int Id { get; set; } 6 | public string? Name { get; set; } 7 | public List Companies { get; set; } = []; 8 | } 9 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/Data/Product.cs: -------------------------------------------------------------------------------- 1 | namespace QuerySpecification.Benchmarks; 2 | 3 | public class Product 4 | { 5 | public int Id { get; set; } 6 | public string? Name { get; set; } 7 | public Store Store { get; set; } = default!; 8 | } 9 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/Data/Store.cs: -------------------------------------------------------------------------------- 1 | namespace QuerySpecification.Benchmarks; 2 | 3 | public class Store 4 | { 5 | public int Id { get; set; } 6 | public string? Name { get; set; } 7 | public Company Company { get; set; } = default!; 8 | public List Products { get; set; } = []; 9 | } 10 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/GloblUsings.cs: -------------------------------------------------------------------------------- 1 | global using BenchmarkDotNet.Attributes; 2 | global using BenchmarkDotNet.Running; 3 | global using Microsoft.EntityFrameworkCore; 4 | global using Pozitron.QuerySpecification; 5 | global using QuerySpecification.Benchmarks; 6 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 3 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Benchmarks/QuerySpecification.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | false 9 | 10 | 11 | 12 | 1701;1702;CA1822;IDE0060 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/AsNoTrackingEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class AsNoTrackingEvaluatorTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | private static readonly AsNoTrackingEvaluator _evaluator = AsNoTrackingEvaluator.Instance; 7 | 8 | [Fact] 9 | public void Applies_GivenAsNoTracking() 10 | { 11 | var spec = new Specification(); 12 | spec.Query.AsNoTracking(); 13 | 14 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 15 | .Expression 16 | .ToString(); 17 | 18 | var expected = DbContext.Countries 19 | .AsNoTracking() 20 | .Expression 21 | .ToString(); 22 | 23 | actual.Should().Be(expected); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/AsNoTrackingWithIdentityResolutionEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class AsNoTrackingWithIdentityResolutionEvaluatorTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | private static readonly AsNoTrackingWithIdentityResolutionEvaluator _evaluator = AsNoTrackingWithIdentityResolutionEvaluator.Instance; 7 | 8 | [Fact] 9 | public void Applies_GivenAsNoTrackingWithIdentityResolution() 10 | { 11 | var spec = new Specification(); 12 | spec.Query.AsNoTrackingWithIdentityResolution(); 13 | 14 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 15 | .Expression 16 | .ToString(); 17 | 18 | var expected = DbContext.Countries 19 | .AsNoTrackingWithIdentityResolution() 20 | .Expression 21 | .ToString(); 22 | 23 | actual.Should().Be(expected); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/AsSplitQueryEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class AsSplitQueryEvaluatorTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | private static readonly AsSplitQueryEvaluator _evaluator = AsSplitQueryEvaluator.Instance; 7 | 8 | [Fact] 9 | public void QueriesMatch_GivenAsSplitQuery() 10 | { 11 | var spec = new Specification(); 12 | spec.Query.AsSplitQuery(); 13 | 14 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 15 | .ToQueryString(); 16 | 17 | var expected = DbContext.Countries 18 | .AsSplitQuery() 19 | .ToQueryString(); 20 | 21 | actual.Should().Be(expected); 22 | } 23 | 24 | [Fact] 25 | public void Applies_GivenAsSplitQuery() 26 | { 27 | var spec = new Specification(); 28 | spec.Query.AsSplitQuery(); 29 | 30 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 31 | .Expression 32 | .ToString(); 33 | 34 | var expected = DbContext.Countries 35 | .AsSplitQuery() 36 | .Expression 37 | .ToString(); 38 | 39 | actual.Should().Be(expected); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/AsTrackingEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class AsTrackingEvaluatorTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | private static readonly AsTrackingEvaluator _evaluator = AsTrackingEvaluator.Instance; 7 | 8 | [Fact] 9 | public void Applies_GivenAsTracking() 10 | { 11 | var spec = new Specification(); 12 | spec.Query.AsTracking(); 13 | 14 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 15 | .Expression 16 | .ToString(); 17 | 18 | var expected = DbContext.Countries 19 | .AsTracking() 20 | .Expression 21 | .ToString(); 22 | 23 | actual.Should().Be(expected); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/IgnoreAutoIncludesEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class IgnoreAutoIncludesEvaluatorTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | private static readonly IgnoreAutoIncludesEvaluator _evaluator = IgnoreAutoIncludesEvaluator.Instance; 7 | 8 | [Fact] 9 | public void QueriesMatch_GivenIgnoreAutoIncludes() 10 | { 11 | var spec = new Specification(); 12 | spec.Query.IgnoreAutoIncludes(); 13 | 14 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 15 | .ToQueryString(); 16 | 17 | var expected = DbContext.Countries 18 | .IgnoreAutoIncludes() 19 | .ToQueryString(); 20 | 21 | actual.Should().Be(expected); 22 | } 23 | 24 | [Fact] 25 | public void Applies_GivenIgnoreAutoIncludes() 26 | { 27 | var spec = new Specification(); 28 | spec.Query.IgnoreAutoIncludes(); 29 | 30 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 31 | .Expression 32 | .ToString(); 33 | 34 | var expected = DbContext.Countries 35 | .IgnoreAutoIncludes() 36 | .Expression 37 | .ToString(); 38 | 39 | actual.Should().Be(expected); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/IgnoreQueryFiltersEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class IgnoreQueryFiltersEvaluatorTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | private static readonly IgnoreQueryFiltersEvaluator _evaluator = IgnoreQueryFiltersEvaluator.Instance; 7 | 8 | [Fact] 9 | public void QueriesMatch_GivenIgnoreQueryFilters() 10 | { 11 | var spec = new Specification(); 12 | spec.Query.IgnoreQueryFilters(); 13 | 14 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 15 | .ToQueryString(); 16 | 17 | var expected = DbContext.Countries 18 | .IgnoreQueryFilters() 19 | .ToQueryString(); 20 | 21 | actual.Should().Be(expected); 22 | } 23 | 24 | [Fact] 25 | public void Applies_GivenIgnoreQueryFilters() 26 | { 27 | var spec = new Specification(); 28 | spec.Query.IgnoreQueryFilters(); 29 | 30 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 31 | .Expression 32 | .ToString(); 33 | 34 | var expected = DbContext.Countries 35 | .IgnoreQueryFilters() 36 | .Expression 37 | .ToString(); 38 | 39 | actual.Should().Be(expected); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/IncludeStringEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class IncludeStringEvaluatorTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | private static readonly IncludeStringEvaluator _evaluator = IncludeStringEvaluator.Instance; 7 | 8 | [Fact] 9 | public void QueriesMatch_GivenIncludeString() 10 | { 11 | var spec = new Specification(); 12 | spec.Query 13 | .Include(nameof(Address)); 14 | 15 | var actual = _evaluator 16 | .Evaluate(DbContext.Stores, spec) 17 | .ToQueryString(); 18 | 19 | var expected = DbContext.Stores 20 | .Include(nameof(Address)) 21 | .ToQueryString(); 22 | 23 | actual.Should().Be(expected); 24 | } 25 | 26 | [Fact] 27 | public void QueriesMatch_GivenMultipleIncludeStrings() 28 | { 29 | var spec = new Specification(); 30 | spec.Query 31 | .Include(nameof(Address)) 32 | .Include($"{nameof(Company)}.{nameof(Country)}"); 33 | 34 | var actual = _evaluator 35 | .Evaluate(DbContext.Stores, spec) 36 | .ToQueryString(); 37 | 38 | var expected = DbContext.Stores 39 | .Include(nameof(Address)) 40 | .Include($"{nameof(Company)}.{nameof(Country)}") 41 | .ToQueryString(); 42 | 43 | actual.Should().Be(expected); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/LikeEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class LikeEvaluatorTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | private static readonly LikeEvaluator _evaluator = LikeEvaluator.Instance; 7 | 8 | [Fact] 9 | public void QueriesMatch_GivenNoLike() 10 | { 11 | var spec = new Specification(); 12 | spec.Query 13 | .Where(x => x.Id > 0); 14 | 15 | var actual = _evaluator.Evaluate(DbContext.Stores, spec) 16 | .ToQueryString(); 17 | 18 | var expected = DbContext.Stores 19 | .ToQueryString(); 20 | 21 | actual.Should().Be(expected); 22 | } 23 | 24 | [Fact] 25 | public void QueriesMatch_GivenSingleLike() 26 | { 27 | var storeTerm = "ab1"; 28 | 29 | var spec = new Specification(); 30 | spec.Query 31 | .Where(x => x.Id > 0) 32 | .Like(x => x.Name, $"%{storeTerm}%"); 33 | 34 | var actual = _evaluator.Evaluate(DbContext.Stores, spec) 35 | .ToQueryString(); 36 | 37 | var expected = DbContext.Stores 38 | .Where(x => EF.Functions.Like(x.Name, $"%{storeTerm}%")) 39 | .ToQueryString(); 40 | 41 | actual.Should().Be(expected); 42 | } 43 | 44 | [Fact] 45 | public void QueriesMatch_GivenMultipleLike() 46 | { 47 | var storeTerm = "ab1"; 48 | var companyTerm = "ab2"; 49 | var countryTerm = "ab3"; 50 | var streetTerm = "ab4"; 51 | 52 | var spec = new Specification(); 53 | spec.Query 54 | .Where(x => x.Id > 0) 55 | .Like(x => x.Name, $"%{storeTerm}%") 56 | .Like(x => x.Company.Name, $"%{companyTerm}%") 57 | .Like(x => x.Company.Country.Name, $"%{countryTerm}%", 3) 58 | .Like(x => x.Address.Street, $"%{streetTerm}%", 2); 59 | 60 | var actual = _evaluator.Evaluate(DbContext.Stores, spec) 61 | .ToQueryString(); 62 | 63 | var expected = DbContext.Stores 64 | .Where(x => EF.Functions.Like(x.Name, $"%{storeTerm}%") 65 | || EF.Functions.Like(x.Company.Name, $"%{companyTerm}%")) 66 | .Where(x => EF.Functions.Like(x.Address.Street, $"%{streetTerm}%")) 67 | .Where(x => EF.Functions.Like(x.Company.Country.Name, $"%{countryTerm}%")) 68 | .ToQueryString(); 69 | 70 | actual.Should().Be(expected); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/LikeExtensionTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class LikeExtensionTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | [Fact] 7 | public void QueriesMatch_GivenSpecWithMultipleLike() 8 | { 9 | var storeTerm = "ab1"; 10 | var companyTerm = "ab2"; 11 | 12 | var spec = new Specification(); 13 | spec.Query 14 | .Like(x11 => x11.Name, $"%{storeTerm}%") 15 | .Like(x22 => x22.Company.Name, $"%{companyTerm}%"); 16 | 17 | var actual = DbContext.Stores 18 | .ApplyLikesAsOrGroup(spec.Items) 19 | .ToQueryString(); 20 | 21 | var expected = DbContext.Stores 22 | .Where(x => EF.Functions.Like(x.Name, $"%{storeTerm}%") 23 | || EF.Functions.Like(x.Company.Name, $"%{companyTerm}%")) 24 | .ToQueryString(); 25 | 26 | actual.Should().Be(expected); 27 | } 28 | 29 | [Fact] 30 | public void QueriesMatch_GivenEmptySpec() 31 | { 32 | var spec = new Specification(); 33 | 34 | var actual = DbContext.Stores 35 | .ApplyLikesAsOrGroup(spec.Items) 36 | .ToQueryString(); 37 | 38 | var expected = DbContext.Stores 39 | .ToQueryString(); 40 | 41 | actual.Should().Be(expected); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/OrderEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class OrderEvaluatorTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | private static readonly OrderEvaluator _evaluator = OrderEvaluator.Instance; 7 | 8 | [Fact] 9 | public void QueriesMatch_GivenOrder() 10 | { 11 | var spec = new Specification(); 12 | spec.Query 13 | .OrderBy(x => x.Id); 14 | 15 | var actual = _evaluator.Evaluate(DbContext.Stores, spec) 16 | .ToQueryString(); 17 | 18 | var expected = DbContext.Stores 19 | .OrderBy(x => x.Id) 20 | .ToQueryString(); 21 | 22 | actual.Should().Be(expected); 23 | } 24 | 25 | [Fact] 26 | public void QueriesMatch_GivenOrderChain() 27 | { 28 | var spec = new Specification(); 29 | spec.Query 30 | .OrderBy(x => x.Id) 31 | .ThenBy(x => x.Name); 32 | 33 | var actual = _evaluator.Evaluate(DbContext.Stores, spec) 34 | .ToQueryString(); 35 | 36 | var expected = DbContext.Stores 37 | .OrderBy(x => x.Id) 38 | .ThenBy(x => x.Name) 39 | .ToQueryString(); 40 | 41 | actual.Should().Be(expected); 42 | } 43 | 44 | [Fact] 45 | public void QueriesMatch_GivenMultipleOrderChains() 46 | { 47 | var spec = new Specification(); 48 | spec.Query 49 | .OrderBy(x => x.Id) 50 | .ThenBy(x => x.Name) 51 | .OrderByDescending(x => x.Name); 52 | 53 | var actual = _evaluator.Evaluate(DbContext.Stores, spec) 54 | .ToQueryString(); 55 | 56 | var expected = DbContext.Stores 57 | .OrderBy(x => x.Id) 58 | .ThenBy(x => x.Name) 59 | .OrderByDescending(x => x.Name) 60 | .ToQueryString(); 61 | 62 | actual.Should().Be(expected); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/ParameterReplacerVisitorTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace Tests.Evaluators; 4 | 5 | public class ParameterReplacerVisitorTests 6 | { 7 | [Fact] 8 | public void ReturnsExpressionWithReplacedParameter() 9 | { 10 | Expression> expected = (y, z) => y == 1; 11 | 12 | Expression> expression = (x, z) => x == 1; 13 | var oldParameter = expression.Parameters[0]; 14 | var newExpression = Expression.Parameter(typeof(int), "y"); 15 | 16 | var visitor = new ParameterReplacerVisitor(oldParameter, newExpression); 17 | var result = visitor.Visit(expression); 18 | 19 | result.ToString().Should().Be(expected.ToString()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/QueryTagEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class QueryTagEvaluatorTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | private static readonly QueryTagEvaluator _evaluator = QueryTagEvaluator.Instance; 7 | 8 | [Fact] 9 | public void QueriesMatch_GivenTag() 10 | { 11 | var tag = "asd"; 12 | 13 | var spec = new Specification(); 14 | spec.Query.TagWith(tag); 15 | 16 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 17 | .ToQueryString(); 18 | 19 | var expected = DbContext.Countries 20 | .TagWith(tag) 21 | .ToQueryString(); 22 | 23 | actual.Should().Be(expected); 24 | } 25 | 26 | [Fact] 27 | public void QueriesMatch_GivenMultipleTags() 28 | { 29 | var tag1 = "asd"; 30 | var tag2 = "qwe"; 31 | 32 | var spec = new Specification(); 33 | spec.Query.TagWith(tag1); 34 | spec.Query.TagWith(tag2); 35 | 36 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 37 | .ToQueryString(); 38 | 39 | var expected = DbContext.Countries 40 | .TagWith(tag1) 41 | .TagWith(tag2) 42 | .ToQueryString(); 43 | 44 | actual.Should().Be(expected); 45 | } 46 | 47 | 48 | [Fact] 49 | public void DoesNothing_GivenNoTag() 50 | { 51 | var spec = new Specification(); 52 | 53 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 54 | .Expression 55 | .ToString(); 56 | 57 | var expected = DbContext.Countries 58 | .AsQueryable() 59 | .Expression 60 | .ToString(); 61 | 62 | actual.Should().Be(expected); 63 | } 64 | 65 | [Fact] 66 | public void Applies_GivenSingleTag() 67 | { 68 | var tag = "asd"; 69 | 70 | var spec = new Specification(); 71 | spec.Query.TagWith(tag); 72 | 73 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 74 | .Expression 75 | .ToString(); 76 | 77 | var expected = DbContext.Countries 78 | .TagWith(tag) 79 | .Expression 80 | .ToString(); 81 | 82 | actual.Should().Be(expected); 83 | } 84 | 85 | [Fact] 86 | public void Applies_GivenTwoTags() 87 | { 88 | var tag1 = "asd"; 89 | var tag2 = "qwe"; 90 | 91 | var spec = new Specification(); 92 | spec.Query 93 | .TagWith(tag1) 94 | .TagWith(tag2); 95 | 96 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 97 | .Expression 98 | .ToString(); 99 | 100 | var expected = DbContext.Countries 101 | .TagWith(tag1) 102 | .TagWith(tag2) 103 | .Expression 104 | .ToString(); 105 | 106 | actual.Should().Be(expected); 107 | } 108 | 109 | [Fact] 110 | public void Applies_GivenMultipleTags() 111 | { 112 | var tag1 = "asd"; 113 | var tag2 = "qwe"; 114 | var tag3 = "zxc"; 115 | 116 | var spec = new Specification(); 117 | spec.Query 118 | .TagWith(tag1) 119 | .TagWith(tag2) 120 | .TagWith(tag3); 121 | 122 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 123 | .Expression 124 | .ToString(); 125 | 126 | var expected = DbContext.Countries 127 | .TagWith(tag1) 128 | .TagWith(tag2) 129 | .TagWith(tag3) 130 | .Expression 131 | .ToString(); 132 | 133 | actual.Should().Be(expected); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Evaluators/WhereEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | [Collection("SharedCollection")] 4 | public class WhereEvaluatorTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | private static readonly WhereEvaluator _evaluator = WhereEvaluator.Instance; 7 | 8 | [Fact] 9 | public void QueriesMatch_GivenWhereExpressions() 10 | { 11 | var id = 10; 12 | var name = "Country1"; 13 | 14 | var spec = new Specification(); 15 | spec.Query 16 | .Where(x => x.Id > id) 17 | .Where(x => x.Name == name); 18 | 19 | var actual = _evaluator.Evaluate(DbContext.Countries, spec) 20 | .ToQueryString(); 21 | 22 | var expected = DbContext.Countries 23 | .Where(x => x.Id > id) 24 | .Where(x => x.Name == name) 25 | .ToQueryString(); 26 | 27 | actual.Should().Be(expected); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/Data/Address.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Fixture; 2 | 3 | public record Address 4 | { 5 | public int Id { get; set; } 6 | public string? Street { get; set; } 7 | 8 | public int StoreId { get; set; } 9 | public Store Store { get; set; } = default!; 10 | } 11 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/Data/Bar.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Fixture; 2 | 3 | public class Bar 4 | { 5 | public int Id { get; set; } 6 | public string? Dummy { get; set; } 7 | 8 | private readonly List _barChildren = []; 9 | public IReadOnlyCollection BarChildren => _barChildren.AsReadOnly(); 10 | } 11 | 12 | public class BarChild 13 | { 14 | public int Id { get; set; } 15 | public string? Dummy { get; set; } 16 | 17 | public int BarId { get; set; } 18 | public Bar Bar { get; set; } = default!; 19 | } 20 | 21 | public class BarDerived : BarChild 22 | { 23 | public int BarDerivedInfoId { get; set; } 24 | public BarDerivedInfo BarDerivedInfo { get; set; } = default!; 25 | } 26 | 27 | public class BarDerivedInfo 28 | { 29 | public int Id { get; set; } 30 | public string? Name { get; set; } 31 | } 32 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/Data/Company.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Fixture; 2 | 3 | public record Company 4 | { 5 | public int Id { get; set; } 6 | public required string Name { get; set; } 7 | 8 | public int CountryId { get; set; } 9 | public Country Country { get; set; } = default!; 10 | 11 | public List Stores { get; set; } = []; 12 | } 13 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/Data/Country.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Fixture; 2 | 3 | public record Country 4 | { 5 | public int Id { get; set; } 6 | public int No { get; set; } 7 | public string? Name { get; set; } 8 | public bool IsDeleted { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/Data/Foo.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Fixture; 2 | 3 | public class Foo 4 | { 5 | public int Id { get; set; } 6 | public string? Dummy { get; set; } 7 | public OuterNavigation OuterNavigation { get; set; } = default!; 8 | 9 | public List ListNavigation => _listNavigation; 10 | private readonly List _listNavigation = []; 11 | public IEnumerable IEnumerableNavigation => _iEnumerableNavigation.AsEnumerable(); 12 | private readonly List _iEnumerableNavigation = []; 13 | 14 | public IReadOnlyCollection IReadOnlyCollectionNavigation => _iReadOnlyCollectionNavigation.AsReadOnly(); 15 | private readonly List _iReadOnlyCollectionNavigation = []; 16 | 17 | public IReadOnlyList IReadOnlyListNavigation => _iReadOnlyListNavigation.AsReadOnly(); 18 | private readonly List _iReadOnlyListNavigation = []; 19 | } 20 | 21 | public class OuterNavigation 22 | { 23 | public int Id { get; set; } 24 | public string? Dummy { get; set; } 25 | 26 | public InnerNavigation InnerNavigation { get; set; } = default!; 27 | public List ListNavigation => _listNavigation; 28 | private readonly List _listNavigation = []; 29 | public IEnumerable IEnumerableNavigation => _iEnumerableNavigation.AsEnumerable(); 30 | private readonly List _iEnumerableNavigation = []; 31 | 32 | public IReadOnlyCollection IReadOnlyCollectionNavigation => _iReadOnlyCollectionNavigation.AsReadOnly(); 33 | private readonly List _iReadOnlyCollectionNavigation = []; 34 | 35 | public IReadOnlyList IReadOnlyListNavigation => _iReadOnlyListNavigation.AsReadOnly(); 36 | private readonly List _iReadOnlyListNavigation = []; 37 | } 38 | 39 | public class InnerNavigation 40 | { 41 | public int Id { get; set; } 42 | public string? Dummy { get; set; } 43 | public InnerNavigation2 InnerNavigation2 { get; set; } = default!; 44 | public List ListNavigation2 { get; set; } = []; 45 | } 46 | 47 | public class InnerNavigation2 48 | { 49 | public int Id { get; set; } 50 | public string? Dummy { get; set; } 51 | } 52 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/Data/Product.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Fixture; 2 | 3 | public record Product 4 | { 5 | public int Id { get; set; } 6 | public string? Name { get; set; } 7 | 8 | public int StoreId { get; set; } 9 | public Store Store { get; set; } = default!; 10 | 11 | public List? Images { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/Data/ProductImage.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Fixture; 2 | 3 | public record ProductImage 4 | { 5 | public int Id { get; set; } 6 | public string? ImageUrl { get; set; } 7 | public int ProductId { get; set; } 8 | public Product Product { get; set; } = default!; 9 | } 10 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/Data/Store.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Fixture; 2 | 3 | public record Store 4 | { 5 | public int Id { get; set; } 6 | public string? Name { get; set; } 7 | public string? City { get; set; } 8 | 9 | public int CompanyId { get; set; } 10 | public Company Company { get; set; } = default!; 11 | 12 | public Address Address { get; set; } = default!; 13 | 14 | public List Products { get; set; } = []; 15 | } 16 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/IntegrationTest.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Fixture; 2 | 3 | public class IntegrationTest(TestFactory testFactory) : IAsyncLifetime 4 | { 5 | protected TestDbContext DbContext { get; private set; } = default!; 6 | 7 | public Task InitializeAsync() 8 | { 9 | DbContext = new TestDbContext(testFactory.DbContextOptions); 10 | return Task.CompletedTask; 11 | } 12 | 13 | public async Task DisposeAsync() 14 | { 15 | await DbContext.DisposeAsync(); 16 | await testFactory.ResetDatabase(); 17 | } 18 | 19 | public async Task SeedAsync(TEntity entity) where TEntity : class 20 | { 21 | using var dbContext = new TestDbContext(testFactory.DbContextOptions); 22 | dbContext.Add(entity); 23 | await dbContext.SaveChangesAsync(); 24 | } 25 | 26 | public async Task SeedRangeAsync(TEntity[] entities) where TEntity : class 27 | { 28 | using var dbContext = new TestDbContext(testFactory.DbContextOptions); 29 | dbContext.AddRange(entities); 30 | await dbContext.SaveChangesAsync(); 31 | } 32 | 33 | public async Task SeedRangeAsync(IEnumerable entities) 34 | { 35 | using var dbContext = new TestDbContext(testFactory.DbContextOptions); 36 | dbContext.AddRange(entities); 37 | await dbContext.SaveChangesAsync(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/Repository.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using AutoMapper.QueryableExtensions; 3 | 4 | namespace Tests.Fixture; 5 | 6 | public class Repository(DbContext context) : RepositoryWithMapper(context) where T : class 7 | { 8 | private static readonly Lazy _mapper = new(() => 9 | { 10 | var config = new MapperConfiguration(cfg => 11 | { 12 | cfg.AddMaps(typeof(Repository<>).Assembly); 13 | }); 14 | return config.CreateMapper(); 15 | }); 16 | 17 | protected override IQueryable Map(IQueryable source) 18 | { 19 | var result = source 20 | .ProjectTo(_mapper.Value.ConfigurationProvider); 21 | 22 | return result; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/SharedCollection.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Fixture; 2 | 3 | [CollectionDefinition("SharedCollection")] 4 | public class SharedCollection : ICollectionFixture 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Fixture; 2 | 3 | public class TestDbContext(DbContextOptions options) : DbContext(options) 4 | { 5 | public DbSet Bars => Set(); 6 | public DbSet Foos => Set(); 7 | public DbSet Countries => Set(); 8 | public DbSet Companies => Set(); 9 | public DbSet Stores => Set(); 10 | public DbSet
Addresses => Set
(); 11 | public DbSet Products => Set(); 12 | 13 | protected override void OnModelCreating(ModelBuilder modelBuilder) 14 | { 15 | modelBuilder.Entity() 16 | .HasOne(x => x.Address) 17 | .WithOne(x => x.Store) 18 | .HasForeignKey
(x => x.StoreId); 19 | 20 | modelBuilder.Entity() 21 | .HasMany() 22 | .WithOne(x => x.Country) 23 | .HasForeignKey(x => x.CountryId); 24 | 25 | modelBuilder.Entity() 26 | .HasQueryFilter(x => !x.IsDeleted); 27 | 28 | modelBuilder.Entity() 29 | .HasBaseType(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Fixture/TestFactory.cs: -------------------------------------------------------------------------------- 1 | using MartinCostello.SqlLocalDb; 2 | using Respawn; 3 | using Testcontainers.MsSql; 4 | 5 | namespace Tests.Fixture; 6 | 7 | public class TestFactory : IAsyncLifetime 8 | { 9 | // Flag to force using Docker SQL Server. Update it manually if you want to avoid localDb locally. 10 | private const bool _forceDocker = false; 11 | 12 | private string _connectionString = default!; 13 | private Respawner _respawner = default!; 14 | private MsSqlContainer? _dbContainer = null; 15 | 16 | public DbContextOptions DbContextOptions { get; private set; } = default!; 17 | 18 | public Task ResetDatabase() => _respawner.ResetAsync(_connectionString); 19 | 20 | public async Task InitializeAsync() 21 | { 22 | using (var localDB = new SqlLocalDbApi()) 23 | { 24 | if (_forceDocker || !localDB.IsLocalDBInstalled()) 25 | { 26 | _dbContainer = CreateContainer(); 27 | await _dbContainer.StartAsync(); 28 | _connectionString = _dbContainer.GetConnectionString(); 29 | } 30 | else 31 | { 32 | _connectionString = "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=QuerySpecificationTestsDB;Integrated Security=SSPI;TrustServerCertificate=True;"; 33 | } 34 | } 35 | 36 | Console.WriteLine($"Connection string: {_connectionString}"); 37 | 38 | DbContextOptions = new DbContextOptionsBuilder() 39 | .UseSqlServer(_connectionString) 40 | .EnableDetailedErrors() 41 | .EnableSensitiveDataLogging() 42 | .Options; 43 | 44 | using var dbContext = new TestDbContext(DbContextOptions); 45 | 46 | //await dbContext.Database.EnsureDeletedAsync(); 47 | await dbContext.Database.EnsureCreatedAsync(); 48 | 49 | _respawner = await Respawner.CreateAsync(_connectionString, new RespawnerOptions 50 | { 51 | DbAdapter = DbAdapter.SqlServer, 52 | SchemasToInclude = ["dbo"], 53 | }); 54 | 55 | await ResetDatabase(); 56 | } 57 | 58 | public async Task DisposeAsync() 59 | { 60 | if (_dbContainer is not null) 61 | { 62 | await _dbContainer.StopAsync(); 63 | } 64 | else 65 | { 66 | //using var dbContext = new TestDbContext(DbContextOptions); 67 | //await dbContext.Database.EnsureDeletedAsync(); 68 | } 69 | } 70 | 71 | private static MsSqlContainer CreateContainer() => new MsSqlBuilder() 72 | .WithImage("mcr.microsoft.com/mssql/server:2022-latest") 73 | .WithName("QuerySpecificationTestsDB") 74 | .WithPassword("P@ssW0rd!") 75 | .Build(); 76 | } 77 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using FluentAssertions; 2 | global using Microsoft.EntityFrameworkCore; 3 | global using Pozitron.QuerySpecification; 4 | global using Tests.Fixture; 5 | global using Xunit; 6 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/QuerySpecification.EntityFrameworkCore.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Repositories/RepositoryTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Tests.Repositories; 4 | 5 | [Collection("SharedCollection")] 6 | public class RepositoryTests(TestFactory factory) : IntegrationTest(factory) 7 | { 8 | [Fact] 9 | public void Constructor_SetsDbContext() 10 | { 11 | var repo = new Repository(DbContext); 12 | 13 | Accessors.DbContextOf(repo).Should().BeSameAs(DbContext); 14 | Accessors.SpecificationEvaluatorOf(repo).Should().BeSameAs(SpecificationEvaluator.Default); 15 | Accessors.PaginationSettingsOf(repo).Should().BeSameAs(PaginationSettings.Default); 16 | } 17 | 18 | [Fact] 19 | public void Constructor_SetsDbContextAndEvaluator() 20 | { 21 | var evaluator = new SpecificationEvaluator(); 22 | var repo = new Repository(DbContext, evaluator); 23 | 24 | Accessors.DbContextOf(repo).Should().BeSameAs(DbContext); 25 | Accessors.SpecificationEvaluatorOf(repo).Should().BeSameAs(evaluator); 26 | Accessors.PaginationSettingsOf(repo).Should().BeSameAs(PaginationSettings.Default); 27 | } 28 | 29 | [Fact] 30 | public void Constructor_SetsDbContextAndPaginationSettings() 31 | { 32 | var paginationSettings = new PaginationSettings(20, 200); 33 | var repo = new Repository(DbContext, paginationSettings); 34 | 35 | Accessors.DbContextOf(repo).Should().BeSameAs(DbContext); 36 | Accessors.SpecificationEvaluatorOf(repo).Should().BeSameAs(SpecificationEvaluator.Default); 37 | Accessors.PaginationSettingsOf(repo).Should().BeSameAs(paginationSettings); 38 | } 39 | 40 | [Fact] 41 | public void Constructor_SetsDbContextAndEvaluatorAndPaginationSettings() 42 | { 43 | var evaluator = new SpecificationEvaluator(); 44 | var paginationSettings = new PaginationSettings(20, 200); 45 | var repo = new Repository(DbContext, evaluator, paginationSettings); 46 | 47 | Accessors.DbContextOf(repo).Should().BeSameAs(DbContext); 48 | Accessors.SpecificationEvaluatorOf(repo).Should().BeSameAs(evaluator); 49 | Accessors.PaginationSettingsOf(repo).Should().BeSameAs(paginationSettings); 50 | } 51 | 52 | public class Repository : RepositoryWithMapper where T : class 53 | { 54 | public Repository(DbContext context) 55 | : base(context) 56 | { 57 | } 58 | 59 | public Repository(DbContext dbContext, SpecificationEvaluator specificationEvaluator) 60 | : base(dbContext, specificationEvaluator) 61 | { 62 | } 63 | 64 | public Repository(DbContext dbContext, PaginationSettings paginationSettings) 65 | : base(dbContext, paginationSettings) 66 | { 67 | } 68 | 69 | public Repository(DbContext dbContext, SpecificationEvaluator specificationEvaluator, PaginationSettings paginationSettings) 70 | : base(dbContext, specificationEvaluator, paginationSettings) 71 | { 72 | } 73 | 74 | protected override IQueryable Map(IQueryable source) 75 | => throw new NotImplementedException(); 76 | } 77 | 78 | private class Accessors where T : class 79 | { 80 | [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_dbContext")] 81 | public static extern ref DbContext DbContextOf(RepositoryBase @this); 82 | 83 | [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_evaluator")] 84 | public static extern ref SpecificationEvaluator SpecificationEvaluatorOf(RepositoryBase @this); 85 | 86 | [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_paginationSettings")] 87 | public static extern ref PaginationSettings PaginationSettingsOf(RepositoryWithMapper @this); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Repositories/Repository_ListTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Repositories; 2 | 3 | [Collection("SharedCollection")] 4 | public class Repository_ListTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | public record CountryDto(string? Name); 7 | 8 | [Fact] 9 | public async Task ListAsync_ReturnsAllItems() 10 | { 11 | var expected = new List 12 | { 13 | new() { Name = "a" }, 14 | new() { Name = "b" }, 15 | new() { Name = "c" }, 16 | }; 17 | await SeedRangeAsync(expected); 18 | 19 | var repo = new Repository(DbContext); 20 | 21 | var result = await repo.ListAsync(); 22 | 23 | result.Should().HaveSameCount(expected); 24 | result.Should().BeEquivalentTo(expected); 25 | } 26 | 27 | [Fact] 28 | public async Task ListAsync_ReturnsFilteredItems_GivenSpec() 29 | { 30 | var expected = new List 31 | { 32 | new() { Name = "b" }, 33 | new() { Name = "b" }, 34 | new() { Name = "b" }, 35 | }; 36 | await SeedRangeAsync( 37 | [ 38 | new() { Name = "a" }, 39 | new() { Name = "c" }, 40 | .. expected, 41 | new() { Name = "d" }, 42 | ]); 43 | 44 | var repo = new Repository(DbContext); 45 | var spec = new Specification(); 46 | spec.Query 47 | .Where(x => x.Name == "b"); 48 | 49 | var result = await repo.ListAsync(spec); 50 | 51 | result.Should().HaveSameCount(expected); 52 | result.Should().BeEquivalentTo(expected); 53 | } 54 | 55 | [Fact] 56 | public async Task ListAsync_ReturnsFilteredItems_GivenProjectionSpec() 57 | { 58 | var expected = new List 59 | { 60 | new("b"), 61 | new("b"), 62 | new("b"), 63 | }; 64 | await SeedRangeAsync( 65 | [ 66 | new() { Name = "a" }, 67 | new() { Name = "c" }, 68 | new() { Name = "b" }, 69 | new() { Name = "b" }, 70 | new() { Name = "b" }, 71 | new() { Name = "d" }, 72 | ]); 73 | 74 | var repo = new Repository(DbContext); 75 | var spec = new Specification(); 76 | spec.Query 77 | .Where(x => x.Name == "b") 78 | .Select(x => new CountryDto(x.Name)); 79 | 80 | var result = await repo.ListAsync(spec); 81 | 82 | result.Should().HaveSameCount(expected); 83 | result.Should().BeEquivalentTo(expected); 84 | } 85 | 86 | [Fact] 87 | public async Task AsAsyncEnumerable_ReturnsFilteredItems_GivenSpec() 88 | { 89 | var expected = new List 90 | { 91 | new() { Name = "b1" }, 92 | new() { Name = "b2" }, 93 | new() { Name = "b3" }, 94 | }; 95 | await SeedRangeAsync( 96 | [ 97 | new() { Name = "a" }, 98 | new() { Name = "c" }, 99 | .. expected, 100 | new() { Name = null }, 101 | ]); 102 | 103 | var repo = new Repository(DbContext); 104 | var spec = new Specification(); 105 | spec.Query 106 | .Like(x => x.Name, "b%") 107 | .OrderBy(x => x.Name); 108 | 109 | var suffix = 1; 110 | await foreach (var item in repo.AsAsyncEnumerable(spec)) 111 | { 112 | item.Name.Should().Be($"b{suffix++}"); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/QuerySpecification.EntityFrameworkCore.Tests/Repositories/Repository_WriteTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Repositories; 2 | 3 | [Collection("SharedCollection")] 4 | public class Repository_WriteTests(TestFactory factory) : IntegrationTest(factory) 5 | { 6 | [Fact] 7 | public async Task AddAsync_ShouldAddEntity() 8 | { 9 | var repo = new Repository(DbContext); 10 | var country = new Country 11 | { 12 | Name = Guid.NewGuid().ToString(), 13 | }; 14 | 15 | await repo.AddAsync(country); 16 | DbContext.ChangeTracker.Clear(); 17 | 18 | var countriesInDb = await DbContext.Countries.IgnoreQueryFilters().ToListAsync(); 19 | countriesInDb.Should().ContainSingle(); 20 | countriesInDb.First().Name.Should().Be(country.Name); 21 | } 22 | 23 | [Fact] 24 | public async Task AddAsync_ShouldAddMultipleEntities() 25 | { 26 | var repo = new Repository(DbContext); 27 | var countries = new[] 28 | { 29 | new Country { Name = Guid.NewGuid().ToString() }, 30 | new Country { Name = Guid.NewGuid().ToString() }, 31 | new Country { Name = Guid.NewGuid().ToString() }, 32 | }; 33 | 34 | await repo.AddRangeAsync(countries); 35 | DbContext.ChangeTracker.Clear(); 36 | 37 | var countriesInDb = await DbContext.Countries.ToListAsync(); 38 | countriesInDb.Should().HaveCount(3); 39 | countriesInDb.Select(x => x.Name).Should().BeEquivalentTo(countries.Select(x => x.Name)); 40 | } 41 | 42 | [Fact] 43 | public async Task UpdateAsync_ShouldUpdateEntity() 44 | { 45 | var repo = new Repository(DbContext); 46 | var country = new Country 47 | { 48 | Name = Guid.NewGuid().ToString(), 49 | }; 50 | await SeedAsync(country); 51 | 52 | country = await DbContext.Countries.FirstAsync(); 53 | country.Name = Guid.NewGuid().ToString(); 54 | await repo.UpdateAsync(country); 55 | DbContext.ChangeTracker.Clear(); 56 | 57 | var countriesInDb = await DbContext.Countries.ToListAsync(); 58 | countriesInDb.Should().NotBeNull(); 59 | countriesInDb.Should().ContainSingle(); 60 | countriesInDb.First().Name.Should().Be(country.Name); 61 | } 62 | 63 | [Fact] 64 | public async Task DeleteAsync_ShouldDeleteEntity() 65 | { 66 | var repo = new Repository(DbContext); 67 | var country = new Country 68 | { 69 | Name = Guid.NewGuid().ToString(), 70 | }; 71 | await SeedAsync(country); 72 | 73 | var countryInDb = await DbContext.Countries.FirstAsync(); 74 | await repo.DeleteAsync(countryInDb); 75 | DbContext.ChangeTracker.Clear(); 76 | 77 | var countriesInDb = await DbContext.Countries.ToListAsync(); 78 | countriesInDb.Should().BeEmpty(); 79 | } 80 | 81 | [Fact] 82 | public async Task DeleteAsync_ShouldDeleteMultipleEntities() 83 | { 84 | var repo = new Repository(DbContext); 85 | var countries = new[] 86 | { 87 | new Country { Name = Guid.NewGuid().ToString() }, 88 | new Country { Name = Guid.NewGuid().ToString() }, 89 | new Country { Name = Guid.NewGuid().ToString() }, 90 | }; 91 | await SeedRangeAsync(countries); 92 | 93 | var countriesInDb = await DbContext.Countries.ToListAsync(); 94 | await repo.DeleteRangeAsync(countriesInDb); 95 | DbContext.ChangeTracker.Clear(); 96 | 97 | countriesInDb = await DbContext.Countries.ToListAsync(); 98 | countriesInDb.Should().BeEmpty(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_AsNoTracking.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_AsNoTracking 4 | { 5 | public record Customer(int Id, string Name); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoAsNoTracking() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.AsNoTracking.Should().Be(false); 14 | spec2.AsNoTracking.Should().Be(false); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenAsNoTrackingWithFalseCondition() 19 | { 20 | var spec1 = new Specification(); 21 | spec1.Query 22 | .AsNoTracking(false); 23 | 24 | var spec2 = new Specification(); 25 | spec2.Query 26 | .AsNoTracking(false); 27 | 28 | spec1.AsNoTracking.Should().Be(false); 29 | spec2.AsNoTracking.Should().Be(false); 30 | } 31 | 32 | [Fact] 33 | public void SetsAsNoTracking_GivenAsNoTracking() 34 | { 35 | var spec1 = new Specification(); 36 | spec1.Query 37 | .AsNoTracking(); 38 | 39 | var spec2 = new Specification(); 40 | spec2.Query 41 | .AsNoTracking(); 42 | 43 | spec1.AsNoTracking.Should().Be(true); 44 | spec2.AsNoTracking.Should().Be(true); 45 | } 46 | 47 | [Fact] 48 | public void SetsAsNoTracking_GivenOtherTrackingBehavior() 49 | { 50 | var spec1 = new Specification(); 51 | spec1.Query 52 | .AsTracking() 53 | .AsNoTrackingWithIdentityResolution() 54 | .AsNoTracking(); 55 | 56 | var spec2 = new Specification(); 57 | spec2.Query 58 | .AsTracking() 59 | .AsNoTrackingWithIdentityResolution() 60 | .AsNoTracking(); 61 | 62 | spec1.AsTracking.Should().Be(false); 63 | spec1.AsNoTrackingWithIdentityResolution.Should().Be(false); 64 | spec1.AsNoTracking.Should().Be(true); 65 | spec2.AsTracking.Should().Be(false); 66 | spec2.AsNoTrackingWithIdentityResolution.Should().Be(false); 67 | spec2.AsNoTracking.Should().Be(true); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_AsNoTrackingWithIdentityResolution.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_AsNoTrackingWithIdentityResolution 4 | { 5 | public record Customer(int Id, string Name); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoAsNoTrackingWithIdentityResolution() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.AsNoTrackingWithIdentityResolution.Should().Be(false); 14 | spec2.AsNoTrackingWithIdentityResolution.Should().Be(false); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenAsNoTrackingWithIdentityResolutionWithFalseCondition() 19 | { 20 | var spec1 = new Specification(); 21 | spec1.Query 22 | .AsNoTrackingWithIdentityResolution(false); 23 | 24 | var spec2 = new Specification(); 25 | spec2.Query 26 | .AsNoTrackingWithIdentityResolution(false); 27 | 28 | spec1.AsNoTrackingWithIdentityResolution.Should().Be(false); 29 | spec2.AsNoTrackingWithIdentityResolution.Should().Be(false); 30 | } 31 | 32 | [Fact] 33 | public void SetsAsNoTrackingWithIdentityResolution_GivenAsNoTrackingWithIdentityResolution() 34 | { 35 | var spec1 = new Specification(); 36 | spec1.Query 37 | .AsNoTrackingWithIdentityResolution(); 38 | 39 | var spec2 = new Specification(); 40 | spec2.Query 41 | .AsNoTrackingWithIdentityResolution(); 42 | 43 | spec1.AsNoTrackingWithIdentityResolution.Should().Be(true); 44 | spec2.AsNoTrackingWithIdentityResolution.Should().Be(true); 45 | } 46 | 47 | [Fact] 48 | public void SetsAsNoTrackingWithIdentityResolution_GivenOtherTrackingBehavior() 49 | { 50 | var spec1 = new Specification(); 51 | spec1.Query 52 | .AsNoTracking() 53 | .AsTracking() 54 | .AsNoTrackingWithIdentityResolution(); 55 | 56 | var spec2 = new Specification(); 57 | spec2.Query 58 | .AsNoTracking() 59 | .AsTracking() 60 | .AsNoTrackingWithIdentityResolution(); 61 | 62 | spec1.AsNoTracking.Should().Be(false); 63 | spec1.AsTracking.Should().Be(false); 64 | spec1.AsNoTrackingWithIdentityResolution.Should().Be(true); 65 | spec2.AsNoTracking.Should().Be(false); 66 | spec2.AsTracking.Should().Be(false); 67 | spec2.AsNoTrackingWithIdentityResolution.Should().Be(true); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_AsSplitQuery.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_AsSplitQuery 4 | { 5 | public record Customer(int Id, string Name); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoAsSplitQuery() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.AsSplitQuery.Should().Be(false); 14 | spec2.AsSplitQuery.Should().Be(false); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenAsSplitQueryWithFalseCondition() 19 | { 20 | var spec1 = new Specification(); 21 | spec1.Query 22 | .AsSplitQuery(false); 23 | 24 | var spec2 = new Specification(); 25 | spec2.Query 26 | .AsSplitQuery(false); 27 | 28 | spec1.AsSplitQuery.Should().Be(false); 29 | spec2.AsSplitQuery.Should().Be(false); 30 | } 31 | 32 | [Fact] 33 | public void SetsAsSplitQuery_GivenAsSplitQuery() 34 | { 35 | var spec1 = new Specification(); 36 | spec1.Query 37 | .AsSplitQuery(); 38 | 39 | var spec2 = new Specification(); 40 | spec2.Query 41 | .AsSplitQuery(); 42 | 43 | spec1.AsSplitQuery.Should().Be(true); 44 | spec2.AsSplitQuery.Should().Be(true); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_AsTracking.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_AsTracking 4 | { 5 | public record Customer(int Id, string Name); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoAsTracking() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.AsTracking.Should().Be(false); 14 | spec2.AsTracking.Should().Be(false); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenAsTrackingWithFalseCondition() 19 | { 20 | var spec1 = new Specification(); 21 | spec1.Query 22 | .AsTracking(false); 23 | 24 | var spec2 = new Specification(); 25 | spec2.Query 26 | .AsTracking(false); 27 | 28 | spec1.AsTracking.Should().Be(false); 29 | spec2.AsTracking.Should().Be(false); 30 | } 31 | 32 | [Fact] 33 | public void SetsAsNoTracking_GivenAsTracking() 34 | { 35 | var spec1 = new Specification(); 36 | spec1.Query 37 | .AsTracking(); 38 | 39 | var spec2 = new Specification(); 40 | spec2.Query 41 | .AsTracking(); 42 | 43 | spec1.AsTracking.Should().Be(true); 44 | spec2.AsTracking.Should().Be(true); 45 | } 46 | 47 | [Fact] 48 | public void SetsAsTracking_GivenOtherTrackingBehavior() 49 | { 50 | var spec1 = new Specification(); 51 | spec1.Query 52 | .AsNoTracking() 53 | .AsNoTrackingWithIdentityResolution() 54 | .AsTracking(); 55 | 56 | var spec2 = new Specification(); 57 | spec2.Query 58 | .AsNoTracking() 59 | .AsNoTrackingWithIdentityResolution() 60 | .AsTracking(); 61 | 62 | spec1.AsNoTracking.Should().Be(false); 63 | spec1.AsNoTrackingWithIdentityResolution.Should().Be(false); 64 | spec1.AsTracking.Should().Be(true); 65 | spec2.AsNoTracking.Should().Be(false); 66 | spec2.AsNoTrackingWithIdentityResolution.Should().Be(false); 67 | spec2.AsTracking.Should().Be(true); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_IgnoreAutoIncludes.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_IgnoreAutoIncludes 4 | { 5 | public record Customer(int Id, string Name); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoIgnoreAutoIncludes() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.IgnoreAutoIncludes.Should().Be(false); 14 | spec2.IgnoreAutoIncludes.Should().Be(false); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenIgnoreAutoIncludesWithFalseCondition() 19 | { 20 | var spec1 = new Specification(); 21 | spec1.Query 22 | .IgnoreAutoIncludes(false); 23 | 24 | var spec2 = new Specification(); 25 | spec2.Query 26 | .IgnoreAutoIncludes(false); 27 | 28 | spec1.IgnoreAutoIncludes.Should().Be(false); 29 | spec2.IgnoreAutoIncludes.Should().Be(false); 30 | } 31 | 32 | [Fact] 33 | public void SetsIgnoreAutoIncludes_GivenIgnoreAutoIncludes() 34 | { 35 | var spec1 = new Specification(); 36 | spec1.Query 37 | .IgnoreAutoIncludes(); 38 | 39 | var spec2 = new Specification(); 40 | spec2.Query 41 | .IgnoreAutoIncludes(); 42 | 43 | spec1.IgnoreAutoIncludes.Should().Be(true); 44 | spec2.IgnoreAutoIncludes.Should().Be(true); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_IgnoreQueryFilters.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_IgnoreQueryFilters 4 | { 5 | public record Customer(int Id, string Name); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoIgnoreQueryFilters() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.IgnoreQueryFilters.Should().Be(false); 14 | spec2.IgnoreQueryFilters.Should().Be(false); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenIgnoreQueryFiltersWithFalseCondition() 19 | { 20 | var spec1 = new Specification(); 21 | spec1.Query 22 | .IgnoreQueryFilters(false); 23 | 24 | var spec2 = new Specification(); 25 | spec2.Query 26 | .IgnoreQueryFilters(false); 27 | 28 | spec1.IgnoreQueryFilters.Should().Be(false); 29 | spec2.IgnoreQueryFilters.Should().Be(false); 30 | } 31 | 32 | [Fact] 33 | public void SetsIgnoreQueryFilters_GivenIgnoreQueryFilters() 34 | { 35 | var spec1 = new Specification(); 36 | spec1.Query 37 | .IgnoreQueryFilters(); 38 | 39 | var spec2 = new Specification(); 40 | spec2.Query 41 | .IgnoreQueryFilters(); 42 | 43 | spec1.IgnoreQueryFilters.Should().Be(true); 44 | spec2.IgnoreQueryFilters.Should().Be(true); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_Include.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_Include 4 | { 5 | public record Customer(int Id, Address Address, Contact Contact); 6 | public record Address(int Id, string City); 7 | public record Contact(int Id, string Email); 8 | 9 | [Fact] 10 | public void DoesNothing_GivenNoInclude() 11 | { 12 | var spec1 = new Specification(); 13 | var spec2 = new Specification(); 14 | 15 | spec1.IncludeExpressions.Should().BeEmpty(); 16 | spec2.IncludeExpressions.Should().BeEmpty(); 17 | } 18 | 19 | [Fact] 20 | public void DoesNothing_GivenIncludeWithFalseCondition() 21 | { 22 | var spec1 = new Specification(); 23 | spec1.Query 24 | .Include(x => x.Address, false); 25 | 26 | var spec2 = new Specification(); 27 | spec2.Query 28 | .Include(x => x.Address, false); 29 | 30 | spec1.IncludeExpressions.Should().BeEmpty(); 31 | spec2.IncludeExpressions.Should().BeEmpty(); 32 | } 33 | 34 | [Fact] 35 | public void AddsInclude_GivenInclude() 36 | { 37 | Expression> expr = x => x.Address; 38 | 39 | var spec1 = new Specification(); 40 | spec1.Query 41 | .Include(expr); 42 | 43 | var spec2 = new Specification(); 44 | spec2.Query 45 | .Include(expr); 46 | 47 | spec1.IncludeExpressions.Should().ContainSingle(); 48 | spec1.IncludeExpressions.First().LambdaExpression.Should().BeSameAs(expr); 49 | spec1.IncludeExpressions.First().Type.Should().Be(IncludeType.Include); 50 | spec2.IncludeExpressions.Should().ContainSingle(); 51 | spec2.IncludeExpressions.First().LambdaExpression.Should().BeSameAs(expr); 52 | spec2.IncludeExpressions.First().Type.Should().Be(IncludeType.Include); 53 | } 54 | 55 | [Fact] 56 | public void AddsInclude_GivenMultipleInclude() 57 | { 58 | var spec1 = new Specification(); 59 | spec1.Query 60 | .Include(x => x.Address) 61 | .Include(x => x.Contact); 62 | 63 | var spec2 = new Specification(); 64 | spec2.Query 65 | .Include(x => x.Address) 66 | .Include(x => x.Contact); 67 | 68 | spec1.IncludeExpressions.Should().HaveCount(2); 69 | spec1.IncludeExpressions.Should().AllSatisfy(x => x.Type.Should().Be(IncludeType.Include)); 70 | spec2.IncludeExpressions.Should().HaveCount(2); 71 | spec2.IncludeExpressions.Should().AllSatisfy(x => x.Type.Should().Be(IncludeType.Include)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_IncludeString.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_IncludeString 4 | { 5 | public record Customer(int Id, Address Address, Contact Contact); 6 | public record Address(int Id, string City); 7 | public record Contact(int Id, string Email); 8 | 9 | [Fact] 10 | public void DoesNothing_GivenNoIncludeString() 11 | { 12 | var spec1 = new Specification(); 13 | var spec2 = new Specification(); 14 | 15 | spec1.IncludeStrings.Should().BeEmpty(); 16 | spec2.IncludeStrings.Should().BeEmpty(); 17 | } 18 | 19 | [Fact] 20 | public void DoesNothing_GivenIncludeStringWithFalseCondition() 21 | { 22 | var spec1 = new Specification(); 23 | spec1.Query 24 | .Include(nameof(Address), false); 25 | 26 | var spec2 = new Specification(); 27 | spec2.Query 28 | .Include(nameof(Address), false); 29 | 30 | spec1.IncludeStrings.Should().BeEmpty(); 31 | spec2.IncludeStrings.Should().BeEmpty(); 32 | } 33 | 34 | [Fact] 35 | public void AddsIncludeString_GivenIncludeString() 36 | { 37 | var includeString = nameof(Address); 38 | var spec1 = new Specification(); 39 | spec1.Query 40 | .Include(includeString); 41 | 42 | var spec2 = new Specification(); 43 | spec2.Query 44 | .Include(includeString); 45 | 46 | spec1.IncludeStrings.Should().ContainSingle(); 47 | spec1.IncludeStrings.First().Should().BeSameAs(includeString); 48 | spec2.IncludeStrings.Should().ContainSingle(); 49 | spec2.IncludeStrings.First().Should().BeSameAs(includeString); 50 | } 51 | 52 | [Fact] 53 | public void AddsIncludeString_GivenMultipleIncludeString() 54 | { 55 | var spec1 = new Specification(); 56 | spec1.Query 57 | .Include(nameof(Address)) 58 | .Include(nameof(Contact)); 59 | 60 | var spec2 = new Specification(); 61 | spec2.Query 62 | .Include(nameof(Address)) 63 | .Include(nameof(Contact)); 64 | 65 | spec1.IncludeStrings.Should().HaveCount(2); 66 | spec2.IncludeStrings.Should().HaveCount(2); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_Like.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_Like 4 | { 5 | public record Customer(int Id, string FirstName, string LastName); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoLike() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.LikeExpressions.Should().BeEmpty(); 14 | spec2.LikeExpressions.Should().BeEmpty(); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenLikeWithFalseCondition() 19 | { 20 | var spec1 = new Specification(); 21 | spec1.Query 22 | .Like(x => x.FirstName, "%a%", false); 23 | 24 | var spec2 = new Specification(); 25 | spec2.Query 26 | .Like(x => x.FirstName, "%a%", false); 27 | 28 | spec1.LikeExpressions.Should().BeEmpty(); 29 | spec2.LikeExpressions.Should().BeEmpty(); 30 | } 31 | 32 | [Fact] 33 | public void AddsLike_GivenSingleLike() 34 | { 35 | Expression> expr = x => x.FirstName; 36 | var pattern = "%a%"; 37 | 38 | var spec1 = new Specification(); 39 | spec1.Query 40 | .Like(expr, pattern); 41 | 42 | var spec2 = new Specification(); 43 | spec2.Query 44 | .Like(expr, pattern); 45 | 46 | spec1.LikeExpressions.Should().ContainSingle(); 47 | spec1.LikeExpressions.First().KeySelector.Should().BeSameAs(expr); 48 | spec1.LikeExpressions.First().Pattern.Should().Be(pattern); 49 | spec1.LikeExpressions.First().Group.Should().Be(1); 50 | spec2.LikeExpressions.Should().ContainSingle(); 51 | spec2.LikeExpressions.First().KeySelector.Should().BeSameAs(expr); 52 | spec2.LikeExpressions.First().Pattern.Should().Be(pattern); 53 | spec2.LikeExpressions.First().Group.Should().Be(1); 54 | } 55 | 56 | [Fact] 57 | public void AddsLike_GivenMultipleLikeInSameGroup() 58 | { 59 | var spec1 = new Specification(); 60 | spec1.Query 61 | .Like(x => x.FirstName, "%a%") 62 | .Like(x => x.LastName, "%a%"); 63 | 64 | var spec2 = new Specification(); 65 | spec2.Query 66 | .Like(x => x.FirstName, "%a%") 67 | .Like(x => x.LastName, "%a%"); 68 | 69 | spec1.LikeExpressions.Should().HaveCount(2); 70 | spec1.LikeExpressions.Should().AllSatisfy(x => x.Group.Should().Be(1)); 71 | spec2.LikeExpressions.Should().HaveCount(2); 72 | spec2.LikeExpressions.Should().AllSatisfy(x => x.Group.Should().Be(1)); 73 | } 74 | 75 | [Fact] 76 | public void AddsLike_GivenMultipleLikeInDifferentGroups() 77 | { 78 | var spec1 = new Specification(); 79 | spec1.Query 80 | .Like(x => x.FirstName, "%a%", 1) 81 | .Like(x => x.LastName, "%a%", 2); 82 | 83 | var spec2 = new Specification(); 84 | spec2.Query 85 | .Like(x => x.FirstName, "%a%", 1) 86 | .Like(x => x.LastName, "%a%", 2); 87 | 88 | spec1.LikeExpressions.Should().HaveCount(2); 89 | spec1.LikeExpressions.Should().OnlyHaveUniqueItems(x => x.Group); 90 | spec2.LikeExpressions.Should().HaveCount(2); 91 | spec2.LikeExpressions.Should().OnlyHaveUniqueItems(x => x.Group); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_OrderBy.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_OrderBy 4 | { 5 | public record Customer(int Id, string FirstName, string LastName); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoOrderBy() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.OrderExpressions.Should().BeEmpty(); 14 | spec2.OrderExpressions.Should().BeEmpty(); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenOrderByWithFalseCondition() 19 | { 20 | var spec1 = new Specification(); 21 | spec1.Query 22 | .OrderBy(x => x.FirstName, false); 23 | 24 | var spec2 = new Specification(); 25 | spec2.Query 26 | .OrderBy(x => x.FirstName, false); 27 | 28 | spec1.OrderExpressions.Should().BeEmpty(); 29 | spec2.OrderExpressions.Should().BeEmpty(); 30 | } 31 | 32 | [Fact] 33 | public void AddsOrderBy_GivenOrderBy() 34 | { 35 | Expression> expr = x => x.FirstName; 36 | var spec1 = new Specification(); 37 | spec1.Query 38 | .OrderBy(expr); 39 | 40 | var spec2 = new Specification(); 41 | spec2.Query 42 | .OrderBy(expr); 43 | 44 | spec1.OrderExpressions.Should().ContainSingle(); 45 | spec1.OrderExpressions.First().KeySelector.Should().BeSameAs(expr); 46 | spec1.OrderExpressions.First().Type.Should().Be(OrderType.OrderBy); 47 | spec2.OrderExpressions.Should().ContainSingle(); 48 | spec2.OrderExpressions.First().KeySelector.Should().BeSameAs(expr); 49 | spec2.OrderExpressions.First().Type.Should().Be(OrderType.OrderBy); 50 | } 51 | 52 | [Fact] 53 | public void AddsOrderBy_GivenMultipleOrderBy() 54 | { 55 | var spec1 = new Specification(); 56 | spec1.Query 57 | .OrderBy(x => x.FirstName) 58 | .OrderBy(x => x.LastName); 59 | 60 | var spec2 = new Specification(); 61 | spec2.Query 62 | .OrderBy(x => x.FirstName) 63 | .OrderBy(x => x.LastName); 64 | 65 | spec1.OrderExpressions.Should().HaveCount(2); 66 | spec1.OrderExpressions.Should().AllSatisfy(x => x.Type.Should().Be(OrderType.OrderBy)); 67 | spec2.OrderExpressions.Should().HaveCount(2); 68 | spec2.OrderExpressions.Should().AllSatisfy(x => x.Type.Should().Be(OrderType.OrderBy)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_OrderByDescending.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_OrderByDescending 4 | { 5 | public record Customer(int Id, string FirstName, string LastName); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoOrderByDescending() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.OrderExpressions.Should().BeEmpty(); 14 | spec2.OrderExpressions.Should().BeEmpty(); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenOrderByDescendingWithFalseCondition() 19 | { 20 | var spec1 = new Specification(); 21 | spec1.Query 22 | .OrderByDescending(x => x.FirstName, false); 23 | 24 | var spec2 = new Specification(); 25 | spec2.Query 26 | .OrderByDescending(x => x.FirstName, false); 27 | 28 | spec1.OrderExpressions.Should().BeEmpty(); 29 | spec2.OrderExpressions.Should().BeEmpty(); 30 | } 31 | 32 | [Fact] 33 | public void AddsOrderByDescending_GivenOrderByDescending() 34 | { 35 | Expression> expr = x => x.FirstName; 36 | var spec1 = new Specification(); 37 | spec1.Query 38 | .OrderByDescending(expr); 39 | 40 | var spec2 = new Specification(); 41 | spec2.Query 42 | .OrderByDescending(expr); 43 | 44 | spec1.OrderExpressions.Should().ContainSingle(); 45 | spec1.OrderExpressions.First().KeySelector.Should().BeSameAs(expr); 46 | spec1.OrderExpressions.First().Type.Should().Be(OrderType.OrderByDescending); 47 | spec2.OrderExpressions.Should().ContainSingle(); 48 | spec2.OrderExpressions.First().KeySelector.Should().BeSameAs(expr); 49 | spec2.OrderExpressions.First().Type.Should().Be(OrderType.OrderByDescending); 50 | } 51 | 52 | [Fact] 53 | public void AddsOrderByDescending_GivenMultipleOrderByDescending() 54 | { 55 | var spec1 = new Specification(); 56 | spec1.Query 57 | .OrderByDescending(x => x.FirstName) 58 | .OrderByDescending(x => x.LastName); 59 | 60 | var spec2 = new Specification(); 61 | spec2.Query 62 | .OrderByDescending(x => x.FirstName) 63 | .OrderByDescending(x => x.LastName); 64 | 65 | spec1.OrderExpressions.Should().HaveCount(2); 66 | spec1.OrderExpressions.Should().AllSatisfy(x => x.Type.Should().Be(OrderType.OrderByDescending)); 67 | spec2.OrderExpressions.Should().HaveCount(2); 68 | spec2.OrderExpressions.Should().AllSatisfy(x => x.Type.Should().Be(OrderType.OrderByDescending)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_Select.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_Select 4 | { 5 | public record Customer(int Id, string FirstName, string LastName); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoSelect() 9 | { 10 | var spec = new Specification(); 11 | 12 | spec.Selector.Should().BeNull(); 13 | } 14 | 15 | [Fact] 16 | public void AddsSelector_GivenSelect() 17 | { 18 | Expression> expr = x => x.FirstName; 19 | 20 | var spec = new Specification(); 21 | spec.Query 22 | .Select(expr); 23 | 24 | spec.Selector.Should().NotBeNull(); 25 | spec.Selector.Should().BeSameAs(expr); 26 | } 27 | 28 | [Fact] 29 | public void OverwritesSelector_GivenMultipleSelect() 30 | { 31 | Expression> expr = x => x.FirstName; 32 | 33 | var spec = new Specification(); 34 | spec.Query 35 | .Select(x => x.LastName); 36 | spec.Query 37 | .Select(expr); 38 | 39 | spec.Selector.Should().NotBeNull(); 40 | spec.Selector.Should().BeSameAs(expr); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_SelectMany.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_SelectMany 4 | { 5 | public record Customer(int Id, List FirstName, List LastName); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoSelectMany() 9 | { 10 | var spec = new Specification(); 11 | 12 | spec.SelectorMany.Should().BeNull(); 13 | } 14 | 15 | [Fact] 16 | public void AddsSelectorMany_GivenSelectMany() 17 | { 18 | Expression>> expr = x => x.FirstName; 19 | 20 | var spec = new Specification(); 21 | spec.Query 22 | .SelectMany(expr); 23 | 24 | spec.SelectorMany.Should().NotBeNull(); 25 | spec.SelectorMany.Should().BeSameAs(expr); 26 | } 27 | 28 | [Fact] 29 | public void OverwritesSelectorMany_GivenMultipleSelectMany() 30 | { 31 | Expression>> expr = x => x.FirstName; 32 | 33 | var spec = new Specification(); 34 | spec.Query 35 | .SelectMany(x => x.LastName); 36 | spec.Query 37 | .SelectMany(expr); 38 | 39 | spec.SelectorMany.Should().NotBeNull(); 40 | spec.SelectorMany.Should().BeSameAs(expr); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_Skip.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_Skip 4 | { 5 | public record Customer(int Id, string Name); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoSkip() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.Skip.Should().BeLessThan(0); 14 | spec2.Skip.Should().BeLessThan(0); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenSkipWithFalseCondition() 19 | { 20 | var skip = 1; 21 | 22 | var spec1 = new Specification(); 23 | spec1.Query 24 | .Skip(skip, false); 25 | 26 | var spec2 = new Specification(); 27 | spec2.Query 28 | .Skip(skip, false); 29 | 30 | spec1.Skip.Should().BeLessThan(0); 31 | spec2.Skip.Should().BeLessThan(0); 32 | } 33 | 34 | [Fact] 35 | public void SetsSkip_GivenSkip() 36 | { 37 | var skip = 1; 38 | 39 | var spec1 = new Specification(); 40 | spec1.Query 41 | .Skip(skip); 42 | 43 | var spec2 = new Specification(); 44 | spec2.Query 45 | .Skip(skip); 46 | 47 | spec1.Skip.Should().Be(skip); 48 | spec2.Skip.Should().Be(skip); 49 | } 50 | 51 | [Fact] 52 | public void OverwritesSkip_GivenNewSkip() 53 | { 54 | var skip = 1; 55 | var skipNew = 2; 56 | 57 | var spec1 = new Specification(); 58 | spec1.Query 59 | .Skip(skip) 60 | .Skip(skipNew); 61 | 62 | var spec2 = new Specification(); 63 | spec2.Query 64 | .Skip(skip) 65 | .Skip(skipNew); 66 | 67 | spec1.Skip.Should().Be(skipNew); 68 | spec2.Skip.Should().Be(skipNew); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_TagWith.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_TagWith 4 | { 5 | public record Customer(int Id, string Name); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoTag() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.QueryTags.Should().BeSameAs(Enumerable.Empty()); 14 | spec2.QueryTags.Should().BeSameAs(Enumerable.Empty()); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenTagWithFalseCondition() 19 | { 20 | var spec1 = new Specification(); 21 | spec1.Query 22 | .TagWith("asd", false); 23 | 24 | var spec2 = new Specification(); 25 | spec2.Query 26 | .TagWith("asd", false); 27 | 28 | spec1.QueryTags.Should().BeSameAs(Enumerable.Empty()); 29 | spec2.QueryTags.Should().BeSameAs(Enumerable.Empty()); 30 | } 31 | 32 | [Fact] 33 | public void SetsTag_GivenSingleTag() 34 | { 35 | var tag = "asd"; 36 | 37 | var spec1 = new Specification(); 38 | spec1.Query 39 | .TagWith(tag); 40 | 41 | var spec2 = new Specification(); 42 | spec2.Query 43 | .TagWith(tag); 44 | 45 | spec1.QueryTags.Should().ContainSingle(); 46 | spec1.QueryTags.First().Should().Be(tag); 47 | spec2.QueryTags.Should().ContainSingle(); 48 | spec2.QueryTags.First().Should().Be(tag); 49 | } 50 | 51 | [Fact] 52 | public void SetsTags_GivenTwoTags() 53 | { 54 | var tag1 = "asd"; 55 | var tag2 = "qwe"; 56 | 57 | var spec1 = new Specification(); 58 | spec1.Query 59 | .TagWith(tag1) 60 | .TagWith(tag2); 61 | 62 | var spec2 = new Specification(); 63 | spec2.Query 64 | .TagWith(tag1) 65 | .TagWith(tag2); 66 | 67 | spec1.QueryTags.Should().HaveCount(2); 68 | spec1.QueryTags.First().Should().Be(tag1); 69 | spec1.QueryTags.Skip(1).First().Should().Be(tag2); 70 | spec2.QueryTags.Should().HaveCount(2); 71 | spec2.QueryTags.First().Should().Be(tag1); 72 | spec2.QueryTags.Skip(1).First().Should().Be(tag2); 73 | } 74 | 75 | [Fact] 76 | public void SetsTags_GivenMultipleTags() 77 | { 78 | var tag1 = "asd"; 79 | var tag2 = "qwe"; 80 | var tag3 = "zxc"; 81 | 82 | var spec1 = new Specification(); 83 | spec1.Query 84 | .TagWith(tag1) 85 | .TagWith(tag2) 86 | .TagWith(tag3); 87 | 88 | var spec2 = new Specification(); 89 | spec2.Query 90 | .TagWith(tag1) 91 | .TagWith(tag2) 92 | .TagWith(tag3); 93 | 94 | spec1.QueryTags.Should().HaveCount(3); 95 | spec1.QueryTags.First().Should().Be(tag1); 96 | spec1.QueryTags.Skip(1).First().Should().Be(tag2); 97 | spec1.QueryTags.Skip(2).First().Should().Be(tag3); 98 | 99 | spec2.QueryTags.Should().HaveCount(3); 100 | spec2.QueryTags.First().Should().Be(tag1); 101 | spec2.QueryTags.Skip(1).First().Should().Be(tag2); 102 | spec2.QueryTags.Skip(2).First().Should().Be(tag3); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_Take.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_Take 4 | { 5 | public record Customer(int Id, string Name); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoTake() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.Take.Should().BeLessThan(0); 14 | spec2.Take.Should().BeLessThan(0); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenTakeWithFalseCondition() 19 | { 20 | var take = 1; 21 | 22 | var spec1 = new Specification(); 23 | spec1.Query 24 | .Take(take, false); 25 | 26 | var spec2 = new Specification(); 27 | spec2.Query 28 | .Take(take, false); 29 | 30 | spec1.Take.Should().BeLessThan(0); 31 | spec2.Take.Should().BeLessThan(0); 32 | } 33 | 34 | [Fact] 35 | public void SetsTake_GivenTake() 36 | { 37 | var take = 1; 38 | 39 | var spec1 = new Specification(); 40 | spec1.Query 41 | .Take(take); 42 | 43 | var spec2 = new Specification(); 44 | spec2.Query 45 | .Take(take); 46 | 47 | spec1.Take.Should().Be(take); 48 | spec2.Take.Should().Be(take); 49 | } 50 | 51 | [Fact] 52 | public void OverwritesTake_GivenNewTake() 53 | { 54 | var take = 1; 55 | var takeNew = 2; 56 | 57 | var spec1 = new Specification(); 58 | spec1.Query 59 | .Take(take) 60 | .Take(takeNew); 61 | 62 | var spec2 = new Specification(); 63 | spec2.Query 64 | .Take(take) 65 | .Take(takeNew); 66 | 67 | spec1.Take.Should().Be(takeNew); 68 | spec2.Take.Should().Be(takeNew); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_Where.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_Where 4 | { 5 | public record Customer(int Id, string Name); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoWhere() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.WhereExpressions.Should().BeEmpty(); 14 | spec2.WhereExpressions.Should().BeEmpty(); 15 | } 16 | 17 | [Fact] 18 | public void DoesNothing_GivenWhereWithFalseCondition() 19 | { 20 | var spec1 = new Specification(); 21 | spec1.Query 22 | .Where(x => x.Id > 1, false); 23 | 24 | var spec2 = new Specification(); 25 | spec2.Query 26 | .Where(x => x.Id > 1, false); 27 | 28 | spec1.WhereExpressions.Should().BeEmpty(); 29 | spec2.WhereExpressions.Should().BeEmpty(); 30 | } 31 | 32 | [Fact] 33 | public void AddsWhere_GivenWhere() 34 | { 35 | Expression> expr = x => x.Id > 1; 36 | var spec1 = new Specification(); 37 | spec1.Query 38 | .Where(expr); 39 | 40 | var spec2 = new Specification(); 41 | spec2.Query 42 | .Where(expr); 43 | 44 | spec1.WhereExpressions.Should().ContainSingle(); 45 | spec1.WhereExpressions.First().Filter.Should().BeSameAs(expr); 46 | spec2.WhereExpressions.Should().ContainSingle(); 47 | spec2.WhereExpressions.First().Filter.Should().BeSameAs(expr); 48 | } 49 | 50 | [Fact] 51 | public void AddsWhere_GivenMultipleWhere() 52 | { 53 | var spec1 = new Specification(); 54 | spec1.Query 55 | .Where(x => x.Id > 1) 56 | .Where(x => x.Id > 2); 57 | 58 | var spec2 = new Specification(); 59 | spec2.Query 60 | .Where(x => x.Id > 1) 61 | .Where(x => x.Id > 2); 62 | 63 | spec1.WhereExpressions.Should().HaveCount(2); 64 | spec2.WhereExpressions.Should().HaveCount(2); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/Builder_WithCacheKey.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Builders; 2 | 3 | public class Builder_WithCacheKey 4 | { 5 | public record Customer(int Id, string Name); 6 | 7 | [Fact] 8 | public void DoesNothing_GivenNoWithCacheKey() 9 | { 10 | var spec1 = new Specification(); 11 | var spec2 = new Specification(); 12 | 13 | spec1.CacheKey.Should().BeNull(); 14 | spec1.HasCacheKey.Should().BeFalse(); 15 | 16 | spec2.CacheKey.Should().BeNull(); 17 | spec2.HasCacheKey.Should().BeFalse(); 18 | } 19 | 20 | [Fact] 21 | public void DoesNothing_GivenWithCacheKeyWithFalseCondition() 22 | { 23 | var key = "someKey"; 24 | 25 | var spec1 = new Specification(); 26 | spec1.Query 27 | .Where(x=> x.Id > 0) 28 | .WithCacheKey(key, false); 29 | 30 | var spec2 = new Specification(); 31 | spec2.Query 32 | .Where(x=> x.Id > 0) 33 | .WithCacheKey(key, false); 34 | 35 | spec1.CacheKey.Should().BeNull(); 36 | spec1.HasCacheKey.Should().BeFalse(); 37 | 38 | spec2.CacheKey.Should().BeNull(); 39 | spec2.HasCacheKey.Should().BeFalse(); 40 | } 41 | 42 | [Fact] 43 | public void SetsCacheKey_GivenWithCacheKey() 44 | { 45 | var key = "someKey"; 46 | 47 | var spec1 = new Specification(); 48 | spec1.Query 49 | .WithCacheKey(key); 50 | 51 | var spec2 = new Specification(); 52 | spec2.Query 53 | .WithCacheKey(key); 54 | 55 | spec1.CacheKey.Should().Be(key); 56 | spec1.HasCacheKey.Should().BeTrue(); 57 | 58 | spec1.CacheKey.Should().Be(key); 59 | spec2.HasCacheKey.Should().BeTrue(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Builders/SpecificationBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Tests.Builders; 4 | 5 | // The behavior is already extensively tested. 6 | // These are just dummy tests to ensure the methods call the underlying Specification methods. 7 | public class SpecificationBuilderTests 8 | { 9 | public record Customer(int Id, string Name); 10 | 11 | [Fact] 12 | public void Add() 13 | { 14 | var spec1 = new Specification(); 15 | var spec2 = new Specification(); 16 | spec1.Query.Add(1, "test"); 17 | spec2.Add(1, "test"); 18 | 19 | AssertEquals(spec1, spec2); 20 | 21 | var spec3 = new Specification(); 22 | var spec4 = new Specification(); 23 | spec3.Query.Add(1, "test"); 24 | spec4.Add(1, "test"); 25 | 26 | AssertEquals(spec1, spec2); 27 | } 28 | 29 | [Fact] 30 | public void AddOrUpdate() 31 | { 32 | var spec1 = new Specification(); 33 | var spec2 = new Specification(); 34 | spec1.Query.AddOrUpdate(1, "test"); 35 | spec2.AddOrUpdate(1, "test"); 36 | 37 | AssertEquals(spec1, spec2); 38 | 39 | var spec3 = new Specification(); 40 | var spec4 = new Specification(); 41 | spec3.Query.AddOrUpdate(1, "test"); 42 | spec4.AddOrUpdate(1, "test"); 43 | 44 | AssertEquals(spec1, spec2); 45 | } 46 | 47 | private static void AssertEquals(Specification spec1, Specification spec2) 48 | { 49 | var items1 = Accessors.Items(spec1); 50 | var items2 = Accessors.Items(spec2); 51 | items1.Should().Equal(items2); 52 | } 53 | 54 | private class Accessors 55 | { 56 | [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")] 57 | public static extern ref SpecItem[]? Items(Specification @this); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Evaluators/OrderEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | public class OrderEvaluatorTests 4 | { 5 | private static readonly OrderEvaluator _evaluator = OrderEvaluator.Instance; 6 | 7 | public record Customer(int Id, string? Name = null); 8 | 9 | [Fact] 10 | public void OrdersItemsAscending_GivenOrderBy() 11 | { 12 | List input = [new(3), new(1), new(2), new(5), new(4)]; 13 | List expected = [new(1), new(2), new(3), new(4), new(5)]; 14 | 15 | var spec = new Specification(); 16 | spec.Query 17 | .OrderBy(x => x.Id); 18 | 19 | Assert(spec, input, expected); 20 | } 21 | 22 | [Fact] 23 | public void OrdersItemsDescending_GivenOrderByDescending() 24 | { 25 | List input = [new(3), new(1), new(2), new(5), new(4)]; 26 | List expected = [new(5), new(4), new(3), new(2), new(1)]; 27 | 28 | var spec = new Specification(); 29 | spec.Query 30 | .OrderByDescending(x => x.Id); 31 | 32 | Assert(spec, input, expected); 33 | } 34 | 35 | [Fact] 36 | public void OrdersItems_GivenOrderByThenBy() 37 | { 38 | List input = [new(3, "c"), new(1, "b"), new(1, "a")]; 39 | List expected = [new(1, "a"), new(1, "b"), new(3, "c")]; 40 | 41 | var spec = new Specification(); 42 | spec.Query 43 | .OrderBy(x => x.Id) 44 | .ThenBy(x => x.Name); 45 | 46 | Assert(spec, input, expected); 47 | } 48 | 49 | [Fact] 50 | public void OrdersItems_GivenOrderByThenByDescending() 51 | { 52 | List input = [new(3, "c"), new(1, "a"), new(1, "b")]; 53 | List expected = [new(1, "b"), new(1, "a"), new(3, "c")]; 54 | 55 | var spec = new Specification(); 56 | spec.Query 57 | .OrderBy(x => x.Id) 58 | .ThenByDescending(x => x.Name); 59 | 60 | Assert(spec, input, expected); 61 | } 62 | 63 | [Fact] 64 | public void OrdersItems_GivenOrderByDescendingThenBy() 65 | { 66 | List input = [new(1, "b"), new(1, "a"), new(3, "c")]; 67 | List expected = [new(3, "c"), new(1, "a"), new(1, "b")]; 68 | 69 | var spec = new Specification(); 70 | spec.Query 71 | .OrderByDescending(x => x.Id) 72 | .ThenBy(x => x.Name); 73 | 74 | Assert(spec, input, expected); 75 | } 76 | 77 | [Fact] 78 | public void OrdersItems_GivenOrderByDescendingThenByDescending() 79 | { 80 | List input = [new(1, "a"), new(1, "b"), new(3, "c")]; 81 | List expected = [new(3, "c"), new(1, "b"), new(1, "a")]; 82 | 83 | var spec = new Specification(); 84 | spec.Query 85 | .OrderByDescending(x => x.Id) 86 | .ThenByDescending(x => x.Name); 87 | 88 | Assert(spec, input, expected); 89 | } 90 | 91 | [Fact] 92 | public void DoesNotOrder_GivenNoOrder() 93 | { 94 | List input = [new(3), new(1), new(2), new(5), new(4)]; 95 | List expected = [new(3), new(1), new(2), new(5), new(4)]; 96 | var spec = new Specification(); 97 | 98 | Assert(spec, input, expected); 99 | } 100 | 101 | private static void Assert(Specification spec, List input, List expected) where T : class 102 | { 103 | var actualForIEnumerable = _evaluator.Evaluate(input, spec); 104 | actualForIEnumerable.Should().NotBeNull(); 105 | actualForIEnumerable.Should().Equal(expected); 106 | 107 | var actualForIQueryable = _evaluator.Evaluate(input.AsQueryable(), spec); 108 | actualForIQueryable.Should().NotBeNull(); 109 | actualForIQueryable.Should().Equal(expected); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Evaluators/PaginationExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | public class PaginationExtensionsTests 4 | { 5 | public record Customer(int Id); 6 | 7 | [Fact] 8 | public void Filters_GivenPaginatedSpec() 9 | { 10 | List input = [new(1), new(2), new(3), new(4), new(5)]; 11 | List expected = [new(3), new(4)]; 12 | 13 | var spec = new Specification(); 14 | spec.Query 15 | .Skip(2) 16 | .Take(2); 17 | 18 | var actual = input.ApplyPaging(spec); 19 | actual.Should().Equal(expected); 20 | 21 | actual = input.AsQueryable().ApplyPaging(spec); 22 | actual.Should().Equal(expected); 23 | 24 | var pagination = new Pagination(input.Count, 2, 2); 25 | actual = input.AsQueryable().ApplyPaging(pagination); 26 | actual.Should().Equal(expected); 27 | } 28 | 29 | [Fact] 30 | public void Filters_GivenPaginatedSpecWithSelect() 31 | { 32 | List input = [new(1), new(2), new(3), new(4), new(5)]; 33 | List expected = [new(3), new(4)]; 34 | 35 | var spec = new Specification(); 36 | spec.Query 37 | .Skip(2) 38 | .Take(2) 39 | .Select(x => x); 40 | 41 | var actual = input.ApplyPaging(spec); 42 | actual.Should().Equal(expected); 43 | 44 | actual = input.AsQueryable().ApplyPaging(spec); 45 | actual.Should().Equal(expected); 46 | } 47 | 48 | [Fact] 49 | public void DoesNotFilter_GivenEmptySpec() 50 | { 51 | List input = [new(1), new(2), new(3), new(4), new(5)]; 52 | List expected = [new(1), new(2), new(3), new(4), new(5)]; 53 | 54 | var spec = new Specification(); 55 | 56 | var actual = input.ApplyPaging(spec); 57 | actual.Should().Equal(expected); 58 | 59 | actual = input.AsQueryable().ApplyPaging(spec); 60 | actual.Should().Equal(expected); 61 | } 62 | 63 | [Fact] 64 | public void DoesNotFilter_GivenSpecWithSelectAndNoPagination() 65 | { 66 | List input = [new(1), new(2), new(3), new(4), new(5)]; 67 | List expected = [new(1), new(2), new(3), new(4), new(5)]; 68 | 69 | var spec = new Specification(); 70 | spec.Query 71 | .Select(x => x); 72 | 73 | var actual = input.ApplyPaging(spec); 74 | actual.Should().Equal(expected); 75 | 76 | actual = input.AsQueryable().ApplyPaging(spec); 77 | actual.Should().Equal(expected); 78 | } 79 | 80 | [Fact] 81 | public void DoesNotFilter_GivenNegativeTakeSkip() 82 | { 83 | List input = [new(1), new(2), new(3), new(4), new(5)]; 84 | List expected = [new(1), new(2), new(3), new(4), new(5)]; 85 | 86 | var spec = new Specification(); 87 | spec.Query 88 | .Skip(-1) 89 | .Take(-1); 90 | 91 | var actual = input.ApplyPaging(spec); 92 | actual.Should().Equal(expected); 93 | 94 | actual = input.AsQueryable().ApplyPaging(spec); 95 | actual.Should().Equal(expected); 96 | } 97 | 98 | [Fact] 99 | public void DoesNotFilter_GivenZeroSkip() 100 | { 101 | List input = [new(1), new(2), new(3), new(4), new(5)]; 102 | List expected = [new(1), new(2), new(3), new(4), new(5)]; 103 | 104 | var spec = new Specification(); 105 | spec.Query 106 | .Skip(0); 107 | 108 | var actual = input.ApplyPaging(spec); 109 | actual.Should().Equal(expected); 110 | 111 | actual = input.AsQueryable().ApplyPaging(spec); 112 | actual.Should().Equal(expected); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Evaluators/WhereEvaluatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Evaluators; 2 | 3 | public class WhereEvaluatorTests 4 | { 5 | private static readonly WhereEvaluator _evaluator = WhereEvaluator.Instance; 6 | 7 | public record Customer(int Id); 8 | 9 | [Fact] 10 | public void Filters_GivenWhereExpression() 11 | { 12 | List input = [new(1), new(2), new(3), new(4), new(5)]; 13 | List expected = [new(4), new(5)]; 14 | 15 | var spec = new Specification(); 16 | spec.Query 17 | .Where(x => x.Id > 3); 18 | 19 | Assert(spec, input, expected); 20 | } 21 | 22 | [Fact] 23 | public void DoesNotFilter_GivenNoWhereExpression() 24 | { 25 | List input = [new(1), new(2), new(3), new(4), new(5)]; 26 | List expected = [new(1), new(2), new(3), new(4), new(5)]; 27 | 28 | var spec = new Specification(); 29 | 30 | Assert(spec, input, expected); 31 | } 32 | 33 | private static void Assert(Specification spec, List input, List expected) where T : class 34 | { 35 | var actualForIEnumerable = _evaluator.Evaluate(input, spec); 36 | actualForIEnumerable.Should().NotBeNull(); 37 | actualForIEnumerable.Should().Equal(expected); 38 | 39 | var actualForIQueryable = _evaluator.Evaluate(input.AsQueryable(), spec); 40 | actualForIQueryable.Should().NotBeNull(); 41 | actualForIQueryable.Should().Equal(expected); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Exceptions/ConcurrentSelectorsExceptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Exceptions; 2 | 3 | public class ConcurrentSelectorsExceptionTests 4 | { 5 | private const string _defaultMessage = "Concurrent specification selector transforms defined. Ensure only one of the Select() or SelectMany() transforms is used in the same specification!"; 6 | 7 | [Fact] 8 | public void ThrowWithDefaultConstructor() 9 | { 10 | Action sut = () => throw new ConcurrentSelectorsException(); 11 | 12 | sut.Should().Throw() 13 | .WithMessage(_defaultMessage); 14 | } 15 | 16 | [Fact] 17 | public void ThrowWithInnerException() 18 | { 19 | var inner = new Exception("test"); 20 | Action sut = () => throw new ConcurrentSelectorsException(inner); 21 | 22 | sut.Should().Throw() 23 | .WithMessage(_defaultMessage) 24 | .WithInnerException() 25 | .WithMessage("test"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Exceptions/EntityNotFoundExceptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Exceptions; 2 | 3 | public class EntityNotFoundExceptionTests 4 | { 5 | [Fact] 6 | public void ThrowWithDefaultConstructor() 7 | { 8 | var message = "The queried entity was not found!"; 9 | Action sut = () => throw new EntityNotFoundException(); 10 | 11 | sut.Should().Throw() 12 | .WithMessage(message); 13 | } 14 | 15 | [Fact] 16 | public void ThrowWithParameterConstructor() 17 | { 18 | var entityName = "test"; 19 | var message = $"The queried entity: test was not found!"; 20 | 21 | Action sut = () => throw new EntityNotFoundException(entityName); 22 | 23 | sut.Should().Throw() 24 | .WithMessage(message); 25 | } 26 | 27 | [Fact] 28 | public void ThrowWithInnerException() 29 | { 30 | var inner = new Exception("test"); 31 | var entityName = "test"; 32 | var message = $"The queried entity: test was not found!"; 33 | 34 | Action sut = () => throw new EntityNotFoundException(entityName, inner); 35 | 36 | sut.Should().Throw() 37 | .WithMessage(message) 38 | .WithInnerException() 39 | .WithMessage("test"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Exceptions/InvalidLikePatternExceptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Exceptions; 2 | 3 | public class InvalidLikePatternExceptionTests 4 | { 5 | private const string _defaultMessage = "Invalid like pattern: " + _pattern; 6 | private const string _pattern = "x"; 7 | 8 | [Fact] 9 | public void ThrowWithDefaultConstructor() 10 | { 11 | Action sut = () => throw new InvalidLikePatternException(_pattern); 12 | 13 | sut.Should().Throw() 14 | .WithMessage(_defaultMessage); 15 | } 16 | 17 | [Fact] 18 | public void ThrowWithInnerException() 19 | { 20 | var inner = new Exception("test"); 21 | Action sut = () => throw new InvalidLikePatternException(_pattern, inner); 22 | 23 | sut.Should().Throw() 24 | .WithMessage(_defaultMessage) 25 | .WithInnerException() 26 | .WithMessage("test"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Exceptions/SelectorNotFoundExceptionTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Exceptions; 2 | 3 | public class SelectorNotFoundExceptionTests 4 | { 5 | private const string _defaultMessage = "The specification must have a selector transform defined. Ensure either Select() or SelectMany() is used in the specification!"; 6 | 7 | [Fact] 8 | public void ThrowWithDefaultConstructor() 9 | { 10 | Action sut = () => throw new SelectorNotFoundException(); 11 | 12 | sut.Should().Throw() 13 | .WithMessage(_defaultMessage); 14 | } 15 | 16 | [Fact] 17 | public void ThrowWithInnerException() 18 | { 19 | var inner = new Exception("test"); 20 | Action sut = () => throw new SelectorNotFoundException(inner); 21 | 22 | sut.Should().Throw() 23 | .WithMessage(_defaultMessage) 24 | .WithInnerException() 25 | .WithMessage("test"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using FluentAssertions; 2 | global using Pozitron.QuerySpecification; 3 | global using System.Linq.Expressions; 4 | global using Xunit; 5 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Paging/PagedResultTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Paging; 2 | 3 | public class PagedResultTests 4 | { 5 | [Fact] 6 | public void Constructor_SetDataAndPagination() 7 | { 8 | var data = new List { 1, 2, 3 }; 9 | var pagination = new Pagination(1, 10, 3); 10 | 11 | var pagedResult = new PagedResult(data, pagination); 12 | 13 | pagedResult.Data.Should().Equal(data); 14 | pagedResult.Pagination.Should().Be(pagination); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Paging/PaginationSettingsTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Paging; 2 | 3 | public class PaginationSettingsTests 4 | { 5 | [Fact] 6 | public void Default_Values() 7 | { 8 | var settings = PaginationSettings.Default; 9 | 10 | settings.DefaultPage.Should().Be(1); 11 | settings.DefaultPageSize.Should().Be(10); 12 | settings.DefaultPageSizeLimit.Should().Be(50); 13 | } 14 | 15 | [Fact] 16 | public void Custom_Values() 17 | { 18 | var settings = new PaginationSettings(5, 100); 19 | 20 | settings.DefaultPage.Should().Be(1); 21 | settings.DefaultPageSize.Should().Be(5); 22 | settings.DefaultPageSizeLimit.Should().Be(100); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/QuerySpecification.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tests/QuerySpecification.Tests/Validators/WhereValidatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace Tests.Validators; 2 | 3 | public class WhereValidatorTests 4 | { 5 | private static readonly WhereValidator _validator = WhereValidator.Instance; 6 | 7 | public record Customer(int Id, string Name); 8 | 9 | [Fact] 10 | public void ReturnsTrue_GivenEmptySpec() 11 | { 12 | var customer = new Customer(1, "Customer1"); 13 | 14 | var spec = new Specification(); 15 | 16 | var result = _validator.IsValid(customer, spec); 17 | 18 | result.Should().BeTrue(); 19 | } 20 | 21 | [Fact] 22 | public void ReturnsTrue_GivenSpecWithSingleWhere_WithValidEntity() 23 | { 24 | var customer = new Customer(1, "Customer1"); 25 | 26 | var spec = new Specification(); 27 | spec.Query 28 | .Where(x => x.Id == 1); 29 | 30 | var result = _validator.IsValid(customer, spec); 31 | 32 | result.Should().BeTrue(); 33 | } 34 | 35 | [Fact] 36 | public void ReturnsFalse_GivenSpecWithSingleWhere_WithInvalidEntity() 37 | { 38 | var customer = new Customer(1, "Customer1"); 39 | 40 | var spec = new Specification(); 41 | spec.Query 42 | .Where(x => x.Id == 2); 43 | 44 | var result = _validator.IsValid(customer, spec); 45 | 46 | result.Should().BeFalse(); 47 | } 48 | 49 | [Fact] 50 | public void ReturnsTrue_GivenSpecWithMultipleWhere_WithValidEntity() 51 | { 52 | var customer = new Customer(1, "Customer1"); 53 | 54 | var spec = new Specification(); 55 | spec.Query 56 | .Where(x => x.Id == 1) 57 | .Where(x => x.Name == "Customer1"); 58 | 59 | var result = _validator.IsValid(customer, spec); 60 | 61 | result.Should().BeTrue(); 62 | } 63 | 64 | [Fact] 65 | public void ReturnsFalse_GivenSpecWithMultipleWhere_WithSingleInvalidValue() 66 | { 67 | var customer = new Customer(1, "Customer1"); 68 | 69 | var spec = new Specification(); 70 | spec.Query 71 | .Where(x => x.Id == 2) 72 | .Where(x => x.Name == "Customer1"); 73 | 74 | var result = _validator.IsValid(customer, spec); 75 | 76 | result.Should().BeFalse(); 77 | } 78 | 79 | [Fact] 80 | public void ReturnsFalse_GivenSpecWithMultipleWhere_WithAllInvalidValues() 81 | { 82 | var customer = new Customer(1, "Customer1"); 83 | 84 | var spec = new Specification(); 85 | spec.Query 86 | .Where(x => x.Id == 2) 87 | .Where(x => x.Name == "Customer2"); 88 | 89 | var result = _validator.IsValid(customer, spec); 90 | 91 | result.Should().BeFalse(); 92 | } 93 | } 94 | --------------------------------------------------------------------------------