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