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<<EOF' >> $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** <sub><sup>2025-11-19 ([ba0427f...ba0427f](https://github.com/ClaveConsulting/Expressionify/compare/ba0427ffd6c2453538092f8ea503ba8af9499ba4...ba0427ffd6c2453538092f8ea503ba8af9499ba4?diff=split))</sup></sub>
### 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<string, object?\>\)
\- 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 <molinetf@medgate\.ch\>
<br>
## **9.1.0** <sub><sup>2025-03-21 ([579137a...579137a](https://github.com/ClaveConsulting/Expressionify/compare/579137a7550db48071b2a568718011031d3a3244...579137a7550db48071b2a568718011031d3a3244?diff=split))</sup></sub>
### Features
- handle nullable propagation expression in arguments ([579137a](https://github.com/ClaveConsulting/Expressionify/commit/579137a7550db48071b2a568718011031d3a3244))
<br>
## **9.0.1** <sub><sup>2025-03-21 ([a928912...a928912](https://github.com/ClaveConsulting/Expressionify/compare/a928912b9559dc3963cb1f2be47abf196b7ea991...a928912b9559dc3963cb1f2be47abf196b7ea991?diff=split))</sup></sub>
*no relevant changes*
<br>
## **9.0.0** <sub><sup>2024-12-27 ([0a25faa...ab25144](https://github.com/ClaveConsulting/Expressionify/compare/0a25faa81d7fef929450b4548bef80c7784b7869...ab251447ae87ffcca3c4c8d9f7d0492a63dcb604?diff=split))</sup></sub>
### BREAKING CHANGES
- supports EF9 ([ab25144](https://github.com/ClaveConsulting/Expressionify/commit/ab251447ae87ffcca3c4c8d9f7d0492a63dcb604))
\-\-\-\-\-\-\-\-\-
Co\-authored\-by: Fabien Molinet <molinetf@medgate\.ch\>
<br>
## **6.7.1** <sub><sup>2024-10-31 ([ad5c447...ddc49b2](https://github.com/ClaveConsulting/Expressionify/compare/ad5c4470f9eb8bc91284c556f719f01b6d0dab49...ddc49b22ebb0feeb77c6c4c7b460117a4a33ef74?diff=split))</sup></sub>
*no relevant changes*
<br>
## **6.7.0** <sub><sup>2022-12-14 ([50641f0...3fae05d](https://github.com/ClaveConsulting/Expressionify/compare/50641f0924d179f8c6cceb0ab1c1eea473ac9428...3fae05d19585f2ffa7b23d533c4ab16d98a61f10?diff=split))</sup></sub>
### 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))
<br>
## **6.6.4** <sub><sup>2024-11-11 ([2658a0f...2658a0f](https://github.com/ClaveConsulting/Expressionify/compare/2658a0f86c3062e60e2391e43e25fcd690bbfe4f...2658a0f86c3062e60e2391e43e25fcd690bbfe4f?diff=split))</sup></sub>
*no relevant changes*
<br>
## **6.6.3** <sub><sup>2023-07-16 ([4c34cb9...4c34cb9](https://github.com/ClaveConsulting/Expressionify/compare/4c34cb964e517ec5609cc820d969011c7359c447...4c34cb964e517ec5609cc820d969011c7359c447?diff=split))</sup></sub>
*no relevant changes*
<br>
## **6.6.2** <sub><sup>2022-12-14 ([174ff0d...753ab7a](https://github.com/ClaveConsulting/Expressionify/compare/174ff0d...753ab7a?diff=split))</sup></sub>
*no relevant changes*
## **6.6.1** <sub><sup>2022-12-12 ([6ae459e...2f5c15f](https://github.com/ClaveConsulting/Expressionify/compare/6ae459e...2f5c15f?diff=split))</sup></sub>
### 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** <sub><sup>2022-11-04 ([e0c50b9...118d2eb](https://github.com/ClaveConsulting/Expressionify/compare/e0c50b9...118d2eb?diff=split))</sup></sub>
### 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** <sub><sup>2022-04-29 ([b8b62c1...b8b62c1](https://github.com/ClaveConsulting/Expressionify/compare/b8b62c1...b8b62c1?diff=split))</sup></sub>
### Features
* Use QueryCompiler to support Include\(\) ([b8b62c1](https://github.com/ClaveConsulting/Expressionify/commit/b8b62c1))
## **6.4.1** <sub><sup>2022-04-29 ([993a4c7...7e03b8c](https://github.com/ClaveConsulting/Expressionify/compare/993a4c7...7e03b8c?diff=split))</sup></sub>
### 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** <sub><sup>2022-04-29 ([c8a8dce...01bb336](https://github.com/ClaveConsulting/Expressionify/compare/c8a8dce...01bb336?diff=split))</sup></sub>
### 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** <sub><sup>2022-04-21 ([c8a8dce...c8a8dce](https://github.com/ClaveConsulting/Expressionify/compare/c8a8dce...c8a8dce?diff=split))</sup></sub>
### 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
================================================
<Project>
<Target Name="Wipe" AfterTargets="Clean">
<RemoveDir Directories="$(TargetDir)" /> <!-- bin -->
<RemoveDir Directories="$(ProjectDir)$(BaseIntermediateOutputPath)" /> <!-- obj -->
</Target>
</Project>
================================================
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
[][1] [][1] [][2] [][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
<ItemGroup>
<PackageReference Include="Clave.Expressionify" Version="6.6.0" />
<PackageReference Include="Clave.Expressionify.Generator" Version="6.6.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
```
## 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<MyDbContext>(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
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<DebugType>embedded</DebugType>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
</ItemGroup>
<PropertyGroup>
<Title>Clave.Expressionify</Title>
<PackageIconUrl>https://raw.githubusercontent.com/ClaveConsulting/logo/master/png/logo_noText.png</PackageIconUrl>
<RepositoryUrl>https://github.com/ClaveConsulting/Expressionify</RepositoryUrl>
<PackageProjectUrl>https://github.com/ClaveConsulting/Expressionify</PackageProjectUrl>
<Authors>Clave Consulting</Authors>
<Description>Use extension methods in Entity Framework Core queries</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
</Project>
================================================
FILE: src/Clave.Expressionify/DbContextOptionsExtensions.cs
================================================
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Clave.Expressionify
{
public static class DbContextOptionsExtensions
{
/// <summary>
/// Use Expressionify within your queries.
/// Transforms your expressions by replacing any [Expressionify] extension methods with the expressionised versions of those methods.
/// </summary>
public static DbContextOptionsBuilder<TContext> UseExpressionify<TContext>(this DbContextOptionsBuilder<TContext> optionsBuilder, Action<ExpressionifyDbContextOptionsBuilder>? expressionifyOptionsAction = null)
where TContext : DbContext
{
return (DbContextOptionsBuilder<TContext>)UseExpressionify((DbContextOptionsBuilder)optionsBuilder, expressionifyOptionsAction);
}
/// <summary>
/// Use Expressionify within your queries.
/// Transforms your expressions by replacing any [Expressionify] extension methods with the expressionised versions of those methods.
/// </summary>
public static DbContextOptionsBuilder UseExpressionify(this DbContextOptionsBuilder optionsBuilder, Action<ExpressionifyDbContextOptionsBuilder>? 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<ExpressionifyDbContextOptionsExtension>()
?? new ExpressionifyDbContextOptionsExtension();
}
}
================================================
FILE: src/Clave.Expressionify/ExpressionEvaluationMode.cs
================================================
namespace Clave.Expressionify;
public enum ExpressionEvaluationMode
{
/// <summary> Always check for <code>[Expressionify]</code> extension methods when executing a query. </summary>
FullCompatibilityButSlow = 0,
/// <summary>
/// Use the EF compiled query cache and only check for <code>[Expressionify]</code> extension methods when a query gets cached.<br/>
/// Not all queries work with this mode enabled. For those queries who don't, you get an <code>InvalidOperationException</code> and you need
/// to call <code>query.Expressionify()</code> explicitly.<br/>
/// This is the case for <code>[Expressionify]</code>-methods that introduce new query-parameters either directly, or indirectly via an EF optimization.
/// </summary>
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<T> : IQueryable<T>, IOrderedQueryable<T>, IAsyncEnumerable<T>
{
private readonly ExpressionableQueryProvider _provider;
public ExpressionableQuery(ExpressionableQueryProvider provider, Expression expression)
{
_provider = provider;
Expression = expression;
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return _provider.ExecuteQuery<T>(Expression).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _provider.ExecuteQuery<T>(Expression).GetEnumerator();
}
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
return _provider.ExecuteQueryAsync<T>(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 = "<Pending>")]
public class ExpressionableQueryCompiler : IQueryCompiler
{
private readonly IQueryCompiler _decoratedCompiler;
public ExpressionableQueryCompiler(IQueryCompiler decoratedCompiler)
{
_decoratedCompiler = decoratedCompiler;
}
public Func<QueryContext, TResult> CreateCompiledAsyncQuery<TResult>(Expression query) => _decoratedCompiler.CreateCompiledAsyncQuery<TResult>(Visit(query));
public Func<QueryContext, TResult> CreateCompiledQuery<TResult>(Expression query) => _decoratedCompiler.CreateCompiledQuery<TResult>(Visit(query));
public TResult Execute<TResult>(Expression query) => _decoratedCompiler.Execute<TResult>(Visit(query));
public TResult ExecuteAsync<TResult>(Expression query, CancellationToken cancellationToken) => _decoratedCompiler.ExecuteAsync<TResult>(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<Func<QueryContext, TResult>> PrecompileQuery<TResult>(Expression query, bool async) => _decoratedCompiler.PrecompileQuery<TResult>(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<TElement> CreateQuery<TElement>(Expression expression) => new ExpressionableQuery<TElement>(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<IQueryable>(this, expression);
}
catch (System.Reflection.TargetInvocationException e)
{
throw e.InnerException ?? e;
}
}
internal IEnumerable<T> ExecuteQuery<T>(Expression expression) => _underlyingQueryProvider.CreateQuery<T>(Visit(expression)).AsEnumerable();
internal IAsyncEnumerable<T> ExecuteQueryAsync<T>(Expression expression) => _underlyingQueryProvider.CreateQuery<T>(Visit(expression)).AsAsyncEnumerable();
public TResult Execute<TResult>(Expression expression) => _underlyingQueryProvider.Execute<TResult>(Visit(expression));
public object? Execute(Expression expression) => _underlyingQueryProvider.Execute(Visit(expression));
public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken = default)
{
if (_underlyingQueryProvider is IAsyncQueryProvider provider)
{
return provider.ExecuteAsync<TResult>(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));
/// <summary>
/// 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.
/// </summary>
/// <param name="setAction">An action to set the option.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
private ExpressionifyDbContextOptionsBuilder WithOption(Func<ExpressionifyDbContextOptionsExtension, ExpressionifyDbContextOptionsExtension> setAction)
{
var extension = setAction(_optionsBuilder.Options.FindExtension<ExpressionifyDbContextOptionsExtension>()!);
((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 = "<Pending>")]
public void ApplyServices(IServiceCollection services)
{
if (EvaluationMode == ExpressionEvaluationMode.FullCompatibilityButSlow)
AddDecorator<IQueryCompiler, ExpressionableQueryCompiler>(services);
else if (EvaluationMode == ExpressionEvaluationMode.LimitedCompatibilityButCached)
AddDecorator<IQueryTranslationPreprocessorFactory, ExpressionifyQueryTranslationPreprocessorFactory>(services);
else
throw new NotSupportedException($"Unsupported {nameof(EvaluationMode)}");
}
private static void AddDecorator<TService, TDecorator>(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<string, string> 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
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">the type of the queryable</typeparam>
/// <param name="source">The input queryable</param>
/// <returns>A queryable which has any of the tagged extension methods replaced.</returns>
public static IQueryable<T> Expressionify<T>(this IQueryable<T> source)
{
if (source is ExpressionableQuery<T> result)
{
return result;
}
return new ExpressionableQueryProvider(source.Provider).CreateQuery<T>(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<T>(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 = "<Pending>")]
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<string, object?> 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<string, object?>)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 = "<Pending>")]
private class ThrowOnParameterAccess : Dictionary<string, object?>
{
// 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<MethodInfo, LambdaExpression?> MethodToExpressionMap = new ConcurrentDictionary<MethodInfo, LambdaExpression?>();
private readonly Dictionary<ParameterExpression, Expression> _replacements = new Dictionary<ParameterExpression, Expression>();
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<object>());
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<Expression> 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
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<DebugType>embedded</DebugType>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<!-- Generates a package at build -->
<IncludeBuildOutput>false</IncludeBuildOutput>
<!-- Do not include the generator as a lib dependency -->
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<!-- Mark as development dependency and include PrivateAssets/IncludeAssets when installing package -->
<DevelopmentDependency>true</DevelopmentDependency>
</PropertyGroup>
<PropertyGroup>
<Title>Clave.Expressionify.Generator</Title>
<PackageIconUrl>https://raw.githubusercontent.com/ClaveConsulting/logo/master/png/logo_noText.png</PackageIconUrl>
<RepositoryUrl>https://github.com/ClaveConsulting/Expressionify</RepositoryUrl>
<PackageProjectUrl>https://github.com/ClaveConsulting/Expressionify</PackageProjectUrl>
<Authors>Clave Consulting</Authors>
<Description>Use extension methods in Entity Framework Core queries</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<!-- Package the generator in the analyzer directory of the nuget package -->
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="_._" Pack="true" PackagePath="lib/netstandard2.0" Visible="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" Version="4.1.0" PrivateAssets="all" />
</ItemGroup>
</Project>
================================================
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<DiagnosticDescriptor> 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<string> 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<Document> FixMissingStatic(Document contextDocument, SyntaxNode root, MethodDeclarationSyntax method)
{
return Task.FromResult(contextDocument.WithSyntaxRoot(root.ReplaceNode(method, method.AddModifiers(Token(SyntaxKind.StaticKeyword)))));
}
private static Task<Document> 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<TypeDeclarationSyntax> Tail)? Path)
{
public static Expressioned Create(MethodDeclarationSyntax m, Compilation compilation) => new(
m,
m.ToExpressionMethod(compilation),
m.Ancestors().OfType<TypeDeclarationSyntax>().Reverse().HeadAndTail());
}
private static void Execute(GeneratorExecutionContext context, IEnumerable<MethodDeclarationSyntax> methods)
{
try
{
var i = 0;
static MemberDeclarationSyntax[] Group(IEnumerable<Expressioned> 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<TypeDeclarationSyntax> Types = new HashSet<TypeDeclarationSyntax>();
public readonly HashSet<MethodDeclarationSyntax> Methods = new HashSet<MethodDeclarationSyntax>();
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<TypeDeclarationSyntax>().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<T> Tail)? HeadAndTail<T>(this IEnumerable<T> enumerable) => enumerable.GetEnumerator().HeadAndTail();
public static (T Head, IEnumerator<T> Tail)? HeadAndTail<T>(this IEnumerator<T> 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<TypeDeclarationSyntax>().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<MemberDeclarationSyntax> members)
=> TypeDeclaration(type.Kind(), type.Identifier)
.WithModifiers(type.Modifiers)
.WithTypeParameterList(type.TypeParameterList)
.AddMembers(members.ToArray());
public static SyntaxNode WithOnlyTheseTypes(this SyntaxNode root, IEnumerable<MemberDeclarationSyntax> members)
{
var namespaceName = root.DescendantNodes()
.OfType<NamespaceDeclarationSyntax>()
.FirstOrDefault()?.Name
?? root.DescendantNodes()
.OfType<FileScopedNamespaceDeclarationSyntax>()
.FirstOrDefault().Name;
var usings = root.DescendantNodes()
.OfType<UsingDirectiveSyntax>()
.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<TypeSyntax> 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;
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit
{
}
}
================================================
FILE: src/Clave.Expressionify.Generator/_._
================================================
================================================
FILE: tests/Clave.Expressionify.Generator.Tests/Clave.Expressionify.Generator.Tests.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing.NUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.NUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.NUnit" Version="1.1.1" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../src/Clave.Expressionify.Generator/Clave.Expressionify.Generator.csproj" />
</ItemGroup>
</Project>
================================================
FILE: tests/Clave.Expressionify.Generator.Tests/CodeFixTests.cs
================================================
using System.Threading.Tasks;
using NUnit.Framework;
using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.CodeFixVerifier<Clave.Expressionify.Generator.ExpressionifyAnalyzer, Clave.Expressionify.Generator.ExpressionifyCodeFixProvider>;
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<Clave.Expressionify.Generator.ExpressionifySourceGenerator>;
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<System.Func<int, int>> 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<System.Func<int, int>> 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<System.Func<int, int>> Foo_Expressionify_0() => (int x) => 8;
public partial class Nested
{
private static System.Linq.Expressions.Expression<System.Func<int, int>> 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<System.Func<int, string?>> 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<System.Func<int, string>> 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>(T x) => ""bar"";
}
}",
@"#nullable enable
namespace ConsoleApplication1
{
public partial class Extensions
{
private static System.Linq.Expressions.Expression<System.Func<T, string>> Foo_Expressionify_0<T>() => (T x) => ""bar"";
}
}", TestName = "Generic extension method")]
[TestCase(@"namespace ConsoleApplication1
{
public partial class Extensions
{
[Expressionify]
public static string Foo<T>(T x) where T : System.Collections.IEnumerable => ""bar"";
}
}",
@"#nullable enable
namespace ConsoleApplication1
{
public partial class Extensions
{
private static System.Linq.Expressions.Expression<System.Func<T, string>> Foo_Expressionify_0<T>()
where T : System.Collections.IEnumerable => (T x) => ""bar"";
}
}", TestName = "Generic extension method with constraints")]
[TestCase(@"namespace ConsoleApplication1
{
public partial class Extensions<T>
{
[Expressionify]
public static string Foo(T x) => ""bar"";
}
}",
@"#nullable enable
namespace ConsoleApplication1
{
public partial class Extensions<T>
{
private static System.Linq.Expressions.Expression<System.Func<T, string>> 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<System.Func<DateTime?, int?>> GetYear_Expressionify_0() => (DateTime? x) => x!.Value.Year;
private static System.Linq.Expressions.Expression<System.Func<DateTime?, string?>> GetYearString_Expressionify_0() => (DateTime? x) => (x!.Value.AddDays(1).Year)!.ToString();
private static System.Linq.Expressions.Expression<System.Func<byte[]?, byte?>> First_Expressionify_0() => (byte[]? x) => x![0];
private static System.Linq.Expressions.Expression<System.Func<byte[]?, string?>> FirstString_Expressionify_0() => (byte[]? x) => x![0].ToString();
private static System.Linq.Expressions.Expression<System.Func<DateTime? []?, int?>> FirstYear_Expressionify_0() => (DateTime? []? x) => x![0]!.Value.Year;
private static System.Linq.Expressions.Expression<System.Func<DateTime?, int?>> 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<TSourceGenerator>
where TSourceGenerator : ISourceGenerator, new()
{
public class Test : CSharpSourceGeneratorTest<TSourceGenerator, NUnitVerifier>
{
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<string, ReportDiagnostic> 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
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="Shouldly" Version="4.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../src/Clave.Expressionify/Clave.Expressionify.csproj" />
<ProjectReference Include="../../src/Clave.Expressionify.Generator/Clave.Expressionify.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
================================================
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<TestEntity> 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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<ExpressionifyDbContextOptionsExtension>()!;
var debugInfo = new Dictionary<string, string>();
extension.Info.PopulateDebugInfo(debugInfo);
debugInfo["Expressionify:EvaluationMode"].ShouldBe(mode.ToString());
}
[Test]
public void UseExpressionify_EvaluationMode_DefaultsToLimitedCompatibilityButCached()
{
var options = GetOptions();
var extension = options.FindExtension<ExpressionifyDbContextOptionsExtension>()!;
var debugInfo = new Dictionary<string, string>();
extension.Info.PopulateDebugInfo(debugInfo);
debugInfo["Expressionify:EvaluationMode"].ShouldBe(ExpressionEvaluationMode.LimitedCompatibilityButCached.ToString());
}
private DbContextOptions GetOptions(Action<ExpressionifyDbContextOptionsBuilder>? optionsAction = null, bool useExpressionify = true)
{
var builder = new DbContextOptionsBuilder<TestDbContext>().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<T>(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<string> 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<T>
{
[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<object>()) as Expression<Func<int, int>>;
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<object>()) as Expression<Func<string, Record1>>;
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<object>()) as Expression<Func<int, int>>;
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<object>()) as Expression<Func<string, int>>;
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<object>()) as Expression<Func<string, int>>;
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<object>()) as Expression<Func<IEnumerable<string>, 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<string>.Foo(x))
.ToList();
result.ShouldBe(new[] { 8, 8, 8 });
}
}
}
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
SYMBOL INDEX (204 symbols across 38 files)
FILE: src/Clave.Expressionify.Generator/ExpressionifyAnalyzer.cs
class ExpressionifyAnalyzer (line 10) | [DiagnosticAnalyzer(LanguageNames.CSharp)]
method Initialize (line 46) | public override void Initialize(AnalysisContext context)
method Analyze (line 53) | private static void Analyze(SyntaxNodeAnalysisContext context)
FILE: src/Clave.Expressionify.Generator/ExpressionifyCodeFixProvider.cs
class ExpressionifyCodeFixProvider (line 16) | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Expressionify...
method GetFixAllProvider (line 23) | public sealed override FixAllProvider GetFixAllProvider() => WellKnown...
method RegisterCodeFixesAsync (line 25) | public sealed override async Task RegisterCodeFixesAsync(CodeFixContex...
method FixMissingStatic (line 54) | private static Task<Document> FixMissingStatic(Document contextDocumen...
method FixMissingPartial (line 59) | private static Task<Document> FixMissingPartial(Document contextDocume...
FILE: src/Clave.Expressionify.Generator/ExpressionifySourceGenerator.cs
class ExpressionifySourceGenerator (line 14) | [Generator]
method Initialize (line 19) | public void Initialize(GeneratorInitializationContext context)
method Execute (line 24) | public void Execute(GeneratorExecutionContext context)
type Expressioned (line 30) | private record Expressioned(
method Execute (line 41) | private static void Execute(GeneratorExecutionContext context, IEnumer...
class ExpressionifySyntaxReceiver (line 79) | private class ExpressionifySyntaxReceiver : ISyntaxReceiver
method OnVisitSyntaxNode (line 85) | public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
FILE: src/Clave.Expressionify.Generator/Extensions.cs
class Extensions (line 5) | internal static class Extensions
method HeadAndTail (line 7) | public static (T Head, IEnumerator<T> Tail)? HeadAndTail<T>(this IEnum...
method HeadAndTail (line 9) | public static (T Head, IEnumerator<T> Tail)? HeadAndTail<T>(this IEnum...
FILE: src/Clave.Expressionify.Generator/Internals/Checks.cs
class Checks (line 8) | public static class Checks
method HasExpressionifyAttribute (line 10) | public static bool HasExpressionifyAttribute(this MethodDeclarationSyn...
method IsStatic (line 13) | public static bool IsStatic(this MethodDeclarationSyntax method) =>
method Includes (line 16) | public static bool Includes(this SyntaxTokenList modifiers, SyntaxKind...
method HasExpressionBody (line 19) | public static bool HasExpressionBody(this MethodDeclarationSyntax meth...
method FindAncestorMissingPartialKeyword (line 22) | public static TypeDeclarationSyntax? FindAncestorMissingPartialKeyword...
FILE: src/Clave.Expressionify.Generator/Internals/ClassGenerator.cs
class ClassGenerator (line 9) | public static class ClassGenerator
method WithOnlyTheseMembers (line 11) | public static TypeDeclarationSyntax WithOnlyTheseMembers(this TypeDecl...
method WithOnlyTheseTypes (line 17) | public static SyntaxNode WithOnlyTheseTypes(this SyntaxNode root, IEnu...
FILE: src/Clave.Expressionify.Generator/Internals/ExpressionRewriter.cs
class ExpressionRewriter (line 8) | internal sealed class ExpressionRewriter : CSharpSyntaxRewriter
method ExpressionRewriter (line 13) | public ExpressionRewriter(Compilation compilation, SyntaxNode root)
method VisitExpression (line 19) | public ExpressionSyntax VisitExpression(ExpressionSyntax expression) =>
method Visit (line 22) | public override SyntaxNode Visit(SyntaxNode node)
method VisitConditionalAccessExpression (line 37) | public override SyntaxNode? VisitConditionalAccessExpression(Condition...
FILE: src/Clave.Expressionify.Generator/Internals/PropertyGenerator.cs
class PropertyGenerator (line 9) | public static class PropertyGenerator
method GeneratedName (line 11) | public static MethodDeclarationSyntax GeneratedName(this MethodDeclara...
method ToExpressionMethod (line 14) | public static MethodDeclarationSyntax ToExpressionMethod(
method GetBody (line 25) | private static ParenthesizedLambdaExpressionSyntax GetBody(
method GetExpressionType (line 37) | private static QualifiedNameSyntax GetExpressionType(MethodDeclaration...
method Func (line 45) | private static QualifiedNameSyntax Func(SeparatedSyntaxList<TypeSyntax...
method Expression (line 52) | private static QualifiedNameSyntax Expression(TypeSyntax genericPart) =>
FILE: src/Clave.Expressionify.Generator/IsExternalInit.cs
class IsExternalInit (line 8) | [EditorBrowsable(EditorBrowsableState.Never)]
FILE: src/Clave.Expressionify/DbContextOptionsExtensions.cs
class DbContextOptionsExtensions (line 7) | public static class DbContextOptionsExtensions
method UseExpressionify (line 13) | public static DbContextOptionsBuilder<TContext> UseExpressionify<TCont...
method UseExpressionify (line 23) | public static DbContextOptionsBuilder UseExpressionify(this DbContextO...
method GetOrCreateExtension (line 33) | private static ExpressionifyDbContextOptionsExtension GetOrCreateExten...
FILE: src/Clave.Expressionify/ExpressionEvaluationMode.cs
type ExpressionEvaluationMode (line 3) | public enum ExpressionEvaluationMode
FILE: src/Clave.Expressionify/ExpressionableQuery.cs
class ExpressionableQuery (line 10) | public class ExpressionableQuery<T> : IQueryable<T>, IOrderedQueryable<T...
method ExpressionableQuery (line 14) | public ExpressionableQuery(ExpressionableQueryProvider provider, Expre...
method GetEnumerator (line 20) | IEnumerator<T> IEnumerable<T>.GetEnumerator()
method GetEnumerator (line 25) | IEnumerator IEnumerable.GetEnumerator()
method GetAsyncEnumerator (line 30) | public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancel...
FILE: src/Clave.Expressionify/ExpressionableQueryCompiler.cs
class ExpressionableQueryCompiler (line 9) | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Intern...
method ExpressionableQueryCompiler (line 14) | public ExpressionableQueryCompiler(IQueryCompiler decoratedCompiler)
method CreateCompiledAsyncQuery (line 18) | public Func<QueryContext, TResult> CreateCompiledAsyncQuery<TResult>(E...
method CreateCompiledQuery (line 20) | public Func<QueryContext, TResult> CreateCompiledQuery<TResult>(Expres...
method Execute (line 22) | public TResult Execute<TResult>(Expression query) => _decoratedCompile...
method ExecuteAsync (line 24) | public TResult ExecuteAsync<TResult>(Expression query, CancellationTok...
method Visit (line 26) | private static Expression Visit(Expression exp) => new ExpressionifyVi...
method PrecompileQuery (line 29) | public Expression<Func<QueryContext, TResult>> PrecompileQuery<TResult...
FILE: src/Clave.Expressionify/ExpressionableQueryProvider.cs
class ExpressionableQueryProvider (line 11) | public class ExpressionableQueryProvider : IAsyncQueryProvider
method ExpressionableQueryProvider (line 15) | public ExpressionableQueryProvider(IQueryProvider underlyingQueryProvi...
method CreateQuery (line 20) | public IQueryable<TElement> CreateQuery<TElement>(Expression expressio...
method CreateQuery (line 22) | public IQueryable CreateQuery(Expression expression)
method ExecuteQuery (line 40) | internal IEnumerable<T> ExecuteQuery<T>(Expression expression) => _und...
method ExecuteQueryAsync (line 42) | internal IAsyncEnumerable<T> ExecuteQueryAsync<T>(Expression expressio...
method Execute (line 44) | public TResult Execute<TResult>(Expression expression) => _underlyingQ...
method Execute (line 46) | public object? Execute(Expression expression) => _underlyingQueryProvi...
method ExecuteAsync (line 48) | public TResult ExecuteAsync<TResult>(Expression expression, Cancellati...
method Visit (line 58) | private static Expression Visit(Expression exp) => new ExpressionifyVi...
FILE: src/Clave.Expressionify/ExpressionifyAttribute.cs
class ExpressionifyAttribute (line 5) | [AttributeUsage(AttributeTargets.Method)]
FILE: src/Clave.Expressionify/ExpressionifyDbContextOptionsBuilder.cs
class ExpressionifyDbContextOptionsBuilder (line 7) | public class ExpressionifyDbContextOptionsBuilder
method ExpressionifyDbContextOptionsBuilder (line 11) | internal ExpressionifyDbContextOptionsBuilder(DbContextOptionsBuilder ...
method WithEvaluationMode (line 13) | public ExpressionifyDbContextOptionsBuilder WithEvaluationMode(Express...
method WithOption (line 21) | private ExpressionifyDbContextOptionsBuilder WithOption(Func<Expressio...
FILE: src/Clave.Expressionify/ExpressionifyDbContextOptionsExtension.cs
class ExpressionifyDbContextOptionsExtension (line 12) | public class ExpressionifyDbContextOptionsExtension : IDbContextOptionsE...
method ExpressionifyDbContextOptionsExtension (line 14) | public ExpressionifyDbContextOptionsExtension()
method ExpressionifyDbContextOptionsExtension (line 17) | public ExpressionifyDbContextOptionsExtension(ExpressionifyDbContextOp...
method ApplyServices (line 25) | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Inte...
method AddDecorator (line 38) | private static void AddDecorator<TService, TDecorator>(IServiceCollect...
method Validate (line 59) | public void Validate(IDbContextOptions options)
method WithEvaluationMode (line 64) | public ExpressionifyDbContextOptionsExtension WithEvaluationMode(Expre...
method Clone (line 71) | private ExpressionifyDbContextOptionsExtension Clone() => new(this);
class ExtensionInfo (line 73) | private class ExtensionInfo : DbContextOptionsExtensionInfo
method ExtensionInfo (line 77) | public ExtensionInfo(ExpressionifyDbContextOptionsExtension extensio...
method GetServiceProviderHashCode (line 85) | public override int GetServiceProviderHashCode()
method ShouldUseSameServiceProvider (line 91) | public override bool ShouldUseSameServiceProvider(DbContextOptionsEx...
method PopulateDebugInfo (line 98) | public override void PopulateDebugInfo(IDictionary<string, string> d...
FILE: src/Clave.Expressionify/ExpressionifyExtension.cs
class ExpressionifyExtension (line 7) | public static class ExpressionifyExtension
method Expressionify (line 17) | public static IQueryable<T> Expressionify<T>(this IQueryable<T> source)
method MatchesTypeOf (line 27) | internal static bool MatchesTypeOf(this MethodInfo property, MethodInf...
method CreateInstance (line 35) | internal static T CreateInstance<T>(this Type type, params object?[]? ...
FILE: src/Clave.Expressionify/ExpressionifyQueryTranslationPreprocessor.cs
class ExpressionifyQueryTranslationPreprocessor (line 9) | public class ExpressionifyQueryTranslationPreprocessor : QueryTranslatio...
method ExpressionifyQueryTranslationPreprocessor (line 13) | public ExpressionifyQueryTranslationPreprocessor(
method Process (line 22) | public override Expression Process(Expression query)
method EvaluateExpression (line 33) | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Inte...
class ThrowOnParameterAccess (line 64) | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Inte...
method CreateException (line 72) | private static InvalidOperationException CreateException()
method Add (line 83) | public new void Add(string key, object? value)
method TryAdd (line 86) | public new bool TryAdd(string key, object? value)
method TryGetValue (line 89) | public new bool TryGetValue(string key, out object? value)
method ContainsKey (line 92) | public new bool ContainsKey(string key)
FILE: src/Clave.Expressionify/ExpressionifyQueryTranslationPreprocessorFactory.cs
class ExpressionifyQueryTranslationPreprocessorFactory (line 5) | public class ExpressionifyQueryTranslationPreprocessorFactory : IQueryTr...
method ExpressionifyQueryTranslationPreprocessorFactory (line 10) | public ExpressionifyQueryTranslationPreprocessorFactory(IQueryTranslat...
method Create (line 16) | public QueryTranslationPreprocessor Create(QueryCompilationContext que...
FILE: src/Clave.Expressionify/ExpressionifyVisitor.cs
class ExpressionifyVisitor (line 10) | public class ExpressionifyVisitor : ExpressionVisitor
method VisitMethodCall (line 18) | protected override Expression VisitMethodCall(MethodCallExpression node)
method GetMethodExpression (line 32) | private static object? GetMethodExpression(MethodInfo method)
method GetFullName (line 75) | private static string GetFullName(Type? type)
method VisitParameter (line 85) | protected override Expression VisitParameter(ParameterExpression node)
method RegisterReplacementParameters (line 92) | private void RegisterReplacementParameters(IReadOnlyCollection<Express...
method UnregisterReplacementParameters (line 103) | private void UnregisterReplacementParameters(LambdaExpression expressi...
FILE: tests/Clave.Expressionify.Generator.Tests/CodeFixTests.cs
class CodeFixTests (line 7) | public class CodeFixTests
method TestNothing (line 9) | [Test]
method TestOkMethod (line 17) | [Test]
method TestWithoutNamespace (line 36) | [Test]
method TestWithFileScopedNamespace (line 54) | [Test]
method TestMissingStatic (line 73) | [Test]
method TestNotExpressionBody (line 109) | [Test]
method TestNotInPartialClass (line 132) | [Test]
FILE: tests/Clave.Expressionify.Generator.Tests/CodeGeneratorTests.cs
class CodeGeneratorTests (line 10) | [TestFixture]
method TestGenerator (line 25) | [TestCase(@"namespace ConsoleApplication1
method VerifyGenerated (line 235) | public async Task VerifyGenerated(string source, string generated)
FILE: tests/Clave.Expressionify.Generator.Tests/Verifiers/CSharpSourceGeneratorVerifier.cs
class CSharpSourceGeneratorVerifier (line 10) | public static class CSharpSourceGeneratorVerifier<TSourceGenerator>
class Test (line 13) | public class Test : CSharpSourceGeneratorTest<TSourceGenerator, NUnitV...
method Test (line 15) | public Test()
method CreateCompilationOptions (line 19) | protected override CompilationOptions CreateCompilationOptions()
method GetNullableWarningsFromCompiler (line 28) | private static ImmutableDictionary<string, ReportDiagnostic> GetNull...
method CreateParseOptions (line 37) | protected override ParseOptions CreateParseOptions()
FILE: tests/Clave.Expressionify.Tests/DbContextExtensions/TestDbContext.cs
class TestDbContext (line 5) | public class TestDbContext : DbContext
method TestDbContext (line 7) | public TestDbContext(DbContextOptions options) : base(options)
FILE: tests/Clave.Expressionify.Tests/DbContextExtensions/TestEntity.cs
class TestEntity (line 5) | public class TestEntity
class TestAddress (line 12) | public class TestAddress
class TestView (line 18) | public class TestView
class TestTimeProvider (line 24) | public static class TestTimeProvider
FILE: tests/Clave.Expressionify.Tests/DbContextExtensions/TestEntityExtensions.cs
class TestEntityExtensions (line 3) | public static partial class TestEntityExtensions
method GetName (line 5) | [Expressionify]
method NameEquals (line 8) | [Expressionify]
method IsJohnDoe (line 11) | [Expressionify]
method IsSomething (line 14) | [Expressionify]
method IsRecent (line 17) | [Expressionify]
method ToTestView (line 20) | [Expressionify]
FILE: tests/Clave.Expressionify.Tests/DbContextExtensions/Tests.cs
class Tests (line 10) | public class Tests
method UseExpressionifyInConfig_ExpandsExpression_CanTranslate (line 12) | [Test]
method UseExpressionifyInQuery_ExpandsExpression_CanTranslate (line 22) | [Test]
method UseExpressionifyInQueryAndConfig_ExpandsExpression_CanTranslate (line 32) | [Test]
method DontUseExpressionify_EfSelectsWholeEntity (line 43) | [Test]
method Expressionify_ShouldHandleWhereWithParameters_AfterExpansion (line 56) | [Test]
method UseExpressionify_ShouldHandleConstants (line 65) | [TestCase(ExpressionEvaluationMode.FullCompatibilityButSlow)]
method UseExpressionify_ShouldHandleWhereWithParameters (line 75) | [TestCase(ExpressionEvaluationMode.FullCompatibilityButSlow)]
method UseExpressionify_EvaluationModeAlways_ShouldHandleWhereWithNewParameters (line 86) | [Test]
method UseExpressionify_EvaluationModeAlways_ShouldHandleWhereWithExternalServices (line 95) | [Test]
method UseExpressionify_EvaluationModeCached_CannotHandleNewParameters (line 104) | [Test]
method UseExpressionify_EvaluationModeCached_CannotHandleParametersFromExternalServices (line 114) | [Test]
method UseExpressionify_ShouldProduceSameOutputAsExpressionify (line 124) | [Test]
method UseExpressionify_ShouldProduceSameOutputAsExpressionify_InAllModes (line 134) | [TestCase(ExpressionEvaluationMode.FullCompatibilityButSlow)]
method UseExpressionify_EvaluationModeAlways_ShouldHandleEvaluatableExpressions (line 147) | [Test]
method UseExpressionify_WithEvaluationMode_SetsEvaluationMode (line 156) | [TestCase(ExpressionEvaluationMode.FullCompatibilityButSlow)]
method UseExpressionify_EvaluationMode_DefaultsToLimitedCompatibilityButCached (line 169) | [Test]
method GetOptions (line 181) | private DbContextOptions GetOptions(Action<ExpressionifyDbContextOptio...
FILE: tests/Clave.Expressionify.Tests/First/ExtensionMethods.cs
class ExtensionMethods (line 6) | public static partial class ExtensionMethods
method ToInt (line 8) | [Expressionify]
method ToDouble (line 11) | [Expressionify]
method Pluss (line 14) | [Expressionify]
method Squared (line 17) | [Expressionify]
method Squared (line 20) | [Expressionify]
method GetName (line 23) | [Expressionify]
FILE: tests/Clave.Expressionify.Tests/Samples/Class1.cs
class Class1 (line 3) | public static partial class Class1
method Foo (line 5) | [Expressionify]
FILE: tests/Clave.Expressionify.Tests/Samples/Class2.cs
class Class2 (line 3) | public static partial class Class2
method Bar (line 5) | public static int Bar(int x) => 0;
FILE: tests/Clave.Expressionify.Tests/Samples/Class3.cs
class Class3 (line 3) | public static partial class Class3
method Foo (line 5) | [Expressionify]
method Foo (line 8) | [Expressionify]
FILE: tests/Clave.Expressionify.Tests/Samples/Class4.cs
class Class4 (line 6) | public static partial class Class4
method Foo (line 8) | [Expressionify]
method Something (line 11) | [Expressionify]
class NestedClass1 (line 14) | public static partial class NestedClass1
method Bar (line 16) | [Expressionify]
FILE: tests/Clave.Expressionify.Tests/Samples/GenericClass.cs
class GenericClass (line 3) | public partial class GenericClass<T>
method Foo (line 5) | [Expressionify]
FILE: tests/Clave.Expressionify.Tests/Samples/IThing.cs
type IThing (line 9) | public interface IThing
class Thing1 (line 14) | public class Thing1 : IThing
class Thing2 (line 19) | public class Thing2 : IThing
FILE: tests/Clave.Expressionify.Tests/Samples/Record1.cs
type Record1 (line 3) | public partial record Record1(string Name)
FILE: tests/Clave.Expressionify.Tests/Second/ExtensionMethods.cs
class ExtensionMethods (line 5) | public static partial class ExtensionMethods
method ToInt (line 7) | [Expressionify]
method ToDouble (line 10) | [Expressionify]
method Pluss (line 13) | [Expressionify]
method Squared (line 16) | [Expressionify]
FILE: tests/Clave.Expressionify.Tests/Tests.cs
class Tests (line 14) | public class Tests
method TestClass (line 16) | [Test]
method TestRecord (line 26) | [Test]
method TestNonExpressionify (line 36) | [Test]
method TestOverload (line 42) | [Test]
method TestMethodGroup (line 60) | [Test]
method TestExpressionifyClass (line 78) | [Test]
method TestExpressionifyNestedClass (line 95) | [Test]
method TestExpressionifyRecord (line 112) | [Test]
method TestMethodParameterUsedTwice (line 129) | [Test]
method TestMethodParameterUsedTwiceWithOverload (line 146) | [Test]
method TestMethodWithMultipleArguments (line 163) | [Test]
method TestMethodCalledMultipleTimes (line 180) | [Test]
method TestExpressionifiedTwice (line 197) | [Test]
method TestGenericExpression (line 223) | [Test]
method TestGenericType (line 241) | [Test]
Condensed preview — 59 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (133K chars).
[
{
"path": ".github/workflows/publish.yml",
"chars": 2241,
"preview": "name: Publish\n\non:\n push:\n branches: [master]\n\njobs:\n build:\n runs-on: ubuntu-latest\n\n steps:\n # Checkou"
},
{
"path": ".github/workflows/pull-request.yml",
"chars": 3919,
"preview": "name: Check Pull-request\n\non: pull_request_target\n\njobs:\n version:\n name: Get version\n outputs:\n version: ${"
},
{
"path": ".github/workflows/test-report.yml",
"chars": 542,
"preview": "name: 'Test Report'\non:\n workflow_run:\n workflows: ['Check Pull-request']\n types:\n - completed\njobs:\n repor"
},
{
"path": ".gitignore",
"chars": 70,
"preview": "bin/\nobj/\n/output\n/nugets\nTestResults/\n/.vs\n*.user\n/temp-changelog.md\n"
},
{
"path": ".vscode/launch.json",
"chars": 1159,
"preview": "{\n // Use IntelliSense to find out which attributes exist for C# debugging\n // Use hover for the description of the "
},
{
"path": ".vscode/settings.json",
"chars": 418,
"preview": "{\n \"dotnet-test-explorer.testProjectPath\": \"**/*.Tests.csproj\",\n \"editor.codeLens\": true,\n \"csharp.testsCodeLen"
},
{
"path": ".vscode/tasks.json",
"chars": 1352,
"preview": "{\n \"version\": \"2.0.0\",\n \"tasks\": [\n {\n \"label\": \"build\",\n \"command\": \"dotnet\",\n "
},
{
"path": "CHANGELOG.md",
"chars": 8010,
"preview": "## **10.0.0** <sub><sup>2025-11-19 ([ba0427f...ba0427f](https://github.com/ClaveConsulting/Expressionify/compare/ba"
},
{
"path": "Directory.build.targets",
"chars": 222,
"preview": "<Project>\n <Target Name=\"Wipe\" AfterTargets=\"Clean\">\n <RemoveDir Directories=\"$(TargetDir)\" /> <!-- bin -->\n <Rem"
},
{
"path": "Expressionify.sln",
"chars": 6650,
"preview": "\r\nMicrosoft Visual Studio Solution File, Format Version 12.00\r\n# Visual Studio Version 17\r\nVisualStudioVersion = 17.0.3"
},
{
"path": "License.md",
"chars": 1072,
"preview": "The MIT License\n\nCopyright (c) Clave Consulting\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "Readme.md",
"chars": 8735,
"preview": "# Expressionify\n\n[][1] []\r\n public class Expr"
},
{
"path": "src/Clave.Expressionify/ExpressionifyDbContextOptionsBuilder.cs",
"chars": 1308,
"preview": "using System;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\n\nnamespace Clave"
},
{
"path": "src/Clave.Expressionify/ExpressionifyDbContextOptionsExtension.cs",
"chars": 4666,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Microsoft.EntityFrameworkCore.Infrastructure;\n"
},
{
"path": "src/Clave.Expressionify/ExpressionifyExtension.cs",
"chars": 1798,
"preview": "using System;\nusing System.Linq;\nusing System.Reflection;\n\nnamespace Clave.Expressionify\n{\n public static class Expr"
},
{
"path": "src/Clave.Expressionify/ExpressionifyQueryTranslationPreprocessor.cs",
"chars": 4723,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq.Expressions;\nusing Microsoft.EntityFrameworkCore.Quer"
},
{
"path": "src/Clave.Expressionify/ExpressionifyQueryTranslationPreprocessorFactory.cs",
"chars": 945,
"preview": "using Microsoft.EntityFrameworkCore.Query;\n\nnamespace Clave.Expressionify;\n\npublic class ExpressionifyQueryTranslationP"
},
{
"path": "src/Clave.Expressionify/ExpressionifyVisitor.cs",
"chars": 4107,
"preview": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Lin"
},
{
"path": "src/Clave.Expressionify.Generator/AnalyzerReleases.Shipped.md",
"chars": 459,
"preview": "; Shipped analyzer releases\n; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers"
},
{
"path": "src/Clave.Expressionify.Generator/AnalyzerReleases.Unshipped.md",
"chars": 155,
"preview": "; Unshipped analyzer release\n; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzer"
},
{
"path": "src/Clave.Expressionify.Generator/Clave.Expressionify.Generator.csproj",
"chars": 2075,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>netstandard2.0</TargetFramework>\n "
},
{
"path": "src/Clave.Expressionify.Generator/ExpressionifyAnalyzer.cs",
"chars": 3364,
"preview": "using System.Collections.Immutable;\nusing Clave.Expressionify.Generator.Internals;\nusing Microsoft.CodeAnalysis;\nusing "
},
{
"path": "src/Clave.Expressionify.Generator/ExpressionifyCodeFixProvider.cs",
"chars": 2977,
"preview": "using System.Collections.Immutable;\nusing System.Composition;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Mi"
},
{
"path": "src/Clave.Expressionify.Generator/ExpressionifySourceGenerator.cs",
"chars": 4371,
"preview": "using System;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CSharp.Syntax;\nusing Microsoft.CodeAnalysis.Tex"
},
{
"path": "src/Clave.Expressionify.Generator/Extensions.cs",
"chars": 501,
"preview": "using System.Collections.Generic;\n\nnamespace Clave.Expressionify.Generator\n{\n internal static class Extensions\n {"
},
{
"path": "src/Clave.Expressionify.Generator/Internals/Checks.cs",
"chars": 1097,
"preview": "using System.Linq;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CSharp;\nusing Microsoft.CodeAnalysis.CSha"
},
{
"path": "src/Clave.Expressionify.Generator/Internals/ClassGenerator.cs",
"chars": 1394,
"preview": "using System.Collections.Generic;\nusing System.Linq;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CSharp.S"
},
{
"path": "src/Clave.Expressionify.Generator/Internals/ExpressionRewriter.cs",
"chars": 1774,
"preview": "namespace Clave.Expressionify.Generator.Internals;\n\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CSharp;\nu"
},
{
"path": "src/Clave.Expressionify.Generator/Internals/PropertyGenerator.cs",
"chars": 2732,
"preview": "using System.Linq;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CSharp;\nusing Microsoft.CodeAnalysis.CShar"
},
{
"path": "src/Clave.Expressionify.Generator/IsExternalInit.cs",
"chars": 354,
"preview": "namespace System.Runtime.CompilerServices\n{\n using System.ComponentModel;\n /// <summary>\n /// Reserved to be u"
},
{
"path": "src/Clave.Expressionify.Generator/_._",
"chars": 0,
"preview": ""
},
{
"path": "tests/Clave.Expressionify.Generator.Tests/Clave.Expressionify.Generator.Tests.csproj",
"chars": 1358,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <TargetFramework>net10.0</TargetFramework>\n <Lang"
},
{
"path": "tests/Clave.Expressionify.Generator.Tests/CodeFixTests.cs",
"chars": 5644,
"preview": "using System.Threading.Tasks;\nusing NUnit.Framework;\nusing Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.CodeFixV"
},
{
"path": "tests/Clave.Expressionify.Generator.Tests/CodeGeneratorTests.cs",
"chars": 7407,
"preview": "using System;\nusing System.Text;\nusing System.Threading.Tasks;\nusing Microsoft.CodeAnalysis.Text;\nusing NUnit.Framework"
},
{
"path": "tests/Clave.Expressionify.Generator.Tests/Verifiers/CSharpSourceGeneratorVerifier.cs",
"chars": 1756,
"preview": "using System;\nusing System.Collections.Immutable;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CSharp;\nus"
},
{
"path": "tests/Clave.Expressionify.Tests/Clave.Expressionify.Tests.csproj",
"chars": 989,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\r\n\r\n <PropertyGroup>\r\n <TargetFramework>net10.0</TargetFramework>\r\n <"
},
{
"path": "tests/Clave.Expressionify.Tests/DbContextExtensions/TestDbContext.cs",
"chars": 307,
"preview": "using Microsoft.EntityFrameworkCore;\n\nnamespace Clave.Expressionify.Tests.DbContextExtensions\n{\n public class TestDb"
},
{
"path": "tests/Clave.Expressionify.Tests/DbContextExtensions/TestEntity.cs",
"chars": 619,
"preview": "using System;\n\nnamespace Clave.Expressionify.Tests.DbContextExtensions\n{\n public class TestEntity\n {\n publ"
},
{
"path": "tests/Clave.Expressionify.Tests/DbContextExtensions/TestEntityExtensions.cs",
"chars": 1058,
"preview": "namespace Clave.Expressionify.Tests.DbContextExtensions\n{\n public static partial class TestEntityExtensions\n {\n "
},
{
"path": "tests/Clave.Expressionify.Tests/DbContextExtensions/Tests.cs",
"chars": 10108,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Microsoft.EntityFrameworkCore;\nusing NUnit.Fra"
},
{
"path": "tests/Clave.Expressionify.Tests/First/ExtensionMethods.cs",
"chars": 761,
"preview": "using System;\nusing Clave.Expressionify.Tests.Samples;\n\nnamespace Clave.Expressionify.Tests.First\n{\n public static pa"
},
{
"path": "tests/Clave.Expressionify.Tests/Samples/Class1.cs",
"chars": 167,
"preview": "namespace Clave.Expressionify.Tests.Samples\n{\n public static partial class Class1\n {\n [Expressionify]\n "
},
{
"path": "tests/Clave.Expressionify.Tests/Samples/Class2.cs",
"chars": 142,
"preview": "namespace Clave.Expressionify.Tests.Samples\n{\n public static partial class Class2\n {\n public static int Bar"
},
{
"path": "tests/Clave.Expressionify.Tests/Samples/Class3.cs",
"chars": 207,
"preview": "namespace Clave.Expressionify.Tests.Samples;\n\npublic static partial class Class3\n{\n [Expressionify]\n public static"
},
{
"path": "tests/Clave.Expressionify.Tests/Samples/Class4.cs",
"chars": 488,
"preview": "using System.Collections.Generic;\nusing System.Linq;\n\nnamespace Clave.Expressionify.Tests.Samples\n{\n public static pa"
},
{
"path": "tests/Clave.Expressionify.Tests/Samples/GenericClass.cs",
"chars": 172,
"preview": "namespace Clave.Expressionify.Tests.Samples\n{\n public partial class GenericClass<T>\n {\n [Expressionify]\n "
},
{
"path": "tests/Clave.Expressionify.Tests/Samples/IThing.cs",
"chars": 407,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\n\nna"
},
{
"path": "tests/Clave.Expressionify.Tests/Samples/Record1.cs",
"chars": 204,
"preview": "namespace Clave.Expressionify.Tests.Samples\n{\n public partial record Record1(string Name)\n {\n [Expressioni"
},
{
"path": "tests/Clave.Expressionify.Tests/Second/ExtensionMethods.cs",
"chars": 545,
"preview": "using System;\n\nnamespace Clave.Expressionify.Tests.Second\n{\n public static partial class ExtensionMethods\n {\n "
},
{
"path": "tests/Clave.Expressionify.Tests/Tests.cs",
"chars": 7539,
"preview": "using System;\nusing System.Collections.Generic;\nusing System.Diagnostics;\nusing System.Linq;\nusing System.Linq.Expressio"
}
]
About this extraction
This page contains the full source code of the ClaveConsulting/Expressionify GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 59 files (120.4 KB), approximately 29.6k tokens, and a symbol index with 204 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.