Add code
This commit is contained in:
parent
5e54ffb9bf
commit
dfb7faebb6
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
* text=auto eol=lf
|
||||
*.{cmd,[cC][mM][dD]} text eol=crlf
|
||||
*.{bat,[bB][aA][tT]} text eol=crlf
|
||||
512
.gitignore
vendored
Normal file
512
.gitignore
vendored
Normal file
@ -0,0 +1,512 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,dotnetcore,rider
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio,visualstudiocode,dotnetcore,rider
|
||||
|
||||
### DotnetCore ###
|
||||
# .NET Core build folders
|
||||
bin/
|
||||
obj/
|
||||
|
||||
# Common node modules locations
|
||||
/node_modules
|
||||
/wwwroot/node_modules
|
||||
|
||||
### Rider ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
# Support for Project snippet scope
|
||||
.vscode/*.code-snippets
|
||||
|
||||
# Ignore code-workspaces
|
||||
*.code-workspace
|
||||
|
||||
### VisualStudio ###
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
### VisualStudio Patch ###
|
||||
# Additional files built by Visual Studio
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,dotnetcore,rider
|
||||
|
||||
13
.idea/.gitignore
generated
vendored
Normal file
13
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider ignored files
|
||||
/projectSettingsUpdater.xml
|
||||
/.idea.querying.iml
|
||||
/modules.xml
|
||||
/contentModel.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
13
.idea/.idea.Baguette.Querying/.idea/.gitignore
generated
vendored
Normal file
13
.idea/.idea.Baguette.Querying/.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider ignored files
|
||||
/.idea.Baguette.Querying.iml
|
||||
/projectSettingsUpdater.xml
|
||||
/contentModel.xml
|
||||
/modules.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
4
.idea/.idea.Baguette.Querying/.idea/encodings.xml
generated
Normal file
4
.idea/.idea.Baguette.Querying/.idea/encodings.xml
generated
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
4
.idea/.idea.Baguette.Querying/.idea/vcs.xml
generated
Normal file
4
.idea/.idea.Baguette.Querying/.idea/vcs.xml
generated
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings" defaultProject="true" />
|
||||
</project>
|
||||
4
.idea/encodings.xml
generated
Normal file
4
.idea/encodings.xml
generated
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
8
.idea/indexLayout.xml
generated
Normal file
8
.idea/indexLayout.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
45
Baguette.Querying.sln
Normal file
45
Baguette.Querying.sln
Normal file
@ -0,0 +1,45 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Baguette.Querying", "src\Baguette.Querying\Baguette.Querying.csproj", "{2908BE42-FE0E-44D0-9075-285818D639D2}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Baguette.Querying.Generator", "src\Baguette.Querying.Generator\Baguette.Querying.Generator.csproj", "{C9411E00-E0BB-4852-A09A-AB6AB792F25E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Baguette.Querying.Generator.Abstractions", "src\Baguette.Querying.Generator.Abstractions\Baguette.Querying.Generator.Abstractions.csproj", "{C33795B5-7073-4F92-AE2C-71B06455EF78}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Baguette.Querying.Console", "samples\Baguette.Querying.Console\Baguette.Querying.Console.csproj", "{EE47F375-D903-4DE6-8AB6-8A0BB58551AE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Baguette.Querying.EntityFrameworkCore", "src\Baguette.Querying.EntityFrameworkCore\Baguette.Querying.EntityFrameworkCore.csproj", "{5D207E64-77EE-43E7-ADED-793B8773A0C9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{AB466252-3569-4093-BE8A-E86BD46538C2}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2908BE42-FE0E-44D0-9075-285818D639D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2908BE42-FE0E-44D0-9075-285818D639D2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2908BE42-FE0E-44D0-9075-285818D639D2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2908BE42-FE0E-44D0-9075-285818D639D2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C9411E00-E0BB-4852-A09A-AB6AB792F25E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C9411E00-E0BB-4852-A09A-AB6AB792F25E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C9411E00-E0BB-4852-A09A-AB6AB792F25E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C9411E00-E0BB-4852-A09A-AB6AB792F25E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C33795B5-7073-4F92-AE2C-71B06455EF78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C33795B5-7073-4F92-AE2C-71B06455EF78}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C33795B5-7073-4F92-AE2C-71B06455EF78}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C33795B5-7073-4F92-AE2C-71B06455EF78}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE47F375-D903-4DE6-8AB6-8A0BB58551AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE47F375-D903-4DE6-8AB6-8A0BB58551AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE47F375-D903-4DE6-8AB6-8A0BB58551AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE47F375-D903-4DE6-8AB6-8A0BB58551AE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5D207E64-77EE-43E7-ADED-793B8773A0C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5D207E64-77EE-43E7-ADED-793B8773A0C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5D207E64-77EE-43E7-ADED-793B8773A0C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5D207E64-77EE-43E7-ADED-793B8773A0C9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{EE47F375-D903-4DE6-8AB6-8A0BB58551AE} = {AB466252-3569-4093-BE8A-E86BD46538C2}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
6
global.json
Normal file
6
global.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.100",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Baguette.Querying.Generator.Abstractions\Baguette.Querying.Generator.Abstractions.csproj"/>
|
||||
<ProjectReference Include="..\..\src\Baguette.Querying.Generator\Baguette.Querying.Generator.csproj" OutputItemType="Analyzer"/>
|
||||
<ProjectReference Include="..\..\src\Baguette.Querying\Baguette.Querying.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
148
samples/Baguette.Querying.Console/Program.cs
Normal file
148
samples/Baguette.Querying.Console/Program.cs
Normal file
@ -0,0 +1,148 @@
|
||||
// See https://aka.ms/new-console-template for more information
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using AutoMapper;
|
||||
using AutoMapper.QueryableExtensions;
|
||||
using Baguette.Querying;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Test;
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddQueryProfiles(typeof(Model));
|
||||
|
||||
services.AddAutoMapper(typeof(Model));
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var processor = provider.GetRequiredService<IQueryProcessor>();
|
||||
var mapper = provider.GetRequiredService<IMapper>();
|
||||
|
||||
var list = new[]
|
||||
{
|
||||
new Model
|
||||
{
|
||||
FirstName = "Jens",
|
||||
LastName = "De Craecker",
|
||||
BirthDate = new DateOnly(1998, 6, 30),
|
||||
Nested = new NestedModel
|
||||
{
|
||||
FirstName = "Jens",
|
||||
LastName = "De Craecker",
|
||||
BirthDate = new DateOnly(1998, 6, 30),
|
||||
NestedInNested = new NestedNestedModel
|
||||
{
|
||||
Text = "Hello World!"
|
||||
}
|
||||
},
|
||||
Nested2 = new NestedModel
|
||||
{
|
||||
FirstName = "Jens",
|
||||
LastName = "De Craecker",
|
||||
BirthDate = new DateOnly(1998, 6, 30),
|
||||
NestedInNested = new NestedNestedModel
|
||||
{
|
||||
Text = "Hello World!"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var filters = new ModelFilters()
|
||||
{
|
||||
// FirstName = "Jens",
|
||||
// Sorting = new[]
|
||||
// {
|
||||
// new SortingOptions()
|
||||
// {
|
||||
// SortBy = "LastName"
|
||||
// }
|
||||
// }
|
||||
Search = "Jens"
|
||||
};
|
||||
|
||||
var query = list.AsQueryable().ApplyFilters(processor, filters).ProjectTo<ResponseModel>(mapper.ConfigurationProvider);
|
||||
var result = query.ToList();
|
||||
|
||||
//
|
||||
// var json = JsonSerializer.Serialize(filters, typeof(ModelFilters));
|
||||
//
|
||||
// var filters2 = JsonSerializer.Deserialize<ModelFilters>(json);
|
||||
//
|
||||
// var result = list.AsQueryable().ApplyFilters(processor, filters).ToList();
|
||||
//
|
||||
// var pipelineProvider = provider.GetRequiredService<IQueryPipelineProvider>();
|
||||
//
|
||||
// var result2 = list.AsQueryable().Where(pipelineProvider.GetPredicate<ModelFilters, Model>(filters)).ToList();
|
||||
|
||||
return;
|
||||
|
||||
class MappingProfile : Profile
|
||||
{
|
||||
public MappingProfile()
|
||||
{
|
||||
CreateMap<Model, ResponseModel>();
|
||||
}
|
||||
}
|
||||
|
||||
public record ResponseModel(string FirstName, string LastName);
|
||||
|
||||
[GenerateFilters(IncludeSorting = true, IncludePaging = true)]
|
||||
public record Model
|
||||
{
|
||||
[FilterMode(FilterMode.Contains)]
|
||||
[Searchable]
|
||||
public required string FirstName { get; init; }
|
||||
|
||||
[SortAlias("Surname")]
|
||||
[SortAlias("")]
|
||||
[Searchable]
|
||||
public required string LastName { get; init; }
|
||||
|
||||
public required DateOnly BirthDate { get; init; }
|
||||
|
||||
[FilterMode(FilterMode.InList)]
|
||||
public TestEnum TestEnum { get; init; }
|
||||
|
||||
[SortAlias("NestedAlias")]
|
||||
[SortAlias("")]
|
||||
[Searchable]
|
||||
public required NestedModel Nested { get; init; }
|
||||
|
||||
public required NestedModel Nested2 { get; init; }
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly partial record struct ModelFilters { }
|
||||
|
||||
namespace Test
|
||||
{
|
||||
public enum TestEnum
|
||||
{
|
||||
Hello
|
||||
}
|
||||
|
||||
[GenerateNestedFilters]
|
||||
public record NestedModel
|
||||
{
|
||||
[FilterMode(FilterMode.Contains)]
|
||||
[FilterName("HelloWorld")]
|
||||
public required string FirstName { get; init; }
|
||||
|
||||
[Searchable]
|
||||
public required string LastName { get; init; }
|
||||
|
||||
public required DateOnly BirthDate { get; init; }
|
||||
|
||||
[FilterMode(FilterMode.InList)]
|
||||
public TestEnum TestEnum { get; init; }
|
||||
|
||||
[SortAlias("NestedNestedAlias")]
|
||||
public required NestedNestedModel NestedInNested { get; init; }
|
||||
}
|
||||
|
||||
[GenerateNestedFilters]
|
||||
public record NestedNestedModel
|
||||
{
|
||||
[SortAlias("TextAlias")]
|
||||
public required string Text { get; init; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Baguette.Querying\Baguette.Querying.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
|
||||
namespace Baguette.Querying.EntityFrameworkCore;
|
||||
|
||||
public static class DbContextOptionsBuilderExtensions
|
||||
{
|
||||
public static DbContextOptionsBuilder AddQuerying(this DbContextOptionsBuilder builder, params Type[] assemblyMarkerTypes)
|
||||
{
|
||||
if (builder == null)
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
|
||||
((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(new QueryingDbContextOptionsExtension(assemblyMarkerTypes));
|
||||
builder.ReplaceService<IAsyncQueryProvider, ServiceQueryProvider>();
|
||||
|
||||
// builder.AddInterceptors(new InjectQueryInterceptor());
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
// using System.Linq.Expressions;
|
||||
// using Baguette.Querying.Visitors;
|
||||
// using Microsoft.EntityFrameworkCore.Query.Internal;
|
||||
//
|
||||
// namespace Baguette.Querying.EntityFrameworkCore;
|
||||
//
|
||||
// public class DefaultQueryInjector<TFilters, TModel> : IQueryInjector<TFilters, TModel> where TFilters : notnull where TModel : notnull
|
||||
// {
|
||||
// private readonly IQueryProcessor processor;
|
||||
//
|
||||
// public DefaultQueryInjector(IQueryProcessor processor)
|
||||
// {
|
||||
// this.processor = processor;
|
||||
// }
|
||||
//
|
||||
// private static IQueryable<TModel> Empty { get; } = Enumerable.Empty<TModel>().AsQueryable();
|
||||
//
|
||||
// public Expression Inject(Expression expression, TFilters filters)
|
||||
// {
|
||||
// var query = processor.ProcessQuery(Empty, filters);
|
||||
//
|
||||
// var visitor = new ReplaceExpressionVisitor(Empty.Expression, expression);
|
||||
//
|
||||
// return visitor.Visit(query.Expression);
|
||||
// }
|
||||
//
|
||||
// public Expression Inject(Expression expression, object filters)
|
||||
// {
|
||||
// if (filters is not TFilters typedFilters)
|
||||
// throw new ArgumentException("Filters is not of the correct type.", nameof(filters));
|
||||
//
|
||||
// return Inject(expression, typedFilters);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #pragma warning disable EF1001
|
||||
|
||||
14
src/Baguette.Querying.EntityFrameworkCore/IQueryInjector.cs
Normal file
14
src/Baguette.Querying.EntityFrameworkCore/IQueryInjector.cs
Normal file
@ -0,0 +1,14 @@
|
||||
// using System.Linq.Expressions;
|
||||
//
|
||||
// namespace Baguette.Querying.EntityFrameworkCore;
|
||||
//
|
||||
// public interface IQueryInjector
|
||||
// {
|
||||
// Expression Inject(Expression expression, object filters);
|
||||
// }
|
||||
//
|
||||
// public interface IQueryInjector<in TFilters, TModel> : IQueryInjector where TFilters : notnull where TModel : notnull
|
||||
// {
|
||||
// Expression Inject(Expression expression, TFilters filters);
|
||||
// }
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Baguette.Querying.EntityFrameworkCore;
|
||||
|
||||
internal static class InfrastructureExtensions
|
||||
{
|
||||
public static IServiceProvider GetApplicationServiceProvider(this IInfrastructure<IServiceProvider> infrastructure)
|
||||
{
|
||||
var extension = infrastructure.Instance.GetRequiredService<IDbContextOptions>().FindExtension<CoreOptionsExtension>();
|
||||
|
||||
if (extension is null)
|
||||
throw new InvalidOperationException("Core options not found.");
|
||||
|
||||
if (extension.ApplicationServiceProvider is null)
|
||||
throw new InvalidOperationException("Application service provider not registered.");
|
||||
|
||||
return extension.ApplicationServiceProvider;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
// using System.Linq.Expressions;
|
||||
// using Microsoft.EntityFrameworkCore;
|
||||
// using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
// using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
// using Microsoft.Extensions.DependencyInjection;
|
||||
//
|
||||
// namespace Baguette.Querying.EntityFrameworkCore;
|
||||
//
|
||||
// public class InjectQueryInterceptor : IQueryExpressionInterceptor
|
||||
// {
|
||||
// public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData)
|
||||
// {
|
||||
// if (eventData.Context is null)
|
||||
// return queryExpression;
|
||||
//
|
||||
// var visitor = new InjectExpressionVisitor(eventData.Context);
|
||||
//
|
||||
// return visitor.Visit(queryExpression);
|
||||
// }
|
||||
//
|
||||
// private class InjectExpressionVisitor : ExpressionVisitor
|
||||
// {
|
||||
// private readonly DbContext context;
|
||||
//
|
||||
// public InjectExpressionVisitor(DbContext context)
|
||||
// {
|
||||
// this.context = context;
|
||||
// }
|
||||
//
|
||||
// protected override Expression VisitMethodCall(MethodCallExpression node)
|
||||
// {
|
||||
// if (!node.Method.IsGenericMethod || node.Method.GetGenericMethodDefinition() != QueryableProcessorExtensions.ApplyFiltersGenericMethodInfo)
|
||||
// return base.VisitMethodCall(node);
|
||||
//
|
||||
// var injector = (IQueryInjector)context
|
||||
// .GetInfrastructure()
|
||||
// .GetRequiredService(typeof(IQueryInjector<,>).MakeGenericType(node.Method.GetGenericArguments()));
|
||||
//
|
||||
// return injector.Inject(node.Arguments[0], ((ConstantExpression)node.Arguments[1]).Value!);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace Baguette.Querying.EntityFrameworkCore;
|
||||
|
||||
public static class QueryableProcessorExtensions
|
||||
{
|
||||
// internal static MethodInfo ApplyFiltersGenericMethodInfo { get; } = typeof(QueryableProcessorExtensions).GetMethod(nameof(ApplyFilters))!;
|
||||
//
|
||||
// public static async Task<PaginatedResult<TModel>> PaginateAsync<TFilters, TModel>(
|
||||
// this IQueryable<TModel> baseQuery,
|
||||
// TFilters filters,
|
||||
// CancellationToken cancellationToken = default) where TFilters : IPageable where TModel : notnull
|
||||
// {
|
||||
// if (baseQuery == null)
|
||||
// throw new ArgumentNullException(nameof(baseQuery));
|
||||
//
|
||||
// var query = baseQuery.ApplyFilters(filters);
|
||||
//
|
||||
// var count = !filters.Paging.ExcludeTotal ? (int?)await query.CountAsync(cancellationToken) : null;
|
||||
// var results = await query.Skip(filters.Paging.Offset).Take(filters.Paging.Length).ToListAsync(cancellationToken);
|
||||
//
|
||||
// return new PaginatedResult<TModel>(results, count, !filters.Paging.ExcludeTotal);
|
||||
// }
|
||||
//
|
||||
// public static IQueryable<TModel> ApplyFilters<TFilters, TModel>(this IQueryable<TModel> baseQuery, [NotParameterized] TFilters filters) where TFilters : notnull where TModel : notnull
|
||||
// {
|
||||
// if (baseQuery == null)
|
||||
// throw new ArgumentNullException(nameof(baseQuery));
|
||||
//
|
||||
// if (filters == null)
|
||||
// throw new ArgumentNullException(nameof(filters));
|
||||
//
|
||||
// return baseQuery.Provider.CreateQuery<TModel>(Expression.Call(null, ApplyFiltersMethodInfoCache<TFilters, TModel>.MethodInfo, baseQuery.Expression, Expression.Constant(filters)));
|
||||
// }
|
||||
//
|
||||
// private static class ApplyFiltersMethodInfoCache<TFilters, TModel> where TFilters : notnull where TModel : notnull
|
||||
// {
|
||||
// public static MethodInfo MethodInfo { get; } = ApplyFiltersGenericMethodInfo.MakeGenericMethod(typeof(TFilters), typeof(TModel));
|
||||
// }
|
||||
|
||||
public static async Task<PaginatedResult<TModel>> PaginateAsync<TFilters, TModel>(
|
||||
this IQueryable<TModel> baseQuery,
|
||||
TFilters filters,
|
||||
CancellationToken cancellationToken = default) where TFilters : IPageable where TModel : notnull
|
||||
{
|
||||
if (baseQuery == null)
|
||||
throw new ArgumentNullException(nameof(baseQuery));
|
||||
|
||||
if (filters == null)
|
||||
throw new ArgumentNullException(nameof(filters));
|
||||
|
||||
var query = baseQuery.ApplyFilters(filters);
|
||||
|
||||
var count = !filters.Paging.ExcludeTotal ? (int?)await query.CountAsync(cancellationToken) : null;
|
||||
var results = await query.Skip(filters.Paging.Offset).Take(filters.Paging.Length).ToListAsync(cancellationToken);
|
||||
|
||||
return new PaginatedResult<TModel>(results, count, !filters.Paging.ExcludeTotal);
|
||||
}
|
||||
|
||||
public static IQueryable<TModel> ApplyFilters<TFilters, TModel>(this IQueryable<TModel> baseQuery, TFilters filters) where TFilters : notnull where TModel : notnull
|
||||
{
|
||||
if (baseQuery == null)
|
||||
throw new ArgumentNullException(nameof(baseQuery));
|
||||
|
||||
if (filters == null)
|
||||
throw new ArgumentNullException(nameof(filters));
|
||||
|
||||
if (baseQuery.Provider is not IInfrastructure<IServiceProvider> infrastructure)
|
||||
throw new NotSupportedException("QueryProvider doesn't provide the service provider.");
|
||||
|
||||
var processor = infrastructure.GetService<IQueryProcessor>();
|
||||
|
||||
return processor.ProcessQuery(baseQuery, filters);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Baguette.Querying.EntityFrameworkCore;
|
||||
|
||||
public class QueryingDbContextOptionsExtension : IDbContextOptionsExtension
|
||||
{
|
||||
private readonly Type[] assemblyMarkerTypes;
|
||||
|
||||
public QueryingDbContextOptionsExtension(Type[] assemblyMarkerTypes)
|
||||
{
|
||||
this.assemblyMarkerTypes = assemblyMarkerTypes;
|
||||
}
|
||||
|
||||
public DbContextOptionsExtensionInfo Info => new QueryingDbContextOptionsExtensionInfo(this);
|
||||
|
||||
public void ApplyServices(IServiceCollection services)
|
||||
{
|
||||
services.AddQueryProfiles(assemblyMarkerTypes);
|
||||
|
||||
// services.AddTransient(typeof(IQueryInjector<,>), typeof(DefaultQueryInjector<,>));
|
||||
}
|
||||
|
||||
public void Validate(IDbContextOptions options) { }
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
namespace Baguette.Querying.EntityFrameworkCore;
|
||||
|
||||
public class QueryingDbContextOptionsExtensionInfo : DbContextOptionsExtensionInfo
|
||||
{
|
||||
public QueryingDbContextOptionsExtensionInfo(IDbContextOptionsExtension extension) : base(extension) { }
|
||||
|
||||
public override bool IsDatabaseProvider { get; } = false;
|
||||
|
||||
public override string LogFragment { get; } = "";
|
||||
|
||||
public override int GetServiceProviderHashCode() => 0;
|
||||
|
||||
public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => true;
|
||||
|
||||
public override void PopulateDebugInfo(IDictionary<string, string> debugInfo) { }
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Query.Internal;
|
||||
|
||||
namespace Baguette.Querying.EntityFrameworkCore;
|
||||
|
||||
// public class ServiceQueryProvider : IAsyncQueryProvider, IInfrastructure<IServiceProvider>
|
||||
// #pragma warning restore EF1001
|
||||
// {
|
||||
// private readonly IAsyncQueryProvider inner;
|
||||
// private readonly IServiceProvider provider;
|
||||
//
|
||||
// #pragma warning disable EF1001
|
||||
// public ServiceQueryProvider(IAsyncQueryProvider inner, IServiceProvider provider) => (this.inner, this.provider) = (inner, provider);
|
||||
// #pragma warning restore EF1001
|
||||
//
|
||||
// IServiceProvider IInfrastructure<IServiceProvider>.Instance => provider;
|
||||
//
|
||||
// public IQueryable CreateQuery(Expression expression) => inner.CreateQuery(expression);
|
||||
//
|
||||
// public IQueryable<TElement> CreateQuery<TElement>(Expression expression) => inner.CreateQuery<TElement>(expression);
|
||||
//
|
||||
// public object? Execute(Expression expression) => inner.Execute(expression);
|
||||
//
|
||||
// public TResult Execute<TResult>(Expression expression) => inner.Execute<TResult>(expression);
|
||||
//
|
||||
// public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken = default) => inner.ExecuteAsync<TResult>(expression, cancellationToken);
|
||||
// }
|
||||
#pragma warning disable EF1001
|
||||
public class ServiceQueryProvider : EntityQueryProvider, IInfrastructure<IServiceProvider>
|
||||
#pragma warning restore EF1001
|
||||
{
|
||||
private readonly IServiceProvider provider;
|
||||
|
||||
#pragma warning disable EF1001
|
||||
public ServiceQueryProvider(IQueryCompiler queryCompiler, IServiceProvider provider) : base(queryCompiler)
|
||||
{
|
||||
this.provider = provider;
|
||||
}
|
||||
#pragma warning restore EF1001
|
||||
IServiceProvider IInfrastructure<IServiceProvider>.Instance => provider;
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Baguette.Querying</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,4 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class DisableSortingAttribute : Attribute;
|
||||
12
src/Baguette.Querying.Generator.Abstractions/FilterMode.cs
Normal file
12
src/Baguette.Querying.Generator.Abstractions/FilterMode.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public enum FilterMode
|
||||
{
|
||||
ByConvention,
|
||||
Ignore,
|
||||
Equal,
|
||||
InRange,
|
||||
Contains,
|
||||
InList,
|
||||
InDateRange
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class FilterModeAttribute(FilterMode mode) : Attribute
|
||||
{
|
||||
public FilterMode Mode { get; } = mode;
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class FilterNameAttribute(string name) : Attribute
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class GenerateFiltersAttribute : Attribute // TODO: Add exclude profile option
|
||||
{
|
||||
public string? FilterName { get; init; }
|
||||
|
||||
public string? FilterNamespace { get; init; }
|
||||
|
||||
public string? ProfileName { get; init; }
|
||||
|
||||
public string? ProfileNamespace { get; init; }
|
||||
|
||||
public bool IncludeSorting { get; init; }
|
||||
|
||||
public bool IncludePaging { get; init; }
|
||||
|
||||
public string? SearchName { get; init; }
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
||||
public class GenerateNestedFiltersAttribute : Attribute
|
||||
{
|
||||
public string? FilterName { get; init; }
|
||||
|
||||
public string? FilterNamespace { get; init; }
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class SearchableAttribute : Attribute;
|
||||
@ -0,0 +1,7 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
|
||||
public class SortAliasAttribute(string name) : Attribute
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
}
|
||||
23
src/Baguette.Querying.Generator/ArgumentUtilities.cs
Normal file
23
src/Baguette.Querying.Generator/ArgumentUtilities.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Collections.Immutable;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public static class ArgumentUtilities
|
||||
{
|
||||
public static T GetValue<T>(IEnumerable<KeyValuePair<string, TypedConstant>> collection, string key, T defaultValue)
|
||||
{
|
||||
var t = collection.Where(p => p.Key == key).Select(p => p.Value).FirstOrDefault();
|
||||
|
||||
return t.Value is not null ? (T)t.Value : defaultValue;
|
||||
}
|
||||
|
||||
public static ImmutableArray<string?> GetValues(IEnumerable<KeyValuePair<string, TypedConstant>> collection, string key)
|
||||
{
|
||||
var t = collection.Where(p => p.Key == key).Select(p => p.Value).FirstOrDefault();
|
||||
|
||||
return t.IsNull
|
||||
? ImmutableArray<string?>.Empty
|
||||
: t.Values.Select(c => (string?)c.Value).ToImmutableArray();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>true</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<RootNamespace>Baguette.Querying</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" PrivateAssets="all"/>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all"/>
|
||||
<PackageReference Include="PolySharp" Version="1.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="$(OutputPath)\*.dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,18 @@
|
||||
namespace Baguette.Querying.Configuration;
|
||||
|
||||
public static class AttributeArgumentConstants
|
||||
{
|
||||
public const string FilterName = nameof(FilterName);
|
||||
|
||||
public const string FilterNamespace = nameof(FilterNamespace);
|
||||
|
||||
public const string ProfileName = nameof(ProfileName);
|
||||
|
||||
public const string ProfileNamespace = nameof(ProfileNamespace);
|
||||
|
||||
public const string IncludeSorting = nameof(IncludeSorting);
|
||||
|
||||
public const string IncludePaging = nameof(IncludePaging);
|
||||
|
||||
public const string SearchName = nameof(SearchName);
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
namespace Baguette.Querying.Configuration;
|
||||
|
||||
public static class AttributeConstants
|
||||
{
|
||||
public const string GenerateFiltersAttribute = $"{NamespaceConstants.RootNamespace}.{nameof(GenerateFiltersAttribute)}";
|
||||
|
||||
public const string GenerateNestedFiltersAttribute = $"{NamespaceConstants.RootNamespace}.{nameof(GenerateNestedFiltersAttribute)}";
|
||||
|
||||
public const string FilterModeAttribute = $"{NamespaceConstants.RootNamespace}.{nameof(FilterModeAttribute)}";
|
||||
|
||||
public const string ExcludeSortingAttribute = $"{NamespaceConstants.RootNamespace}.{nameof(ExcludeSortingAttribute)}";
|
||||
|
||||
public const string SortAliasAttribute = $"{NamespaceConstants.RootNamespace}.{nameof(SortAliasAttribute)}";
|
||||
|
||||
public const string FilterNameAttribute = $"{NamespaceConstants.RootNamespace}.{nameof(FilterNameAttribute)}";
|
||||
|
||||
public const string SearchableAttribute = $"{NamespaceConstants.RootNamespace}.{nameof(SearchableAttribute)}";
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
namespace Baguette.Querying.Configuration;
|
||||
|
||||
public static class GenerationConstants
|
||||
{
|
||||
public const string Indentation = " ";
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace Baguette.Querying.Configuration;
|
||||
|
||||
public static class NamespaceConstants
|
||||
{
|
||||
public const string RootNamespace = "Baguette.Querying";
|
||||
|
||||
public const string RangeNamespace = RootNamespace;
|
||||
}
|
||||
83
src/Baguette.Querying.Generator/FilterGenerationContext.cs
Normal file
83
src/Baguette.Querying.Generator/FilterGenerationContext.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Baguette.Querying.Configuration;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public record FilterGenerationContext : FilterGenerationContextBase
|
||||
{
|
||||
private readonly string? profileName;
|
||||
private readonly string? profileNamespace;
|
||||
private readonly string? searchName;
|
||||
|
||||
private FilterGenerationContext(INamespaceOrTypeSymbol symbol) : base(symbol) { }
|
||||
|
||||
public required bool IncludeSorting { get; init; }
|
||||
|
||||
public required bool IncludePaging { get; init; }
|
||||
|
||||
protected override string FilterSuffix => "Filters";
|
||||
|
||||
[NotNull]
|
||||
public required string? ProfileName
|
||||
{
|
||||
get => profileName ?? FilterName + ProfileSuffix;
|
||||
init => profileName = value;
|
||||
}
|
||||
|
||||
[NotNull]
|
||||
public required string? ProfileNamespace
|
||||
{
|
||||
get => profileNamespace ?? FilterNamespace;
|
||||
init => profileNamespace = value;
|
||||
}
|
||||
|
||||
public string ProfileNameFull => CreateFullName(ProfileNamespace, ProfileName);
|
||||
|
||||
protected virtual string ProfileSuffix => "Profile";
|
||||
|
||||
[NotNull]
|
||||
public required string? SearchName
|
||||
{
|
||||
get => searchName ?? "Search";
|
||||
init => searchName = value;
|
||||
}
|
||||
|
||||
private static string CreateFullName(string targetNamespace, string targetName)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(targetNamespace) ? $"global::{targetNamespace}.{targetName}" : targetName;
|
||||
}
|
||||
|
||||
public static FilterGenerationContext Create(ITypeSymbol symbol, AttributeData attributeData)
|
||||
{
|
||||
if (attributeData.AttributeClass?.ToString() is not AttributeConstants.GenerateFiltersAttribute)
|
||||
throw new ArgumentException($"Attribute is not a {AttributeConstants.GenerateFiltersAttribute}", nameof(attributeData));
|
||||
|
||||
return new FilterGenerationContext(symbol)
|
||||
{
|
||||
FilterName = ArgumentUtilities.GetValue(attributeData.NamedArguments, AttributeArgumentConstants.FilterName, (string?)null),
|
||||
FilterNamespace = ArgumentUtilities.GetValue(attributeData.NamedArguments, AttributeArgumentConstants.FilterNamespace, (string?)null),
|
||||
ProfileName = ArgumentUtilities.GetValue(attributeData.NamedArguments, AttributeArgumentConstants.ProfileName, (string?)null),
|
||||
ProfileNamespace = ArgumentUtilities.GetValue(attributeData.NamedArguments, AttributeArgumentConstants.ProfileNamespace, (string?)null),
|
||||
IncludeSorting = ArgumentUtilities.GetValue(attributeData.NamedArguments, AttributeArgumentConstants.IncludeSorting, false),
|
||||
IncludePaging = ArgumentUtilities.GetValue(attributeData.NamedArguments, AttributeArgumentConstants.IncludePaging, false),
|
||||
SearchName = ArgumentUtilities.GetValue(attributeData.NamedArguments, AttributeArgumentConstants.SearchName, (string?)null)
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryCreate(ITypeSymbol symbol, [NotNullWhen(true)] out FilterGenerationContext? context)
|
||||
{
|
||||
var attributeData = symbol.GetAttributes().FirstOrDefault(x => x.AttributeClass?.ToString() == AttributeConstants.GenerateFiltersAttribute);
|
||||
|
||||
if (attributeData is null)
|
||||
{
|
||||
context = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
context = Create(symbol, attributeData);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public abstract record FilterGenerationContextBase
|
||||
{
|
||||
private readonly string? filterName;
|
||||
private readonly string? filterNamespace;
|
||||
|
||||
protected FilterGenerationContextBase(INamespaceOrTypeSymbol symbol)
|
||||
{
|
||||
ModelName = symbol.Name;
|
||||
|
||||
ModelNamespace = !symbol.ContainingNamespace.IsGlobalNamespace
|
||||
? symbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)[8..] // Strip global:: from the namespace
|
||||
: "";
|
||||
|
||||
Properties = GetProperties(symbol);
|
||||
}
|
||||
|
||||
public string ModelName { get; }
|
||||
|
||||
public string ModelNamespace { get; }
|
||||
|
||||
public string ModelNameFull => CreateFullName(ModelNamespace, ModelName);
|
||||
|
||||
[NotNull]
|
||||
public required string? FilterName
|
||||
{
|
||||
get => filterName ?? ModelName + FilterSuffix;
|
||||
init => filterName = value;
|
||||
}
|
||||
|
||||
[NotNull]
|
||||
public required string? FilterNamespace
|
||||
{
|
||||
get => filterNamespace ?? ModelNamespace;
|
||||
init => filterNamespace = value;
|
||||
}
|
||||
|
||||
public string FilterNameFull => CreateFullName(FilterNamespace, FilterName);
|
||||
|
||||
protected abstract string FilterSuffix { get; }
|
||||
|
||||
public ImmutableArray<FilterProperty> Properties { get; }
|
||||
|
||||
private static string CreateFullName(string targetNamespace, string targetName)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(targetNamespace) ? $"global::{targetNamespace}.{targetName}" : targetName;
|
||||
}
|
||||
|
||||
private static ImmutableArray<FilterProperty> GetProperties(INamespaceOrTypeSymbol symbol)
|
||||
{
|
||||
return symbol
|
||||
.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => p.GetMethod is not null && p.DeclaredAccessibility == Accessibility.Public)
|
||||
.Select(x => new FilterProperty(x))
|
||||
.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public static class FilterGenerationContextExtensions
|
||||
{
|
||||
public static IEnumerable<(string FilterType, string Name)> GetFilterProperties(this FilterGenerationContextBase context)
|
||||
{
|
||||
foreach (var property in context.Properties)
|
||||
{
|
||||
if (!property.Include)
|
||||
continue;
|
||||
|
||||
yield return (property.FilterType, property.FilterName);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<(TypeMapping TypeMapping, string FilterName, string ModelName)> GetTypeMappings(this FilterGenerationContextBase context)
|
||||
{
|
||||
foreach (var property in context.Properties)
|
||||
{
|
||||
if (!property.Include)
|
||||
continue;
|
||||
|
||||
var (filterName, modelName) = (property.FilterName, property.ModelName);
|
||||
|
||||
if (!property.IsNested)
|
||||
{
|
||||
yield return (property.TypeMapping, filterName, modelName);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var (nestedTypeMapping, nestedFilterName, nestedModelName) in property.NestedContext.GetTypeMappings())
|
||||
{
|
||||
yield return (nestedTypeMapping, $"{filterName}.{nestedFilterName}", $"{modelName}.{nestedModelName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<(string SortingKey, string ModelName)> GetSortProperties(this FilterGenerationContextBase context)
|
||||
{
|
||||
foreach (var property in context.Properties)
|
||||
{
|
||||
if (!property.IncludeSorting)
|
||||
continue;
|
||||
|
||||
var (sortKey, modelName) = (property.FilterName, property.ModelName);
|
||||
|
||||
if (!property.IsNested)
|
||||
{
|
||||
yield return (sortKey, modelName);
|
||||
|
||||
foreach (var alias in property.Aliases)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(alias))
|
||||
continue;
|
||||
|
||||
yield return (alias, modelName);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var (nestedSortKey, nestedModelName) in property.NestedContext.GetSortProperties())
|
||||
{
|
||||
yield return ($"{sortKey}.{nestedSortKey}", $"{modelName}.{nestedModelName}");
|
||||
|
||||
foreach (var alias in property.Aliases)
|
||||
{
|
||||
yield return (!string.IsNullOrWhiteSpace(alias) ? $"{alias}.{nestedSortKey}" : nestedSortKey, $"{modelName}.{nestedModelName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<string> GetSearchableProperties(this FilterGenerationContextBase context)
|
||||
{
|
||||
foreach (var property in context.Properties)
|
||||
{
|
||||
if (!property.IsSearchable)
|
||||
continue;
|
||||
|
||||
var modelName = property.ModelName;
|
||||
|
||||
if (!property.IsNested)
|
||||
{
|
||||
yield return modelName;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var nestedProperty in property.NestedContext.GetSearchableProperties())
|
||||
{
|
||||
yield return $"{modelName}.{nestedProperty}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/Baguette.Querying.Generator/FilterGenerator.cs
Normal file
41
src/Baguette.Querying.Generator/FilterGenerator.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Baguette.Querying.Configuration;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public class FilterGenerator(FilterGenerationContext context) : FilterGeneratorBase(context)
|
||||
{
|
||||
protected override IEnumerable<string> ImplementingInterfaces
|
||||
{
|
||||
get
|
||||
{
|
||||
if (context.IncludeSorting)
|
||||
yield return $"global::{NamespaceConstants.RootNamespace}.IMultiSortable";
|
||||
|
||||
if (context.IncludePaging)
|
||||
yield return $"global::{NamespaceConstants.RootNamespace}.IPageable";
|
||||
}
|
||||
}
|
||||
|
||||
protected override void WriteBody()
|
||||
{
|
||||
if (context.IncludeSorting)
|
||||
{
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}public global::System.Collections.Generic.IEnumerable<global::{NamespaceConstants.RootNamespace}.SortingOptions>? Sorting {{ get; init; }}");
|
||||
StringBuilder.AppendLine();
|
||||
}
|
||||
|
||||
if (context.IncludePaging)
|
||||
{
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}public global::{NamespaceConstants.RootNamespace}.PagingOptions Paging {{ get; init; }}");
|
||||
StringBuilder.AppendLine();
|
||||
}
|
||||
|
||||
if (context.GetSearchableProperties().Any())
|
||||
{
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}public string? {context.SearchName} {{ get; init; }}");
|
||||
StringBuilder.AppendLine();
|
||||
}
|
||||
|
||||
base.WriteBody();
|
||||
}
|
||||
}
|
||||
47
src/Baguette.Querying.Generator/FilterGeneratorBase.cs
Normal file
47
src/Baguette.Querying.Generator/FilterGeneratorBase.cs
Normal file
@ -0,0 +1,47 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public class FilterGeneratorBase(FilterGenerationContextBase context) : GeneratorBase
|
||||
{
|
||||
protected override string GeneratedFileName => $"{context.FilterName}.g.cs";
|
||||
|
||||
protected override string Namespace => context.FilterNamespace;
|
||||
|
||||
protected virtual IEnumerable<string> ImplementingInterfaces
|
||||
{
|
||||
get { yield break; }
|
||||
}
|
||||
|
||||
protected override void WriteStartBody()
|
||||
{
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}public readonly partial record struct {context.FilterName}{GetImplementingInterfacesText()}");
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}{{");
|
||||
|
||||
Indent();
|
||||
|
||||
return;
|
||||
|
||||
string GetImplementingInterfacesText()
|
||||
{
|
||||
if (ImplementingInterfaces.ToList() is not { } list || list is { Count: 0 })
|
||||
return string.Empty;
|
||||
|
||||
return " : " + string.Join(", ", list);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void WriteBody()
|
||||
{
|
||||
foreach (var (filterType, name) in context.GetFilterProperties())
|
||||
{
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}public {filterType} {name} {{ get; init; }}");
|
||||
StringBuilder.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void WriteEndBody()
|
||||
{
|
||||
Unindent();
|
||||
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}}}");
|
||||
}
|
||||
}
|
||||
108
src/Baguette.Querying.Generator/FilterProperty.cs
Normal file
108
src/Baguette.Querying.Generator/FilterProperty.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Baguette.Querying.Configuration;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public record FilterProperty
|
||||
{
|
||||
public FilterProperty(IPropertySymbol symbol)
|
||||
{
|
||||
ModelName = symbol.Name;
|
||||
ModelType = symbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
FilterMode = GetFilterMode(symbol);
|
||||
NestedContext = GetContext(symbol.Type);
|
||||
FilterName = GetFilterName(symbol);
|
||||
Aliases = GetAliases(symbol);
|
||||
HasExludeSortingAttribute = symbol.GetAttributes().Any(x => x.AttributeClass?.ToString() == AttributeConstants.ExcludeSortingAttribute);
|
||||
HasSearchableAttribute = symbol.GetAttributes().Any(x => x.AttributeClass?.ToString() == AttributeConstants.SearchableAttribute);
|
||||
}
|
||||
|
||||
public string ModelName { get; }
|
||||
|
||||
public string ModelType { get; }
|
||||
|
||||
public int FilterMode { get; }
|
||||
|
||||
public TypeMapping? TypeMapping => FindTypeMapping(ModelType, FilterMode);
|
||||
|
||||
public NestedFilterGenerationContext? NestedContext { get; }
|
||||
|
||||
[MemberNotNullWhen(true, nameof(FilterType), nameof(TypeMapping))]
|
||||
public bool Include => FilterType is not null;
|
||||
|
||||
[MemberNotNullWhen(true, nameof(FilterType), nameof(TypeMapping))]
|
||||
public bool IncludeSorting => Include && !HasExludeSortingAttribute;
|
||||
|
||||
[MemberNotNullWhen(true, nameof(FilterType), nameof(TypeMapping), nameof(NestedContext))]
|
||||
public bool IsNested => Include && NestedContext is not null;
|
||||
|
||||
public string? FilterType => GetFilterType(TypeMapping, NestedContext);
|
||||
|
||||
public string FilterName { get; }
|
||||
|
||||
[MemberNotNullWhen(true, nameof(FilterType), nameof(TypeMapping))]
|
||||
public bool IsSearchable => Include && HasSearchableAttribute;
|
||||
|
||||
public ImmutableArray<string> Aliases { get; }
|
||||
|
||||
private bool HasExludeSortingAttribute { get; }
|
||||
|
||||
private bool HasSearchableAttribute { get; }
|
||||
|
||||
private static string GetFilterName(ISymbol symbol)
|
||||
{
|
||||
var attribute = symbol.GetAttributes().FirstOrDefault(x => x.AttributeClass?.ToString() == AttributeConstants.FilterNameAttribute);
|
||||
|
||||
if (attribute is null)
|
||||
return symbol.Name;
|
||||
|
||||
var filterName = (string)attribute.ConstructorArguments[0].Value!;
|
||||
|
||||
return !string.IsNullOrWhiteSpace(filterName)
|
||||
? filterName
|
||||
: symbol.Name;
|
||||
}
|
||||
|
||||
private static string? GetFilterType(TypeMapping? typeMapping, NestedFilterGenerationContext? generationContext)
|
||||
{
|
||||
return typeMapping is null
|
||||
? null
|
||||
: generationContext is null
|
||||
? typeMapping.FilterType
|
||||
: typeMapping.Kind is TypeMapping.FilterKind.Equal
|
||||
? generationContext.FilterNameFull
|
||||
: null;
|
||||
}
|
||||
|
||||
private static TypeMapping? FindTypeMapping(string modelType, int filterMode)
|
||||
{
|
||||
return TypeMapping.FindTypeMapping(modelType, filterMode);
|
||||
}
|
||||
|
||||
private static int GetFilterMode(ISymbol symbol)
|
||||
{
|
||||
var filterModeAttribute = symbol.GetAttributes().FirstOrDefault(x => x.AttributeClass?.ToString() == AttributeConstants.FilterModeAttribute);
|
||||
|
||||
return filterModeAttribute is not null
|
||||
? (int)filterModeAttribute.ConstructorArguments[0].Value!
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static NestedFilterGenerationContext? GetContext(ITypeSymbol symbol)
|
||||
{
|
||||
return NestedFilterGenerationContext.TryCreate(symbol, out var context) ? context : null;
|
||||
}
|
||||
|
||||
private static ImmutableArray<string> GetAliases(IPropertySymbol symbol)
|
||||
{
|
||||
return symbol
|
||||
.GetAttributes()
|
||||
.Where(x => x.AttributeClass?.ToString() == AttributeConstants.SortAliasAttribute)
|
||||
.Select(x => (string)(x.ConstructorArguments[0].Value ?? string.Empty))
|
||||
.Select(x => x.Trim())
|
||||
.Distinct()
|
||||
.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
71
src/Baguette.Querying.Generator/GeneratorBase.cs
Normal file
71
src/Baguette.Querying.Generator/GeneratorBase.cs
Normal file
@ -0,0 +1,71 @@
|
||||
using System.Text;
|
||||
using Baguette.Querying.Configuration;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public abstract class GeneratorBase
|
||||
{
|
||||
protected StringBuilder StringBuilder { get; } = new();
|
||||
|
||||
protected string CurrentIndentation { get; private set; } = string.Empty;
|
||||
|
||||
protected abstract string Namespace { get; }
|
||||
|
||||
protected abstract string GeneratedFileName { get; }
|
||||
|
||||
private void WriteStartNamespace()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Namespace))
|
||||
return;
|
||||
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}namespace {Namespace}");
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}{{");
|
||||
|
||||
Indent();
|
||||
}
|
||||
|
||||
protected abstract void WriteStartBody();
|
||||
|
||||
protected abstract void WriteBody();
|
||||
|
||||
protected abstract void WriteEndBody();
|
||||
|
||||
private void WriteEndNamespace()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Namespace))
|
||||
return;
|
||||
|
||||
Unindent();
|
||||
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}}}");
|
||||
}
|
||||
|
||||
protected void Indent()
|
||||
{
|
||||
CurrentIndentation += GenerationConstants.Indentation;
|
||||
}
|
||||
|
||||
protected void Unindent()
|
||||
{
|
||||
if (CurrentIndentation.Length == 0)
|
||||
return;
|
||||
|
||||
CurrentIndentation = CurrentIndentation[..^GenerationConstants.Indentation.Length];
|
||||
}
|
||||
|
||||
public void Generate(SourceProductionContext productionContext)
|
||||
{
|
||||
StringBuilder.Clear();
|
||||
StringBuilder.AppendLine("// <auto-generated/>");
|
||||
StringBuilder.AppendLine("#nullable enable");
|
||||
|
||||
WriteStartNamespace();
|
||||
WriteStartBody();
|
||||
WriteBody();
|
||||
WriteEndBody();
|
||||
WriteEndNamespace();
|
||||
|
||||
productionContext.AddSource(GeneratedFileName, StringBuilder.ToString());
|
||||
}
|
||||
}
|
||||
43
src/Baguette.Querying.Generator/IncrementalGenerator.cs
Normal file
43
src/Baguette.Querying.Generator/IncrementalGenerator.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using Baguette.Querying.Configuration;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
[Generator]
|
||||
public class IncrementalFilterGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var filterProvider = context.SyntaxProvider
|
||||
.ForAttributeWithMetadataName(
|
||||
AttributeConstants.GenerateFiltersAttribute,
|
||||
static (node, _) => node is TypeDeclarationSyntax,
|
||||
static (ctx, _) => (ModelSymbol: (ITypeSymbol)ctx.TargetSymbol, ctx.Attributes))
|
||||
.SelectMany((x, _) => x.Attributes.Select(y => (TargetSymbol: x.ModelSymbol, Attribute: y)))
|
||||
.Select((x, _) => FilterGenerationContext.Create(x.TargetSymbol, x.Attribute));
|
||||
|
||||
context.RegisterSourceOutput(filterProvider, Generate);
|
||||
|
||||
var nestedFilterProvider = context.SyntaxProvider
|
||||
.ForAttributeWithMetadataName(
|
||||
AttributeConstants.GenerateNestedFiltersAttribute,
|
||||
static (node, _) => node is TypeDeclarationSyntax,
|
||||
static (ctx, _) => (ModelSymbol: (ITypeSymbol)ctx.TargetSymbol, ctx.Attributes))
|
||||
.SelectMany((x, _) => x.Attributes.Select(y => (TargetSymbol: x.ModelSymbol, Attribute: y)))
|
||||
.Select((x, _) => NestedFilterGenerationContext.Create(x.TargetSymbol, x.Attribute));
|
||||
|
||||
context.RegisterSourceOutput(nestedFilterProvider, Generate);
|
||||
}
|
||||
|
||||
private static void Generate(SourceProductionContext productionContext, FilterGenerationContext generationContext)
|
||||
{
|
||||
new FilterGenerator(generationContext).Generate(productionContext);
|
||||
new QueryProfileGenerator(generationContext).Generate(productionContext);
|
||||
}
|
||||
|
||||
private static void Generate(SourceProductionContext productionContext, NestedFilterGenerationContext generationContext)
|
||||
{
|
||||
new FilterGeneratorBase(generationContext).Generate(productionContext);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Baguette.Querying.Configuration;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public record NestedFilterGenerationContext : FilterGenerationContextBase
|
||||
{
|
||||
private NestedFilterGenerationContext(INamespaceOrTypeSymbol symbol) : base(symbol) { }
|
||||
|
||||
protected override string FilterSuffix => "NestedFilters";
|
||||
|
||||
public static NestedFilterGenerationContext Create(ITypeSymbol symbol, AttributeData attributeData)
|
||||
{
|
||||
if (attributeData.AttributeClass?.ToString() is not AttributeConstants.GenerateNestedFiltersAttribute)
|
||||
throw new ArgumentException($"Attribute is not a {AttributeConstants.GenerateNestedFiltersAttribute}", nameof(attributeData));
|
||||
|
||||
return new NestedFilterGenerationContext(symbol)
|
||||
{
|
||||
FilterName = ArgumentUtilities.GetValue(attributeData.NamedArguments, AttributeArgumentConstants.FilterName, (string?)null),
|
||||
FilterNamespace = ArgumentUtilities.GetValue(attributeData.NamedArguments, AttributeArgumentConstants.FilterNamespace, (string?)null)
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryCreate(ITypeSymbol symbol, [NotNullWhen(true)] out NestedFilterGenerationContext? context)
|
||||
{
|
||||
var attributeData = symbol.GetAttributes().FirstOrDefault(x => x.AttributeClass?.ToString() == AttributeConstants.GenerateNestedFiltersAttribute);
|
||||
|
||||
if (attributeData is null)
|
||||
{
|
||||
context = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
context = Create(symbol, attributeData);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Debug Console": {
|
||||
"commandName": "DebugRoslynComponent",
|
||||
"targetProject": "../../samples/Baguette.Querying.Console/Baguette.Querying.Console.csproj"
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/Baguette.Querying.Generator/QueryProfileGenerator.cs
Normal file
84
src/Baguette.Querying.Generator/QueryProfileGenerator.cs
Normal file
@ -0,0 +1,84 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public class QueryProfileGenerator(FilterGenerationContext context) : GeneratorBase
|
||||
{
|
||||
private const string BuilderParameter = "builder";
|
||||
|
||||
protected override string Namespace => context.ProfileNamespace;
|
||||
|
||||
protected override string GeneratedFileName => $"{context.ProfileName}.g.cs";
|
||||
|
||||
protected override void WriteStartBody()
|
||||
{
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}public sealed class {context.ProfileName} : global::Baguette.Querying.IQueryProfile<{context.FilterNameFull}, {context.ModelNameFull}>");
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}{{");
|
||||
|
||||
Indent();
|
||||
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}public void Configure(global::Baguette.Querying.IQueryBuilder<{context.FilterNameFull}, {context.ModelNameFull}> {BuilderParameter})");
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}{{");
|
||||
|
||||
Indent();
|
||||
}
|
||||
|
||||
protected override void WriteBody()
|
||||
{
|
||||
foreach (var (typeMapping, filterName, modelName) in context.GetTypeMappings())
|
||||
{
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}{typeMapping.GetFilterCode(BuilderParameter, filterName, modelName)}");
|
||||
}
|
||||
|
||||
WriteSearch();
|
||||
|
||||
WriteSorting();
|
||||
|
||||
return;
|
||||
|
||||
void WriteSearch()
|
||||
{
|
||||
var searchProperties = context.GetSearchableProperties().ToList();
|
||||
|
||||
if (searchProperties.Count == 0)
|
||||
return;
|
||||
|
||||
StringBuilder.AppendLine(
|
||||
$"{CurrentIndentation}global::Baguette.Querying.QueryBuilderFilterExtensions.AddMultiSearch({BuilderParameter}, filters => filters.{context.SearchName}, {string.Join(", ", searchProperties.Select(x => $"model => model.{x}"))});");
|
||||
}
|
||||
|
||||
void WriteSorting()
|
||||
{
|
||||
if (!context.IncludeSorting)
|
||||
return;
|
||||
|
||||
var sortProperties = context.GetSortProperties().ToList();
|
||||
|
||||
if (sortProperties.Count == 0)
|
||||
return;
|
||||
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}global::Baguette.Querying.QueryBuilderSortingExtensions.AddMultiSorting({BuilderParameter}, sortingBuilder =>");
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}{{");
|
||||
|
||||
Indent();
|
||||
|
||||
foreach (var (sortingKey, modelName) in sortProperties)
|
||||
{
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}global::Baguette.Querying.QueryBuilderSortingExtensions.AddSorting(sortingBuilder, \"{sortingKey}\", model => model.{modelName});");
|
||||
}
|
||||
|
||||
Unindent();
|
||||
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}}});");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void WriteEndBody()
|
||||
{
|
||||
Unindent();
|
||||
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}}}");
|
||||
|
||||
Unindent();
|
||||
|
||||
StringBuilder.AppendLine($"{CurrentIndentation}}}");
|
||||
}
|
||||
}
|
||||
93
src/Baguette.Querying.Generator/TypeMapping.cs
Normal file
93
src/Baguette.Querying.Generator/TypeMapping.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using Baguette.Querying.Configuration;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public record TypeMapping(string ModelType, string FilterType, TypeMapping.FilterKind Kind, int Mode)
|
||||
{
|
||||
public enum FilterKind
|
||||
{
|
||||
Equal,
|
||||
Contains,
|
||||
InList,
|
||||
InRange,
|
||||
InDateRange,
|
||||
InDateOnlyRange
|
||||
}
|
||||
|
||||
private static IEnumerable<TypeMapping> Mappings { get; } = CreateTypeMappings().ToList();
|
||||
|
||||
public string GetFilterCode(string builderName, string filterPropertyName, string modelPropertyName)
|
||||
{
|
||||
var format = Kind switch
|
||||
{
|
||||
FilterKind.Equal => "global::Baguette.Querying.QueryBuilderFilterExtensions.AddEqualsFilter({0}, filters => filters.{1}, model => model.{2});",
|
||||
FilterKind.Contains => "global::Baguette.Querying.QueryBuilderFilterExtensions.AddTextFilter({0}, filters => filters.{1}, model => model.{2});",
|
||||
FilterKind.InList => "global::Baguette.Querying.QueryBuilderFilterExtensions.AddInListFilter({0}, filters => filters.{1}, model => model.{2});",
|
||||
FilterKind.InRange => "global::Baguette.Querying.QueryBuilderFilterExtensions.AddInRangeFilter({0}, filters => filters.{1}, model => model.{2});",
|
||||
FilterKind.InDateRange => "global::Baguette.Querying.QueryBuilderFilterExtensions.AddInDateRangeFilter({0}, filters => filters.{1}, model => model.{2});",
|
||||
FilterKind.InDateOnlyRange => "global::Baguette.Querying.QueryBuilderFilterExtensions.AddInDateOnlyRangeFilter({0}, filters => filters.{1}, model => model.{2});",
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
|
||||
return string.Format(format, builderName, filterPropertyName, modelPropertyName);
|
||||
}
|
||||
|
||||
private static IEnumerable<TypeMapping> CreateTypeMappings()
|
||||
{
|
||||
var numericTypes = new[] { "int", "long", "double", "decimal" };
|
||||
|
||||
foreach (var numericType in numericTypes)
|
||||
{
|
||||
yield return new TypeMapping(numericType, $"global::{NamespaceConstants.RangeNamespace}.FilterRange<{numericType}>", FilterKind.InRange, 0);
|
||||
yield return new TypeMapping(numericType, $"global::{NamespaceConstants.RangeNamespace}.FilterRange<{numericType}>", FilterKind.InRange, 3);
|
||||
yield return new TypeMapping(numericType + "?", $"global::{NamespaceConstants.RangeNamespace}.FilterRange<{numericType}>", FilterKind.InRange, 0);
|
||||
yield return new TypeMapping(numericType + "?", $"global::{NamespaceConstants.RangeNamespace}.FilterRange<{numericType}>", FilterKind.InRange, 3);
|
||||
}
|
||||
|
||||
yield return new TypeMapping("bool", "bool?", FilterKind.Equal, 0);
|
||||
yield return new TypeMapping("bool?", "bool?", FilterKind.Equal, 0);
|
||||
yield return new TypeMapping("bool", "bool?", FilterKind.Equal, 2);
|
||||
yield return new TypeMapping("bool?", "bool?", FilterKind.Equal, 2);
|
||||
yield return new TypeMapping("string", "string?", FilterKind.Contains, 0);
|
||||
yield return new TypeMapping("string?", "string?", FilterKind.Contains, 0);
|
||||
yield return new TypeMapping("string", "string?", FilterKind.Contains, 4);
|
||||
yield return new TypeMapping("string?", "string?", FilterKind.Contains, 4);
|
||||
yield return new TypeMapping("string", "string?", FilterKind.Equal, 2);
|
||||
yield return new TypeMapping("string?", "string?", FilterKind.Equal, 2);
|
||||
yield return new TypeMapping("string", "global::System.Collections.Generic.IReadOnlyCollection<string>?", FilterKind.InList, 5);
|
||||
yield return new TypeMapping("string?", "global::System.Collections.Generic.IReadOnlyCollection<string>?", FilterKind.InList, 5);
|
||||
yield return new TypeMapping("global::System.DateTimeOffset", $"global::{NamespaceConstants.RangeNamespace}.DateTimeOffsetFilterRange", FilterKind.InDateRange, 0);
|
||||
yield return new TypeMapping("global::System.DateTimeOffset?", $"global::{NamespaceConstants.RangeNamespace}.DateTimeOffsetFilterRange", FilterKind.InDateRange, 0);
|
||||
yield return new TypeMapping("global::System.DateTimeOffset", $"global::{NamespaceConstants.RangeNamespace}.DateTimeOffsetFilterRange", FilterKind.InDateRange, 6);
|
||||
yield return new TypeMapping("global::System.DateTimeOffset?", $"global::{NamespaceConstants.RangeNamespace}.DateTimeOffsetFilterRange", FilterKind.InDateRange, 6);
|
||||
yield return new TypeMapping("global::System.DateTimeOffset", "global::System.DateTimeOffset?", FilterKind.Equal, 2);
|
||||
yield return new TypeMapping("global::System.DateTimeOffset?", "global::System.DateTimeOffset?", FilterKind.Equal, 2);
|
||||
yield return new TypeMapping("global::System.DateTime", $"global::{NamespaceConstants.RangeNamespace}.DateTimeFilterRange", FilterKind.InDateRange, 0);
|
||||
yield return new TypeMapping("global::System.DateTime?", $"global::{NamespaceConstants.RangeNamespace}.DateTimeFilterRange", FilterKind.InDateRange, 0);
|
||||
yield return new TypeMapping("global::System.DateTime", $"global::{NamespaceConstants.RangeNamespace}.DateTimeFilterRange", FilterKind.InDateRange, 6);
|
||||
yield return new TypeMapping("global::System.DateTime?", $"global::{NamespaceConstants.RangeNamespace}.DateTimeFilterRange", FilterKind.InDateRange, 6);
|
||||
yield return new TypeMapping("global::System.DateTime", "global::System.DateTime?", FilterKind.Equal, 2);
|
||||
yield return new TypeMapping("global::System.DateTime?", "global::System.DateTime?", FilterKind.Equal, 2);
|
||||
yield return new TypeMapping("global::System.DateOnly", $"global::{NamespaceConstants.RangeNamespace}.DateOnlyFilterRange", FilterKind.InDateOnlyRange, 0);
|
||||
yield return new TypeMapping("global::System.DateOnly?", $"global::{NamespaceConstants.RangeNamespace}.DateOnlyFilterRange", FilterKind.InDateOnlyRange, 0);
|
||||
yield return new TypeMapping("global::System.DateOnly", $"global::{NamespaceConstants.RangeNamespace}.DateOnlyFilterRange", FilterKind.InDateOnlyRange, 6);
|
||||
yield return new TypeMapping("global::System.DateOnly?", $"global::{NamespaceConstants.RangeNamespace}.DateOnlyFilterRange", FilterKind.InDateOnlyRange, 6);
|
||||
yield return new TypeMapping("global::System.DateOnly", "global::System.DateOnly?", FilterKind.Equal, 2);
|
||||
yield return new TypeMapping("global::System.DateOnly?", "global::System.DateOnly?", FilterKind.Equal, 2);
|
||||
}
|
||||
|
||||
public static TypeMapping? FindTypeMapping(string modelType, int mode)
|
||||
{
|
||||
var defaultMapping = Mappings.FirstOrDefault(m => m.ModelType == modelType && m.Mode == mode);
|
||||
|
||||
if (defaultMapping is not null)
|
||||
return defaultMapping;
|
||||
|
||||
return mode switch
|
||||
{
|
||||
2 or 0 => new TypeMapping(modelType, modelType.EndsWith("?") ? modelType : modelType + "?", FilterKind.Equal, mode),
|
||||
5 => new TypeMapping(modelType, $"global::System.Collections.Generic.IReadOnlyCollection<{modelType}>?", FilterKind.InList, mode),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
17
src/Baguette.Querying/Baguette.Querying.csproj
Normal file
17
src/Baguette.Querying/Baguette.Querying.csproj
Normal file
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<PackageId>Baguette.Querying</PackageId>
|
||||
<Description>Provides querying based on a pre-defined model.</Description>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0"/>
|
||||
<PackageReference Include="Scrutor" Version="4.2.2" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
8
src/Baguette.Querying/DateOnlyFilterRange.cs
Normal file
8
src/Baguette.Querying/DateOnlyFilterRange.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public readonly record struct DateOnlyFilterRange
|
||||
{
|
||||
public DateOnly? Min { get; init; }
|
||||
|
||||
public DateOnly? Max { get; init; }
|
||||
}
|
||||
8
src/Baguette.Querying/DateTimeFilterRange.cs
Normal file
8
src/Baguette.Querying/DateTimeFilterRange.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public readonly record struct DateTimeFilterRange
|
||||
{
|
||||
public DateTime? Min { get; init; }
|
||||
|
||||
public DateTime? Max { get; init; }
|
||||
}
|
||||
8
src/Baguette.Querying/DateTimeOffsetFilterRange.cs
Normal file
8
src/Baguette.Querying/DateTimeOffsetFilterRange.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public readonly record struct DateTimeOffsetFilterRange
|
||||
{
|
||||
public DateTimeOffset? Min { get; init; }
|
||||
|
||||
public DateTimeOffset? Max { get; init; }
|
||||
}
|
||||
32
src/Baguette.Querying/DefaultQueryBuilder.cs
Normal file
32
src/Baguette.Querying/DefaultQueryBuilder.cs
Normal file
@ -0,0 +1,32 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public class DefaultQueryBuilder<TFilters, TModel> : IQueryBuilder<TFilters, TModel> where TModel : notnull
|
||||
{
|
||||
private readonly List<Func<QueryPipeline<TFilters, TModel>, QueryPipeline<TFilters, TModel>>> queryBuilders = [];
|
||||
|
||||
public void Add(Func<IQueryable<TModel>, TFilters, IQueryable<TModel>> queryBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(queryBuilder);
|
||||
|
||||
queryBuilders.Add(next => (query, filters) => next(queryBuilder(query, filters), filters));
|
||||
}
|
||||
|
||||
public QueryPipeline<TFilters, TModel> Build()
|
||||
{
|
||||
var actualPipeline = BuildPipeline();
|
||||
|
||||
return InvokePipeline;
|
||||
|
||||
IQueryable<TModel> InvokePipeline(IQueryable<TModel> query, TFilters filters)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
return actualPipeline(query, filters);
|
||||
}
|
||||
}
|
||||
|
||||
private QueryPipeline<TFilters, TModel> BuildPipeline()
|
||||
{
|
||||
return queryBuilders.AsEnumerable().Reverse().Aggregate((QueryPipeline<TFilters, TModel>)((query, _) => query), (next, builder) => builder(next));
|
||||
}
|
||||
}
|
||||
31
src/Baguette.Querying/DefaultQueryPipelineProvider.cs
Normal file
31
src/Baguette.Querying/DefaultQueryPipelineProvider.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public class DefaultQueryPipelineProvider(IServiceProvider provider) : IQueryPipelineProvider
|
||||
{
|
||||
public QueryPipeline<TFilters, TModel> GetPipeline<TFilters, TModel>() where TModel : notnull
|
||||
{
|
||||
return provider.GetRequiredService<IQueryPipelineProvider<TFilters, TModel>>().Pipeline;
|
||||
}
|
||||
}
|
||||
|
||||
public class DefaultQueryPipelineProvider<TFilters, TModel>(IEnumerable<IQueryProfile<TFilters, TModel>> profiles) : IQueryPipelineProvider<TFilters, TModel>
|
||||
where TModel : notnull
|
||||
{
|
||||
private QueryPipeline<TFilters, TModel>? pipeline;
|
||||
|
||||
public QueryPipeline<TFilters, TModel> Pipeline => pipeline ??= BuildPipeline();
|
||||
|
||||
private QueryPipeline<TFilters, TModel> BuildPipeline()
|
||||
{
|
||||
var builder = new DefaultQueryBuilder<TFilters, TModel>();
|
||||
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
profile.Configure(builder);
|
||||
}
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
}
|
||||
22
src/Baguette.Querying/DefaultQueryProcessor.cs
Normal file
22
src/Baguette.Querying/DefaultQueryProcessor.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public class DefaultQueryProcessor : IQueryProcessor
|
||||
{
|
||||
private readonly IQueryPipelineProvider pipelineProvider;
|
||||
|
||||
public DefaultQueryProcessor(IQueryPipelineProvider pipelineProvider)
|
||||
{
|
||||
this.pipelineProvider = pipelineProvider;
|
||||
}
|
||||
|
||||
public IQueryable<TModel> ProcessQuery<TFilters, TModel>(IQueryable<TModel> baseQuery, TFilters filters) where TModel : notnull
|
||||
{
|
||||
if (baseQuery == null)
|
||||
throw new ArgumentNullException(nameof(baseQuery));
|
||||
|
||||
if (filters == null)
|
||||
throw new ArgumentNullException(nameof(filters));
|
||||
|
||||
return pipelineProvider.GetPipeline<TFilters, TModel>()(baseQuery, filters);
|
||||
}
|
||||
}
|
||||
10
src/Baguette.Querying/FilterRange.cs
Normal file
10
src/Baguette.Querying/FilterRange.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public readonly record struct FilterRange<T> where T : struct, INumber<T>
|
||||
{
|
||||
public T? Min { get; init; }
|
||||
|
||||
public T? Max { get; init; }
|
||||
}
|
||||
6
src/Baguette.Querying/IMultiSortable.cs
Normal file
6
src/Baguette.Querying/IMultiSortable.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public interface IMultiSortable
|
||||
{
|
||||
IEnumerable<SortingOptions>? Sorting { get; }
|
||||
}
|
||||
6
src/Baguette.Querying/IPageable.cs
Normal file
6
src/Baguette.Querying/IPageable.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public interface IPageable
|
||||
{
|
||||
PagingOptions Paging { get; }
|
||||
}
|
||||
8
src/Baguette.Querying/IQueryBuilder.cs
Normal file
8
src/Baguette.Querying/IQueryBuilder.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public interface IQueryBuilder<TFilters, TModel> where TModel : notnull
|
||||
{
|
||||
void Add(Func<IQueryable<TModel>, TFilters, IQueryable<TModel>> queryBuilder);
|
||||
|
||||
QueryPipeline<TFilters, TModel> Build();
|
||||
}
|
||||
11
src/Baguette.Querying/IQueryPipelineProvider.cs
Normal file
11
src/Baguette.Querying/IQueryPipelineProvider.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public interface IQueryPipelineProvider
|
||||
{
|
||||
QueryPipeline<TFilters, TModel> GetPipeline<TFilters, TModel>() where TModel : notnull;
|
||||
}
|
||||
|
||||
public interface IQueryPipelineProvider<in TFilters, TModel> where TModel : notnull
|
||||
{
|
||||
QueryPipeline<TFilters, TModel> Pipeline { get; }
|
||||
}
|
||||
7
src/Baguette.Querying/IQueryProcessor.cs
Normal file
7
src/Baguette.Querying/IQueryProcessor.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
[Obsolete]
|
||||
public interface IQueryProcessor
|
||||
{
|
||||
IQueryable<TModel> ProcessQuery<TFilters, TModel>(IQueryable<TModel> baseQuery, TFilters filters) where TModel : notnull;
|
||||
}
|
||||
6
src/Baguette.Querying/IQueryProfile.cs
Normal file
6
src/Baguette.Querying/IQueryProfile.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public interface IQueryProfile<TFilters, TModel> where TModel : notnull
|
||||
{
|
||||
void Configure(IQueryBuilder<TFilters, TModel> builder);
|
||||
}
|
||||
6
src/Baguette.Querying/ISortable.cs
Normal file
6
src/Baguette.Querying/ISortable.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public interface ISortable
|
||||
{
|
||||
SortingOptions Sorting { get; }
|
||||
}
|
||||
9
src/Baguette.Querying/LambdaQueryProfile.cs
Normal file
9
src/Baguette.Querying/LambdaQueryProfile.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public class LambdaQueryProfile<TFilters, TModel>(Action<IQueryBuilder<TFilters, TModel>> configure) : IQueryProfile<TFilters, TModel>
|
||||
where TModel : notnull
|
||||
{
|
||||
private readonly Action<IQueryBuilder<TFilters, TModel>> configure = configure ?? throw new ArgumentNullException(nameof(configure));
|
||||
|
||||
public void Configure(IQueryBuilder<TFilters, TModel> builder) => configure(builder);
|
||||
}
|
||||
13
src/Baguette.Querying/PaginatedResult.cs
Normal file
13
src/Baguette.Querying/PaginatedResult.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public class PaginatedResult<T>(IEnumerable<T> items, int? total, bool hasTotal)
|
||||
{
|
||||
public IEnumerable<T> Items { get; } = items;
|
||||
|
||||
public int? Total { get; } = total;
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Total))]
|
||||
public bool HasTotal { get; } = hasTotal;
|
||||
}
|
||||
10
src/Baguette.Querying/PagingOptions.cs
Normal file
10
src/Baguette.Querying/PagingOptions.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public readonly record struct PagingOptions
|
||||
{
|
||||
public int Length { get; init; }
|
||||
|
||||
public int Offset { get; init; }
|
||||
|
||||
public bool ExcludeTotal { get; init; }
|
||||
}
|
||||
73
src/Baguette.Querying/QueryBuilderExtensions.cs
Normal file
73
src/Baguette.Querying/QueryBuilderExtensions.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using System.Linq.Expressions;
|
||||
using Baguette.Querying.Visitors;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public static class QueryBuilderExtensions
|
||||
{
|
||||
public static void AddTransform<TFilters, TTransform, TModel>(
|
||||
this IQueryBuilder<TFilters, TModel> builder, Func<TFilters, TTransform> selector,
|
||||
Action<IQueryBuilder<TTransform, TModel>> configureFilter)
|
||||
where TModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentNullException.ThrowIfNull(selector);
|
||||
ArgumentNullException.ThrowIfNull(configureFilter);
|
||||
|
||||
var filterBuilder = new DefaultQueryBuilder<TTransform, TModel>();
|
||||
|
||||
configureFilter(filterBuilder);
|
||||
|
||||
var transformPipeline = filterBuilder.Build();
|
||||
|
||||
builder.Add((query, filters) => transformPipeline(query, selector(filters)));
|
||||
}
|
||||
|
||||
public static void AddFilter<TFilters, TModel>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
Expression<Func<TFilters, TModel, bool>> predicate,
|
||||
Func<TFilters, bool>? guard = null)
|
||||
where TModel : notnull // Shorthand for Add(query => query.Where(predicate))
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
|
||||
builder.Add((query, filters) =>
|
||||
{
|
||||
if (guard is not null && !guard(filters))
|
||||
return query;
|
||||
|
||||
var replacedPredicate = predicate.Body.Replace(predicate.Parameters[0], Expression.Constant(filters));
|
||||
|
||||
return query.Where(Expression.Lambda<Func<TModel, bool>>(replacedPredicate, predicate.Parameters[1]));
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddPropertyFilter<TFilters, TModel, TFilterProperty, TModelProperty>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
Func<TFilters, TFilterProperty> filterSelector,
|
||||
Expression<Func<TModel, TModelProperty>> modelSelector,
|
||||
Expression<Func<TFilterProperty, TModelProperty, bool>> predicate,
|
||||
Func<TFilterProperty, bool>? guard = null)
|
||||
where TModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentNullException.ThrowIfNull(filterSelector);
|
||||
ArgumentNullException.ThrowIfNull(modelSelector);
|
||||
ArgumentNullException.ThrowIfNull(predicate);
|
||||
|
||||
builder.Add((query, filters) =>
|
||||
{
|
||||
var filter = filterSelector(filters);
|
||||
|
||||
if (guard is not null && !guard(filter))
|
||||
return query;
|
||||
|
||||
var replacedExpression = predicate.Body
|
||||
.Replace(predicate.Parameters[0], Expression.Constant(filter))
|
||||
.Replace(predicate.Parameters[1], modelSelector.Body);
|
||||
|
||||
return query.Where(Expression.Lambda<Func<TModel, bool>>(replacedExpression, modelSelector.Parameters));
|
||||
});
|
||||
}
|
||||
}
|
||||
131
src/Baguette.Querying/QueryBuilderFilterExtensions.cs
Normal file
131
src/Baguette.Querying/QueryBuilderFilterExtensions.cs
Normal file
@ -0,0 +1,131 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Numerics;
|
||||
using Baguette.Querying.Visitors;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public static class QueryBuilderFilterExtensions
|
||||
{
|
||||
public static void AddMultiSearch<TFilters, TModel>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
Func<TFilters, string?> filterSelector,
|
||||
params Expression<Func<TModel, string?>>[] modelSelectors)
|
||||
where TModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentNullException.ThrowIfNull(filterSelector);
|
||||
ArgumentNullException.ThrowIfNull(modelSelectors);
|
||||
|
||||
if (modelSelectors.Length == 0)
|
||||
return;
|
||||
|
||||
var filterParameter = Expression.Parameter(typeof(string), "search");
|
||||
var modelParameter = Expression.Parameter(typeof(TModel), "model");
|
||||
|
||||
var expression = modelSelectors
|
||||
.Select(m => m.Body.Replace(m.Parameters[0], modelParameter))
|
||||
.Select(m => Expression.AndAlso(Expression.NotEqual(m, Expression.Constant(null, m.Type)),
|
||||
Expression.Call(Expression.Call(m, nameof(string.ToLower), null), "Contains", null, filterParameter)))
|
||||
.Aggregate(Expression.OrElse);
|
||||
|
||||
var predicate = Expression.Lambda<Func<string?, TModel, bool>>(expression, filterParameter, modelParameter);
|
||||
|
||||
builder.AddTransform(x => filterSelector(x)?.ToLower(), nestedBuilder => nestedBuilder.AddFilter(predicate, filter => !string.IsNullOrEmpty(filter)));
|
||||
}
|
||||
|
||||
public static void AddTextFilter<TFilters, TModel>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
Func<TFilters, string?> filterSelector,
|
||||
Expression<Func<TModel, string?>> modelSelector)
|
||||
where TFilters : notnull where TModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(filterSelector);
|
||||
|
||||
builder.AddPropertyFilter(
|
||||
filters => filterSelector(filters)?.ToLower(),
|
||||
modelSelector,
|
||||
(filter, model) => model != null && model.ToLower().Contains(filter!),
|
||||
filter => !string.IsNullOrEmpty(filter));
|
||||
}
|
||||
|
||||
public static void AddInListFilter<TFilters, TModel, TItem>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
Func<TFilters, IReadOnlyCollection<TItem>?> filterSelector,
|
||||
Expression<Func<TModel, TItem>> modelSelector)
|
||||
where TFilters : notnull where TModel : notnull
|
||||
{
|
||||
builder.AddPropertyFilter(filterSelector, modelSelector, (filter, model) => filter!.Contains(model), filter => filter is not null && filter.Count != 0);
|
||||
}
|
||||
|
||||
public static void AddEqualsFilter<TFilters, TModel, TItem>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
Func<TFilters, TItem?> filterSelector,
|
||||
Expression<Func<TModel, TItem>> modelSelector)
|
||||
where TFilters : notnull where TModel : notnull
|
||||
{
|
||||
builder.AddPropertyFilter(filterSelector, modelSelector, (filter, model) => Equals(filter, model), filter => filter is string text ? !string.IsNullOrEmpty(text) : filter is not null);
|
||||
}
|
||||
|
||||
public static void AddInRangeFilter<TFilters, TModel, T>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
Func<TFilters, FilterRange<T>> filterSelector,
|
||||
Expression<Func<TModel, T>> modelSelector)
|
||||
where TFilters : notnull where TModel : notnull where T : struct, INumber<T>
|
||||
{
|
||||
var rangeParameter = Expression.Parameter(typeof(FilterRange<T>), "range");
|
||||
var modelParameter = Expression.Parameter(typeof(T), "model");
|
||||
|
||||
var minProperty = Expression.Property(rangeParameter, typeof(FilterRange<T>), "Min");
|
||||
var maxProperty = Expression.Property(rangeParameter, typeof(FilterRange<T>), "Max");
|
||||
|
||||
var minExpression = Expression.OrElse(Expression.Equal(minProperty, Expression.Constant(null, typeof(T?))),
|
||||
Expression.GreaterThanOrEqual(modelParameter, Expression.Property(minProperty, typeof(T?), "Value")));
|
||||
|
||||
var maxExpression = Expression.OrElse(Expression.Equal(maxProperty, Expression.Constant(null, typeof(T?))),
|
||||
Expression.LessThanOrEqual(modelParameter, Expression.Property(maxProperty, typeof(T?), "Value")));
|
||||
|
||||
builder.AddPropertyFilter(
|
||||
filterSelector,
|
||||
modelSelector,
|
||||
Expression.Lambda<Func<FilterRange<T>, T, bool>>(Expression.AndAlso(minExpression, maxExpression), rangeParameter, modelParameter),
|
||||
range => range.Min is not null || range.Max is not null);
|
||||
}
|
||||
|
||||
public static void AddInDateRangeFilter<TFilters, TModel>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
Func<TFilters, DateTimeOffsetFilterRange> filterSelector,
|
||||
Expression<Func<TModel, DateTimeOffset?>> modelSelector)
|
||||
where TFilters : notnull where TModel : notnull
|
||||
{
|
||||
builder.AddPropertyFilter(
|
||||
filterSelector,
|
||||
modelSelector,
|
||||
(range, model) => (range.Min == null || model >= range.Min.Value) && (range.Max == null || model <= range.Max),
|
||||
range => range.Min is not null || range.Max is not null);
|
||||
}
|
||||
|
||||
public static void AddInDateRangeFilter<TFilters, TModel>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
Func<TFilters, DateTimeFilterRange> filterSelector,
|
||||
Expression<Func<TModel, DateTime?>> modelSelector) where TFilters : notnull where TModel : notnull
|
||||
{
|
||||
builder.AddPropertyFilter(
|
||||
filterSelector,
|
||||
modelSelector,
|
||||
(range, model) => (range.Min == null || model >= range.Min.Value) && (range.Max == null || model <= range.Max),
|
||||
range => range.Min is not null || range.Max is not null);
|
||||
}
|
||||
|
||||
public static void AddInDateOnlyRangeFilter<TFilters, TModel>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
Func<TFilters, DateOnlyFilterRange> filterSelector,
|
||||
Expression<Func<TModel, DateOnly?>> modelSelector)
|
||||
where TFilters : notnull where TModel : notnull
|
||||
{
|
||||
builder.AddPropertyFilter(
|
||||
filterSelector,
|
||||
modelSelector,
|
||||
(range, model) => (range.Min == null || model >= range.Min.Value) && (range.Max == null || model <= range.Max),
|
||||
range => range.Min is not null || range.Max is not null);
|
||||
}
|
||||
}
|
||||
50
src/Baguette.Querying/QueryBuilderSortingExtensions.cs
Normal file
50
src/Baguette.Querying/QueryBuilderSortingExtensions.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public static class QueryBuilderSortingExtensions
|
||||
{
|
||||
public static void AddSorting<TFilters, TModel, TProperty>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
string key,
|
||||
Expression<Func<TModel, TProperty>> selector)
|
||||
where TFilters : ISortable where TModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentNullException.ThrowIfNull(selector);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(key);
|
||||
|
||||
builder.Add((query, filters) =>
|
||||
{
|
||||
if (filters.Sorting.SortBy != key)
|
||||
return query;
|
||||
|
||||
return query.Expression.Type == typeof(IOrderedQueryable<TModel>)
|
||||
? ((IOrderedQueryable<TModel>)query).ThenBy(selector, filters.Sorting.SortDescending)
|
||||
: query.OrderBy(selector, filters.Sorting.SortDescending);
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddMultiSorting<TFilters, TModel>(
|
||||
this IQueryBuilder<TFilters, TModel> builder,
|
||||
Action<IQueryBuilder<SortingFilter, TModel>> configureSorting)
|
||||
where TFilters : IMultiSortable where TModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
ArgumentNullException.ThrowIfNull(configureSorting);
|
||||
|
||||
var sortingBuilder = new DefaultQueryBuilder<SortingFilter, TModel>();
|
||||
|
||||
configureSorting(sortingBuilder);
|
||||
|
||||
var sortingPipeline = sortingBuilder.Build();
|
||||
|
||||
builder.Add((query, filters) =>
|
||||
{
|
||||
if (filters.Sorting is null || !filters.Sorting.Any())
|
||||
return query;
|
||||
|
||||
return filters.Sorting.Aggregate(query, (current, sorting) => sortingPipeline(current, new SortingFilter(sorting)));
|
||||
});
|
||||
}
|
||||
}
|
||||
3
src/Baguette.Querying/QueryPipeline.cs
Normal file
3
src/Baguette.Querying/QueryPipeline.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public delegate IQueryable<TModel> QueryPipeline<in TFilters, TModel>(IQueryable<TModel> query, TFilters filters) where TModel : notnull;
|
||||
58
src/Baguette.Querying/QueryPipelineProviderExtensions.cs
Normal file
58
src/Baguette.Querying/QueryPipelineProviderExtensions.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using Baguette.Querying.Visitors;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public static class QueryPipelineProviderExtensions
|
||||
{
|
||||
[Pure]
|
||||
public static Expression<Func<TModel, bool>> GetPredicate<TFilters, TModel>(this IQueryPipelineProvider provider, TFilters filters) where TFilters : notnull where TModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
|
||||
var pipeline = provider.GetPipeline<TFilters, TModel>();
|
||||
var query = pipeline(Enumerable.Empty<TModel>().AsQueryable(), filters);
|
||||
|
||||
var visitor = new WhereExpressionVisitor<TModel>();
|
||||
|
||||
visitor.Visit(query.Expression);
|
||||
|
||||
return visitor.Predicate;
|
||||
}
|
||||
|
||||
private class WhereExpressionVisitor<TModel> : ExpressionVisitor
|
||||
{
|
||||
private readonly MethodInfo method = new Func<IQueryable<TModel>, Expression<Func<TModel, bool>>, IQueryable<TModel>>(Queryable.Where).Method;
|
||||
private readonly List<Expression<Func<TModel, bool>>> predicates = [];
|
||||
|
||||
public Expression<Func<TModel, bool>> Predicate
|
||||
{
|
||||
get
|
||||
{
|
||||
if (predicates.Count == 0)
|
||||
return _ => true;
|
||||
|
||||
return predicates.Aggregate((res, curr) =>
|
||||
{
|
||||
var parameter = Expression.Parameter(typeof(TModel), "model");
|
||||
|
||||
return Expression.Lambda<Func<TModel, bool>>(Expression.And(res.Body.Replace(res.Parameters[0], parameter), curr.Body.Replace(curr.Parameters[0], parameter)), parameter);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override Expression VisitMethodCall(MethodCallExpression node)
|
||||
{
|
||||
if (node.Method != method)
|
||||
return base.VisitMethodCall(node);
|
||||
|
||||
var unary = (UnaryExpression)node.Arguments[1];
|
||||
|
||||
predicates.Add((Expression<Func<TModel, bool>>)unary.Operand);
|
||||
|
||||
return base.VisitMethodCall(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/Baguette.Querying/QueryServiceCollectionExtensions.cs
Normal file
46
src/Baguette.Querying/QueryServiceCollectionExtensions.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public static class QueryServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection ConfigureQuery<TFilters, TModel>(
|
||||
this IServiceCollection services,
|
||||
Action<IQueryBuilder<TFilters, TModel>> configure)
|
||||
where TModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.AddQuerying();
|
||||
services.AddSingleton<IQueryProfile<TFilters, TModel>>(new LambdaQueryProfile<TFilters, TModel>(configure));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddQuerying(this IServiceCollection services)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.TryAddTransient<IQueryProcessor, DefaultQueryProcessor>();
|
||||
services.TryAddSingleton(typeof(IQueryPipelineProvider<,>), typeof(DefaultQueryPipelineProvider<,>));
|
||||
services.TryAddSingleton<IQueryPipelineProvider, DefaultQueryPipelineProvider>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddQueryProfiles(this IServiceCollection services, params Type[] assemblyMarkerTypes)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
|
||||
services.AddQuerying();
|
||||
|
||||
services.Scan(scanner => scanner
|
||||
.FromAssembliesOf(assemblyMarkerTypes)
|
||||
.AddClasses(classes => classes.AssignableTo(typeof(IQueryProfile<,>)))
|
||||
.AsImplementedInterfaces()
|
||||
.WithSingletonLifetime());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
16
src/Baguette.Querying/QueryableExtensions.cs
Normal file
16
src/Baguette.Querying/QueryableExtensions.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public static class QueryableExtensions
|
||||
{
|
||||
public static IOrderedQueryable<T> OrderBy<T, TProperty>(this IQueryable<T> query, Expression<Func<T, TProperty>> selector, bool desc)
|
||||
{
|
||||
return desc ? query.OrderByDescending(selector) : query.OrderBy(selector);
|
||||
}
|
||||
|
||||
public static IOrderedQueryable<T> ThenBy<T, TProperty>(this IOrderedQueryable<T> query, Expression<Func<T, TProperty>> selector, bool desc)
|
||||
{
|
||||
return desc ? query.ThenByDescending(selector) : query.ThenBy(selector);
|
||||
}
|
||||
}
|
||||
26
src/Baguette.Querying/QueryableProcessorExtensions.cs
Normal file
26
src/Baguette.Querying/QueryableProcessorExtensions.cs
Normal file
@ -0,0 +1,26 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public static class QueryableProcessorExtensions
|
||||
{
|
||||
public static IQueryable<TModel> ApplyFilters<TFilters, TModel>(this IQueryable<TModel> baseQuery, IQueryProcessor processor, TFilters filters)
|
||||
where TFilters : notnull where TModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(baseQuery);
|
||||
ArgumentNullException.ThrowIfNull(processor);
|
||||
|
||||
return processor.ProcessQuery(baseQuery, filters);
|
||||
}
|
||||
|
||||
public static PaginatedResult<TModel> Paginate<TFilters, TModel>(this IQueryable<TModel> baseQuery, IQueryProcessor processor, TFilters filters)
|
||||
where TFilters : notnull, IPageable where TModel : notnull
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(baseQuery);
|
||||
ArgumentNullException.ThrowIfNull(processor);
|
||||
|
||||
var query = processor.ProcessQuery(baseQuery, filters);
|
||||
var count = !filters.Paging.ExcludeTotal ? (int?)query.Count() : null;
|
||||
var results = query.Skip(filters.Paging.Offset).Take(filters.Paging.Length).ToList();
|
||||
|
||||
return new PaginatedResult<TModel>(results, count, !filters.Paging.ExcludeTotal);
|
||||
}
|
||||
}
|
||||
3
src/Baguette.Querying/SortingFilter.cs
Normal file
3
src/Baguette.Querying/SortingFilter.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public record SortingFilter(SortingOptions Sorting) : ISortable;
|
||||
3
src/Baguette.Querying/SortingOptions.cs
Normal file
3
src/Baguette.Querying/SortingOptions.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace Baguette.Querying;
|
||||
|
||||
public readonly record struct SortingOptions(string? SortBy, bool SortDescending);
|
||||
30
src/Baguette.Querying/Visitors/ExpressionExtensions.cs
Normal file
30
src/Baguette.Querying/Visitors/ExpressionExtensions.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Baguette.Querying.Visitors;
|
||||
|
||||
public static class ExpressionExtensions
|
||||
{
|
||||
[Pure]
|
||||
public static Expression Replace(this Expression expression, Expression source, Expression target)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(expression);
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
ArgumentNullException.ThrowIfNull(target);
|
||||
|
||||
return new ReplaceExpressionVisitor(source, target).Visit(expression);
|
||||
}
|
||||
|
||||
private class ReplaceExpressionVisitor(Expression source, Expression target) : ExpressionVisitor
|
||||
{
|
||||
[return: NotNullIfNotNull(nameof(node))]
|
||||
public override Expression? Visit(Expression? node)
|
||||
{
|
||||
if (node == source)
|
||||
return target;
|
||||
|
||||
return base.Visit(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user