Repository: ClaveConsulting/Expressionify Branch: master Commit: 72aa27b5f723 Files: 59 Total size: 120.4 KB Directory structure: gitextract_kk0ht2wy/ ├── .github/ │ └── workflows/ │ ├── publish.yml │ ├── pull-request.yml │ └── test-report.yml ├── .gitignore ├── .vscode/ │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── CHANGELOG.md ├── Directory.build.targets ├── Expressionify.sln ├── License.md ├── Readme.md ├── git-conventional-commits.json ├── global.json ├── src/ │ ├── Clave.Expressionify/ │ │ ├── Clave.Expressionify.csproj │ │ ├── DbContextOptionsExtensions.cs │ │ ├── ExpressionEvaluationMode.cs │ │ ├── ExpressionableQuery.cs │ │ ├── ExpressionableQueryCompiler.cs │ │ ├── ExpressionableQueryProvider.cs │ │ ├── ExpressionifyAttribute.cs │ │ ├── ExpressionifyDbContextOptionsBuilder.cs │ │ ├── ExpressionifyDbContextOptionsExtension.cs │ │ ├── ExpressionifyExtension.cs │ │ ├── ExpressionifyQueryTranslationPreprocessor.cs │ │ ├── ExpressionifyQueryTranslationPreprocessorFactory.cs │ │ └── ExpressionifyVisitor.cs │ └── Clave.Expressionify.Generator/ │ ├── AnalyzerReleases.Shipped.md │ ├── AnalyzerReleases.Unshipped.md │ ├── Clave.Expressionify.Generator.csproj │ ├── ExpressionifyAnalyzer.cs │ ├── ExpressionifyCodeFixProvider.cs │ ├── ExpressionifySourceGenerator.cs │ ├── Extensions.cs │ ├── Internals/ │ │ ├── Checks.cs │ │ ├── ClassGenerator.cs │ │ ├── ExpressionRewriter.cs │ │ └── PropertyGenerator.cs │ ├── IsExternalInit.cs │ └── _._ └── tests/ ├── Clave.Expressionify.Generator.Tests/ │ ├── Clave.Expressionify.Generator.Tests.csproj │ ├── CodeFixTests.cs │ ├── CodeGeneratorTests.cs │ └── Verifiers/ │ └── CSharpSourceGeneratorVerifier.cs └── Clave.Expressionify.Tests/ ├── Clave.Expressionify.Tests.csproj ├── DbContextExtensions/ │ ├── TestDbContext.cs │ ├── TestEntity.cs │ ├── TestEntityExtensions.cs │ └── Tests.cs ├── First/ │ └── ExtensionMethods.cs ├── Samples/ │ ├── Class1.cs │ ├── Class2.cs │ ├── Class3.cs │ ├── Class4.cs │ ├── GenericClass.cs │ ├── IThing.cs │ └── Record1.cs ├── Second/ │ └── ExtensionMethods.cs └── Tests.cs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish on: push: branches: [master] jobs: build: runs-on: ubuntu-latest steps: # Checkout all commits, so we get previous tags - uses: actions/checkout@v4 with: fetch-depth: 0 # This is needed so that we can create git tags - name: Set git user run: | git config --local user.name "ci-bot" git config --local user.email "github@clave.no" - uses: actions/setup-node@v4 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' - name: Get version id: version run: echo "version=$(npx -q git-conventional-commits version)" >> $GITHUB_OUTPUT - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 9.0.x - name: Restore dependencies run: dotnet restore - name: Build env: VERSION: ${{ steps.version.outputs.version }} run: dotnet build --no-restore --configuration release -p:Version=${{ env.VERSION }} - name: Test run: dotnet test --no-build --configuration release --logger "trx;LogFileName=test-results.trx" - name: Report test results uses: dorny/test-reporter@v1 if: success() || failure() # run this step even if previous step failed with: name: Tests path: '**/test-results.trx' reporter: dotnet-trx fail-on-error: 'false' - name: Changelog run: npx -q git-conventional-commits changelog --file CHANGELOG.md - name: Pack env: VERSION: ${{ steps.version.outputs.version }} run: dotnet pack --no-build --include-symbols --configuration release -p:PackageVersion=${{ env.VERSION }} --output ./nugets - name: Push run: dotnet nuget push "**/*.nupkg" --api-key ${{ secrets.NUGET }} --source https://api.nuget.org/v3/index.json --skip-duplicate - name: Tag env: VERSION: ${{ steps.version.outputs.version }} run: | git commit -am "Created release v${{ env.VERSION }}" git tag "v${{ env.VERSION }}" git push origin "v${{ env.VERSION }}" git push origin ================================================ FILE: .github/workflows/pull-request.yml ================================================ name: Check Pull-request on: pull_request_target jobs: version: name: Get version outputs: version: ${{ steps.version.outputs.version }} changelog: ${{ steps.changelog.outputs.changelog }} runs-on: ubuntu-latest steps: # Checkout all commits, so we get previous tags # The ref and repository is needed since we use on: pull_request_target - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} #repository: ${{ github.event.pull_request.head.repo.full_name }} fetch-depth: 0 fetch-tags: true - uses: actions/setup-node@v4 with: node-version: '20.x' registry-url: 'https://registry.npmjs.org' - name: Get version id: version env: PR_REF: ${{ github.event.pull_request.head.ref }} run: echo "version=$(npx -q git-conventional-commits version)-${PR_REF}.${{ github.run_number }}" >> $GITHUB_OUTPUT # Generate changelog - name: Get the changelog id: conventional-commits run: npx -q git-conventional-commits changelog --file 'temp_changelog.md' - name: Output changelog id: changelog run: | echo 'changelog<> $GITHUB_OUTPUT cat temp_changelog.md >> $GITHUB_OUTPUT echo 'EOF' >> $GITHUB_OUTPUT test: name: Run unit tests runs-on: ubuntu-latest needs: version steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Setup .NET uses: actions/setup-dotnet@v4 with: global-json-file: global.json dotnet-version: 6.0.x - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --no-restore --configuration release -p:Version=${{ needs.version.outputs.version }} - name: Test run: dotnet test --no-build --configuration release --logger "trx;LogFileName=test-results.trx" - name: Pack run: dotnet pack --no-build --include-symbols --configuration release -p:PackageVersion=${{ needs.version.outputs.version }} --output ./nugets - name: Upload test-results uses: actions/upload-artifact@v4 if: success() || failure() # run this step even if previous step failed with: name: test-results path: '**/test-results.trx' - name: Upload package uses: actions/upload-artifact@v4 with: name: package path: '**/*.nupkg' comment: name: Comment on pull-request runs-on: ubuntu-latest needs: version steps: # Create a comment in the pull request using the tags created - uses: marocchino/sticky-pull-request-comment@v1 if: needs.version.outputs.changelog != '' with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} message: | The following versions will be created when this pull-request is merged: ${{ needs.version.outputs.changelog }} --- Pre-release package `${{ needs.version.outputs.version }}` can be pushed to nuget - uses: marocchino/sticky-pull-request-comment@v1 if: needs.version.outputs.changelog == '' with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} message: | No packages will be created when this pull-request is merged publish: name: Publish pre-release runs-on: ubuntu-latest environment: pre-release needs: test timeout-minutes: 30 steps: - name: Download artifact uses: actions/download-artifact@v4 with: name: package path: './nuget' - name: Push run: dotnet nuget push "**/*.nupkg" --api-key ${{ secrets.NUGET }} --source https://api.nuget.org/v3/index.json --skip-duplicate ================================================ FILE: .github/workflows/test-report.yml ================================================ name: 'Test Report' on: workflow_run: workflows: ['Check Pull-request'] types: - completed jobs: report: runs-on: ubuntu-latest steps: - uses: dorny/test-reporter@v1 with: artifact: test-results # artifact name name: Tests # Name of the check run which will be created path: '**/*.trx' # Path to test results (inside artifact .zip) reporter: dotnet-trx # Format of test results fail-on-error: 'false' ================================================ FILE: .gitignore ================================================ bin/ obj/ /output /nugets TestResults/ /.vs *.user /temp-changelog.md ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to find out which attributes exist for C# debugging // Use hover for the description of the existing attributes // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md "version": "0.2.0", "configurations": [ { "name": ".NET Core Launch (console)", "type": "coreclr", "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. "program": "${workspaceFolder}/src/Clave.Expressionify.Tasks/bin/Debug/netcoreapp5.0/Clave.Expressionify.Tasks.dll", "args": [], "cwd": "${workspaceFolder}/src/Clave.Expressionify.Tasks", // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console "console": "internalConsole", "stopAtEntry": false }, { "name": ".NET Core Attach", "type": "coreclr", "request": "attach", "processId": "${command:pickProcess}" } ] } ================================================ FILE: .vscode/settings.json ================================================ { "dotnet-test-explorer.testProjectPath": "**/*.Tests.csproj", "editor.codeLens": true, "csharp.testsCodeLens.enabled": true, "dotnet-test-explorer.showCodeLens": true, "editor.formatOnSave": true, "editor.tabSize": 4, "editor.detectIndentation": false, "dotnet-test-explorer.autoWatch": false, "dotnet-test-explorer.addProblems": true, "dotnet-test-explorer.treeMode": "flat" } ================================================ FILE: .vscode/tasks.json ================================================ { "version": "2.0.0", "tasks": [ { "label": "build", "command": "dotnet", "type": "process", "args": [ "build", "${workspaceFolder}/src/Clave.Expressionify.Tasks/Clave.Expressionify.Tasks.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" }, { "label": "publish", "command": "dotnet", "type": "process", "args": [ "publish", "${workspaceFolder}/src/Clave.Expressionify.Tasks/Clave.Expressionify.Tasks.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" }, { "label": "watch", "command": "dotnet", "type": "process", "args": [ "watch", "run", "${workspaceFolder}/src/Clave.Expressionify.Tasks/Clave.Expressionify.Tasks.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" } ] } ================================================ FILE: CHANGELOG.md ================================================ ## **10.0.0** 2025-11-19 ([ba0427f...ba0427f](https://github.com/ClaveConsulting/Expressionify/compare/ba0427ffd6c2453538092f8ea503ba8af9499ba4...ba0427ffd6c2453538092f8ea503ba8af9499ba4?diff=split)) ### Features - migrate to EF Core 10 and \.NET 10 \(\#40\) ([ba0427f](https://github.com/ClaveConsulting/Expressionify/commit/ba0427ffd6c2453538092f8ea503ba8af9499ba4)) ### BREAKING CHANGES - Upgrade to EF Core 10 and \.NET 10 ([ba0427f](https://github.com/ClaveConsulting/Expressionify/commit/ba0427ffd6c2453538092f8ea503ba8af9499ba4)) This is a major version upgrade that requires: \- \.NET 10 SDK \- EF Core 10 packages \- Updated parameter naming in generated SQL \(@\_\_p\_0 \-\> @p\) Core Changes: \- Upgrade Microsoft\.EntityFrameworkCore from 9\.0\.0 to 10\.0\.0 \- Upgrade to \.NET 10 SDK and target framework \(net10\.0\) \- Replace ParameterExtractingExpressionVisitor with ExpressionTreeFuncletizer \- Remove IParameterValues interface \(replaced with Dictionary\) \- Delete ParameterExtractingExpressionVisitor\.cs \(~700 lines of internal EF Core code\) \- Update parameter detection strategy to check dictionary count after funcletization \- Update test expectations for simplified parameter naming \- Remove obsolete test \(EF Core 10 optimizer handles constant folding\) All 51 tests passing\. Co\-authored\-by: Fabien Molinet
## **9.1.0** 2025-03-21 ([579137a...579137a](https://github.com/ClaveConsulting/Expressionify/compare/579137a7550db48071b2a568718011031d3a3244...579137a7550db48071b2a568718011031d3a3244?diff=split)) ### Features - handle nullable propagation expression in arguments ([579137a](https://github.com/ClaveConsulting/Expressionify/commit/579137a7550db48071b2a568718011031d3a3244))
## **9.0.1** 2025-03-21 ([a928912...a928912](https://github.com/ClaveConsulting/Expressionify/compare/a928912b9559dc3963cb1f2be47abf196b7ea991...a928912b9559dc3963cb1f2be47abf196b7ea991?diff=split)) *no relevant changes*
## **9.0.0** 2024-12-27 ([0a25faa...ab25144](https://github.com/ClaveConsulting/Expressionify/compare/0a25faa81d7fef929450b4548bef80c7784b7869...ab251447ae87ffcca3c4c8d9f7d0492a63dcb604?diff=split)) ### BREAKING CHANGES - supports EF9 ([ab25144](https://github.com/ClaveConsulting/Expressionify/commit/ab251447ae87ffcca3c4c8d9f7d0492a63dcb604)) \-\-\-\-\-\-\-\-\- Co\-authored\-by: Fabien Molinet
## **6.7.1** 2024-10-31 ([ad5c447...ddc49b2](https://github.com/ClaveConsulting/Expressionify/compare/ad5c4470f9eb8bc91284c556f719f01b6d0dab49...ddc49b22ebb0feeb77c6c4c7b460117a4a33ef74?diff=split)) *no relevant changes*
## **6.7.0** 2022-12-14 ([50641f0...3fae05d](https://github.com/ClaveConsulting/Expressionify/compare/50641f0924d179f8c6cceb0ab1c1eea473ac9428...3fae05d19585f2ffa7b23d533c4ab16d98a61f10?diff=split)) ### Features - Implemented generic methods ([50641f0](https://github.com/ClaveConsulting/Expressionify/commit/50641f0924d179f8c6cceb0ab1c1eea473ac9428)) - Generic classes can contain expressionify methods ([3fae05d](https://github.com/ClaveConsulting/Expressionify/commit/3fae05d19585f2ffa7b23d533c4ab16d98a61f10))
## **6.6.4** 2024-11-11 ([2658a0f...2658a0f](https://github.com/ClaveConsulting/Expressionify/compare/2658a0f86c3062e60e2391e43e25fcd690bbfe4f...2658a0f86c3062e60e2391e43e25fcd690bbfe4f?diff=split)) *no relevant changes*
## **6.6.3** 2023-07-16 ([4c34cb9...4c34cb9](https://github.com/ClaveConsulting/Expressionify/compare/4c34cb964e517ec5609cc820d969011c7359c447...4c34cb964e517ec5609cc820d969011c7359c447?diff=split)) *no relevant changes*
## **6.6.2** 2022-12-14 ([174ff0d...753ab7a](https://github.com/ClaveConsulting/Expressionify/compare/174ff0d...753ab7a?diff=split)) *no relevant changes* ## **6.6.1** 2022-12-12 ([6ae459e...2f5c15f](https://github.com/ClaveConsulting/Expressionify/compare/6ae459e...2f5c15f?diff=split)) ### Bug Fixes * Generated source files end in \.g\.cs ([6ae459e](https://github.com/ClaveConsulting/Expressionify/commit/6ae459e)) * tests ([2f5c15f](https://github.com/ClaveConsulting/Expressionify/commit/2f5c15f)) ### ??? * Replacing Environment\.NewLine with actual new lines to make Git handle line ending differences between platforms\. Ref https://github\.com/dotnet/roslyn/issues/51437\#issuecomment\-784750434 ([aa1a1b5](https://github.com/ClaveConsulting/Expressionify/commit/aa1a1b5)) ## **6.6.0** 2022-11-04 ([e0c50b9...118d2eb](https://github.com/ClaveConsulting/Expressionify/compare/e0c50b9...118d2eb?diff=split)) ### Features * Added support for the EF compiled query cachen when using \.UseExpressionify\(\) ([e0c50b9](https://github.com/ClaveConsulting/Expressionify/commit/e0c50b9)) ### Bug Fixes * Mark Generator as development dependency ([d8650c2](https://github.com/ClaveConsulting/Expressionify/commit/d8650c2)) * Fixed failing tests and an outdated exception message ([1a82a24](https://github.com/ClaveConsulting/Expressionify/commit/1a82a24)) * Running tests against the pull request, not the target ([118d2eb](https://github.com/ClaveConsulting/Expressionify/commit/118d2eb)) ### ??? * Use result of ParameterExtractingExpressionVisitor ([6de12cc](https://github.com/ClaveConsulting/Expressionify/commit/6de12cc)) * Default query caching ([c56b4d3](https://github.com/ClaveConsulting/Expressionify/commit/c56b4d3)) * Renamed ExpressionEvaluationMode enums ([07f00a8](https://github.com/ClaveConsulting/Expressionify/commit/07f00a8)) ## **6.5.0** 2022-04-29 ([b8b62c1...b8b62c1](https://github.com/ClaveConsulting/Expressionify/compare/b8b62c1...b8b62c1?diff=split)) ### Features * Use QueryCompiler to support Include\(\) ([b8b62c1](https://github.com/ClaveConsulting/Expressionify/commit/b8b62c1)) ## **6.4.1** 2022-04-29 ([993a4c7...7e03b8c](https://github.com/ClaveConsulting/Expressionify/compare/993a4c7...7e03b8c?diff=split)) ### Bug Fixes * support all features that \.Expressionify\(\) does ([993a4c7](https://github.com/ClaveConsulting/Expressionify/commit/993a4c7)) ### ??? * doc: Improved readme ([9e1032a](https://github.com/ClaveConsulting/Expressionify/commit/9e1032a)) ## **6.4.0** 2022-04-29 ([c8a8dce...01bb336](https://github.com/ClaveConsulting/Expressionify/compare/c8a8dce...01bb336?diff=split)) ### Features * Added \.UseExpressionify\(\) on DbContext\-Configuration, removing the need to always call \.Expressionify\(\) on each query ([c8a8dce](https://github.com/ClaveConsulting/Expressionify/commit/c8a8dce)) ### ??? * Merge pull request \#14 from jhartmann123/db\-context\-options ([cd6b113](https://github.com/ClaveConsulting/Expressionify/commit/cd6b113)) * run on ubuntu ([ba62561](https://github.com/ClaveConsulting/Expressionify/commit/ba62561)) * Updated dependencies and inlined the test\-report ([fc8a819](https://github.com/ClaveConsulting/Expressionify/commit/fc8a819)) * Fixed linefeed difference between windows and linux ([69e8a80](https://github.com/ClaveConsulting/Expressionify/commit/69e8a80)) * other way around ([caf6cd6](https://github.com/ClaveConsulting/Expressionify/commit/caf6cd6)) * Fixed failed build ([01bb336](https://github.com/ClaveConsulting/Expressionify/commit/01bb336)) ## **6.4.0** 2022-04-21 ([c8a8dce...c8a8dce](https://github.com/ClaveConsulting/Expressionify/compare/c8a8dce...c8a8dce?diff=split)) ### Features * Added \.UseExpressionify\(\) on DbContext\-Configuration, removing the need to always call \.Expressionify\(\) on each query ([c8a8dce](https://github.com/ClaveConsulting/Expressionify/commit/c8a8dce)) ================================================ FILE: Directory.build.targets ================================================ ================================================ FILE: Expressionify.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.32112.339 MinimumVisualStudioVersion = 15.0.26124.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3B1DEF26-D170-4DCD-8B0D-36D845932167}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Clave.Expressionify", "src\Clave.Expressionify\Clave.Expressionify.csproj", "{8D60D663-B6FF-4158-AE86-E352756D001C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{4CD5351B-51D6-421B-889F-88576FA41B09}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Clave.Expressionify.Tests", "tests\Clave.Expressionify.Tests\Clave.Expressionify.Tests.csproj", "{E4AA7774-4C03-4883-B51A-F9171D4F27F0}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{942AA318-76C0-464F-9ECA-51F55157A1F5}" ProjectSection(SolutionItems) = preProject Directory.build.targets = Directory.build.targets git-conventional-commits.json = git-conventional-commits.json .github\workflows\publish.yml = .github\workflows\publish.yml Readme.md = Readme.md .github\workflows\pull-request.yml = .github\workflows\pull-request.yml .github\workflows\test-report.yml = .github\workflows\test-report.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Clave.Expressionify.Generator", "src\Clave.Expressionify.Generator\Clave.Expressionify.Generator.csproj", "{4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Clave.Expressionify.Generator.Tests", "tests\Clave.Expressionify.Generator.Tests\Clave.Expressionify.Generator.Tests.csproj", "{1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {8D60D663-B6FF-4158-AE86-E352756D001C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8D60D663-B6FF-4158-AE86-E352756D001C}.Debug|Any CPU.Build.0 = Debug|Any CPU {8D60D663-B6FF-4158-AE86-E352756D001C}.Debug|x64.ActiveCfg = Debug|Any CPU {8D60D663-B6FF-4158-AE86-E352756D001C}.Debug|x64.Build.0 = Debug|Any CPU {8D60D663-B6FF-4158-AE86-E352756D001C}.Debug|x86.ActiveCfg = Debug|Any CPU {8D60D663-B6FF-4158-AE86-E352756D001C}.Debug|x86.Build.0 = Debug|Any CPU {8D60D663-B6FF-4158-AE86-E352756D001C}.Release|Any CPU.ActiveCfg = Release|Any CPU {8D60D663-B6FF-4158-AE86-E352756D001C}.Release|Any CPU.Build.0 = Release|Any CPU {8D60D663-B6FF-4158-AE86-E352756D001C}.Release|x64.ActiveCfg = Release|Any CPU {8D60D663-B6FF-4158-AE86-E352756D001C}.Release|x64.Build.0 = Release|Any CPU {8D60D663-B6FF-4158-AE86-E352756D001C}.Release|x86.ActiveCfg = Release|Any CPU {8D60D663-B6FF-4158-AE86-E352756D001C}.Release|x86.Build.0 = Release|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Debug|x64.ActiveCfg = Debug|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Debug|x64.Build.0 = Debug|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Debug|x86.ActiveCfg = Debug|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Debug|x86.Build.0 = Debug|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Release|Any CPU.ActiveCfg = Release|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Release|Any CPU.Build.0 = Release|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Release|x64.ActiveCfg = Release|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Release|x64.Build.0 = Release|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Release|x86.ActiveCfg = Release|Any CPU {E4AA7774-4C03-4883-B51A-F9171D4F27F0}.Release|x86.Build.0 = Release|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Debug|x64.ActiveCfg = Debug|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Debug|x64.Build.0 = Debug|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Debug|x86.ActiveCfg = Debug|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Debug|x86.Build.0 = Debug|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Release|Any CPU.Build.0 = Release|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Release|x64.ActiveCfg = Release|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Release|x64.Build.0 = Release|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Release|x86.ActiveCfg = Release|Any CPU {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6}.Release|x86.Build.0 = Release|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Debug|Any CPU.Build.0 = Debug|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Debug|x64.ActiveCfg = Debug|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Debug|x64.Build.0 = Debug|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Debug|x86.ActiveCfg = Debug|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Debug|x86.Build.0 = Debug|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Release|Any CPU.Build.0 = Release|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Release|x64.ActiveCfg = Release|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Release|x64.Build.0 = Release|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Release|x86.ActiveCfg = Release|Any CPU {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {8D60D663-B6FF-4158-AE86-E352756D001C} = {3B1DEF26-D170-4DCD-8B0D-36D845932167} {E4AA7774-4C03-4883-B51A-F9171D4F27F0} = {4CD5351B-51D6-421B-889F-88576FA41B09} {4AC8A74F-99FB-4396-B2FD-71B9BDD788D6} = {3B1DEF26-D170-4DCD-8B0D-36D845932167} {1A89B026-78F1-44AE-B0D3-2D6BEC732AFE} = {4CD5351B-51D6-421B-889F-88576FA41B09} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E0421317-E314-433D-A10E-9C19B5AF343C} EndGlobalSection EndGlobal ================================================ FILE: License.md ================================================ The MIT License Copyright (c) Clave Consulting Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Readme.md ================================================ # Expressionify [![Nuget](https://img.shields.io/nuget/v/Clave.Expressionify)][1] [![Nuget](https://img.shields.io/nuget/dt/Clave.Expressionify)][1] [![Build Status](https://claveconsulting.visualstudio.com/Nugets/_apis/build/status/ClaveConsulting.Expressionify?branchName=master)][2] [![Azure DevOps tests](https://img.shields.io/azure-devops/tests/ClaveConsulting/Nugets/14)][2] > Use extension methods in Entity Framework Core queries ## Installing Install these two nuget packages: * `Clave.Expressionify` * `Clave.Expressionify.Generator` Make sure to install the second one properly: ```xml runtime; build; native; contentfiles; analyzers; buildtransitive all ``` ## How to use 0) Setup your database context with `.UseExpressionify()` 1) Mark the `public static` expression method with the `[Expressionify]` attribute. 2) Mark the class with the method as `partial`. 3) Use the extension method in the query ## Example Lets say you have this code: ```csharp var users = await db.Users .Where(user => user.DateOfBirth < DateTime.Now.AddYears(-18)) .ToListAsync(); ``` That second line is a bit long, so it would be nice to pull it out as a reusable extension method: ```csharp public static class Extensions { public static bool IsOver18(this User user) => user.DateOfBirth < DateTime.Now.AddYears(-18); } // ... var users = await db.Users .Where(user => user.IsOver18()) .ToListAsync(); ``` Unfortunately this forces Entity Framework to run the query in memory, rather than in the database. That's not very efficient... But, with just one additional line of code we can get Entity Framework to understand how translate our extension method to SQL ```diff - public static class Extensions + public static partial class Extensions { + [Expressionify] public static bool IsOver18(this User user) => user.DateOfBirth < DateTime.Now.AddYears(-18); } ``` ## Setup The simplest way to add expressionify support is to configure the database context: ```csharp services .AddDbContext(o => o .UseSqlServer(configuration.GetConnectionString("DefaultConnection")) .UseExpressionify()); ``` Make sure to call `.UseExpressionify()` after `.UseSqlServer()` (or whatever other sql provider you want to use). The alternative is to only call `.Expressionify()` in the queries where you want it: ```csharp var users = await db.Users .Expressionify() .Where(user => user.DateOfBirth < DateTime.Now.AddYears(-18)) .ToListAsync(); ``` ### Query caching When configuring the DbContext with `.UseExpressionify()`, Expressionify tries to use the EntityFramework query cache by default. This way the expression tree is only processed once and then cached. However, this comes with [some limitations](#query-caching-limitations). Expressionify throws an exception if your query cannot be cached. To fix this, you either have to call `.Expressionify()` explicitly on the query or disable query caching: ```csharp .UseExpressionify(o => o.WithEvaluationMode(ExpressionEvaluationMode.FullCompatibilityButSlow)); ``` ## Upgrading from 3.1 to 5.0 Version 5 works with net 5.0, and has a few other changes. It relies on [Source generators](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/) and Roslyn Analyzers for generating the code, instead of some very clumpsy msbuild code. This means that you will get help if you forget to mark the methods correctly. These are the breaking changes: * The class containing the method no longer needs to be `static`. * The class containing the method now has to be marked as `partial`. * The method no longer needs to be `public`, it can be private or internal. ## Limitations Expressionify uses the Roslyn code analyzer and generator to look for `static` methods with expression bodies tagged with the `[Expressionify]` attribute in `partial` classes. ```csharp public static partial class Extensions { // ✔ OK [Expressionify] public static int ToInt(this string value) => Convert.ToInt32(value); // ✔ OK (it can be private) [Expressionify] private static int ToInt(this string value) => Convert.ToInt32(value); // ❌ Not ok (it's not static) [Expressionify] public int ToInt(this string value) => Convert.ToInt32(value); // ❌ Not ok (it's missing the attribute) public static int ToInt(this string value) => Convert.ToInt32(value); // ❌ Not ok (it doesn't have an expression body) [Expressionify] public static int ToInt(this string value) { return Convert.ToInt32(value); } } // ❌ Not ok (it's not a partial class) public static class Extensions { [Expressionify] public static int ToInt(this string value) => Convert.ToInt32(value); } ``` ### Query caching limitations Using [query caching](#query-caching) works fine unless you introduce new query parameters in your `[Expressionify]` method. In that case you'll get an `InvalidOperationException` telling you to explicitly call `.Expressionify()` on the query, as the query cannot be translated. Examples: ```csharp public static partial class Extensions { // Example: users.Where(u => u.IsOver18()) // ✔ OK for ExpressionEvaluationMode.FullCompatibilityButSlow // ✔ OK for ExpressionEvaluationMode.LimitedCompatibilityButCached // The expression can be translated to SQL without introducing new parameters [Expressionify] public static bool IsOver18(this User user) => user.DateOfBirth < DateTime.Now.AddYears(-18); // Example: users.Where(u => u.IsOlderThan(18)) // ✔ OK for ExpressionEvaluationMode.FullCompatibilityButSlow // ✔ OK for ExpressionEvaluationMode.LimitedCompatibilityButCached // The parameter 'years' is already present in the query itself. No new parameters are introduced when expanding the query. [Expressionify] public static bool IsOlderThan(this User user, int years) => user.DateOfBirth < DateTime.Now.AddYears(-years); // Example: users.Where(u => u.WasAddedRecently()) // ✔ OK for ExpressionEvaluationMode.FullCompatibilityButSlow // ❌ Not ok for ExpressionEvaluationMode.LimitedCompatibilityButCached // ✔ OK for ExpressionEvaluationMode.LimitedCompatibilityButCached when explicitly expanding the query with 'query.Expressionify()' // 'TimeProvider.UtcNow' is a new parameter that is not known in the query before calling '.Expressionify()'. [Expressionify] public static bool WasAddedRecently(this User user) => user.Created >= TimeProvider.UtcNow.AddDays(-1); // Example: users.Select(u => u.ToTestView(null)) // ✔ OK for ExpressionEvaluationMode.FullCompatibilityButSlow // ❌ Not ok for ExpressionEvaluationMode.LimitedCompatibilityButCached // ✔ OK for ExpressionEvaluationMode.LimitedCompatibilityButCached when explicitly expanding the query with 'query.Expressionify()' // With the input 'null' on the address, the expression 'address == null ? null : address.Street' gets replaced with a // new parameter for the value 'null'. [Expressionify] public static TestView ToTestView(this TestEntity testEntity, TestAddress? address) => new() { Name = testEntity.Name, Street = address == null ? null : address.Street }; } ``` ## Inspiration and help The first part of this project relies heavily on the work done by [Luke McGregor](https://twitter.com/staticv0id) in his [LinqExpander](https://github.com/lukemcgregor/LinqExpander) project, as described in his article on [composable repositories - nesting expressions](https://blog.staticvoid.co.nz/2016/composable_repositories_-_nesting_extensions/), and on the updated code by [Ben Cull](https://twitter.com/BenWhoLikesBeer) in his article [Expression and Projection Magic for Entity Framework Core ](https://benjii.me/2018/01/expression-projection-magic-entity-framework-core/). The second part of this project uses Roslyn to analyze and generate code, and part of it is built directly on code by [Carlos Mendible](https://twitter.com/cmendibl3) from his article [Create a class with .NET Core and Roslyn](https://carlos.mendible.com/2017/03/02/create-a-class-with-net-core-and-roslyn/). The rest is stitched together from various Stack Overflow answers and code snippets found on GitHub. [1]: https://www.nuget.org/packages/Clave.Expressionify/ [2]: https://claveconsulting.visualstudio.com/Nugets/_build/latest?definitionId=14 ================================================ FILE: git-conventional-commits.json ================================================ { "convention": { "commitTypes": [ "feat", "fix", "perf", "refactor", "style", "test", "build", "ops", "docs", "merge" ], "commitScopes": [], "releaseTagGlobPattern": "v[0-9]*.[0-9]*.[0-9]*", "issueRegexPattern": "(^|\\s)#\\d+(\\s|$)" }, "changelog": { "commitTypes": [ "feat", "fix", "perf", "merge", "?" ], "commitScopes": [], "commitIgnoreRegexPattern": "^(WIP|chore)", "includeInvalidCommits": false, "headlines": { "feat": "Features", "fix": "Bug Fixes", "perf": "Performance Improvements", "merge": "Merged Branches", "breakingChange": "BREAKING CHANGES" }, "commitUrl": "https://github.com/ClaveConsulting/Expressionify/commit/%commit%", "commitRangeUrl": "https://github.com/ClaveConsulting/Expressionify/compare/%from%...%to%?diff=split", "issueUrl": "https://github.com/ClaveConsulting/Expressionify/issues/%issue%" } } ================================================ FILE: global.json ================================================ { "sdk": { "version": "10.0.100", "rollForward": "feature" } } ================================================ FILE: src/Clave.Expressionify/Clave.Expressionify.csproj ================================================  net10.0 embedded latest enable Clave.Expressionify https://raw.githubusercontent.com/ClaveConsulting/logo/master/png/logo_noText.png https://github.com/ClaveConsulting/Expressionify https://github.com/ClaveConsulting/Expressionify Clave Consulting Use extension methods in Entity Framework Core queries MIT ================================================ FILE: src/Clave.Expressionify/DbContextOptionsExtensions.cs ================================================ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; namespace Clave.Expressionify { public static class DbContextOptionsExtensions { /// /// Use Expressionify within your queries. /// Transforms your expressions by replacing any [Expressionify] extension methods with the expressionised versions of those methods. /// public static DbContextOptionsBuilder UseExpressionify(this DbContextOptionsBuilder optionsBuilder, Action? expressionifyOptionsAction = null) where TContext : DbContext { return (DbContextOptionsBuilder)UseExpressionify((DbContextOptionsBuilder)optionsBuilder, expressionifyOptionsAction); } /// /// Use Expressionify within your queries. /// Transforms your expressions by replacing any [Expressionify] extension methods with the expressionised versions of those methods. /// public static DbContextOptionsBuilder UseExpressionify(this DbContextOptionsBuilder optionsBuilder, Action? expressionifyOptionsAction = null) { var extension = GetOrCreateExtension(optionsBuilder); ((IDbContextOptionsBuilderInfrastructure)optionsBuilder).AddOrUpdateExtension(extension); expressionifyOptionsAction?.Invoke(new ExpressionifyDbContextOptionsBuilder(optionsBuilder)); return optionsBuilder; } private static ExpressionifyDbContextOptionsExtension GetOrCreateExtension(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.Options.FindExtension() ?? new ExpressionifyDbContextOptionsExtension(); } } ================================================ FILE: src/Clave.Expressionify/ExpressionEvaluationMode.cs ================================================ namespace Clave.Expressionify; public enum ExpressionEvaluationMode { /// Always check for [Expressionify] extension methods when executing a query. FullCompatibilityButSlow = 0, /// /// Use the EF compiled query cache and only check for [Expressionify] extension methods when a query gets cached.
/// Not all queries work with this mode enabled. For those queries who don't, you get an InvalidOperationException and you need /// to call query.Expressionify() explicitly.
/// This is the case for [Expressionify]-methods that introduce new query-parameters either directly, or indirectly via an EF optimization. ///
LimitedCompatibilityButCached = 1 } ================================================ FILE: src/Clave.Expressionify/ExpressionableQuery.cs ================================================ using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; namespace Clave.Expressionify { public class ExpressionableQuery : IQueryable, IOrderedQueryable, IAsyncEnumerable { private readonly ExpressionableQueryProvider _provider; public ExpressionableQuery(ExpressionableQueryProvider provider, Expression expression) { _provider = provider; Expression = expression; } IEnumerator IEnumerable.GetEnumerator() { return _provider.ExecuteQuery(Expression).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return _provider.ExecuteQuery(Expression).GetEnumerator(); } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { return _provider.ExecuteQueryAsync(Expression).GetAsyncEnumerator(cancellationToken); } public Type ElementType => typeof(T); public Expression Expression { get; } public IQueryProvider Provider => _provider; } } ================================================ FILE: src/Clave.Expressionify/ExpressionableQueryCompiler.cs ================================================ using System; using System.Linq.Expressions; using System.Threading; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Internal; namespace Clave.Expressionify { [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "")] public class ExpressionableQueryCompiler : IQueryCompiler { private readonly IQueryCompiler _decoratedCompiler; public ExpressionableQueryCompiler(IQueryCompiler decoratedCompiler) { _decoratedCompiler = decoratedCompiler; } public Func CreateCompiledAsyncQuery(Expression query) => _decoratedCompiler.CreateCompiledAsyncQuery(Visit(query)); public Func CreateCompiledQuery(Expression query) => _decoratedCompiler.CreateCompiledQuery(Visit(query)); public TResult Execute(Expression query) => _decoratedCompiler.Execute(Visit(query)); public TResult ExecuteAsync(Expression query, CancellationToken cancellationToken) => _decoratedCompiler.ExecuteAsync(Visit(query), cancellationToken); private static Expression Visit(Expression exp) => new ExpressionifyVisitor().Visit(exp); #pragma warning disable EF9100 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. public Expression> PrecompileQuery(Expression query, bool async) => _decoratedCompiler.PrecompileQuery(query, async); #pragma warning restore EF9100 } } ================================================ FILE: src/Clave.Expressionify/ExpressionableQueryProvider.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Query; namespace Clave.Expressionify { public class ExpressionableQueryProvider : IAsyncQueryProvider { private readonly IQueryProvider _underlyingQueryProvider; public ExpressionableQueryProvider(IQueryProvider underlyingQueryProvider) { _underlyingQueryProvider = underlyingQueryProvider; } public IQueryable CreateQuery(Expression expression) => new ExpressionableQuery(this, expression); public IQueryable CreateQuery(Expression expression) { try { var type = expression.Type.GetElementType(); if(type == null) throw new Exception($"Expression type is strange {expression.Type.FullName}"); return typeof(ExpressionableQuery<>) .MakeGenericType(type) .CreateInstance(this, expression); } catch (System.Reflection.TargetInvocationException e) { throw e.InnerException ?? e; } } internal IEnumerable ExecuteQuery(Expression expression) => _underlyingQueryProvider.CreateQuery(Visit(expression)).AsEnumerable(); internal IAsyncEnumerable ExecuteQueryAsync(Expression expression) => _underlyingQueryProvider.CreateQuery(Visit(expression)).AsAsyncEnumerable(); public TResult Execute(Expression expression) => _underlyingQueryProvider.Execute(Visit(expression)); public object? Execute(Expression expression) => _underlyingQueryProvider.Execute(Visit(expression)); public TResult ExecuteAsync(Expression expression, CancellationToken cancellationToken = default) { if (_underlyingQueryProvider is IAsyncQueryProvider provider) { return provider.ExecuteAsync(Visit(expression), cancellationToken); } throw new Exception("This shouldn't happen"); } private static Expression Visit(Expression exp) => new ExpressionifyVisitor().Visit(exp); } } ================================================ FILE: src/Clave.Expressionify/ExpressionifyAttribute.cs ================================================ using System; namespace Clave.Expressionify { [AttributeUsage(AttributeTargets.Method)] public class ExpressionifyAttribute : Attribute { } } ================================================ FILE: src/Clave.Expressionify/ExpressionifyDbContextOptionsBuilder.cs ================================================ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; namespace Clave.Expressionify; public class ExpressionifyDbContextOptionsBuilder { private readonly DbContextOptionsBuilder _optionsBuilder; internal ExpressionifyDbContextOptionsBuilder(DbContextOptionsBuilder optionsBuilder) => _optionsBuilder = optionsBuilder; public ExpressionifyDbContextOptionsBuilder WithEvaluationMode(ExpressionEvaluationMode mode) => WithOption(e => e.WithEvaluationMode(mode)); /// /// Sets an option by cloning the extension used to store the settings. This ensures the builder /// does not modify options that are already in use elsewhere. /// /// An action to set the option. /// The same builder instance so that multiple calls can be chained. private ExpressionifyDbContextOptionsBuilder WithOption(Func setAction) { var extension = setAction(_optionsBuilder.Options.FindExtension()!); ((IDbContextOptionsBuilderInfrastructure)_optionsBuilder).AddOrUpdateExtension(extension); return this; } } ================================================ FILE: src/Clave.Expressionify/ExpressionifyDbContextOptionsExtension.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; namespace Clave.Expressionify { public class ExpressionifyDbContextOptionsExtension : IDbContextOptionsExtension { public ExpressionifyDbContextOptionsExtension() { } public ExpressionifyDbContextOptionsExtension(ExpressionifyDbContextOptionsExtension copyFrom) { EvaluationMode = copyFrom.EvaluationMode; } public DbContextOptionsExtensionInfo Info => new ExtensionInfo(this); public ExpressionEvaluationMode EvaluationMode { get; private set; } = ExpressionEvaluationMode.LimitedCompatibilityButCached; [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "")] public void ApplyServices(IServiceCollection services) { if (EvaluationMode == ExpressionEvaluationMode.FullCompatibilityButSlow) AddDecorator(services); else if (EvaluationMode == ExpressionEvaluationMode.LimitedCompatibilityButCached) AddDecorator(services); else throw new NotSupportedException($"Unsupported {nameof(EvaluationMode)}"); } private static void AddDecorator(IServiceCollection services) where TDecorator : TService { var descriptor = services.FirstOrDefault(s => s.ServiceType == typeof(TService)); if (descriptor == null || descriptor.ImplementationType == null && descriptor.ImplementationFactory == null && descriptor.ImplementationInstance == null) throw new InvalidOperationException($"No {typeof(TService).Name} is configured yet. Please configure a database provider first."); // Replace service with decorator. Factory creates the decorator with an instance of the decorated type, based on the original registration. services.Replace(ServiceDescriptor.Describe( descriptor.ServiceType, provider => ActivatorUtilities.CreateInstance(provider, typeof(TDecorator), GetInstance(provider, descriptor)), descriptor.Lifetime)); static object GetInstance(IServiceProvider provider, ServiceDescriptor descriptor) { return descriptor.ImplementationInstance ?? descriptor.ImplementationFactory?.Invoke(provider) ?? ActivatorUtilities.GetServiceOrCreateInstance(provider, descriptor.ImplementationType!); } } public void Validate(IDbContextOptions options) { // No options to validate } public ExpressionifyDbContextOptionsExtension WithEvaluationMode(ExpressionEvaluationMode evaluationMode) { var clone = Clone(); clone.EvaluationMode = evaluationMode; return clone; } private ExpressionifyDbContextOptionsExtension Clone() => new(this); private class ExtensionInfo : DbContextOptionsExtensionInfo { private readonly ExpressionifyDbContextOptionsExtension _extension; public ExtensionInfo(ExpressionifyDbContextOptionsExtension extension) : base(extension) { _extension = extension; } public override bool IsDatabaseProvider => false; public override string LogFragment => string.Empty; public override int GetServiceProviderHashCode() { // Hash all options here return _extension.EvaluationMode.GetHashCode(); } public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) { // Check if all options are the same return other is ExtensionInfo otherInfo && otherInfo._extension.EvaluationMode == _extension.EvaluationMode; } public override void PopulateDebugInfo(IDictionary debugInfo) { debugInfo["Expressionify:EvaluationMode"] = _extension.EvaluationMode.ToString(); // Add values of options here } } } } ================================================ FILE: src/Clave.Expressionify/ExpressionifyExtension.cs ================================================ using System; using System.Linq; using System.Reflection; namespace Clave.Expressionify { public static class ExpressionifyExtension { /// /// Transforms your expression by replacing any [Expressionify] /// extension methods with the expressionised versions of those methods. This allows the extensions to be used /// by another visitor such as EntityFramework. Should be used at the start of a query. /// /// the type of the queryable /// The input queryable /// A queryable which has any of the tagged extension methods replaced. public static IQueryable Expressionify(this IQueryable source) { if (source is ExpressionableQuery result) { return result; } return new ExpressionableQueryProvider(source.Provider).CreateQuery(source.Expression); } internal static bool MatchesTypeOf(this MethodInfo property, MethodInfo method) { var methodTypes = method.GetParameters().Select(p => p.ParameterType).Concat(new[] { method.ReturnType }); var propertyTypes = property.ReturnType.GetGenericArguments()[0].GetGenericArguments(); return methodTypes.SequenceEqual(propertyTypes); } internal static T CreateInstance(this Type type, params object?[]? args) { if (Activator.CreateInstance(type, args) is T result) { return result; } else { throw new Exception($"Type {type.FullName} is not of type {typeof(T).FullName}"); } } } } ================================================ FILE: src/Clave.Expressionify/ExpressionifyQueryTranslationPreprocessor.cs ================================================ using System; using System.Collections.Generic; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.Query.Internal; namespace Clave.Expressionify { public class ExpressionifyQueryTranslationPreprocessor : QueryTranslationPreprocessor { private readonly QueryTranslationPreprocessor _innerPreprocessor; public ExpressionifyQueryTranslationPreprocessor( QueryTranslationPreprocessor innerPreprocessor, QueryTranslationPreprocessorDependencies dependencies, QueryCompilationContext compilationContext) : base(dependencies, compilationContext) { _innerPreprocessor = innerPreprocessor; } public override Expression Process(Expression query) { var visitor = new ExpressionifyVisitor(); query = visitor.Visit(query); if (visitor.HasReplacedCalls) query = EvaluateExpression(query); return _innerPreprocessor.Process(query); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "")] private Expression EvaluateExpression(Expression query) { // 1) Ensure that no new parameters are introduced when creating the query // 2) This expression visitor also makes slight optimizations, like replacing evaluatable expressions. // With EF10, ParameterExtractingExpressionVisitor was removed and replaced by ExpressionTreeFuncletizer // ExpressionTreeFuncletizer now uses Dictionary instead of IParameterValues var funcletizer = new ExpressionTreeFuncletizer( QueryCompilationContext.Model, Dependencies.EvaluatableExpressionFilter, QueryCompilationContext.ContextType, generateContextAccessors: false, QueryCompilationContext.Logger); var throwOnAccess = new ThrowOnParameterAccess(); var result = funcletizer.ExtractParameters(query, throwOnAccess, parameterize: true, clearParameterizedValues: true); // Check if parameters were added by accessing the base Dictionary // ExpressionTreeFuncletizer bypasses our 'new' method overrides because Dictionary<> is not designed // to be subclassed, so we check the Count via the base class after funcletization completes if (((Dictionary)throwOnAccess).Count > 0) { throw new InvalidOperationException( "Adding parameters in a cached query context is not allowed. " + $"Explicitly call .{nameof(ExpressionifyExtension.Expressionify)}() on the query or use {nameof(ExpressionEvaluationMode)}.{nameof(ExpressionEvaluationMode.FullCompatibilityButSlow)}."); } return result; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "")] private class ThrowOnParameterAccess : Dictionary { // This class exists primarily for documentation purposes - to make it clear that parameter // access should throw an exception in cached mode. However, ExpressionTreeFuncletizer bypasses // these 'new' method overrides by calling base Dictionary<> methods directly. // The actual check happens in EvaluateExpression() after funcletization completes. private static InvalidOperationException CreateException() => new InvalidOperationException( "Adding parameters in a cached query context is not allowed. " + $"Explicitly call .{nameof(ExpressionifyExtension.Expressionify)}() on the query or use {nameof(ExpressionEvaluationMode)}.{nameof(ExpressionEvaluationMode.FullCompatibilityButSlow)}."); public new object? this[string key] { get => throw CreateException(); set => throw CreateException(); } public new void Add(string key, object? value) => throw CreateException(); public new bool TryAdd(string key, object? value) => throw CreateException(); public new bool TryGetValue(string key, out object? value) => throw CreateException(); public new bool ContainsKey(string key) => throw CreateException(); public new int Count => throw CreateException(); } } } ================================================ FILE: src/Clave.Expressionify/ExpressionifyQueryTranslationPreprocessorFactory.cs ================================================ using Microsoft.EntityFrameworkCore.Query; namespace Clave.Expressionify; public class ExpressionifyQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory { private readonly IQueryTranslationPreprocessorFactory _innerFactory; private readonly QueryTranslationPreprocessorDependencies _preprocessorDependencies; public ExpressionifyQueryTranslationPreprocessorFactory(IQueryTranslationPreprocessorFactory innerFactory, QueryTranslationPreprocessorDependencies preprocessorDependencies) { _innerFactory = innerFactory; _preprocessorDependencies = preprocessorDependencies; } public QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext) { var preprocessor = _innerFactory.Create(queryCompilationContext); return new ExpressionifyQueryTranslationPreprocessor(preprocessor, _preprocessorDependencies, queryCompilationContext); } } ================================================ FILE: src/Clave.Expressionify/ExpressionifyVisitor.cs ================================================ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace Clave.Expressionify { public class ExpressionifyVisitor : ExpressionVisitor { private static readonly IDictionary MethodToExpressionMap = new ConcurrentDictionary(); private readonly Dictionary _replacements = new Dictionary(); internal bool HasReplacedCalls { get; private set; } protected override Expression VisitMethodCall(MethodCallExpression node) { if (GetMethodExpression(node.Method) is LambdaExpression expression) { HasReplacedCalls = true; RegisterReplacementParameters(node.Arguments, expression); var result = Visit(expression.Body); UnregisterReplacementParameters(expression); return result; } return base.VisitMethodCall(node); } private static object? GetMethodExpression(MethodInfo method) { if (MethodToExpressionMap.TryGetValue(method, out var result)) { return result; } if (!method.IsStatic) { return MethodToExpressionMap[method] = null; } var shouldUseExpression = method.GetCustomAttributes(typeof(ExpressionifyAttribute), false).Any(); if (!shouldUseExpression) { return MethodToExpressionMap[method] = null; } var declaringType = method.DeclaringType!; if (declaringType.IsGenericType) { declaringType = declaringType .GetGenericTypeDefinition() .MakeGenericType(declaringType.GenericTypeArguments); } var methods = declaringType.GetRuntimeMethods(); var expression = methods ?.Where(m => m.Name.StartsWith($"{method.Name}_Expressionify_")) .Select(m => m.IsGenericMethod ? m.MakeGenericMethod(method.GetGenericArguments()) : m) .FirstOrDefault(m => m.MatchesTypeOf(method)) ?.Invoke(null, Array.Empty()); if (expression is LambdaExpression lambdaExpression) { return MethodToExpressionMap[method] = lambdaExpression; } throw new Exception($"Code generation seems to have failed, could not find expresion for method {GetFullName(method.DeclaringType)}.{method.Name}()"); } private static string GetFullName(Type? type) { if(type?.DeclaringType is Type parent) { return GetFullName(parent) + "." + type.Name; } return type?.Name ?? "???"; } protected override Expression VisitParameter(ParameterExpression node) { return _replacements.TryGetValue(node, out var replacement) ? Visit(replacement) : base.VisitParameter(node); } private void RegisterReplacementParameters(IReadOnlyCollection parameterValues, LambdaExpression expressionToVisit) { if (parameterValues.Count != expressionToVisit.Parameters.Count) throw new ArgumentException($"The parameter values count ({parameterValues.Count}) does not match the expression parameter count ({expressionToVisit.Parameters.Count})"); foreach (var (p, v) in expressionToVisit.Parameters.Zip(parameterValues, ValueTuple.Create)) { _replacements.Add(p, v); } } private void UnregisterReplacementParameters(LambdaExpression expressionToVisit) { foreach (var p in expressionToVisit.Parameters) { _replacements.Remove(p); } } } } ================================================ FILE: src/Clave.Expressionify.Generator/AnalyzerReleases.Shipped.md ================================================ ; Shipped analyzer releases ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md ## Release 6.0 ### New Rules Rule ID | Category | Severity | Notes --------|----------|----------|------- EXPR001 | Syntax | Error | ExpressionifyAnalyzer EXPR002 | Syntax | Error | ExpressionifyAnalyzer EXPR003 | Syntax | Error | ExpressionifyAnalyzer EXPR004 | Syntax | Error | ExpressionifyAnalyzer ================================================ FILE: src/Clave.Expressionify.Generator/AnalyzerReleases.Unshipped.md ================================================ ; Unshipped analyzer release ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md ================================================ FILE: src/Clave.Expressionify.Generator/Clave.Expressionify.Generator.csproj ================================================ netstandard2.0 embedded 10.0 enable false false true true Clave.Expressionify.Generator https://raw.githubusercontent.com/ClaveConsulting/logo/master/png/logo_noText.png https://github.com/ClaveConsulting/Expressionify https://github.com/ClaveConsulting/Expressionify Clave Consulting Use extension methods in Entity Framework Core queries MIT ================================================ FILE: src/Clave.Expressionify.Generator/ExpressionifyAnalyzer.cs ================================================ using System.Collections.Immutable; using Clave.Expressionify.Generator.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; namespace Clave.Expressionify.Generator { [DiagnosticAnalyzer(LanguageNames.CSharp)] public class ExpressionifyAnalyzer : DiagnosticAnalyzer { public const string StaticId = "EXPR001"; public const string ExpressionBodyId = "EXPR002"; public const string PartialClassId = "EXPR003"; public static readonly DiagnosticDescriptor StaticRule = new DiagnosticDescriptor( id: StaticId, title: "Method must be static", messageFormat: "Method {0} marked with [Expressionify] must be static", category: "Syntax", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); public static readonly DiagnosticDescriptor ExpressionBodyRule = new DiagnosticDescriptor( id: ExpressionBodyId, title: "Method must have expression body", messageFormat: "Method {0} marked with [Expressionify] must have expression body", category: "Syntax", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); public static readonly DiagnosticDescriptor PartialClassRule = new DiagnosticDescriptor( id: PartialClassId, title: "Class must be partial", messageFormat: "Class containing a method marked with [Expressionify] must be partial", category: "Syntax", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( StaticRule, ExpressionBodyRule, PartialClassRule); public override void Initialize(AnalysisContext context) { context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.ReportDiagnostics); context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.MethodDeclaration); } private static void Analyze(SyntaxNodeAnalysisContext context) { if (!(context.Node is MethodDeclarationSyntax methodDeclaration)) return; if (!methodDeclaration.HasExpressionifyAttribute()) return; if (!methodDeclaration.IsStatic()) { context.ReportDiagnostic(Diagnostic.Create( StaticRule, methodDeclaration.GetLocation(), methodDeclaration.Identifier.ToString())); } if (!methodDeclaration.HasExpressionBody()) { context.ReportDiagnostic(Diagnostic.Create( ExpressionBodyRule, methodDeclaration.GetLocation(), methodDeclaration.Identifier.ToString())); } if (methodDeclaration.FindAncestorMissingPartialKeyword() is SyntaxNode typeNode) { context.ReportDiagnostic(Diagnostic.Create( PartialClassRule, typeNode.GetLocation())); } } } } ================================================ FILE: src/Clave.Expressionify.Generator/ExpressionifyCodeFixProvider.cs ================================================ using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Document = Microsoft.CodeAnalysis.Document; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Clave.Expressionify.Generator { [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ExpressionifyCodeFixProvider)), Shared] public class ExpressionifyCodeFixProvider : CodeFixProvider { public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( ExpressionifyAnalyzer.StaticId, ExpressionifyAnalyzer.PartialClassId); public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); var diagnostic = context.Diagnostics.First(); var diagnosticSpan = diagnostic.Location; // Find the type declaration identified by the diagnostic. var syntaxNode = root!.FindNode(diagnostic.Location.SourceSpan); if (diagnostic.Id == ExpressionifyAnalyzer.StaticId) context.RegisterCodeFix( CodeAction.Create( title: "Add static keyword", createChangedDocument: c => FixMissingStatic(context.Document, root, (syntaxNode as MethodDeclarationSyntax)!), equivalenceKey: ExpressionifyAnalyzer.StaticId), diagnostic); if (diagnostic.Id == ExpressionifyAnalyzer.PartialClassId) context.RegisterCodeFix( CodeAction.Create( title: "Add partial keyword", createChangedDocument: c => FixMissingPartial(context.Document, root, (syntaxNode as ClassDeclarationSyntax)!), equivalenceKey: ExpressionifyAnalyzer.PartialClassId), diagnostic); } private static Task FixMissingStatic(Document contextDocument, SyntaxNode root, MethodDeclarationSyntax method) { return Task.FromResult(contextDocument.WithSyntaxRoot(root.ReplaceNode(method, method.AddModifiers(Token(SyntaxKind.StaticKeyword))))); } private static Task FixMissingPartial(Document contextDocument, SyntaxNode root, ClassDeclarationSyntax @class) { return Task.FromResult(contextDocument.WithSyntaxRoot(root.ReplaceNode(@class, @class.AddModifiers(Token(SyntaxKind.PartialKeyword))))); } } } ================================================ FILE: src/Clave.Expressionify.Generator/ExpressionifySourceGenerator.cs ================================================ using System; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using System.Collections.Generic; using System.Linq; using System.Text; using Clave.Expressionify.Generator.Internals; using Microsoft.CodeAnalysis.CSharp; using System.IO; namespace Clave.Expressionify.Generator { [Generator] public class ExpressionifySourceGenerator : ISourceGenerator { private static readonly string NullableDirective = "#nullable enable\r\n\r\n"; public void Initialize(GeneratorInitializationContext context) { context.RegisterForSyntaxNotifications(() => new ExpressionifySyntaxReceiver()); } public void Execute(GeneratorExecutionContext context) { if (context.SyntaxReceiver is ExpressionifySyntaxReceiver syntaxReceiver) Execute(context, syntaxReceiver.Methods); } private record Expressioned( MethodDeclarationSyntax Original, MethodDeclarationSyntax Replaced, (TypeDeclarationSyntax? Head, IEnumerator Tail)? Path) { public static Expressioned Create(MethodDeclarationSyntax m, Compilation compilation) => new( m, m.ToExpressionMethod(compilation), m.Ancestors().OfType().Reverse().HeadAndTail()); } private static void Execute(GeneratorExecutionContext context, IEnumerable methods) { try { var i = 0; static MemberDeclarationSyntax[] Group(IEnumerable methods) => methods .GroupBy(x => x.Path?.Head, x => x with { Path = x.Path?.Tail.HeadAndTail() }) .Select(g => g.Key!.WithOnlyTheseMembers(g .Where(x => x.Path is null) .Select(x => x.Replaced) .GroupBy(p => p.Identifier.Text) .SelectMany(x => x.Select((y, i) => y.GeneratedName(i))) .Concat(Group(g.Where(x => x.Path is not null))))) .ToArray(); var replacedTypes = methods .Select(x => Expressioned.Create(x, context.Compilation)) .GroupBy(m => m.Original.SyntaxTree.GetRoot(), (root, x) => (root.SyntaxTree.FilePath, root.WithOnlyTheseTypes(Group(x)))); foreach (var (path, source) in replacedTypes) { var sourceCode = NullableDirective + source.ToFullString(); context.AddSource( Path.GetFileNameWithoutExtension(path) + $"_expressionify_{i++}.g.cs", SourceText.From(sourceCode, Encoding.UTF8)); } } catch (Exception e) { context.ReportDiagnostic(Diagnostic.Create( new DiagnosticDescriptor("EXPR001", "Error generating expression", $"{e.Message}\n{e.StackTrace}", "error", DiagnosticSeverity.Error, true, e.StackTrace), Location.None)); } } private class ExpressionifySyntaxReceiver : ISyntaxReceiver { public readonly HashSet Types = new HashSet(); public readonly HashSet Methods = new HashSet(); public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { if (syntaxNode is MethodDeclarationSyntax methodDeclaration) { if (!methodDeclaration.HasExpressionBody()) return; if (!methodDeclaration.HasExpressionifyAttribute()) return; if (!methodDeclaration.IsStatic()) return; if (methodDeclaration.Ancestors().OfType().LastOrDefault() is { } type) { if (!type.Modifiers.Includes(SyntaxKind.PartialKeyword)) return; Methods.Add(methodDeclaration); Types.Add(type); } } } } } } ================================================ FILE: src/Clave.Expressionify.Generator/Extensions.cs ================================================ using System.Collections.Generic; namespace Clave.Expressionify.Generator { internal static class Extensions { public static (T Head, IEnumerator Tail)? HeadAndTail(this IEnumerable enumerable) => enumerable.GetEnumerator().HeadAndTail(); public static (T Head, IEnumerator Tail)? HeadAndTail(this IEnumerator enumerator) { if (!enumerator.MoveNext()) return null; return (enumerator.Current, enumerator); } } } ================================================ FILE: src/Clave.Expressionify.Generator/Internals/Checks.cs ================================================ using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Clave.Expressionify.Generator.Internals { public static class Checks { public static bool HasExpressionifyAttribute(this MethodDeclarationSyntax m) => m.AttributeLists.SelectMany(l => l.Attributes).Any(a => a.Name.ToString() == "Expressionify"); public static bool IsStatic(this MethodDeclarationSyntax method) => method.Modifiers.Includes(SyntaxKind.StaticKeyword); public static bool Includes(this SyntaxTokenList modifiers, SyntaxKind modifier) => modifiers.Any(m => m.IsKind(modifier)); public static bool HasExpressionBody(this MethodDeclarationSyntax method) => method.ExpressionBody is not null; public static TypeDeclarationSyntax? FindAncestorMissingPartialKeyword(this MethodDeclarationSyntax method) => method.Ancestors().OfType().FirstOrDefault(t => !t.Modifiers.Includes(SyntaxKind.PartialKeyword)); } } ================================================ FILE: src/Clave.Expressionify.Generator/Internals/ClassGenerator.cs ================================================ using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Clave.Expressionify.Generator.Internals { public static class ClassGenerator { public static TypeDeclarationSyntax WithOnlyTheseMembers(this TypeDeclarationSyntax type, IEnumerable members) => TypeDeclaration(type.Kind(), type.Identifier) .WithModifiers(type.Modifiers) .WithTypeParameterList(type.TypeParameterList) .AddMembers(members.ToArray()); public static SyntaxNode WithOnlyTheseTypes(this SyntaxNode root, IEnumerable members) { var namespaceName = root.DescendantNodes() .OfType() .FirstOrDefault()?.Name ?? root.DescendantNodes() .OfType() .FirstOrDefault().Name; var usings = root.DescendantNodes() .OfType() .ToArray(); return NamespaceDeclaration(namespaceName) .AddUsings(usings) .AddMembers(members.ToArray()) .NormalizeWhitespace(); } } } ================================================ FILE: src/Clave.Expressionify.Generator/Internals/ExpressionRewriter.cs ================================================ namespace Clave.Expressionify.Generator.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Linq; internal sealed class ExpressionRewriter : CSharpSyntaxRewriter { private readonly Compilation _compilation; private readonly SyntaxNode _root; public ExpressionRewriter(Compilation compilation, SyntaxNode root) { _compilation = compilation; _root = root; } public ExpressionSyntax VisitExpression(ExpressionSyntax expression) => (ExpressionSyntax)Visit(expression); public override SyntaxNode Visit(SyntaxNode node) { var res = base.Visit(node); if (SyntaxFactory.AreEquivalent(node, res)) return res; return _root .ReplaceNode(node, res) .DescendantNodes() .Where(x => x.FullSpan.Start == node.FullSpan.Start) .Where(x => x.FullSpan.Length == res.FullSpan.Length) .First(x => x.GetType() == res.GetType()); } public override SyntaxNode? VisitConditionalAccessExpression(ConditionalAccessExpressionSyntax node) { var expression = Visit(node.Expression); var expressionType = _compilation .ReplaceSyntaxTree(_root.SyntaxTree, expression.SyntaxTree) .GetSemanticModel(expression.SyntaxTree) .GetTypeInfo(expression) .Type; var forgiver = expressionType is INamedTypeSymbol { IsValueType: true, OriginalDefinition.SpecialType: SpecialType.System_Nullable_T, } ? "!.Value" : "!"; return SyntaxFactory.ParseExpression($"{expression}{forgiver}{Visit(node.WhenNotNull)}"); } } ================================================ FILE: src/Clave.Expressionify.Generator/Internals/PropertyGenerator.cs ================================================ using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Clave.Expressionify.Generator.Internals { public static class PropertyGenerator { public static MethodDeclarationSyntax GeneratedName(this MethodDeclarationSyntax p, int i) => p.WithIdentifier(Identifier($"{p.Identifier.Text}_Expressionify_{i}")); public static MethodDeclarationSyntax ToExpressionMethod( this MethodDeclarationSyntax method, Compilation compilation) => MethodDeclaration(GetExpressionType(method), method.Identifier) .WithModifiers(TokenList(Token(SyntaxKind.PrivateKeyword), Token(SyntaxKind.StaticKeyword))) .WithTypeParameterList(method.TypeParameterList) .WithConstraintClauses(method.ConstraintClauses) .WithExpressionBody(ArrowExpressionClause(GetBody(method, compilation))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) .NormalizeWhitespace(); private static ParenthesizedLambdaExpressionSyntax GetBody( BaseMethodDeclarationSyntax method, Compilation compilation) { var expressionRewriter = new ExpressionRewriter(compilation, method.SyntaxTree.GetRoot()); return ParenthesizedLambdaExpression( ParameterList(SeparatedList(method.ParameterList.Parameters.Select(p => p.WithModifiers(TokenList())))), expressionRewriter.VisitExpression(method.ExpressionBody!.Expression) ); } private static QualifiedNameSyntax GetExpressionType(MethodDeclarationSyntax method) => Expression(Func( SeparatedList(method.ParameterList.Parameters .Select(p => p.Type!) .ToArray() .Append(method.ReturnType)) )); private static QualifiedNameSyntax Func(SeparatedSyntaxList types) => QualifiedName( IdentifierName("System"), GenericName(Identifier("Func")) .WithTypeArgumentList(TypeArgumentList(types)) ); private static QualifiedNameSyntax Expression(TypeSyntax genericPart) => QualifiedName( QualifiedName(QualifiedName(IdentifierName("System"), IdentifierName("Linq")), IdentifierName("Expressions")), GenericName(Identifier("Expression")) .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(genericPart))) ); } } ================================================ FILE: src/Clave.Expressionify.Generator/IsExternalInit.cs ================================================ namespace System.Runtime.CompilerServices { using System.ComponentModel; /// /// Reserved to be used by the compiler for tracking metadata. /// This class should not be used by developers in source code. /// [EditorBrowsable(EditorBrowsableState.Never)] internal static class IsExternalInit { } } ================================================ FILE: src/Clave.Expressionify.Generator/_._ ================================================ ================================================ FILE: tests/Clave.Expressionify.Generator.Tests/Clave.Expressionify.Generator.Tests.csproj ================================================  net10.0 latest enable false all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: tests/Clave.Expressionify.Generator.Tests/CodeFixTests.cs ================================================ using System.Threading.Tasks; using NUnit.Framework; using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.CodeFixVerifier; namespace Clave.Expressionify.Generator.Tests { public class CodeFixTests { [Test] public async Task TestNothing() { var test = @""; await Verify.VerifyAnalyzerAsync(test); } [Test] public async Task TestOkMethod() { var test = @" namespace ConsoleApplication1 { public partial class Extensions { [Expressionify] public static int Foo(int x) => 8; } [System.AttributeUsage(System.AttributeTargets.Method)] public class ExpressionifyAttribute : System.Attribute {} }"; await Verify.VerifyAnalyzerAsync(test); } [Test] public async Task TestWithoutNamespace() { var test = @" public partial class Extensions { [Expressionify] public static int Foo(int x) => 8; } [System.AttributeUsage(System.AttributeTargets.Method)] public class ExpressionifyAttribute : System.Attribute {} "; await Verify.VerifyAnalyzerAsync(test); } [Test] public async Task TestWithFileScopedNamespace() { var test = @" namespace ConsoleApplication1; public partial class Extensions { [Expressionify] public static int Foo(int x) => 8; } [System.AttributeUsage(System.AttributeTargets.Method)] public class ExpressionifyAttribute : System.Attribute {} "; await Verify.VerifyAnalyzerAsync(test); } [Test] public async Task TestMissingStatic() { var test = @" namespace ConsoleApplication1 { public partial class Extensions { [Expressionify] public int Foo(int x) => 8; } [System.AttributeUsage(System.AttributeTargets.Method)] public class ExpressionifyAttribute : System.Attribute {} }"; var expected = Verify.Diagnostic(ExpressionifyAnalyzer.StaticRule) .WithSpan("/0/Test0.cs", 6, 25, 7, 52) .WithArguments("Foo"); var fixtest = @" namespace ConsoleApplication1 { public partial class Extensions { [Expressionify] public static int Foo(int x) => 8; } [System.AttributeUsage(System.AttributeTargets.Method)] public class ExpressionifyAttribute : System.Attribute {} }"; await Verify.VerifyCodeFixAsync(test, expected, fixtest); } [Test] public async Task TestNotExpressionBody() { var test = @" namespace ConsoleApplication1 { public static partial class Extensions { [Expressionify] public static int Foo(int x) { return 8; } } [System.AttributeUsage(System.AttributeTargets.Method)] public class ExpressionifyAttribute : System.Attribute {} }"; var expected = Verify.Diagnostic(ExpressionifyAnalyzer.ExpressionBodyRule) .WithSpan("/0/Test0.cs", 6, 25, 7, 67) .WithArguments("Foo"); await Verify.VerifyCodeFixAsync(test, expected, test); } [Test] public async Task TestNotInPartialClass() { var test = @" namespace ConsoleApplication1 { public static class Extensions { [Expressionify] public static int Foo(int x) => 8; } [System.AttributeUsage(System.AttributeTargets.Method)] public class ExpressionifyAttribute : System.Attribute {} }"; var expected = Verify.Diagnostic(ExpressionifyAnalyzer.PartialClassRule) .WithSpan("/0/Test0.cs", 4, 21, 8, 22); await Verify.VerifyAnalyzerAsync(test, new [] { expected }); var fixtest = @" namespace ConsoleApplication1 { public static partial class Extensions { [Expressionify] public static int Foo(int x) => 8; } [System.AttributeUsage(System.AttributeTargets.Method)] public class ExpressionifyAttribute : System.Attribute {} }"; await Verify.VerifyCodeFixAsync(test, expected, fixtest); } } } ================================================ FILE: tests/Clave.Expressionify.Generator.Tests/CodeGeneratorTests.cs ================================================ using System; using System.Text; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; using NUnit.Framework; using Verify = Clave.Expressionify.Generator.Tests.Verifiers.CSharpSourceGeneratorVerifier; namespace Clave.Expressionify.Generator.Tests { [TestFixture] public class CodeGeneratorTests { private const string AttributeCode = @" namespace ConsoleApplication1 { using System; [AttributeUsage(AttributeTargets.Method)] public class ExpressionifyAttribute : Attribute { } }"; // Normal scenario [TestCase(@"namespace ConsoleApplication1 { public partial class Extensions { [Expressionify] public static int Foo(int x) => 8; } }", @"#nullable enable namespace ConsoleApplication1 { public partial class Extensions { private static System.Linq.Expressions.Expression> Foo_Expressionify_0() => (int x) => 8; } }", TestName = "Normal scenario")] // File scoped namespace [TestCase(@"namespace ConsoleApplication1; public partial class Extensions { [Expressionify] public static int Foo(int x) => 8; }", @"#nullable enable namespace ConsoleApplication1 { public partial class Extensions { private static System.Linq.Expressions.Expression> Foo_Expressionify_0() => (int x) => 8; } }", TestName = "File scoped namespace")] // Nested class [TestCase(@"namespace ConsoleApplication1 { public partial class Extensions { [Expressionify] public static int Foo(int x) => 8; public partial class Nested { [Expressionify] public static int Foo(int x) => 8; } } }", @"#nullable enable namespace ConsoleApplication1 { public partial class Extensions { private static System.Linq.Expressions.Expression> Foo_Expressionify_0() => (int x) => 8; public partial class Nested { private static System.Linq.Expressions.Expression> Foo_Expressionify_0() => (int x) => 8; } } }", TestName = "Nested class")] // Nullable [TestCase(@"#nullable enable namespace ConsoleApplication1 { public partial class Extensions { [Expressionify] public static string? Foo(int x) => x < 10 ? null : ""bar""; } }", @"#nullable enable namespace ConsoleApplication1 { public partial class Extensions { private static System.Linq.Expressions.Expression> Foo_Expressionify_0() => (int x) => x < 10 ? null : ""bar""; } }", TestName = "Nullable")] // Nullable enabled but not used [TestCase(@"#nullable enable namespace ConsoleApplication1 { public partial class Extensions { [Expressionify] public static string Foo(int x) => ""bar""; } }", @"#nullable enable namespace ConsoleApplication1 { public partial class Extensions { private static System.Linq.Expressions.Expression> Foo_Expressionify_0() => (int x) => ""bar""; } }", TestName = "Nullable enabled but not used")] [TestCase(@"namespace ConsoleApplication1 { public partial class Extensions { [Expressionify] public static string Foo(T x) => ""bar""; } }", @"#nullable enable namespace ConsoleApplication1 { public partial class Extensions { private static System.Linq.Expressions.Expression> Foo_Expressionify_0() => (T x) => ""bar""; } }", TestName = "Generic extension method")] [TestCase(@"namespace ConsoleApplication1 { public partial class Extensions { [Expressionify] public static string Foo(T x) where T : System.Collections.IEnumerable => ""bar""; } }", @"#nullable enable namespace ConsoleApplication1 { public partial class Extensions { private static System.Linq.Expressions.Expression> Foo_Expressionify_0() where T : System.Collections.IEnumerable => (T x) => ""bar""; } }", TestName = "Generic extension method with constraints")] [TestCase(@"namespace ConsoleApplication1 { public partial class Extensions { [Expressionify] public static string Foo(T x) => ""bar""; } }", @"#nullable enable namespace ConsoleApplication1 { public partial class Extensions { private static System.Linq.Expressions.Expression> Foo_Expressionify_0() => (T x) => ""bar""; } }", TestName = "Generic type")] // Null propagation [TestCase(@"#nullable enable namespace ConsoleApplication1 { using System; public partial class Extensions { [Expressionify] public static int? GetYear(DateTime? x) => x?.Year; [Expressionify] public static string? GetYearString(DateTime? x) => (x?.AddDays(1).Year)?.ToString(); [Expressionify] public static byte? First(byte[]? x) => x?[0]; [Expressionify] public static string? FirstString(byte[]? x) => x?[0].ToString(); [Expressionify] public static int? FirstYear(DateTime?[]? x) => x?[0]?.Year; [Expressionify] public static int? GetYearOfNextDay(DateTime? x) => GetYear(x?.AddDays(1)); } }", @"#nullable enable namespace ConsoleApplication1 { using System; public partial class Extensions { private static System.Linq.Expressions.Expression> GetYear_Expressionify_0() => (DateTime? x) => x!.Value.Year; private static System.Linq.Expressions.Expression> GetYearString_Expressionify_0() => (DateTime? x) => (x!.Value.AddDays(1).Year)!.ToString(); private static System.Linq.Expressions.Expression> First_Expressionify_0() => (byte[]? x) => x![0]; private static System.Linq.Expressions.Expression> FirstString_Expressionify_0() => (byte[]? x) => x![0].ToString(); private static System.Linq.Expressions.Expression> FirstYear_Expressionify_0() => (DateTime? []? x) => x![0]!.Value.Year; private static System.Linq.Expressions.Expression> GetYearOfNextDay_Expressionify_0() => (DateTime? x) => GetYear(x!.Value.AddDays(1)); } }", TestName = "Null propagation")] public async Task TestGenerator(string source, string generated) { await VerifyGenerated(source, generated); } public async Task VerifyGenerated(string source, string generated) { await new Verify.Test { TestState = { Sources = { source, AttributeCode }, GeneratedSources = { (typeof(ExpressionifySourceGenerator), "Test0_expressionify_0.g.cs", SourceText.From(generated.Replace(Environment.NewLine, "\r\n"), Encoding.UTF8, SourceHashAlgorithm.Sha1)), } } }.RunAsync(); } } } ================================================ FILE: tests/Clave.Expressionify.Generator.Tests/Verifiers/CSharpSourceGeneratorVerifier.cs ================================================ using System; using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Testing.Verifiers; namespace Clave.Expressionify.Generator.Tests.Verifiers { public static class CSharpSourceGeneratorVerifier where TSourceGenerator : ISourceGenerator, new() { public class Test : CSharpSourceGeneratorTest { public Test() { } protected override CompilationOptions CreateCompilationOptions() { var compilationOptions = base.CreateCompilationOptions(); return compilationOptions.WithSpecificDiagnosticOptions( compilationOptions.SpecificDiagnosticOptions.SetItems(GetNullableWarningsFromCompiler())); } public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default; private static ImmutableDictionary GetNullableWarningsFromCompiler() { string[] args = { "/warnaserror:nullable" }; var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; return nullableWarnings; } protected override ParseOptions CreateParseOptions() { return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); } } } } ================================================ FILE: tests/Clave.Expressionify.Tests/Clave.Expressionify.Tests.csproj ================================================  net10.0 latest enable false ================================================ FILE: tests/Clave.Expressionify.Tests/DbContextExtensions/TestDbContext.cs ================================================ using Microsoft.EntityFrameworkCore; namespace Clave.Expressionify.Tests.DbContextExtensions { public class TestDbContext : DbContext { public TestDbContext(DbContextOptions options) : base(options) { } public DbSet TestEntities { get; set; } = null!; } } ================================================ FILE: tests/Clave.Expressionify.Tests/DbContextExtensions/TestEntity.cs ================================================ using System; namespace Clave.Expressionify.Tests.DbContextExtensions { public class TestEntity { public int Id { get; set; } public string Name { get; set; } = ""; public DateTime Created { get; set; } } public class TestAddress { public string? City { get; set; } public string? Street { get; set; } } public class TestView { public string? Name { get; set; } public string? Street { get; set; } } public static class TestTimeProvider { public static DateTime UtcNow => new(2022, 3, 4, 5, 6, 7); } } ================================================ FILE: tests/Clave.Expressionify.Tests/DbContextExtensions/TestEntityExtensions.cs ================================================ namespace Clave.Expressionify.Tests.DbContextExtensions { public static partial class TestEntityExtensions { [Expressionify] public static string GetName(this TestEntity testEntity, string prefix) => prefix + " " + testEntity.Name; [Expressionify] public static bool NameEquals(this TestEntity testEntity, string name) => testEntity.Name == name; [Expressionify] public static bool IsJohnDoe(this TestEntity testEntity) => testEntity.Name == "John Doe"; [Expressionify] public static bool IsSomething(this TestEntity testEntity) => testEntity.Name == Name; [Expressionify] public static bool IsRecent(this TestEntity testEntity) => testEntity.Created > TestTimeProvider.UtcNow.AddDays(-1); [Expressionify] public static TestView ToTestView(this TestEntity testEntity, TestAddress? address) => new() { Name = testEntity.Name, Street = address == null ? null : address.Street }; public static string Name => "Something"; } } ================================================ FILE: tests/Clave.Expressionify.Tests/DbContextExtensions/Tests.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using NUnit.Framework; using Shouldly; namespace Clave.Expressionify.Tests.DbContextExtensions { public class Tests { [Test] public void UseExpressionifyInConfig_ExpandsExpression_CanTranslate() { using var dbContext = new TestDbContext(GetOptions()); var query = dbContext.TestEntities.Select(e => e.GetName("oh hi")); var sql = query.ToQueryString(); sql.ShouldStartWith("SELECT 'oh hi ' || \"t\".\"Name\""); } [Test] public void UseExpressionifyInQuery_ExpandsExpression_CanTranslate() { using var dbContext = new TestDbContext(GetOptions(useExpressionify: false)); var query = dbContext.TestEntities.Expressionify().Select(e => e.GetName("oh hi")); var sql = query.ToQueryString(); sql.ShouldStartWith("SELECT 'oh hi ' || \"t\".\"Name\""); } [Test] public void UseExpressionifyInQueryAndConfig_ExpandsExpression_CanTranslate() { using var dbContext = new TestDbContext(GetOptions()); var prefix = "oh hi"; var query = dbContext.TestEntities.Expressionify().Select(e => e.GetName(prefix)); var sql = query.ToQueryString(); sql.ShouldBe($".param set @p 'oh hi '{Environment.NewLine}{Environment.NewLine}SELECT @p || \"t\".\"Name\"{Environment.NewLine}FROM \"TestEntities\" AS \"t\""); } [Test] public void DontUseExpressionify_EfSelectsWholeEntity() { // This is basically a self-test of the test setup. EF should select the whole entity here, instead of the "optimized" version where // the concatenation is done in the statement and only the required name is selected using var dbContext = new TestDbContext(GetOptions(useExpressionify: false)); var prefix = "oh hi"; var query = dbContext.TestEntities.Select(e => e.GetName(prefix)); var sql = query.ToQueryString(); sql.ShouldStartWith("SELECT \"t\".\"Id\", \"t\".\"Created\", \"t\".\"Name\""); } [Test] public void Expressionify_ShouldHandleWhereWithParameters_AfterExpansion() { using var dbContext = new TestDbContext(GetOptions()); var query = dbContext.TestEntities.Expressionify().Where(e => e.IsSomething()); query.ToQueryString().ShouldBe($".param set @Name 'Something'{Environment.NewLine}{Environment.NewLine}SELECT \"t\".\"Id\", \"t\".\"Created\", \"t\".\"Name\"{Environment.NewLine}FROM \"TestEntities\" AS \"t\"{Environment.NewLine}WHERE \"t\".\"Name\" = @Name"); } [TestCase(ExpressionEvaluationMode.FullCompatibilityButSlow)] [TestCase(ExpressionEvaluationMode.LimitedCompatibilityButCached)] public void UseExpressionify_ShouldHandleConstants(ExpressionEvaluationMode mode) { using var dbContext = new TestDbContext(GetOptions(o => o.WithEvaluationMode(mode))); var query = dbContext.TestEntities.Where(e => e.IsJohnDoe()); query.ToQueryString().ShouldBe($"SELECT \"t\".\"Id\", \"t\".\"Created\", \"t\".\"Name\"{Environment.NewLine}FROM \"TestEntities\" AS \"t\"{Environment.NewLine}WHERE \"t\".\"Name\" = 'John Doe'"); } [TestCase(ExpressionEvaluationMode.FullCompatibilityButSlow)] [TestCase(ExpressionEvaluationMode.LimitedCompatibilityButCached)] public void UseExpressionify_ShouldHandleWhereWithParameters(ExpressionEvaluationMode mode) { var name = "oh hi"; using var dbContext = new TestDbContext(GetOptions(o => o.WithEvaluationMode(mode))); var query = dbContext.TestEntities.Where(e => e.NameEquals(name)); query.ToQueryString().ShouldBe($".param set @name 'oh hi'{Environment.NewLine}{Environment.NewLine}SELECT \"t\".\"Id\", \"t\".\"Created\", \"t\".\"Name\"{Environment.NewLine}FROM \"TestEntities\" AS \"t\"{Environment.NewLine}WHERE \"t\".\"Name\" = @name"); } [Test] public void UseExpressionify_EvaluationModeAlways_ShouldHandleWhereWithNewParameters() { using var dbContext = new TestDbContext(GetOptions(optionsAction: o => o.WithEvaluationMode(ExpressionEvaluationMode.FullCompatibilityButSlow))); var query = dbContext.TestEntities.Where(e => e.IsSomething()); query.ToQueryString().ShouldBe($".param set @Name 'Something'{Environment.NewLine}{Environment.NewLine}SELECT \"t\".\"Id\", \"t\".\"Created\", \"t\".\"Name\"{Environment.NewLine}FROM \"TestEntities\" AS \"t\"{Environment.NewLine}WHERE \"t\".\"Name\" = @Name"); } [Test] public void UseExpressionify_EvaluationModeAlways_ShouldHandleWhereWithExternalServices() { using var dbContext = new TestDbContext(GetOptions(optionsAction: o => o.WithEvaluationMode(ExpressionEvaluationMode.FullCompatibilityButSlow))); var query = dbContext.TestEntities.Where(e => e.IsRecent()); query.ToQueryString().ShouldBe($".param set @AddDays '2022-03-03 05:06:07'{Environment.NewLine}{Environment.NewLine}SELECT \"t\".\"Id\", \"t\".\"Created\", \"t\".\"Name\"{Environment.NewLine}FROM \"TestEntities\" AS \"t\"{Environment.NewLine}WHERE \"t\".\"Created\" > @AddDays"); } [Test] public void UseExpressionify_EvaluationModeCached_CannotHandleNewParameters() { using var dbContext = new TestDbContext(GetOptions(optionsAction: o => o.WithEvaluationMode(ExpressionEvaluationMode.LimitedCompatibilityButCached))); var query = dbContext.TestEntities.Where(e => e.IsSomething()); var exception = Should.Throw(() => query.ToQueryString()); exception.Message.ShouldBe("Adding parameters in a cached query context is not allowed. Explicitly call .Expressionify() on the query or use ExpressionEvaluationMode.FullCompatibilityButSlow."); } [Test] public void UseExpressionify_EvaluationModeCached_CannotHandleParametersFromExternalServices() { using var dbContext = new TestDbContext(GetOptions(optionsAction: o => o.WithEvaluationMode(ExpressionEvaluationMode.LimitedCompatibilityButCached))); var query = dbContext.TestEntities.Where(e => e.IsSomething()); var exception = Should.Throw(() => query.ToQueryString()); exception.Message.ShouldBe("Adding parameters in a cached query context is not allowed. Explicitly call .Expressionify() on the query or use ExpressionEvaluationMode.FullCompatibilityButSlow."); } [Test] public void UseExpressionify_ShouldProduceSameOutputAsExpressionify() { using var dbContext = new TestDbContext(GetOptions()); var queryA = dbContext.TestEntities.Where(e => e.IsJohnDoe()); var queryB = dbContext.TestEntities.Expressionify().Where(e => e.IsJohnDoe()); queryA.ToQueryString().ShouldBe(queryB.ToQueryString()); } [TestCase(ExpressionEvaluationMode.FullCompatibilityButSlow)] [TestCase(ExpressionEvaluationMode.LimitedCompatibilityButCached)] public void UseExpressionify_ShouldProduceSameOutputAsExpressionify_InAllModes(ExpressionEvaluationMode mode) { // Note: when not using the result of ParameterExtractingExpressionVisitor, the Cached mode returns another query with an additional concat (which would be unintended) using var dbContext = new TestDbContext(GetOptions(o => o.WithEvaluationMode(mode))); var queryA = dbContext.TestEntities.Select(e => e.GetName("oh hi")); var queryB = dbContext.TestEntities.Expressionify().Select(e => e.GetName("oh hi")); queryA.ToQueryString().ShouldBe(queryB.ToQueryString()); } [Test] public void UseExpressionify_EvaluationModeAlways_ShouldHandleEvaluatableExpressions() { using var dbContext = new TestDbContext(GetOptions(o => o.WithEvaluationMode(ExpressionEvaluationMode.FullCompatibilityButSlow))); var query = dbContext.TestEntities.Select(e => e.ToTestView(null)); query.ToQueryString().ShouldBe($"SELECT \"t\".\"Name\", NULL AS \"Street\"{Environment.NewLine}FROM \"TestEntities\" AS \"t\""); } [TestCase(ExpressionEvaluationMode.FullCompatibilityButSlow)] [TestCase(ExpressionEvaluationMode.LimitedCompatibilityButCached)] public void UseExpressionify_WithEvaluationMode_SetsEvaluationMode(ExpressionEvaluationMode mode) { var options = GetOptions(o => o.WithEvaluationMode(mode)); var extension = options.FindExtension()!; var debugInfo = new Dictionary(); extension.Info.PopulateDebugInfo(debugInfo); debugInfo["Expressionify:EvaluationMode"].ShouldBe(mode.ToString()); } [Test] public void UseExpressionify_EvaluationMode_DefaultsToLimitedCompatibilityButCached() { var options = GetOptions(); var extension = options.FindExtension()!; var debugInfo = new Dictionary(); extension.Info.PopulateDebugInfo(debugInfo); debugInfo["Expressionify:EvaluationMode"].ShouldBe(ExpressionEvaluationMode.LimitedCompatibilityButCached.ToString()); } private DbContextOptions GetOptions(Action? optionsAction = null, bool useExpressionify = true) { var builder = new DbContextOptionsBuilder().UseSqlite("DataSource=:memory:"); if (useExpressionify) builder.UseExpressionify(optionsAction); return builder.Options; } } } ================================================ FILE: tests/Clave.Expressionify.Tests/First/ExtensionMethods.cs ================================================ using System; using Clave.Expressionify.Tests.Samples; namespace Clave.Expressionify.Tests.First { public static partial class ExtensionMethods { [Expressionify] public static int ToInt(this string value) => Convert.ToInt32(value); [Expressionify] public static double ToDouble(this string value) => Convert.ToDouble(value); [Expressionify] public static int Pluss(this string a, string b) => a.ToInt() + b.ToInt(); [Expressionify] public static int Squared(this int a) => a * a; [Expressionify] public static double Squared(this double a) => a * a; [Expressionify] public static string GetName(this T thing) where T : IThing => thing.Name; } } ================================================ FILE: tests/Clave.Expressionify.Tests/Samples/Class1.cs ================================================ namespace Clave.Expressionify.Tests.Samples { public static partial class Class1 { [Expressionify] public static int Foo(int x) => 8; } } ================================================ FILE: tests/Clave.Expressionify.Tests/Samples/Class2.cs ================================================ namespace Clave.Expressionify.Tests.Samples { public static partial class Class2 { public static int Bar(int x) => 0; } } ================================================ FILE: tests/Clave.Expressionify.Tests/Samples/Class3.cs ================================================ namespace Clave.Expressionify.Tests.Samples; public static partial class Class3 { [Expressionify] public static int Foo(int x) => 8; [Expressionify] public static int Foo(string x) => 0; } ================================================ FILE: tests/Clave.Expressionify.Tests/Samples/Class4.cs ================================================ using System.Collections.Generic; using System.Linq; namespace Clave.Expressionify.Tests.Samples { public static partial class Class4 { [Expressionify] private static int Foo(string x) => 8; [Expressionify] public static int Something(IEnumerable x) => x.Select(Foo).Sum(); public static partial class NestedClass1 { [Expressionify] public static string Bar(int x) => $"={8+x}"; } } } ================================================ FILE: tests/Clave.Expressionify.Tests/Samples/GenericClass.cs ================================================ namespace Clave.Expressionify.Tests.Samples { public partial class GenericClass { [Expressionify] public static int Foo(string x) => 8; } } ================================================ FILE: tests/Clave.Expressionify.Tests/Samples/IThing.cs ================================================ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Clave.Expressionify.Tests.Samples { public interface IThing { string Name { get; } } public class Thing1 : IThing { public string Name => "Thing1"; } public class Thing2 : IThing { public string Name => "Thing2"; } } ================================================ FILE: tests/Clave.Expressionify.Tests/Samples/Record1.cs ================================================ namespace Clave.Expressionify.Tests.Samples { public partial record Record1(string Name) { [Expressionify] public static Record1 Create(string name) => new Record1(name); } } ================================================ FILE: tests/Clave.Expressionify.Tests/Second/ExtensionMethods.cs ================================================ using System; namespace Clave.Expressionify.Tests.Second { public static partial class ExtensionMethods { [Expressionify] public static int ToInt(this string value, bool extra) => Convert.ToInt32(value); [Expressionify] public static double ToDouble(this string value) => Convert.ToDouble(value); [Expressionify] public static int Pluss(this string a, string b) => a.ToInt(false) + b.ToInt(false); [Expressionify] public static int Squared(this int a) => a * a; } } ================================================ FILE: tests/Clave.Expressionify.Tests/Tests.cs ================================================ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using Clave.Expressionify.Tests.First; using Clave.Expressionify.Tests.Samples; using NUnit.Framework; using Shouldly; namespace Clave.Expressionify.Tests { public class Tests { [Test] public void TestClass() { var prop = typeof(Class1).GetMethod("Foo_Expressionify_0", BindingFlags.NonPublic | BindingFlags.Static); prop.ShouldNotBeNull(); var expr = prop.Invoke(null, Array.Empty()) as Expression>; expr.ShouldNotBeNull(); expr.Compile().Invoke(1).ShouldBe(8); } [Test] public void TestRecord() { var prop = typeof(Record1).GetMethod("Create_Expressionify_0", BindingFlags.NonPublic | BindingFlags.Static); prop.ShouldNotBeNull(); var expr = prop.Invoke(null, Array.Empty()) as Expression>; expr.ShouldNotBeNull(); expr.Compile().Invoke("test").ShouldBe(new Record1("test")); } [Test] public void TestNonExpressionify() { typeof(Class2).GetProperties().ShouldBeEmpty(); } [Test] public void TestOverload() { typeof(Class3).GetMethods(BindingFlags.NonPublic|BindingFlags.Static).ShouldNotBeEmpty(); var prop0 = typeof(Class3).GetMethod("Foo_Expressionify_0", BindingFlags.NonPublic | BindingFlags.Static); prop0.ShouldNotBeNull(); var expr0 = prop0.Invoke(null, Array.Empty()) as Expression>; expr0.ShouldNotBeNull(); expr0.Compile().Invoke(1).ShouldBe(8); var prop1 = typeof(Class3).GetMethod("Foo_Expressionify_1", BindingFlags.NonPublic | BindingFlags.Static); prop1.ShouldNotBeNull(); var expr1 = prop1.Invoke(null, Array.Empty()) as Expression>; expr1.ShouldNotBeNull(); expr1.Compile().Invoke("test").ShouldBe(0); } [Test] public void TestMethodGroup() { typeof(Class4).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).ShouldNotBeEmpty(); var prop0 = typeof(Class4).GetMethod("Foo_Expressionify_0", BindingFlags.NonPublic | BindingFlags.Static); prop0.ShouldNotBeNull(); var expr0 = prop0.Invoke(null, Array.Empty()) as Expression>; expr0.ShouldNotBeNull(); expr0.Compile().Invoke("1").ShouldBe(8); var prop1 = typeof(Class4).GetMethod("Something_Expressionify_0", BindingFlags.NonPublic | BindingFlags.Static); prop1.ShouldNotBeNull(); var expr1 = prop1.Invoke(null, Array.Empty()) as Expression, int>>; expr1.ShouldNotBeNull(); expr1.Compile().Invoke(new[] { "test" }).ShouldBe(8); } [Test] public void TestExpressionifyClass() { var data = new[]{ "1", "2", "3" }; var result = data.AsQueryable() .Expressionify() .Select(x => x.ToInt()) .ToList(); result.ShouldBe(new[] { 1, 2, 3 }); } [Test] public void TestExpressionifyNestedClass() { var data = new[]{ 1, 2, 3 }; var result = data.AsQueryable() .Expressionify() .Select(x => Class4.NestedClass1.Bar(x)) .ToList(); result.ShouldBe(new[] { "=9", "=10", "=11" }); } [Test] public void TestExpressionifyRecord() { var data = new[]{ "1", "2", "3" }; var result = data.AsQueryable() .Expressionify() .Select(x => Record1.Create(x)) .ToList(); result.ShouldBe(new Record1[] { new("1"), new("2"), new("3") }); } [Test] public void TestMethodParameterUsedTwice() { var data = new[]{ 1, 2, 3 }; var result = data.AsQueryable() .Expressionify() .Select(x => x.Squared()) .ToList(); result.ShouldBe(new[] { 1, 4, 9 }); } [Test] public void TestMethodParameterUsedTwiceWithOverload() { var data = new[]{ 1.0, 2.0, 3.0 }; var result = data.AsQueryable() .Expressionify() .Select(x => x.Squared()) .ToList(); result.ShouldBe(new[] { 1.0, 4.0, 9.0 }); } [Test] public void TestMethodWithMultipleArguments() { var data = new[]{ new {a = "1", b = "5"}, new {a = "3", b = "5"}, new {a = "2", b = "5"}, }; var result = data.AsQueryable() .Expressionify() .Select(x => x.a.Pluss(x.b)) .ToList(); result.ShouldBe(new[] { 6, 8, 7 }); } [Test] public void TestMethodCalledMultipleTimes() { var data = new[]{ new {a = "1", b = "5"}, new {a = "2", b = "5"}, new {a = "3", b = "5"} }; var result = data.AsQueryable() .Expressionify() .Select(x => x.a.ToInt() + x.b.ToInt()) .ToList(); result.ShouldBe(new[] { 6, 7, 8 }); } [Test] public void TestExpressionifiedTwice() { var data = new[]{ "1", "2", "3" }; var sw = Stopwatch.StartNew(); data.AsQueryable() .Expressionify() .Select(x => x.ToDouble()) .ToList(); var firstTime = sw.Elapsed; sw.Restart(); data.AsQueryable() .Expressionify() .Select(x => x.ToDouble()) .ToList(); var secondTime = sw.Elapsed; secondTime.ShouldBeLessThan(firstTime); } [Test] public void TestGenericExpression() { var data = new IThing[] { new Thing1(), new Thing2(), new Thing1() }; var result = data.AsQueryable() .Expressionify() .Select(x => x.GetName()) .ToList(); result.ShouldBe(new[] { "Thing1", "Thing2", "Thing1" }); } [Test] public void TestGenericType() { var data = new string[] { "1", "2", "3" }; var result = data.AsQueryable() .Expressionify() .Select(x => GenericClass.Foo(x)) .ToList(); result.ShouldBe(new[] { 8, 8, 8 }); } } }