├── .github └── workflows │ ├── codeql-analysis-2.yml │ ├── codeql-analysis.yml │ ├── dotnetcore-build.yml │ └── nuget-publish.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dynamic.OData.sln ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── benchmark └── Dynamic.OData.Benchmarks │ ├── BaseBenchmark.cs │ ├── Data │ └── EntityTypeSettings.json │ ├── Dynamic.OData.Benchmarks.csproj │ ├── ODataQueryBenchmarks.cs │ └── Program.cs ├── samples └── Dynamic.OData.Samples │ ├── Controllers │ └── MatchAllController.cs │ ├── Dynamic.OData.Samples.csproj │ ├── GenericEntityRepository.cs │ ├── MatchAllRoutingConvention.cs │ ├── ODataModelSettingsProvider.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Settings │ ├── EntityTypeSettings.json │ └── GenericEntity.cs │ ├── Startup.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── content.css │ └── readme.md ├── src └── Dynamic.OData │ ├── Class1.cs │ ├── CustomEqualityComparer.cs │ ├── Dynamic.OData.csproj │ ├── Exceptions │ ├── FeatureNotSupportedException.cs │ └── InvalidPropertyException.cs │ ├── Helpers │ ├── Interface │ │ ├── IODataQueryValidator.cs │ │ └── IODataRequestHelper.cs │ ├── ODataQueryValidator.cs │ └── ODataRequestHelper.cs │ ├── Interface │ └── IODataFilterManager.cs │ ├── Models │ ├── APIResponseMappingObject.cs │ ├── EdmEntityTypePropertySetting.cs │ ├── EdmEntityTypeSettings.cs │ ├── ODataContext.cs │ └── TypeHandlingConstants.cs │ ├── ODataFilterConstants.cs │ ├── ODataFilterManager.cs │ ├── ODataServiceCollectionExtensions.cs │ ├── ParseContext.cs │ ├── PredicateParsers │ ├── BaseODataPredicateParser.cs │ ├── Interface │ │ └── IODataPredicateParser.cs │ ├── ODataApplyPredicateParser.cs │ ├── ODataFilterPredicateParser.cs │ ├── ODataOrderByPredicateParser.cs │ ├── ODataSelectPredicateParser.cs │ ├── ODataSkipPredicateParser.cs │ └── ODataTopPredicateParser.cs │ └── RequestFilterConstants.cs └── test └── UnitTest └── Dynamic.OData.Tests ├── Data ├── EntityTypeSettings.json └── sampledata.json ├── Dynamic.OData.Tests.csproj ├── ODataPredicateParserTests.cs └── Tests.cs /.github/workflows/codeql-analysis-2.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '25 6 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'csharp' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "Code scanning" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '0 0 * * 0' 10 | 11 | jobs: 12 | CodeQL-Build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | with: 20 | # We must fetch at least the immediate parents so that if this is 21 | # a pull request then we can checkout the head. 22 | fetch-depth: 2 23 | 24 | # If this run was triggered by a pull request event, then checkout 25 | # the head of the pull request instead of the merge commit. 26 | - run: git checkout HEAD^2 27 | if: ${{ github.event_name == 'pull_request' }} 28 | 29 | # Initializes the CodeQL tools for scanning. 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v1 32 | # Override language selection by uncommenting this and choosing your languages 33 | with: 34 | languages: csharp 35 | 36 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 37 | # If this step fails, then you should remove it and run the build manually (see below) 38 | - name: Autobuild 39 | uses: github/codeql-action/autobuild@v1 40 | 41 | # ℹ️ Command-line programs to run using the OS shell. 42 | # 📚 https://git.io/JvXDl 43 | 44 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 45 | # and modify them (or add more) to build your code if your project 46 | # uses a compiled language 47 | 48 | #- run: | 49 | # make bootstrap 50 | # make release 51 | 52 | - name: Perform CodeQL Analysis 53 | uses: github/codeql-action/analyze@v1 54 | -------------------------------------------------------------------------------- /.github/workflows/dotnetcore-build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | # extract branch name 16 | - name: Extract branch name 17 | if: github.event_name != 'pull_request' 18 | shell: bash 19 | run: echo "BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_ENV 20 | id: extract_branch 21 | 22 | # extract branch name on pull request 23 | - name: Extract branch name on pull request 24 | if: github.event_name == 'pull_request' 25 | run: echo "BRANCH_NAME=$(echo ${GITHUB_HEAD_REF})" >> $GITHUB_ENV 26 | 27 | # print branch name 28 | - name: Get branch name 29 | run: echo "The branch name is ${{ env.BRANCH_NAME }}" 30 | 31 | 32 | - uses: actions/checkout@v2 33 | - name: Setup .NET Core 34 | uses: actions/setup-dotnet@v1 35 | with: 36 | dotnet-version: 3.1 37 | 38 | - name: Install dependencies 39 | run: dotnet restore Dynamic.OData.sln 40 | 41 | - name: Build 42 | run: dotnet build Dynamic.OData.sln --configuration Release --no-restore 43 | 44 | - name: Test 45 | run: dotnet test Dynamic.OData.sln --no-build --configuration Release --verbosity m 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/nuget-publish.yml: -------------------------------------------------------------------------------- 1 | name: Nuget Publish 2 | on: 3 | [workflow_dispatch] 4 | jobs: 5 | publish: 6 | name: build, pack & publish 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Setup dotnet 12 | uses: actions/setup-dotnet@v1 13 | with: 14 | dotnet-version: 3.1.x 15 | 16 | # Publish 17 | - name: publish on version change 18 | id: publish_nuget 19 | uses: rohith/publish-nuget@v2 20 | with: 21 | # Filepath of the project to be packaged, relative to root of repository 22 | PROJECT_FILE_PATH: src/Dynamic.OData/Dynamic.OData.csproj 23 | 24 | # NuGet package id, used for version detection & defaults to project name 25 | # PACKAGE_NAME: Core 26 | 27 | # Filepath with version info, relative to root of repository & defaults to PROJECT_FILE_PATH 28 | # VERSION_FILE_PATH: Directory.Build.props 29 | 30 | # Regex pattern to extract version info in a capturing group 31 | # VERSION_REGEX: ^\s*(.*)<\/Version>\s*$ 32 | 33 | # Useful with external providers like Nerdbank.GitVersioning, ignores VERSION_FILE_PATH & VERSION_REGEX 34 | #VERSION_STATIC: 0.0.1 35 | 36 | # Flag to toggle git tagging, enabled by default 37 | # TAG_COMMIT: true 38 | 39 | # Format of the git tag, [*] gets replaced with actual version 40 | # TAG_FORMAT: v* 41 | 42 | # API key to authenticate with NuGet server 43 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 44 | 45 | # NuGet server uri hosting the packages, defaults to https://api.nuget.org 46 | # NUGET_SOURCE: https://api.nuget.org 47 | 48 | # Flag to toggle pushing symbols along with nuget package to the server, disabled by default 49 | # INCLUDE_SYMBOLS: false 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [1.0.0] - 26-Feb-2021 6 | 7 | ### Added 8 | - The first version of the NuGet 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /Dynamic.OData.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30804.86 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dynamic.OData", "src\Dynamic.OData\Dynamic.OData.csproj", "{FE253246-8828-471A-9C04-B36417C08711}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dynamic.OData.Tests", "test\UnitTest\Dynamic.OData.Tests\Dynamic.OData.Tests.csproj", "{4F00660C-3D0D-4FD8-8BDB-91AC5EE66CDF}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dynamic.OData.Benchmarks", "benchmark\Dynamic.OData.Benchmarks\Dynamic.OData.Benchmarks.csproj", "{93FC819E-A6C5-4D18-BE06-EF0079C91291}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dynamic.OData.Samples", "samples\Dynamic.OData.Samples\Dynamic.OData.Samples.csproj", "{B6BF7D71-CE86-4EC2-BA27-A12F08ADA83F}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {FE253246-8828-471A-9C04-B36417C08711}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {FE253246-8828-471A-9C04-B36417C08711}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {FE253246-8828-471A-9C04-B36417C08711}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {FE253246-8828-471A-9C04-B36417C08711}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {4F00660C-3D0D-4FD8-8BDB-91AC5EE66CDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {4F00660C-3D0D-4FD8-8BDB-91AC5EE66CDF}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {4F00660C-3D0D-4FD8-8BDB-91AC5EE66CDF}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {4F00660C-3D0D-4FD8-8BDB-91AC5EE66CDF}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {93FC819E-A6C5-4D18-BE06-EF0079C91291}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {93FC819E-A6C5-4D18-BE06-EF0079C91291}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {93FC819E-A6C5-4D18-BE06-EF0079C91291}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {93FC819E-A6C5-4D18-BE06-EF0079C91291}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {B6BF7D71-CE86-4EC2-BA27-A12F08ADA83F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {B6BF7D71-CE86-4EC2-BA27-A12F08ADA83F}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {B6BF7D71-CE86-4EC2-BA27-A12F08ADA83F}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {B6BF7D71-CE86-4EC2-BA27-A12F08ADA83F}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {AA678C53-7771-428C-8DA2-96B80A98FD33} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build](https://github.com/microsoft/dynamic-odata/actions/workflows/dotnetcore-build.yml/badge.svg)](https://github.com/microsoft/dynamic-odata/actions/workflows/dotnetcore-build.yml) 2 | [![NuGet package](https://img.shields.io/nuget/v/Dynamic.OData.svg)](https://nuget.org/packages/Dynamic.OData) 3 | [![NuGet downloads](https://img.shields.io/nuget/dt/Dynamic.OData.svg)](https://nuget.org/packages/Dynamic.OData) 4 | 5 | # Dynamic OData Query Library 6 | 7 | ## Overview 8 | Dynamic OData is a query library built upon [OData Web API](https://github.com/OData/WebApi) that allows you to query dynamically created 9 | Entity Data Models. 10 | 11 | OData expects the return schema to be static at compile time, there are scenarios where applications would want to construct the return response on the go. 12 | This library helps to achieve that with configurable model and providing metadata which are used at runtime to create dynamic response schema. 13 | 14 | This provides flexibity to have a dynamic schema and still enable the OData magic to work. The library enables you to construct a Controller method of IEnumerable < IEdmEntityObject > return type and then construct this Object using a mapped Dictionary. 15 | 16 | 17 | ## Installation 18 | To install this library, please download the latest version of [NuGet Package](https://www.nuget.org/packages/Dynamic.OData) from [nuget.org](https://www.nuget.org/) and refer it into your project. 19 | 20 | ## How to use 21 | 22 | Refer to the samples at https://github.com/microsoft/dynamic-odata/blob/main/samples 23 | 24 | 25 | ## Testing 26 | 27 | .\src>dotnet test 28 | 29 | |Status|Failed|Passed|Skipped|Total|Duration| 30 | |------|------|------|-------|-----|--------| 31 | |Passed!|0|40|0|40|3 s| 32 | 33 | #### Coverage 34 | 35 | | |Not Covered (Blocks)|Not Covered (%Blocks)|Covered (Blocks)|Covered (%Blocks)| 36 | |---------|--------------------|---------------------|----------------|-----------------| 37 | |.coverage|540|12.73%|3703|87.27%| 38 | 39 | 40 | ## Benchmark 41 | 42 | .\src\benchmark\Dynamic.OData.Benchmarks> dotnet run -c Release --filter * 43 | 44 | BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 45 | Intel Core i7 CPU 860 2.80GHz (Nehalem), 1 CPU, 8 logical and 4 physical cores 46 | .NET Core SDK=5.0.200-preview.20614.14 47 | [Host] : .NET Core 3.1.11 (CoreCLR 4.700.20.56602, CoreFX 4.700.20.56604), X64 RyuJIT 48 | DefaultJob : .NET Core 3.1.11 (CoreCLR 4.700.20.56602, CoreFX 4.700.20.56604), X64 RyuJIT 49 | 50 | 51 | |Method|Mean|Error|StdDev| 52 | |------|----|-----|------| 53 | |ODataGroupByAndAggregate|179.2 ms|10.53 ms|30.20 ms| 54 | 55 | 56 | ## Contributing 57 | 58 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 59 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 60 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 61 | 62 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 63 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 64 | provided by the bot. You will only need to do this once across all repos using our CLA. 65 | 66 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 67 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 68 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 69 | 70 | ## Code of Conduct 71 | 72 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 73 | 74 | 75 | ## Trademarks 76 | 77 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft 78 | trademarks or logos is subject to and must follow 79 | [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). 80 | Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. 81 | Any use of third-party trademarks or logos are subject to those third-party's policies. 82 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | For help and questions about using this project, please create your issues in issue tracker here https://github.com/microsoft/dynamic-odata/issues 10 | 11 | ## Microsoft Support Policy 12 | 13 | Support for this **PROJECT or PRODUCT** is limited to the resources listed above. 14 | -------------------------------------------------------------------------------- /benchmark/Dynamic.OData.Benchmarks/BaseBenchmark.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Dynamic.OData.Helpers; 5 | using Dynamic.OData.Helpers.Interface; 6 | using Dynamic.OData.Models; 7 | using Dynamic.OData.PredicateParsers; 8 | using Dynamic.OData.Samples; 9 | using Microsoft.AspNet.OData.Extensions; 10 | using Microsoft.AspNet.OData.Query; 11 | using Microsoft.AspNetCore.Builder; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.Routing; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using Moq; 16 | using Newtonsoft.Json; 17 | using System; 18 | using System.IO; 19 | 20 | namespace Dynamic.OData.Benchmarks 21 | { 22 | public class BaseBenchmark 23 | { 24 | protected IODataRequestHelper _oDataRequestHelper; 25 | protected IGenericEntityRepository _genericEntityRepository; 26 | protected const string EdmNamespaceName = "Contoso.Model"; 27 | private ServiceProvider _provider; 28 | protected EdmEntityTypeSettings _edmEntityTypeSettings; 29 | protected HttpContext _httpContext; 30 | 31 | protected void BeforeEachBenchmark(int recordCount) 32 | { 33 | var collection = new ServiceCollection(); 34 | collection.AddOData(); 35 | collection.AddODataQueryFilter(); 36 | _provider = collection.BuildServiceProvider(); 37 | var routeBuilder = new RouteBuilder(Mock.Of(x => x.ApplicationServices == _provider)); 38 | routeBuilder.EnableDependencyInjection(); 39 | _oDataRequestHelper = new ODataRequestHelper(); 40 | _edmEntityTypeSettings = GetEdmEntityTypeSettings(); 41 | _httpContext = new DefaultHttpContext(); 42 | _genericEntityRepository = new GenericEntityRepository(recordCount); 43 | _oDataRequestHelper.GetEdmModel(_httpContext.Request, _edmEntityTypeSettings, EdmNamespaceName); 44 | } 45 | private EdmEntityTypeSettings GetEdmEntityTypeSettings() 46 | { 47 | var data = File.ReadAllText(Directory.GetCurrentDirectory() + @"\Data\EntityTypeSettings.json"); 48 | data = data.Replace("//Copyright (c) Microsoft Corporation. All rights reserved.// Licensed under the MIT License. See License.txt in the project root for license information.\r\n", string.Empty).Trim(); 49 | var settings = JsonConvert.DeserializeObject(data); 50 | return settings; 51 | } 52 | 53 | protected void SetRequestHost(Uri uri) 54 | { 55 | _httpContext.Request.Host = new HostString("localhost", 44357); 56 | if (uri != null) 57 | { 58 | _httpContext.Request.Path = uri.LocalPath; 59 | _httpContext.Request.Scheme = "HTTPS"; 60 | _httpContext.Request.QueryString = new QueryString(uri.Query); 61 | } 62 | _httpContext.RequestServices = _provider; 63 | } 64 | 65 | protected ODataFilterManager GetODataFilterManager() 66 | { 67 | var queryValidator = new Mock(); 68 | BaseODataPredicateParser.EdmNamespaceName = "Dynamic.OData.Model"; 69 | queryValidator.Setup(p => p.ValidateQuery(It.IsAny())).Verifiable(); 70 | return new ODataFilterManager( 71 | _oDataRequestHelper, 72 | queryValidator.Object, 73 | new ODataApplyPredicateParser(), 74 | new ODataSelectPredicateParser(), 75 | new ODataTopPredicateParser(), 76 | new ODataSkipPredicateParser(), 77 | new ODataOrderByPredicateParser(), 78 | new ODataFilterPredicateParser() 79 | ); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /benchmark/Dynamic.OData.Benchmarks/Data/EntityTypeSettings.json: -------------------------------------------------------------------------------- 1 | //Copyright (c) Microsoft Corporation. All rights reserved.// Licensed under the MIT License. See License.txt in the project root for license information. 2 | { 3 | "RouteName": "user", 4 | "Properties": [ 5 | { 6 | "PropertyName": "id", 7 | "PropertyType": "Guid", 8 | "IsNullable": false 9 | }, 10 | { 11 | "PropertyName": "Title", 12 | "PropertyType": "String" 13 | }, 14 | { 15 | "PropertyName": "FirstName", 16 | "PropertyType": "String" 17 | }, 18 | { 19 | "PropertyName": "LastName", 20 | "PropertyType": "String" 21 | }, 22 | { 23 | "PropertyName": "Age", 24 | "PropertyType": "Int32" 25 | }, 26 | { 27 | "PropertyName": "Salary", 28 | "PropertyType": "Decimal" 29 | }, 30 | { 31 | "PropertyName": "BornOn", 32 | "PropertyType": "DateTime" 33 | }, 34 | { 35 | "PropertyName": "Department", 36 | "PropertyType": "String" 37 | }, 38 | { 39 | "PropertyName": "EmailAddress", 40 | "PropertyType": "String" 41 | }, 42 | { 43 | "PropertyName": "EmployeeNumber", 44 | "PropertyType": "Int16" 45 | }, 46 | { 47 | "PropertyName": "UniversalId", 48 | "PropertyType": "Int64" 49 | }, 50 | { 51 | "PropertyName": "VacationDaysInHours", 52 | "PropertyType": "Double" 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /benchmark/Dynamic.OData.Benchmarks/Dynamic.OData.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | Exe 8 | netcoreapp3.1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Always 18 | true 19 | PreserveNewest 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /benchmark/Dynamic.OData.Benchmarks/ODataQueryBenchmarks.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using BenchmarkDotNet.Attributes; 5 | using Microsoft.AspNet.OData; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.OData.Edm; 8 | using System; 9 | using System.Collections.Generic; 10 | 11 | namespace Dynamic.OData.Benchmarks 12 | { 13 | public class ODataQueryBenchmarks : BaseBenchmark 14 | { 15 | private static EdmEntityObjectCollection edmEntityObjectCollection; 16 | private static List> queryCollection; 17 | private static string _url = string.Empty; 18 | private static ODataFilterManager _oDataFilterManager; 19 | private const int recordCount = 10000; 20 | public ODataQueryBenchmarks() 21 | { 22 | BeforeEachBenchmark(recordCount); 23 | _url = "https://localhost:44312/odata/entities/user?$apply=groupby((Title),aggregate(Salary with max as MaxSalary))"; 24 | SetRequestHost(new Uri(_url)); 25 | var request = _httpContext.Request; 26 | var entityType = _oDataRequestHelper.GetEdmEntityTypeReference(request); 27 | var collectionType = _oDataRequestHelper.GetEdmCollectionType(request); 28 | 29 | edmEntityObjectCollection = new EdmEntityObjectCollection(new EdmCollectionTypeReference(collectionType)); 30 | queryCollection = new List>(); 31 | var entities = _genericEntityRepository.GetEntities("user"); 32 | foreach (var entity in entities) 33 | { 34 | var dynamicEntityDictionary = entity.PropertyList; 35 | edmEntityObjectCollection.Add(GetEdmEntityObject(dynamicEntityDictionary, entityType)); 36 | queryCollection.Add(dynamicEntityDictionary); 37 | } 38 | _oDataFilterManager = GetODataFilterManager(); 39 | } 40 | 41 | [Benchmark] 42 | public void ODataGroupByAndAggregate() 43 | { 44 | _url = "https://localhost:44312/odata/entities/user?$apply=groupby((Title),aggregate(Salary with max as MaxSalary))"; 45 | _httpContext = new DefaultHttpContext(); 46 | _oDataRequestHelper.GetEdmModel(_httpContext.Request, _edmEntityTypeSettings, EdmNamespaceName); 47 | SetRequestHost(new Uri(_url)); 48 | _oDataFilterManager.ApplyFilter(edmEntityObjectCollection, queryCollection, _httpContext.Request); 49 | } 50 | 51 | private EdmEntityObject GetEdmEntityObject(Dictionary keyValuePairs, IEdmEntityTypeReference edmEntityType) 52 | { 53 | var obj = new EdmEntityObject(edmEntityType); 54 | foreach (var kvp in keyValuePairs) 55 | obj.TrySetPropertyValue(kvp.Key, kvp.Value); 56 | return obj; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /benchmark/Dynamic.OData.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using BenchmarkDotNet.Running; 5 | 6 | namespace Dynamic.OData.Benchmarks 7 | { 8 | class Program 9 | { 10 | static void Main(string[] args) 11 | { 12 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/Controllers/MatchAllController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using MarkdownSharp; 5 | using Microsoft.AspNet.OData; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Dynamic.OData.Helpers.Interface; 8 | using Dynamic.OData.Interface; 9 | using Microsoft.OData.Edm; 10 | using System.Collections.Generic; 11 | using System.IO; 12 | using System.Net; 13 | using System.Threading.Tasks; 14 | 15 | namespace Dynamic.OData.Samples.Controllers 16 | { 17 | public class MatchAllController : ControllerBase 18 | { 19 | private readonly IODataModelSettingsProvider _oDataModelSettingsProvider; 20 | private readonly IODataFilterManager _oDataFilterManager; 21 | private readonly IODataRequestHelper _oDataRequestHelper; 22 | private readonly IGenericEntityRepository _genericEntityRepository; 23 | 24 | public MatchAllController(IODataModelSettingsProvider oDataModelSettingsProvider 25 | , IODataFilterManager oDataFilterManager 26 | , IODataRequestHelper oDataRequestHelper 27 | , IGenericEntityRepository genericEntityRepository) 28 | { 29 | _oDataModelSettingsProvider = oDataModelSettingsProvider; 30 | _oDataFilterManager = oDataFilterManager; 31 | _oDataRequestHelper = oDataRequestHelper; 32 | _genericEntityRepository = genericEntityRepository; 33 | } 34 | 35 | [HttpGet] 36 | public async Task>> Get() 37 | { 38 | var entityType = _oDataRequestHelper.GetEdmEntityTypeReference(Request); 39 | var collectionType = _oDataRequestHelper.GetEdmCollectionType(Request); 40 | 41 | var collection = new EdmEntityObjectCollection(new EdmCollectionTypeReference(collectionType)); 42 | var queryCollection = new List>(); 43 | var modelSettings = _oDataModelSettingsProvider.GetEdmModelSettingsFromRequest(Request); 44 | var entities = _genericEntityRepository.GetEntities(modelSettings.RouteName); 45 | foreach(var entity in entities) 46 | { 47 | var dynamicEntityDictionary = entity.PropertyList; 48 | collection.Add(GetEdmEntityObject(dynamicEntityDictionary, entityType)); 49 | queryCollection.Add(dynamicEntityDictionary); 50 | } 51 | var result = _oDataFilterManager.ApplyFilter(collection, queryCollection, Request); 52 | return Ok(result); 53 | } 54 | 55 | [HttpGet] 56 | [Route("{controller}/help")] 57 | public async Task GetHelpContent() 58 | { 59 | var currentdir = Directory.GetCurrentDirectory(); 60 | var markdownText = System.IO.File.ReadAllText(currentdir + @"\readme.md"); 61 | var cssStyle = System.IO.File.ReadAllText(currentdir + @"\content.css"); 62 | var markdown = new Markdown(); 63 | var inlineCSS = $""; 64 | return new ContentResult 65 | { 66 | ContentType = "text/html", 67 | StatusCode = (int)HttpStatusCode.OK, 68 | Content = $"{inlineCSS}{markdown.Transform(markdownText)}" 69 | }; 70 | } 71 | 72 | private EdmEntityObject GetEdmEntityObject(Dictionary keyValuePairs, IEdmEntityTypeReference edmEntityType) 73 | { 74 | var obj = new EdmEntityObject(edmEntityType); 75 | foreach (var kvp in keyValuePairs) 76 | obj.TrySetPropertyValue(kvp.Key, kvp.Value); 77 | return obj; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/Dynamic.OData.Samples.csproj: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | netcoreapp3.1 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Always 23 | 24 | 25 | 26 | 27 | 28 | Always 29 | 30 | 31 | 32 | 33 | 34 | Always 35 | 36 | 37 | Always 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/GenericEntityRepository.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Bogus; 5 | using Dynamic.OData.Samples.Settings; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | namespace Dynamic.OData.Samples 10 | { 11 | /// 12 | /// Mock repository class. In real world you would get it from some store. 13 | /// 14 | public class GenericEntityRepository : IGenericEntityRepository 15 | { 16 | private static List totalEntityList = new List(); 17 | public GenericEntityRepository(int count = 50) 18 | { 19 | totalEntityList.Clear(); 20 | totalEntityList.AddRange(GetUserEntities(count)); 21 | totalEntityList.AddRange(GetProductEntities()); 22 | } 23 | public IEnumerable GetEntities(string entityName) 24 | { 25 | return totalEntityList.Where(p => string.Equals(p.EntityName, entityName, StringComparison.OrdinalIgnoreCase)); 26 | } 27 | 28 | 29 | /// 30 | /// Gets Product EntityData. The data below is for representation only and is in no way an indicator of real data. 31 | /// 32 | /// 33 | private IEnumerable GetProductEntities() 34 | { 35 | var products = new List(); 36 | products.Add(GetProductEntity(Guid.Parse("2664f337-4a92-4bc4-aabf-a94e818f49a3".ToLower()), "Office 365 E3", 1, "Office 365 Plans", 250, 360, "Office")); 37 | products.Add(GetProductEntity(Guid.Parse("b998ee4d-3c0a-40e7-aa53-26c15bd878e5".ToLower()), "Windows 10", 2, "Consumer OS", 399, 499, "Windows")); 38 | products.Add(GetProductEntity(Guid.Parse("52d691a2-f44c-4644-81c4-1d81190c117c".ToLower()), "XBOX One S", 3, "Gaming Device", 200, 499, "XBox")); 39 | products.Add(GetProductEntity(Guid.Parse("fc3cc48d-02b0-4b58-9d64-f63b2167685a".ToLower()), "Dynamics 365", 4, "Dynamics 365 Business Products", 190, 245, "Dynamics")); 40 | products.Add(GetProductEntity(Guid.Parse("d7428831-4707-46ab-91e8-f84c50476440".ToLower()), "Azure KeyVault", 5, "Azure KeyVault Service for secrent Management", 50, 100, "Azure")); 41 | products.Add(GetProductEntity(Guid.Parse("04ac927e-fabf-40db-a2c7-661ee12939cb".ToLower()), "Azure App Service", 5, "Azure App Service for Web Hosting", 500, 900, "Azure")); 42 | products.Add(GetProductEntity(Guid.Parse("db7ff8e0-2f1c-4455-840e-0eca338dec99".ToLower()), "Azure Functions", 5, "Serverless Azure functions", 80, 170, "Azure")); 43 | products.Add(GetProductEntity(Guid.Parse("db7ff8e0-2f1c-4455-840e-0eca338dec99".ToLower()), "Azure Service Bus", 5, "Serverless Azure functions", 80, 170, "Azure")); 44 | products.Add(GetProductEntity(Guid.Parse("088284f0-dd43-4d51-bbdd-3dd6e964a07a".ToLower()), "Windows Server", 2, "Server OS", 100, 325, "Windows")); 45 | products.Add(GetProductEntity(Guid.Parse("81740a7c-b00f-45a3-b991-e03132a46712".ToLower()), "XBOX 360", 3, "Legacy Gaming Device", 100, 200, "XBox")); 46 | products.Add(GetProductEntity(Guid.Parse("937fb1e8-bafc-4824-af5a-33a453ec2cd2".ToLower()), "Office 365 E5", 1, "Office 365", 280, 430, "Office")); 47 | products.Add(GetProductEntity(Guid.Parse("2a064a38-4c76-4c14-9357-b7fa5e03426d".ToLower()), "Dynamics On Premise", 4, "Dynamics onpremise", 150, 280, "Dynamics")); 48 | return products; 49 | } 50 | 51 | 52 | /// 53 | /// Gets User entity data.This is for representation only and is in no way an indicator of real data. 54 | /// 55 | /// 56 | private IEnumerable GetUserEntities(int count) 57 | { 58 | var users = new List(); 59 | Randomizer.Seed = new Random(3897234); 60 | var titles = new[] { "Project Manager", "Corporate VP", "General Manager", "Software Engineer", "Software Engineer 2", "Senior Software Engineer", "Senior Project Manager"}; 61 | var age = new [] { 22, 28, 35, 50, 55, 44, 33 }; 62 | var salaries = new[] { 200000, 500000, 1000000, 1500000, 150000, 124000 }; 63 | var departments = new[] { "Office", "Windows", "Azure", "Dynamics", "XBox" }; 64 | var vacationDays = new[] { 22.67899, 72.67899, 42.67899, 24.67899, 32.56756, 65.7843 }; 65 | short employeeNumber = 1; 66 | long universalId = 22335678998; 67 | var userGenerator = new Faker() 68 | .CustomInstantiator(f => new User(employeeNumber++, universalId++)) 69 | .RuleFor(p => p.id, q => Guid.NewGuid()) 70 | .RuleFor(p => p.Title, q => q.PickRandom(titles)) 71 | .RuleFor(p => p.FirstName, q => q.Name.FirstName()) 72 | .RuleFor(p => p.LastName, q => q.Name.LastName()) 73 | .RuleFor(p => p.Age, (q, p) => p.Title == "General Manager" || p.Title == "Corporate VP" ? 74 | q.PickRandom(new[] { 44, 50, 55 }) : q.PickRandom(age)) 75 | .RuleFor(p => p.Salary, (q, p) => p.Title == "General Manager" || p.Title == "Corporate VP" ? 76 | q.PickRandom(new[] { 1500000, 1000000, 500000 }) : q.PickRandom(salaries)) 77 | .RuleFor(p => p.BornOn, (q, p) => DateTime.UtcNow.AddYears(-p.Age)) 78 | .RuleFor(p => p.Department, q => q.PickRandom(departments)) 79 | .RuleFor(p => p.EmailAddress, (q, p) => p.FirstName.ToLower() + "." + p.LastName.ToLower() + "@contoso.com") 80 | .RuleFor(p => p.VacationDaysInHours, q => q.PickRandom(vacationDays)); 81 | 82 | var fakeUsers = userGenerator.Generate(count); 83 | foreach (var user in fakeUsers) 84 | users.Add(GetUserEntity(user.id, user.Title, user.FirstName, user.LastName, user.Age, user.Salary, user.BornOn, user.Department, user.EmailAddress, user.EmployeeNumber, user.UniversalId, user.VacationDaysInHours)); 85 | return users; 86 | } 87 | 88 | 89 | 90 | private GenericEntity GetUserEntity(Guid id 91 | , string title 92 | , string firstName 93 | , string lastName 94 | , int age 95 | , decimal salary 96 | , DateTime bornOnDate 97 | , string department 98 | , string emailAddress 99 | , short employeeNumber 100 | , long universalId 101 | , double vacationDaysInHours) 102 | { 103 | var userEntity = new GenericEntity { EntityName = "User", PropertyList = new Dictionary() }; 104 | userEntity.PropertyList.Add("id", id); 105 | userEntity.PropertyList.Add("Title", title); 106 | userEntity.PropertyList.Add("FirstName", firstName); 107 | userEntity.PropertyList.Add("LastName", lastName); 108 | userEntity.PropertyList.Add("Age", age); 109 | userEntity.PropertyList.Add("Salary", salary); 110 | userEntity.PropertyList.Add("BornOn", bornOnDate); 111 | userEntity.PropertyList.Add("Department", department); 112 | userEntity.PropertyList.Add("EmailAddress", emailAddress); 113 | userEntity.PropertyList.Add("EmployeeNumber", employeeNumber); 114 | userEntity.PropertyList.Add("UniversalId", universalId); 115 | userEntity.PropertyList.Add("VacationDaysInHours", vacationDaysInHours); 116 | return userEntity; 117 | } 118 | 119 | private GenericEntity GetProductEntity(Guid id 120 | , string name 121 | , int type 122 | , string description 123 | , decimal unitcost 124 | , decimal unitprice 125 | , string brandname) 126 | { 127 | var userEntity = new GenericEntity { EntityName = "Product", PropertyList = new Dictionary() }; 128 | userEntity.PropertyList.Add("id", id); 129 | userEntity.PropertyList.Add("Name", name); 130 | userEntity.PropertyList.Add("Type", type); 131 | userEntity.PropertyList.Add("Description", description); 132 | userEntity.PropertyList.Add("UnitCost", unitcost); 133 | userEntity.PropertyList.Add("UnitPrice", unitprice); 134 | userEntity.PropertyList.Add("BrandName", brandname); 135 | return userEntity; 136 | } 137 | } 138 | 139 | public interface IGenericEntityRepository 140 | { 141 | IEnumerable GetEntities(string entityName); 142 | } 143 | 144 | public class User 145 | { 146 | public User(short employeeNumber, long universalId) 147 | { 148 | this.EmployeeNumber = employeeNumber; 149 | this.UniversalId = universalId; 150 | } 151 | 152 | public Guid id { get; set; } 153 | public string Title { get; set; } 154 | public string FirstName { get; set; } 155 | public string LastName { get; set; } 156 | 157 | public int Age { get; set; } 158 | 159 | public decimal Salary { get; set; } 160 | 161 | public DateTime BornOn { get; set; } 162 | 163 | public string Department { get; set; } 164 | public string EmailAddress { get; set; } 165 | public short EmployeeNumber { get; set; } 166 | 167 | public long UniversalId { get; set; } 168 | 169 | public double VacationDaysInHours { get; set; } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/MatchAllRoutingConvention.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNet.OData.Routing.Conventions; 5 | using Microsoft.AspNetCore.Mvc.Controllers; 6 | using Microsoft.AspNetCore.Mvc.Infrastructure; 7 | using Microsoft.AspNetCore.Routing; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | 13 | 14 | namespace Dynamic.OData.Samples 15 | { 16 | public class MatchAllRoutingConvention : IODataRoutingConvention 17 | { 18 | private readonly string _matchAllControllerName; 19 | private readonly string _recommendationAction = "actions"; 20 | public MatchAllRoutingConvention(string matchAllControllerName) 21 | { 22 | _matchAllControllerName = matchAllControllerName; 23 | } 24 | 25 | public IEnumerable SelectAction(RouteContext routeContext) 26 | { 27 | //If metadata, route to metadata controller. 28 | if (routeContext == null || routeContext.HttpContext.Request.Path.Value.Contains("metadata", StringComparison.OrdinalIgnoreCase)) 29 | return new MetadataRoutingConvention().SelectAction(routeContext); 30 | 31 | // Get a IActionDescriptorCollectionProvider from the global service provider. 32 | IActionDescriptorCollectionProvider actionCollectionProvider = 33 | routeContext.HttpContext.RequestServices.GetRequiredService(); 34 | 35 | //Get all actions on the match-all controller 36 | IEnumerable actionDescriptors = actionCollectionProvider 37 | .ActionDescriptors.Items.OfType() 38 | .Where(c => c.ControllerName == _matchAllControllerName); 39 | 40 | string path = routeContext.HttpContext.Request.Path.Value; 41 | return actionDescriptors.Where( 42 | c => string.Equals(c.ActionName, "Get", StringComparison.OrdinalIgnoreCase)).ToList(); 43 | 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/ODataModelSettingsProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | using Dynamic.OData.Models; 6 | using Newtonsoft.Json; 7 | using System.IO; 8 | using System.Linq; 9 | 10 | namespace Dynamic.OData.Samples 11 | { 12 | public class ODataModelSettingsProvider : IODataModelSettingsProvider 13 | { 14 | public EdmEntityTypeSettings GetEdmModelSettingsFromRequest(HttpRequest httpRequest) 15 | { 16 | var pathSegments = httpRequest.Path.Value.Split('/'); 17 | string routeName = string.Empty; 18 | if (pathSegments.Length >= 4) 19 | routeName = pathSegments[3]; 20 | 21 | //Read this dynamically from a blob/redis 22 | var entitySettingList = JsonConvert.DeserializeObject(File.ReadAllText(Directory.GetCurrentDirectory() + @"\Settings\EntityTypeSettings.json")); 23 | 24 | return entitySettingList.EdmEntityTypeSettings.FirstOrDefault(predicate => string.Equals(predicate.RouteName,routeName, System.StringComparison.OrdinalIgnoreCase)); 25 | } 26 | } 27 | 28 | public interface IODataModelSettingsProvider 29 | { 30 | EdmEntityTypeSettings GetEdmModelSettingsFromRequest(HttpRequest httpRequest); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace Dynamic.OData.Samples 8 | { 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | CreateHostBuilder(args).Build().Run(); 14 | } 15 | 16 | public static IHostBuilder CreateHostBuilder(string[] args) => 17 | Host.CreateDefaultBuilder(args) 18 | .ConfigureWebHostDefaults(webBuilder => 19 | { 20 | webBuilder.UseStartup(); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | //Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | { 4 | "iisSettings": { 5 | "windowsAuthentication": false, 6 | "anonymousAuthentication": true, 7 | "iisExpress": { 8 | "applicationUrl": "http://localhost:63885", 9 | "sslPort": 44312 10 | } 11 | }, 12 | "$schema": "http://json.schemastore.org/launchsettings.json", 13 | "profiles": { 14 | "IIS Express": { 15 | "commandName": "IISExpress", 16 | "launchBrowser": true, 17 | "launchUrl": "matchall/help", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "Dynamic.OData.Samples": { 23 | "commandName": "Project", 24 | "launchBrowser": true, 25 | "launchUrl": "odata/entities/user", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | }, 29 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/Settings/EntityTypeSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | //Copyright (c) Microsoft Corporation. All rights reserved. 3 | // Licensed under the MIT License. See License.txt in the project root for license information. 4 | 5 | 6 | "EdmEntityTypeSettings": [ 7 | { 8 | "RouteName": "User", 9 | "Properties": [ 10 | { 11 | "PropertyName": "id", 12 | "PropertyType": "Guid", 13 | "IsNullable": false 14 | }, 15 | { 16 | "PropertyName": "Title", 17 | "PropertyType": "String" 18 | }, 19 | { 20 | "PropertyName": "FirstName", 21 | "PropertyType": "String" 22 | }, 23 | { 24 | "PropertyName": "LastName", 25 | "PropertyType": "String" 26 | }, 27 | { 28 | "PropertyName": "Age", 29 | "PropertyType": "Int32" 30 | }, 31 | { 32 | "PropertyName": "Salary", 33 | "PropertyType": "Decimal" 34 | }, 35 | { 36 | "PropertyName": "BornOn", 37 | "PropertyType": "DateTime" 38 | }, 39 | { 40 | "PropertyName": "Department", 41 | "PropertyType": "String" 42 | }, 43 | { 44 | "PropertyName": "EmailAddress", 45 | "PropertyType": "String" 46 | } 47 | ] 48 | }, 49 | { 50 | "RouteName": "Product", 51 | "Properties": [ 52 | { 53 | "PropertyName": "id", 54 | "PropertyType": "Guid", 55 | "IsNullable": false 56 | }, 57 | { 58 | "PropertyName": "Name", 59 | "PropertyType": "String" 60 | }, 61 | { 62 | "PropertyName": "Type", 63 | "PropertyType": "Int32" 64 | }, 65 | { 66 | "PropertyName": "Description", 67 | "PropertyType": "String" 68 | }, 69 | { 70 | "PropertyName": "UnitCost", 71 | "PropertyType": "Decimal" 72 | }, 73 | { 74 | "PropertyName": "UnitPrice", 75 | "PropertyType": "Decimal" 76 | }, 77 | { 78 | "PropertyName": "BrandName", 79 | "PropertyType": "String" 80 | } 81 | ] 82 | } 83 | ] 84 | } -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/Settings/GenericEntity.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Dynamic.OData.Samples.Settings 7 | { 8 | /// 9 | /// A generic entity representing specfic classes. Key is property name value if property value 10 | /// 11 | public class GenericEntity 12 | { 13 | public string EntityName { get; set; } 14 | public Dictionary PropertyList { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/Startup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNet.OData; 5 | using Microsoft.AspNet.OData.Extensions; 6 | using Microsoft.AspNet.OData.Routing.Conventions; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Hosting; 12 | using Dynamic.OData; 13 | using Dynamic.OData.Helpers; 14 | using Microsoft.OData.Edm; 15 | using System.Collections.Generic; 16 | using System.Linq; 17 | 18 | namespace Dynamic.OData.Samples 19 | { 20 | public class Startup 21 | { 22 | public Startup(IConfiguration configuration) 23 | { 24 | Configuration = configuration; 25 | } 26 | 27 | public IConfiguration Configuration { get; } 28 | 29 | // This method gets called by the runtime. Use this method to add services to the container. 30 | public void ConfigureServices(IServiceCollection services) 31 | { 32 | services.AddControllers(); 33 | 34 | services.AddSingleton(); 35 | services.AddSingleton(); 36 | //services.AddScoped(); 37 | services.AddDynamicODataQueryServices(); 38 | 39 | services.AddOData(); 40 | } 41 | 42 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 43 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 44 | { 45 | if (env.IsDevelopment()) 46 | { 47 | app.UseDeveloperExceptionPage(); 48 | } 49 | 50 | app.UseHttpsRedirection(); 51 | 52 | app.UseRouting(); 53 | 54 | app.UseAuthorization(); 55 | 56 | 57 | app.UseEndpoints(endpoints => 58 | { 59 | endpoints.MapControllers(); 60 | endpoints.EnableDependencyInjection(); 61 | endpoints.Filter().Expand().Select().OrderBy().MaxTop(null).Count(); 62 | 63 | endpoints.MapODataRoute("odata", "odata/entities/{entityname}", containerBuilder => 64 | { 65 | containerBuilder.AddService(Microsoft.OData.ServiceLifetime.Scoped, typeof(IEdmModel), sp => 66 | { 67 | var serviceScope = sp.GetRequiredService(); 68 | var modelSettingsProvider = app.ApplicationServices.GetService(); 69 | var odataRequestHelper = new ODataRequestHelper(); 70 | return odataRequestHelper.GetEdmModel(serviceScope.HttpRequest 71 | , modelSettingsProvider.GetEdmModelSettingsFromRequest(serviceScope.HttpRequest), "Microsoft.Contoso.Models"); 72 | }); 73 | 74 | containerBuilder.AddService(Microsoft.OData.ServiceLifetime.Scoped, typeof(IEnumerable), sp => 75 | { 76 | IList routingConventions = ODataRoutingConventions.CreateDefault(); 77 | routingConventions.Insert(0, new MatchAllRoutingConvention("MatchAll")); 78 | return routingConventions.ToList().AsEnumerable(); 79 | }); 80 | }); 81 | }); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | //Copyright (c) Microsoft Corporation. All rights reserved. 3 | // Licensed under the MIT License. See License.txt in the project root for license information. 4 | 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft": "Warning", 9 | "Microsoft.Hosting.Lifetime": "Information" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | //Copyright (c) Microsoft Corporation. All rights reserved. 3 | // Licensed under the MIT License. See License.txt in the project root for license information. 4 | 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft": "Warning", 9 | "Microsoft.Hosting.Lifetime": "Information" 10 | } 11 | }, 12 | "AllowedHosts": "*" 13 | } 14 | -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/content.css: -------------------------------------------------------------------------------- 1 | /*Copyright (c) Microsoft Corporation. All rights reserved. 2 | Licensed under the MIT License. See License.txt in the project root for license information. 3 | */ 4 | 5 | @media print { 6 | *, *:before, *:after { 7 | background: transparent !important; 8 | color: #000 !important; 9 | box-shadow: none !important; 10 | text-shadow: none !important; 11 | } 12 | 13 | a, 14 | a:visited { 15 | text-decoration: underline; 16 | } 17 | 18 | a[href]:after { 19 | content: " (" attr(href) ")"; 20 | } 21 | 22 | abbr[title]:after { 23 | content: " (" attr(title) ")"; 24 | } 25 | 26 | a[href^="#"]:after, 27 | a[href^="javascript:"]:after { 28 | content: ""; 29 | } 30 | 31 | pre, 32 | blockquote { 33 | border: 1px solid #999; 34 | page-break-inside: avoid; 35 | } 36 | 37 | thead { 38 | display: table-header-group; 39 | } 40 | 41 | tr, 42 | img { 43 | page-break-inside: avoid; 44 | } 45 | 46 | img { 47 | max-width: 100% !important; 48 | } 49 | 50 | p, 51 | h2, 52 | h3 { 53 | orphans: 3; 54 | widows: 3; 55 | } 56 | 57 | h2, 58 | h3 { 59 | page-break-after: avoid; 60 | } 61 | } 62 | 63 | html { 64 | font-size: 12px; 65 | } 66 | 67 | @media screen and (min-width: 32rem) and (max-width: 48rem) { 68 | html { 69 | font-size: 15px; 70 | } 71 | } 72 | 73 | @media screen and (min-width: 48rem) { 74 | html { 75 | font-size: 16px; 76 | } 77 | } 78 | 79 | body { 80 | line-height: 1.85; 81 | } 82 | 83 | p, 84 | .air-p { 85 | font-size: 1rem; 86 | margin-bottom: 1.3rem; 87 | } 88 | 89 | h1, 90 | .air-h1, 91 | h2, 92 | .air-h2, 93 | h3, 94 | .air-h3, 95 | h4, 96 | .air-h4 { 97 | margin: 1.414rem 0 .5rem; 98 | font-weight: inherit; 99 | line-height: 1.42; 100 | } 101 | 102 | h1, 103 | .air-h1 { 104 | margin-top: 0; 105 | font-size: 3.998rem; 106 | } 107 | 108 | h2, 109 | .air-h2 { 110 | font-size: 2.827rem; 111 | } 112 | 113 | h3, 114 | .air-h3 { 115 | font-size: 1.999rem; 116 | } 117 | 118 | h4, 119 | .air-h4 { 120 | font-size: 1.414rem; 121 | } 122 | 123 | h5, 124 | .air-h5 { 125 | font-size: 1.121rem; 126 | } 127 | 128 | h6, 129 | .air-h6 { 130 | font-size: .88rem; 131 | } 132 | 133 | small, 134 | .air-small { 135 | font-size: .707em; 136 | } 137 | 138 | /* https://github.com/mrmrs/fluidity */ 139 | 140 | img, 141 | canvas, 142 | iframe, 143 | video, 144 | svg, 145 | select, 146 | textarea { 147 | max-width: 100%; 148 | } 149 | 150 | @import url(http://fonts.googleapis.com/css?family=Open+Sans:300italic,300); 151 | 152 | body { 153 | color: #444; 154 | font-family: 'Open Sans', Helvetica, sans-serif; 155 | font-weight: 300; 156 | margin: 6rem auto 1rem; 157 | max-width: 48rem; 158 | text-align: center; 159 | } 160 | 161 | img { 162 | border-radius: 50%; 163 | height: 200px; 164 | margin: 0 auto; 165 | width: 200px; 166 | } 167 | 168 | a, 169 | a:visited { 170 | color: #3498db; 171 | text-decoration: none; 172 | font-size: small; 173 | } 174 | 175 | a:hover, 176 | a:focus, 177 | a:active { 178 | color: #2980b9; 179 | } 180 | 181 | pre { 182 | background-color: #fafafa; 183 | padding: 1rem; 184 | text-align: left; 185 | } 186 | 187 | blockquote { 188 | margin: 0; 189 | border-left: 5px solid #7a7a7a; 190 | font-style: italic; 191 | padding: 1.33em; 192 | text-align: left; 193 | } 194 | 195 | ul, 196 | ol, 197 | li { 198 | text-align: left; 199 | } 200 | 201 | p { 202 | color: #777; 203 | } -------------------------------------------------------------------------------- /samples/Dynamic.OData.Samples/readme.md: -------------------------------------------------------------------------------- 1 | # Dynamic OData Query Library 2 | 3 | #### This sample has two main purposes 4 | * Query library integration for simple dynamic OData models. Query Library Integration is as part of code-base and is better understood by going through the sample project. 5 | * Sample OData queries 6 | 7 | 8 | 9 | #### Sample OData queries: 10 | 11 | ##### Users 12 | * Metadata 13 | * Get the metdata of the model exposed by the service
14 | 15 | 16 | * Filters
17 | * Get a list of all users
18 | 19 | 20 | * Get a list of all users above the age of 30
21 | [https://localhost:44312/odata/entities/user?$filter=(Age gt 30)]() 22 | 23 | * Get a list of all users who have Software Engineer in their title
24 | [https://localhost:44312/odata/entities/user?$filter=contains(Title,'Software Engineer')]() 25 | 26 | * Get a list of all users who have Software Engineer in their title and have salary greater than 200,000
27 | [https://localhost:44312/odata/entities/user?$filter=contains(Title,'Software Engineer') and Salary gt 200000]() 28 | 29 | * Select
30 | * Select Title and Salary from users
31 | 32 | 33 | * Apply
34 | * Group by title, and find the max salary of that group
35 | [https://localhost:44312/odata/entities/user?$apply=groupby((Title),aggregate(Salary with max as MaxSalary))]() 36 | 37 | * Group by title, and find the Total salary of that group
38 | [https://localhost:44312/odata/entities/user?$apply=groupby((Title),aggregate(Salary with sum as TotalSalary))]() 39 | 40 | * Group by title and list members of that group
41 | [https://localhost:44312/odata/entities/user?$apply=groupby((Title),aggregate(id with Custom.List as Employees))]() 42 | 43 | 44 | * Sorting
45 | * Get a list of all users ordered by FirstName
46 | 47 | 48 | 49 | * Pagination
50 | * Gets a list of top 5 users
51 | 52 | 53 | * Gets a list of users skipping the first 5 users
54 | 55 | 56 | * Pagination using a combination of top & Skip 57 |
Page 1: 58 |
Page 2: 59 | 60 | 61 | ##### Products 62 | * Metadata 63 | * Get the metdata of the model exposed by the service
64 | 65 | 66 | * Filters 67 | * Get a list of all products
68 | 69 | 70 | * Get a list of all products with Azure in its name
71 | [https://localhost:44312/odata/entities/product?$filter=contains(Name,'Azure')]() 72 | 73 | * Select
74 | * Select Name and Description from products
75 | 76 | 77 | * Apply
78 | * Group products by BrandName and list them
79 | [https://localhost:44312/odata/entities/product?$apply=groupby((BrandName),aggregate(id with Custom.List as Products))]() 80 | 81 | * Sorting
82 | * Get a list of all products ordered by BrandName ascending, then by Name descending
83 | [https://localhost:44312/odata/entities/product?$orderby=BrandName asc,Name desc]() 84 | 85 |
86 | ######Copyright (c) Microsoft Corporation. All rights reserved. 87 | ######Licensed under the MIT License. See License.txt in the project root for license information. 88 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Class1.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Dynamic.OData 7 | { 8 | public class Class1 9 | { 10 | 11 | public void Method1() 12 | { 13 | return; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Dynamic.OData/CustomEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace Dynamic.OData 8 | { 9 | public class CustomEqualityComparer : IEqualityComparer> 10 | { 11 | public bool Equals(Dictionary a, Dictionary b) 12 | { 13 | if (a == b) { return true; } 14 | if (a == null || b == null || a.Count != b.Count) { return false; } 15 | return !a.Except(b).Any(); 16 | } 17 | 18 | public int GetHashCode(Dictionary a) 19 | { 20 | return a.ToString().ToLower().GetHashCode(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Dynamic.OData.csproj: -------------------------------------------------------------------------------- 1 |  3 | 4 | 5 | 6 | 7 | netcoreapp3.1 8 | https://github.com/microsoft/dynamic-odata/blob/main/CHANGELOG.md 9 | https://github.com/microsoft/dynamic-odata 10 | https://github.com/microsoft/dynamic-odata 11 | Copyright (c) Microsoft Corporation. 12 | Dynamic OData Team 13 | Microsoft Corporation 14 | odata,dynamic odata, 15 | 16 | LICENSE 17 | 1.0.4 18 | Dynamic OData is a query library built upon OData Web API that allows you to query dynamically created Entity Data Models. 19 | OData expects the return schema to be static at compile time, there are scenarios where applications would want to construct the return response on the go. This library helps to achieve that with configurable model and providing metadata which are used at runtime to create dynamic response schema. 20 | This provides flexibity to have a dynamic schema and still enable the OData magic to work. The library enables you to construct a Controller method of IEnumerable <IEdmEntityObject> return type and then construct this Object using a mapped Dictionary. 21 | 22 | 23 | 24 | netcoreapp3.1 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | True 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Exceptions/FeatureNotSupportedException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Dynamic.OData.Exceptions 7 | { 8 | [Serializable] 9 | public class FeatureNotSupportedException : Exception 10 | { 11 | private const string FeatureNotSupported = "Feature Not Supported: Invalid {0} clause - {1}"; 12 | public FeatureNotSupportedException(string clauseName, string message) : base(string.Format(FeatureNotSupported, clauseName, message)) 13 | { 14 | 15 | } 16 | public FeatureNotSupportedException(string clauseName, string message, Exception innerException) 17 | : base(string.Format(FeatureNotSupported, clauseName, message), innerException) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Exceptions/InvalidPropertyException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Dynamic.OData.Exceptions 7 | { 8 | [Serializable] 9 | public class InvalidPropertyException : Exception 10 | { 11 | private const string InvalidSelectProperty = "Invalid {0} clause - Property {1} does not exist in model"; 12 | public InvalidPropertyException(string clauseName, string propertyName) : base(string.Format(InvalidSelectProperty, clauseName, propertyName)) 13 | { 14 | 15 | } 16 | public InvalidPropertyException(string clauseName, string propertyName, Exception innerException) 17 | : base(string.Format(InvalidSelectProperty, clauseName, propertyName), innerException) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Helpers/Interface/IODataQueryValidator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNet.OData.Query; 5 | 6 | namespace Dynamic.OData.Helpers.Interface 7 | { 8 | public interface IODataQueryValidator 9 | { 10 | void ValidateQuery(ODataQueryOptions oDataQueryOptions); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Helpers/Interface/IODataRequestHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNet.OData.Routing; 5 | using Microsoft.AspNetCore.Http; 6 | using Dynamic.OData.Models; 7 | using Microsoft.OData.Edm; 8 | using System; 9 | using System.Collections.Generic; 10 | 11 | namespace Dynamic.OData.Helpers.Interface 12 | { 13 | public interface IODataRequestHelper 14 | { 15 | public List GetEdmEntityTypeSettings(HttpRequest httpRequest); 16 | IEdmCollectionType GetEdmCollectionType(HttpRequest httpRequest); 17 | 18 | IEdmEntityTypeReference GetEdmEntityTypeReference(HttpRequest httpRequest); 19 | 20 | EdmModel GetEdmModel(HttpRequest httpRequest); 21 | 22 | ODataPath GetODataPath(HttpRequest httpRequest); 23 | 24 | Uri GetODataRelativeUri(HttpRequest httpRequest); 25 | 26 | void SetRequestCount(Microsoft.OData.UriParser.ODataUriParser oDataUriParser, HttpRequest httpRequest, int count); 27 | string GetRouteName(HttpRequest httpRequest); 28 | IEdmModel GetEdmModel(HttpRequest request, EdmEntityTypeSettings edmEntityTypeSettings, string namespaceName); 29 | string ModifyResponse(string responseBody, bool removeODataTypeProperty, bool updateODataContextSuffix, string odataSuffix); 30 | QueryString EscapeQueryString(QueryString querystring); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Helpers/ODataQueryValidator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNet.OData.Query; 5 | using Dynamic.OData.Helpers.Interface; 6 | 7 | namespace Dynamic.OData.Helpers 8 | { 9 | public class ODataQueryValidator : IODataQueryValidator 10 | { 11 | public void ValidateQuery(ODataQueryOptions oDataQueryOptions) 12 | { 13 | oDataQueryOptions.Validate(new ODataValidationSettings()); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Helpers/ODataRequestHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Dynamic.OData.Helpers.Interface; 5 | using Dynamic.OData.Models; 6 | using Microsoft.AspNet.OData.Extensions; 7 | using Microsoft.AspNet.OData.Routing; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.OData.Edm; 10 | using Newtonsoft.Json.Linq; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | using System.Text.RegularExpressions; 15 | 16 | namespace Dynamic.OData.Helpers 17 | { 18 | public class ODataRequestHelper : IODataRequestHelper 19 | { 20 | private const string odataContextKey = "odataContext"; 21 | public List GetEdmEntityTypeSettings(HttpRequest httpRequest) 22 | { 23 | var context = (ODataContext)httpRequest.HttpContext.Items[odataContextKey]; 24 | return context.Settings; 25 | } 26 | public ODataPath GetODataPath(HttpRequest httpRequest) 27 | { 28 | var context = (ODataContext)httpRequest.HttpContext.Items[odataContextKey]; 29 | return context.Path; 30 | } 31 | public QueryString EscapeQueryString(QueryString querystring) 32 | { 33 | var escapedQuery = Regex.Replace(querystring.Value, "(Custom.CountDistinct_|Custom.Count_)%27(.*?)%27(%20)+as", m => m.Groups[1].Value + 34 | "%27" + Uri.EscapeDataString(Uri.UnescapeDataString(m.Groups[2].Value).Replace("'", "''")) + "%27%20as"); 35 | return new QueryString(escapedQuery); 36 | } 37 | public IEdmCollectionType GetEdmCollectionType(HttpRequest httpRequest) 38 | { 39 | var context = (ODataContext)httpRequest.HttpContext.Items[odataContextKey]; 40 | return context.EdmCollectionType; 41 | } 42 | 43 | public IEdmEntityTypeReference GetEdmEntityTypeReference(HttpRequest httpRequest) 44 | { 45 | var context = (ODataContext)httpRequest.HttpContext.Items[odataContextKey]; 46 | return context.EdmEntityTypeReference; 47 | } 48 | 49 | public EdmModel GetEdmModel(HttpRequest httpRequest) 50 | { 51 | var context = (ODataContext)httpRequest.HttpContext.Items[odataContextKey]; 52 | return context.EdmModel; 53 | } 54 | 55 | public Uri GetODataRelativeUri(HttpRequest httpRequest) 56 | { 57 | var splituri = httpRequest.GetUri().ToString().Split('/').Last(); 58 | var requestUri = new Uri(splituri, UriKind.Relative); 59 | return requestUri; 60 | } 61 | 62 | public void SetRequestCount(Microsoft.OData.UriParser.ODataUriParser oDataUriParser, HttpRequest httpRequest, int count) 63 | { 64 | var isCountPredicatePresent = oDataUriParser.ParseCount(); 65 | if (isCountPredicatePresent == true) 66 | httpRequest.ODataFeature().TotalCount = count; 67 | } 68 | 69 | public string GetRouteName(HttpRequest httpRequest) 70 | { 71 | var context = (ODataContext)httpRequest.HttpContext.Items[odataContextKey]; 72 | return context.ActualRouteName; 73 | } 74 | 75 | public IEdmModel GetEdmModel(HttpRequest request, EdmEntityTypeSettings edmEntityTypeSettings, string namespaceName) 76 | { 77 | if (request == null) 78 | throw new ArgumentNullException(nameof(request)); 79 | 80 | if (edmEntityTypeSettings == null) 81 | throw new ArgumentNullException(nameof(edmEntityTypeSettings)); 82 | 83 | if (string.IsNullOrWhiteSpace(namespaceName)) 84 | throw new ArgumentNullException(nameof(namespaceName)); 85 | 86 | string modelName = edmEntityTypeSettings.RouteName.ToLowerInvariant(); 87 | 88 | var odataContext = new ODataContext(); 89 | EdmModel model = new EdmModel(); 90 | EdmEntityContainer container = new EdmEntityContainer(namespaceName, "container"); 91 | model.AddElement(container); 92 | var edmEntityType = new EdmEntityType(namespaceName, modelName); 93 | 94 | foreach (var property in edmEntityTypeSettings.Properties) 95 | { 96 | if (property.PropertyName.Equals("id", StringComparison.OrdinalIgnoreCase)) 97 | { 98 | if (property.IsNullable.HasValue) 99 | edmEntityType.AddKeys(edmEntityType.AddStructuralProperty(property.PropertyName, property.GetEdmPrimitiveTypeKind(), property.IsNullable.Value)); 100 | else 101 | edmEntityType.AddKeys(edmEntityType.AddStructuralProperty(property.PropertyName, property.GetEdmPrimitiveTypeKind())); 102 | } 103 | else 104 | { 105 | 106 | if (property.IsNullable.HasValue) 107 | edmEntityType.AddStructuralProperty(property.PropertyName, property.GetEdmPrimitiveTypeKind(), property.IsNullable.Value); 108 | else 109 | edmEntityType.AddStructuralProperty(property.PropertyName, property.GetEdmPrimitiveTypeKind()); 110 | } 111 | } 112 | model.AddElement(edmEntityType); 113 | container.AddEntitySet(modelName, edmEntityType); 114 | 115 | //Set the context 116 | odataContext.Settings = new List 117 | { 118 | edmEntityTypeSettings 119 | }; 120 | odataContext.Path = new ODataPath(); 121 | odataContext.EdmModel = model; 122 | odataContext.EdmEntityTypeReference = new EdmEntityTypeReference(edmEntityType, true); 123 | odataContext.EdmCollectionType = new EdmCollectionType(odataContext.EdmEntityTypeReference); 124 | odataContext.ActualRouteName = modelName; 125 | request.HttpContext.Items.Add(odataContextKey, odataContext); 126 | return model; 127 | } 128 | 129 | public string ModifyResponse(string responseBody, bool removeODataTypeProperty, bool updateODataContextSuffix, string odataSuffix) 130 | { 131 | var responseObject = JObject.Parse(responseBody); 132 | if(updateODataContextSuffix) 133 | { 134 | var oldContextValue = responseObject["@odata.context"].ToString(); 135 | var newContextValue = oldContextValue.Split("#")[0] + "#" + odataSuffix; 136 | responseObject["@odata.context"] = newContextValue; 137 | } 138 | var type = responseObject["value"].Type; 139 | 140 | if (removeODataTypeProperty && !type.Equals(JTokenType.Boolean)) 141 | { 142 | var itemArray = (JArray)responseObject["value"]; 143 | foreach (var item in itemArray) 144 | { 145 | var itemObj = (JObject)item; 146 | itemObj.Remove("@odata.type"); 147 | } 148 | } 149 | return responseObject.ToString(Newtonsoft.Json.Formatting.None); 150 | } 151 | 152 | //public IEdmModel GetEdmModel(HttpRequest request, List edmEntityTypeSettings, string namespaceName) 153 | //{ 154 | // if (request == null) 155 | // throw new ArgumentNullException(nameof(request)); 156 | 157 | // if (edmEntityTypeSettings == null || edmEntityTypeSettings.Count == 0) 158 | // throw new ArgumentNullException(nameof(edmEntityTypeSettings)); 159 | 160 | // if (string.IsNullOrWhiteSpace(namespaceName)) 161 | // throw new ArgumentNullException(nameof(namespaceName)); 162 | 163 | // var model = new EdmModel(); 164 | // var container = new EdmEntityContainer(namespaceName, "container"); 165 | // var odataContext = new ODataContext(); 166 | // string datasetname = "all"; 167 | // var masterEntityType = new EdmEntityType(namespaceName, datasetname); 168 | // model.AddElement(masterEntityType); 169 | // foreach (var kvp in edmEntityTypeSettings) 170 | // { 171 | // var edmComplexType = new EdmComplexType(namespaceName, kvp.RouteName); 172 | // foreach (var property in kvp.Properties) 173 | // { 174 | // if (property.IsNullable.HasValue) 175 | // edmComplexType.AddStructuralProperty(property.PropertyName, property.GetEdmPrimitiveTypeKind(), property.IsNullable.Value); 176 | // else 177 | // edmComplexType.AddStructuralProperty(property.PropertyName, property.GetEdmPrimitiveTypeKind()); 178 | // } 179 | // model.AddElement(edmComplexType); 180 | // masterEntityType.AddStructuralProperty(kvp.RouteName + "s", new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(edmComplexType, true)))); 181 | // } 182 | // var masterEntitySet = new EdmEntitySet(container, datasetname, masterEntityType); 183 | // container.AddElement(masterEntitySet); 184 | 185 | // //Set the context 186 | // odataContext.Settings = edmEntityTypeSettings.ToList(); 187 | // odataContext.Path = new ODataPath(); 188 | // odataContext.EdmModel = model; 189 | // odataContext.EdmEntityTypeReference = new EdmEntityTypeReference(masterEntityType, true); 190 | // odataContext.EdmCollectionType = new EdmCollectionType(odataContext.EdmEntityTypeReference); 191 | // odataContext.ActualRouteName = datasetname; 192 | // request.HttpContext.Items.Add(odataContextKey, odataContext); 193 | // return model; 194 | //} 195 | } 196 | 197 | public static class RequestExtensions 198 | { 199 | public static Uri GetUri(this HttpRequest request) 200 | { 201 | var uriBuilder = new UriBuilder 202 | { 203 | Scheme = request.Scheme, 204 | Host = request.Host.Host, 205 | Port = request.Host.Port.GetValueOrDefault(80), 206 | Path = request.Path.ToString(), 207 | Query = request.QueryString.ToString() 208 | }; 209 | return uriBuilder.Uri; 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Interface/IODataFilterManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNet.OData; 5 | using Microsoft.AspNetCore.Http; 6 | using System.Collections.Generic; 7 | 8 | namespace Dynamic.OData.Interface 9 | { 10 | public interface IODataFilterManager 11 | { 12 | IEnumerable ApplyFilter(IEnumerable sourceEntities, IEnumerable> queryableSourceEntities, HttpRequest request); 13 | 14 | void SetPropertyValue(Dictionary attributes, IEnumerable> entityTypeDictionary, Dictionary keyValues = null); 15 | 16 | void SetActionInfoValue(Dictionary actionInfo, IEnumerable> entityTypeDictionary, Dictionary keyValues); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Models/APIResponseMappingObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Dynamic.OData.Models 7 | { 8 | public class APIResponseMappingObject 9 | { 10 | public List EdmEntityTypeSettings { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Models/EdmEntityTypePropertySetting.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.OData.Edm; 5 | 6 | namespace Dynamic.OData.Models 7 | { 8 | public class EdmEntityTypePropertySetting 9 | { 10 | public string PropertyName { get; set; } 11 | public string PropertyType { get; set; } 12 | public bool? IsNullable { get; set; } 13 | 14 | public EdmPrimitiveTypeKind GetEdmPrimitiveTypeKind() 15 | { 16 | if (Equals(PropertyType, TypeHandlingConstants.String)) 17 | return EdmPrimitiveTypeKind.String; 18 | if (Equals(PropertyType, TypeHandlingConstants.Int16)) 19 | return EdmPrimitiveTypeKind.Int16; 20 | if (Equals(PropertyType, TypeHandlingConstants.Int32)) 21 | return EdmPrimitiveTypeKind.Int32; 22 | if (Equals(PropertyType, TypeHandlingConstants.Int64)) 23 | return EdmPrimitiveTypeKind.Int64; 24 | if (Equals(PropertyType, TypeHandlingConstants.Guid)) 25 | return EdmPrimitiveTypeKind.Guid; 26 | if (Equals(PropertyType, TypeHandlingConstants.DateTime)) 27 | return EdmPrimitiveTypeKind.DateTimeOffset; 28 | if (Equals(PropertyType, TypeHandlingConstants.Date)) 29 | return EdmPrimitiveTypeKind.Date; 30 | if (Equals(PropertyType, TypeHandlingConstants.TimeOfDay)) 31 | return EdmPrimitiveTypeKind.TimeOfDay; 32 | if (Equals(PropertyType, TypeHandlingConstants.Boolean)) 33 | return EdmPrimitiveTypeKind.Boolean; 34 | if (Equals(PropertyType, TypeHandlingConstants.Double)) 35 | return EdmPrimitiveTypeKind.Double; 36 | if (Equals(PropertyType, TypeHandlingConstants.Decimal)) 37 | return EdmPrimitiveTypeKind.Decimal; 38 | return EdmPrimitiveTypeKind.None; 39 | } 40 | 41 | private bool Equals(string source, string target) 42 | { 43 | return string.Equals(source, target, System.StringComparison.OrdinalIgnoreCase); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Models/EdmEntityTypeSettings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Dynamic.OData.Models 7 | { 8 | public class EdmEntityTypeSettings 9 | { 10 | /// 11 | /// List of Personas that have access on this route 12 | /// 13 | public List Personas { get; set; } 14 | 15 | /// 16 | /// Route name would map to the family of the insight 17 | /// 18 | public string RouteName { get; set; } 19 | 20 | /// 21 | /// Properties which can be specified by the client to query by, outside of OData 22 | /// 23 | public List QueryByAttributes { get; set; } 24 | 25 | /// 26 | /// List of property setting objects 27 | /// 28 | public List Properties { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Models/ODataContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNet.OData.Routing; 5 | using Microsoft.OData.Edm; 6 | using System.Collections.Generic; 7 | 8 | namespace Dynamic.OData.Models 9 | { 10 | public class ODataContext 11 | { 12 | public List Settings { get; set; } 13 | 14 | public ODataPath Path { get; set; } 15 | 16 | public EdmModel EdmModel { get; set; } 17 | 18 | public IEdmCollectionType EdmCollectionType { get; set; } 19 | 20 | public IEdmEntityTypeReference EdmEntityTypeReference { get; set; } 21 | 22 | public string ActualRouteName { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Dynamic.OData/Models/TypeHandlingConstants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Dynamic.OData.Models 5 | { 6 | public class TypeHandlingConstants 7 | { 8 | public static string Date = "Date"; 9 | public static string DateTime = "DateTime"; 10 | public static string TimeOfDay = "TimeOfDay"; 11 | public static string String = "String"; 12 | public static string Int16 = "Int16"; 13 | public static string Int32 = "Int32"; 14 | public static string Int64 = "Int64"; 15 | public static string Double = "Double"; 16 | public static string Guid = "Guid"; 17 | public static string Boolean = "Boolean"; 18 | public static string Decimal = "Decimal"; 19 | public static string None = "None"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Dynamic.OData/ODataFilterConstants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Dynamic.OData 5 | { 6 | public static class ODataFilterConstants 7 | { 8 | public const string AggregationMethod_Custom_List = "Custom.List"; 9 | public const string AggregationMethod_Custom_Count = "Custom.Count"; 10 | public const string AggregationMethod_Custom_CountDistinct = "Custom.CountDistinct"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dynamic.OData/ODataFilterManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Dynamic.OData.Exceptions; 5 | using Dynamic.OData.Helpers.Interface; 6 | using Dynamic.OData.Interface; 7 | using Dynamic.OData.Models; 8 | using Dynamic.OData.PredicateParsers.Interface; 9 | using Microsoft.AspNet.OData; 10 | using Microsoft.AspNet.OData.Extensions; 11 | using Microsoft.AspNet.OData.Query; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.OData.Edm; 14 | using Microsoft.OData.UriParser; 15 | using System; 16 | using System.Collections.Generic; 17 | using System.Linq; 18 | 19 | namespace Dynamic.OData 20 | { 21 | public class ODataFilterManager : IODataFilterManager 22 | { 23 | private List _settings; 24 | private readonly IODataRequestHelper _oDataRequestHelper; 25 | private readonly IODataQueryValidator _oDataQueryValidator; 26 | private readonly IODataPredicateParser _odataApplyPredicateParser; 27 | private readonly IODataPredicateParser _odataSelectPredicateParser; 28 | private readonly IODataPredicateParser _odataTopPredicateParser; 29 | private readonly IODataPredicateParser _odataSkipPredicateParser; 30 | private readonly IODataPredicateParser _odataOrderByPredicateParser; 31 | private readonly IODataPredicateParser _odataFilterPredicateParser; 32 | 33 | public ODataFilterManager(IODataRequestHelper oDataRequestHelper 34 | , IODataQueryValidator oDataQueryValidator 35 | , IODataPredicateParser odataApplyPredicateParser 36 | , IODataPredicateParser odataSelectPredicateParser 37 | , IODataPredicateParser odataTopPredicateParser 38 | , IODataPredicateParser odataSkipPredicateParser 39 | , IODataPredicateParser odataOrderByPredicateParser 40 | , IODataPredicateParser odataFilterPredicateParser) 41 | { 42 | _oDataRequestHelper = oDataRequestHelper ?? throw new ArgumentNullException(nameof(oDataRequestHelper)); 43 | _oDataQueryValidator = oDataQueryValidator ?? throw new ArgumentNullException(nameof(oDataQueryValidator)); 44 | _odataApplyPredicateParser = odataApplyPredicateParser ?? throw new ArgumentNullException(nameof(odataApplyPredicateParser)); 45 | _odataSelectPredicateParser = odataSelectPredicateParser ?? throw new ArgumentNullException(nameof(odataSelectPredicateParser)); 46 | _odataTopPredicateParser = odataTopPredicateParser ?? throw new ArgumentNullException(nameof(odataTopPredicateParser)); 47 | _odataSkipPredicateParser = odataSkipPredicateParser ?? throw new ArgumentNullException(nameof(odataSkipPredicateParser)); 48 | _odataOrderByPredicateParser = odataOrderByPredicateParser ?? throw new ArgumentNullException(nameof(odataOrderByPredicateParser)); 49 | _odataFilterPredicateParser = odataFilterPredicateParser ?? throw new ArgumentNullException(nameof(odataFilterPredicateParser)); 50 | } 51 | 52 | private ODataQueryOptions GetODataQueryOptions(HttpRequest httpRequest) 53 | { 54 | var path = _oDataRequestHelper.GetODataPath(httpRequest); 55 | IEdmEntityTypeReference entityType = _oDataRequestHelper.GetEdmEntityTypeReference(httpRequest); 56 | var model = _oDataRequestHelper.GetEdmModel(httpRequest); 57 | var queryOptions = new ODataQueryOptions(new ODataQueryContext(model, entityType.Definition, path), httpRequest); 58 | return queryOptions; 59 | } 60 | 61 | /// 62 | /// Applies OData Filters in the following order 63 | /// 1. Apply 64 | /// 2. Compute 65 | /// 3. Search 66 | /// 4. Filter 67 | /// 5. Count 68 | /// 6. OrderBy 69 | /// 7. Skip 70 | /// 8. Top 71 | /// 9. Expand 72 | /// 10. Select 73 | /// 11. Format 74 | /// 75 | /// The base Source Entities 76 | /// The base source entities in a queryable form 77 | /// The Http Request 78 | /// 79 | public IEnumerable ApplyFilter(IEnumerable sourceEntities, IEnumerable> queryableSourceEntities, HttpRequest request) 80 | { 81 | _settings = _oDataRequestHelper.GetEdmEntityTypeSettings(request); 82 | var model = _oDataRequestHelper.GetEdmModel(request); 83 | // Escape nested single quote 84 | request.QueryString = _oDataRequestHelper.EscapeQueryString(request.QueryString); 85 | var queryOptions = GetODataQueryOptions(request); 86 | string baseRouteName = _oDataRequestHelper.GetRouteName(request); 87 | 88 | //Append the dynamic route name 89 | if (!string.IsNullOrWhiteSpace(queryOptions.RawValues.Select)) 90 | baseRouteName = $"{baseRouteName}({queryOptions.RawValues.Select})"; 91 | request.HttpContext.Items.Add(RequestFilterConstants.ContextSuffixKey, baseRouteName); 92 | 93 | //We simply return the source collection if it's null or has zero records. Or if we are unable to map settings / query options. 94 | if (queryOptions == null || sourceEntities == null || _settings == null) 95 | return sourceEntities; 96 | if (_settings.Count != 1) 97 | return sourceEntities; 98 | if (sourceEntities.Count() == 0) 99 | return sourceEntities; 100 | 101 | //Validate the query from OData perspective. 102 | _oDataQueryValidator.ValidateQuery(queryOptions); 103 | 104 | var serviceRoot = new Uri(RequestFilterConstants.ODataServiceRoot); 105 | var parser = new ODataUriParser(model, serviceRoot, _oDataRequestHelper.GetODataRelativeUri(request)); 106 | var latestStateDictionary = new Dictionary(); 107 | latestStateDictionary.Add(RequestFilterConstants.GetEntityTypeKeyName("base", 0), _oDataRequestHelper.GetEdmEntityTypeReference(request)); 108 | //Initial Context 109 | var parseContext = new ParseContext 110 | { 111 | Model = model, 112 | Result = sourceEntities, 113 | LatestStateDictionary = latestStateDictionary, 114 | EdmEntityTypeSettings = _settings, 115 | QueryableSourceEntities = queryableSourceEntities 116 | }; 117 | 118 | //The clauses are ordered as per OData Spec. 119 | if (!string.IsNullOrWhiteSpace(queryOptions.RawValues.Apply)) 120 | { 121 | parseContext = _odataApplyPredicateParser.Parse(parser, parseContext); 122 | } 123 | 124 | if (!string.IsNullOrWhiteSpace(queryOptions.RawValues.Filter)) 125 | parseContext = _odataFilterPredicateParser.Parse(parser, parseContext); 126 | 127 | if (!string.IsNullOrWhiteSpace(queryOptions.RawValues.Count)) 128 | _oDataRequestHelper.SetRequestCount(parser, request, parseContext.Result.Count()); 129 | 130 | if (!string.IsNullOrWhiteSpace(queryOptions.RawValues.OrderBy)) 131 | { 132 | parseContext = _odataOrderByPredicateParser.Parse(parser, parseContext); 133 | } 134 | 135 | if (!string.IsNullOrWhiteSpace(queryOptions.RawValues.Skip)) 136 | { 137 | parseContext = _odataSkipPredicateParser.Parse(parser, parseContext); 138 | } 139 | 140 | if (!string.IsNullOrWhiteSpace(queryOptions.RawValues.Top)) 141 | { 142 | parseContext = _odataTopPredicateParser.Parse(parser, parseContext); 143 | } 144 | 145 | if (!string.IsNullOrWhiteSpace(queryOptions.RawValues.Expand)) 146 | throw new FeatureNotSupportedException("Expand", string.Empty); 147 | 148 | if (!string.IsNullOrWhiteSpace(queryOptions.RawValues.Select)) 149 | { 150 | parseContext = _odataSelectPredicateParser.Parse(parser, parseContext); 151 | } 152 | 153 | if (!string.IsNullOrWhiteSpace(queryOptions.RawValues.Format)) 154 | throw new FeatureNotSupportedException("Format", string.Empty); 155 | 156 | 157 | 158 | return parseContext.Result; 159 | } 160 | 161 | public void SetPropertyValue(Dictionary attributes, IEnumerable> entityTypeDictionary, Dictionary keyValues = null) 162 | { 163 | foreach (var entityMap in entityTypeDictionary) 164 | { 165 | var targetAttributeKey = entityMap.Key; 166 | var propertyType = entityMap.Value; 167 | var attribute = attributes.FirstOrDefault(predicate => Equals(predicate.Key, targetAttributeKey)); 168 | if (!attribute.Equals(default(KeyValuePair))) 169 | { 170 | CastAndSetValue(propertyType, targetAttributeKey, attribute.Value, keyValues); 171 | } 172 | else 173 | { 174 | SetValue(targetAttributeKey, null, keyValues); 175 | } 176 | } 177 | } 178 | 179 | public void SetActionInfoValue(Dictionary actionInfo, IEnumerable> entityTypeDictionary, Dictionary keyValues) 180 | { 181 | foreach (var entityMap in entityTypeDictionary) 182 | { 183 | var targetKey = entityMap.Key; 184 | var propertyType = entityMap.Value; 185 | var item = actionInfo.Where(x => x.Key.Equals(targetKey)).FirstOrDefault(); 186 | 187 | if (item.Key != null) 188 | { 189 | CastAndSetValue(propertyType, targetKey, item.Value, keyValues); 190 | } 191 | else 192 | { 193 | SetValue(targetKey, null, keyValues); 194 | } 195 | } 196 | } 197 | 198 | private void SetValue(string targetAttributeKey, T result, Dictionary keyValues) 199 | { 200 | if (keyValues != null) 201 | keyValues[targetAttributeKey] = result; 202 | } 203 | 204 | private void CastAndSetValue(string propertyType, string targetKey, string value, Dictionary keyValues) 205 | { 206 | 207 | if (Equals(propertyType, TypeHandlingConstants.String)) 208 | { 209 | SetValue(targetKey, value, keyValues); 210 | } 211 | else if (Equals(propertyType, TypeHandlingConstants.Guid)) 212 | { 213 | _ = Guid.TryParse(value, out Guid result); 214 | SetValue(targetKey, result, keyValues); 215 | } 216 | else if (Equals(propertyType, TypeHandlingConstants.Int16)) 217 | { 218 | _ = short.TryParse(value, out short result); 219 | SetValue(targetKey, result, keyValues); 220 | } 221 | else if (Equals(propertyType, TypeHandlingConstants.Int32)) 222 | { 223 | _ = int.TryParse(value, out int result); 224 | SetValue(targetKey, result, keyValues); 225 | } 226 | else if (Equals(propertyType, TypeHandlingConstants.Int64)) 227 | { 228 | _ = long.TryParse(value, out long result); 229 | SetValue(targetKey, result, keyValues); 230 | } 231 | else if (Equals(propertyType, TypeHandlingConstants.Double)) 232 | { 233 | _ = double.TryParse(value, out double result); 234 | SetValue(targetKey, result, keyValues); 235 | } 236 | else if (Equals(propertyType, TypeHandlingConstants.Boolean)) 237 | { 238 | _ = bool.TryParse(value, out bool result); 239 | SetValue(targetKey, result, keyValues); 240 | } 241 | else if (Equals(propertyType, TypeHandlingConstants.Date)) 242 | { 243 | _ = Date.TryParse(value, out Date result); 244 | SetValue(targetKey, result, keyValues); 245 | } 246 | else if (Equals(propertyType, TypeHandlingConstants.DateTime)) 247 | { 248 | _ = DateTime.TryParse(value, out DateTime result); 249 | SetValue(targetKey, result, keyValues); 250 | } 251 | else if (Equals(propertyType, TypeHandlingConstants.TimeOfDay)) 252 | { 253 | _ = TimeOfDay.TryParse(value, out TimeOfDay result); 254 | SetValue(targetKey, result, keyValues); 255 | } 256 | else if (Equals(propertyType, TypeHandlingConstants.Decimal)) 257 | { 258 | _ = decimal.TryParse(value, out decimal result); 259 | SetValue(targetKey, result, keyValues); 260 | } 261 | } 262 | 263 | private bool Equals(string source, string target) 264 | { 265 | return string.Equals(source, target, StringComparison.OrdinalIgnoreCase); 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/Dynamic.OData/ODataServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Dynamic.OData.Helpers; 5 | using Dynamic.OData.Helpers.Interface; 6 | using Dynamic.OData.Interface; 7 | using Dynamic.OData.PredicateParsers; 8 | using Dynamic.OData.PredicateParsers.Interface; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | namespace Dynamic.OData 12 | { 13 | public static class ODataServiceCollectionExtensions 14 | { 15 | public static void AddDynamicODataQueryServices(this IServiceCollection services, string edmNamespaceName = null) 16 | { 17 | BaseODataPredicateParser.EdmNamespaceName = !string.IsNullOrWhiteSpace(edmNamespaceName) ? edmNamespaceName : "Dynamic.OData.Model"; 18 | 19 | services.AddScoped(); 20 | services.AddScoped(); 21 | services.AddSingleton(); 22 | services.AddSingleton(); 23 | services.AddSingleton(); 24 | services.AddSingleton(); 25 | services.AddSingleton(); 26 | services.AddSingleton(); 27 | 28 | services.AddScoped(serviceProvider => 29 | { 30 | return new ODataFilterManager( 31 | (IODataRequestHelper)serviceProvider.GetService(typeof(IODataRequestHelper)), 32 | (IODataQueryValidator)serviceProvider.GetService(typeof(IODataQueryValidator)), 33 | (IODataPredicateParser)serviceProvider.GetService(typeof(ODataApplyPredicateParser)), 34 | (IODataPredicateParser)serviceProvider.GetService(typeof(ODataSelectPredicateParser)), 35 | (IODataPredicateParser)serviceProvider.GetService(typeof(ODataTopPredicateParser)), 36 | (IODataPredicateParser)serviceProvider.GetService(typeof(ODataSkipPredicateParser)), 37 | (IODataPredicateParser)serviceProvider.GetService(typeof(ODataOrderByPredicateParser)), 38 | (IODataPredicateParser)serviceProvider.GetService(typeof(ODataFilterPredicateParser)) 39 | ); 40 | }); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Dynamic.OData/ParseContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Dynamic.OData.Models; 5 | using Microsoft.AspNet.OData; 6 | using Microsoft.OData.Edm; 7 | using System.Collections.Generic; 8 | 9 | namespace Dynamic.OData 10 | { 11 | public class ParseContext 12 | { 13 | public IEnumerable Result { get; set; } 14 | 15 | public EdmModel Model { get; set; } 16 | 17 | public IEnumerable> QueryableSourceEntities { get; set; } 18 | 19 | public List EdmEntityTypeSettings { get; set; } 20 | public Dictionary LatestStateDictionary { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Dynamic.OData/PredicateParsers/BaseODataPredicateParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNet.OData; 5 | using Dynamic.OData.Models; 6 | using Microsoft.OData.Edm; 7 | using Microsoft.OData.UriParser; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System; 11 | 12 | namespace Dynamic.OData.PredicateParsers 13 | { 14 | public abstract class BaseODataPredicateParser 15 | { 16 | public static string EdmNamespaceName; 17 | protected const string FeatureNotSupported = "Invalid {0} clause- Current Implementation only supports primitive types"; 18 | protected const string InvalidSelectProperty = "Invalid {0} clause - Property {1} does not exist in model"; 19 | protected string GetStringTypeFromEdmPrimitiveType(EdmPrimitiveTypeKind edmPrimitiveType) 20 | { 21 | switch (edmPrimitiveType) 22 | { 23 | case EdmPrimitiveTypeKind.DateTimeOffset: return TypeHandlingConstants.DateTime; 24 | case EdmPrimitiveTypeKind.TimeOfDay: return TypeHandlingConstants.TimeOfDay; 25 | case EdmPrimitiveTypeKind.Date: return TypeHandlingConstants.Date; 26 | case EdmPrimitiveTypeKind.Int32: return TypeHandlingConstants.Int32; 27 | case EdmPrimitiveTypeKind.Int16: return TypeHandlingConstants.Int16; 28 | case EdmPrimitiveTypeKind.Int64: return TypeHandlingConstants.Int64; 29 | case EdmPrimitiveTypeKind.Double: return TypeHandlingConstants.Double; 30 | case EdmPrimitiveTypeKind.Boolean: return TypeHandlingConstants.Boolean; 31 | case EdmPrimitiveTypeKind.Guid: return TypeHandlingConstants.Guid; 32 | case EdmPrimitiveTypeKind.String: return TypeHandlingConstants.String; 33 | case EdmPrimitiveTypeKind.Decimal: return TypeHandlingConstants.Decimal; 34 | default: return TypeHandlingConstants.None; 35 | } 36 | } 37 | 38 | /// 39 | /// Gets a List for all entities of a group. 40 | /// 41 | /// 42 | /// 43 | /// 44 | protected SubCollectionContext GetList(IEnumerable> group, EdmComplexType edmComplexType, int limit = 0) 45 | { 46 | var queryable = new List>(); 47 | var collectionTypeReference = new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(edmComplexType, true))); 48 | var subCollection = new EdmComplexObjectCollection(collectionTypeReference); 49 | int count = 0; 50 | foreach (var entity in group) 51 | { 52 | var dict = new Dictionary(); 53 | var edmComplexObject = new EdmComplexObject(edmComplexType); 54 | foreach (var propertyKvp in entity) 55 | { 56 | edmComplexObject.TrySetPropertyValue(propertyKvp.Key, propertyKvp.Value); 57 | dict.Add(propertyKvp.Key, propertyKvp.Value); 58 | } 59 | subCollection.Add(edmComplexObject); 60 | queryable.Add(dict); 61 | if (limit >= 1 && ++count == limit) 62 | break; 63 | } 64 | return new SubCollectionContext { Result = subCollection, QueryAbleResult = queryable }; 65 | } 66 | protected IEnumerable GetFilteredResult(IEnumerable> group, string query, ParseContext parseContext) 67 | { 68 | // Create collection from group 69 | var collectionEntityTypeKey = parseContext.LatestStateDictionary 70 | .Keys.FirstOrDefault(p => p.Contains("collectionentitytype")); 71 | var entityRef = (EdmEntityTypeReference)parseContext.LatestStateDictionary[collectionEntityTypeKey]; 72 | var collectionRef = new EdmCollectionTypeReference(new EdmCollectionType(entityRef)); 73 | var collection = new EdmEntityObjectCollection(collectionRef); 74 | foreach (var entity in group) 75 | { 76 | var obj = new EdmEntityObject(entityRef); 77 | foreach (var kvp in entity) 78 | obj.TrySetPropertyValue(kvp.Key, kvp.Value); 79 | collection.Add(obj); 80 | } 81 | // Create filter query using the entity supplied in the lateststatedictionary 82 | var serviceRoot = new Uri(RequestFilterConstants.ODataServiceRoot); 83 | var resource = entityRef.Definition.FullTypeName().Split(".").Last(); 84 | var filterQuery = "/" + resource + "?$filter=" + Uri.EscapeDataString(query.Substring(1, query.Length - 2).Replace("''", "'")); 85 | var oDataUriParser = new ODataUriParser(parseContext.Model, new Uri(filterQuery, UriKind.Relative)); 86 | 87 | // Parse filterquery 88 | var filter = oDataUriParser.ParseFilter(); 89 | var odataFilter = new ODataFilterPredicateParser(); 90 | var filteredResult = odataFilter.ApplyFilter(parseContext.EdmEntityTypeSettings.FirstOrDefault(), collection, filter.Expression); 91 | return filteredResult; 92 | } 93 | protected static string GetAttributeName(SingleValueNode node) 94 | { 95 | var attributeName = string.Empty; 96 | if (node.Kind == QueryNodeKind.SingleValueOpenPropertyAccess) 97 | attributeName = ((SingleValueOpenPropertyAccessNode)node).Name; 98 | else if (node.Kind == QueryNodeKind.SingleValuePropertyAccess) 99 | attributeName = ((SingleValuePropertyAccessNode)node).Property.Name; 100 | else if (node.Kind == QueryNodeKind.SingleValueFunctionCall) 101 | { 102 | foreach (var x in ((SingleValueFunctionCallNode)node).Parameters) 103 | { 104 | attributeName = ((SingleValuePropertyAccessNode)x).Property.Name; 105 | } 106 | 107 | } 108 | else if(node.Kind == QueryNodeKind.Convert) 109 | { 110 | if(((ConvertNode)node).Source.Kind == QueryNodeKind.SingleValuePropertyAccess) 111 | attributeName = ((SingleValuePropertyAccessNode)((ConvertNode)node).Source).Property.Name; 112 | } 113 | return attributeName; 114 | } 115 | 116 | protected static string GetParamAttributeName(QueryNode node) 117 | { 118 | var attributeName = string.Empty; 119 | if (node.Kind == QueryNodeKind.SingleValueOpenPropertyAccess) 120 | attributeName = ((SingleValueOpenPropertyAccessNode)node).Name; 121 | else if (node.Kind == QueryNodeKind.SingleValuePropertyAccess) 122 | attributeName = ((SingleValuePropertyAccessNode)node).Property.Name; 123 | 124 | return attributeName; 125 | } 126 | 127 | protected static EdmPrimitiveTypeKind GetDataType(SingleValueNode node) 128 | { 129 | var dataType = EdmPrimitiveTypeKind.None; 130 | if (node.Kind == QueryNodeKind.SingleValueOpenPropertyAccess) 131 | dataType = ((EdmTypeReference)((SingleValueOpenPropertyAccessNode)node).TypeReference).PrimitiveKind(); 132 | else if (node.Kind == QueryNodeKind.SingleValuePropertyAccess) 133 | dataType = ((EdmTypeReference)((SingleValuePropertyAccessNode)node).TypeReference).PrimitiveKind(); 134 | else if (node.Kind == QueryNodeKind.Constant) 135 | dataType = ((EdmTypeReference)((ConstantNode)node).TypeReference).PrimitiveKind(); 136 | else if (node.Kind == QueryNodeKind.Convert) 137 | dataType = ((EdmTypeReference)((ConvertNode)node).TypeReference).PrimitiveKind(); 138 | else if (node.Kind == QueryNodeKind.SingleValueFunctionCall) 139 | { 140 | foreach (var x in ((SingleValueFunctionCallNode)node).Parameters) 141 | { 142 | dataType = ((EdmTypeReference)((SingleValuePropertyAccessNode)x).TypeReference).PrimitiveKind(); 143 | if (dataType != EdmPrimitiveTypeKind.None) 144 | break; 145 | } 146 | } 147 | 148 | return dataType; 149 | } 150 | 151 | protected static EdmPrimitiveTypeKind GetDataType(QueryNode node) 152 | { 153 | var dataType = EdmPrimitiveTypeKind.None; 154 | if (node.Kind == QueryNodeKind.SingleValueOpenPropertyAccess) 155 | dataType = ((EdmTypeReference)((SingleValueOpenPropertyAccessNode)node).TypeReference).PrimitiveKind(); 156 | else if (node.Kind == QueryNodeKind.SingleValuePropertyAccess) 157 | dataType = ((EdmTypeReference)((SingleValuePropertyAccessNode)node).TypeReference).PrimitiveKind(); 158 | else if (node.Kind == QueryNodeKind.Constant) 159 | dataType = ((EdmTypeReference)((ConstantNode)node).TypeReference).PrimitiveKind(); 160 | else if (node.Kind == QueryNodeKind.Convert) 161 | dataType = ((EdmTypeReference)((ConvertNode)node).TypeReference).PrimitiveKind(); 162 | 163 | 164 | return dataType; 165 | } 166 | 167 | protected object GetPropertyValue(IEdmEntityObject p, string attributeName) 168 | { 169 | p.TryGetPropertyValue(attributeName, out object value); 170 | return value != null ? value : null; 171 | } 172 | protected object GetPropertyValue(IEdmEntityObject p, string attributeName, object attributeValue) 173 | { 174 | if (attributeValue != null) 175 | //specific null handling coz Equals and other aspects would need this 176 | return attributeValue != null ? attributeValue : "--NULL--"; 177 | else 178 | { 179 | p.TryGetPropertyValue(attributeName, out object value); 180 | //specific null handling coz Equals and other aspects would need this 181 | return value != null ? value : "--NULL--"; 182 | } 183 | } 184 | 185 | protected object GetPropertyValue(SingleValueNode node) 186 | { 187 | object value = null; 188 | if (node.Kind == QueryNodeKind.Constant) 189 | value = ((ConstantNode)node).Value; 190 | else if (node.Kind == QueryNodeKind.Convert && ((ConvertNode)node).Source.Kind == QueryNodeKind.Constant) 191 | value = ((ConstantNode)((ConvertNode)node).Source).Value; 192 | 193 | return value; 194 | } 195 | protected object GetPropertyValue(QueryNode node) 196 | { 197 | object value = null; 198 | if (node.Kind == QueryNodeKind.Constant) 199 | value = ((ConstantNode)node).Value; 200 | else if (node.Kind == QueryNodeKind.Convert && ((ConvertNode)node).Source.Kind == QueryNodeKind.Constant) 201 | value = ((ConstantNode)((ConvertNode)node).Source).Value; 202 | 203 | return value; 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Dynamic.OData/PredicateParsers/Interface/IODataPredicateParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Microsoft.OData.UriParser; 5 | 6 | namespace Dynamic.OData.PredicateParsers.Interface 7 | { 8 | public interface IODataPredicateParser 9 | { 10 | ParseContext Parse(ODataUriParser parser, ParseContext parseContext); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Dynamic.OData/PredicateParsers/ODataApplyPredicateParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Dynamic.OData.Exceptions; 5 | using Dynamic.OData.Models; 6 | using Dynamic.OData.PredicateParsers.Interface; 7 | using Microsoft.AspNet.OData; 8 | using Microsoft.AspNet.OData.Extensions; 9 | using Microsoft.OData.Edm; 10 | using Microsoft.OData.UriParser; 11 | using Microsoft.OData.UriParser.Aggregation; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Data; 15 | using System.Linq; 16 | 17 | namespace Dynamic.OData.PredicateParsers 18 | { 19 | /// 20 | /// Apply clause parser and data generator. If this clause is present in query, it's always evaluated first. 21 | /// The error behavior is boolean. Either we parse the query completely, or not at all. There is no fallback and this is intentional. 22 | /// 23 | public class ODataApplyPredicateParser : BaseODataPredicateParser, IODataPredicateParser 24 | { 25 | private const string ApplyParser = "Apply"; 26 | private const int StepIndex = 1; 27 | private ParseContext SourceParseContext; 28 | public ParseContext Parse(ODataUriParser parser, ParseContext sourceParseContext) 29 | { 30 | SourceParseContext = sourceParseContext; 31 | var targetParseContext = new ParseContext(); 32 | var targetQueryableSourceEntities = new List>(); 33 | var sourceEdmSetting = sourceParseContext.EdmEntityTypeSettings.FirstOrDefault(); 34 | var targetEdmSetting = new EdmEntityTypeSettings() 35 | { 36 | RouteName = "Groups", 37 | Personas = sourceEdmSetting.Personas, 38 | Properties = new List() 39 | }; 40 | var latestStateDictionary = new Dictionary(); 41 | 42 | var edmEntityType = new EdmEntityType(EdmNamespaceName, "Groups"); 43 | //This may only be used if we client uses Custom.List as aggregation 44 | var edmComplexType = new EdmComplexType(EdmNamespaceName, "List"); 45 | var aggregatePropList = new Dictionary(); 46 | var applyClause = parser.ParseApply(); 47 | 48 | //We support only single transformation 49 | if (applyClause.Transformations.Count() > 1) 50 | throw new FeatureNotSupportedException(ApplyParser, "Multiple Transformations"); 51 | 52 | if (applyClause.Transformations.Count() == 0) 53 | throw new FeatureNotSupportedException(ApplyParser, "Zero Transformations"); 54 | 55 | foreach (var transformation in applyClause.Transformations) 56 | { 57 | if (transformation.Kind == TransformationNodeKind.GroupBy) 58 | { 59 | var transform = (GroupByTransformationNode)transformation; 60 | 61 | ///Add all the grouping properties 62 | foreach (var groupingProperty in transform.GroupingProperties) 63 | { 64 | var sourceProperty = sourceEdmSetting.Properties.FirstOrDefault(predicate => predicate.PropertyName.Equals(groupingProperty.Name)); 65 | 66 | edmEntityType.AddStructuralProperty(groupingProperty.Name, groupingProperty.TypeReference.PrimitiveKind()); 67 | 68 | targetEdmSetting.Properties.Add(new EdmEntityTypePropertySetting 69 | { 70 | PropertyName = sourceProperty.PropertyName, 71 | PropertyType = sourceProperty.PropertyType, 72 | IsNullable = sourceProperty.IsNullable 73 | }); 74 | } 75 | 76 | 77 | //Add all the aggregate properties 78 | if (transform.ChildTransformations != null) 79 | { 80 | var aggregationProperties = (AggregateTransformationNode)transform.ChildTransformations; 81 | AddAggregationPropertiesToModel(aggregationProperties 82 | , sourceEdmSetting, edmEntityType, aggregatePropList, targetEdmSetting 83 | , edmComplexType, latestStateDictionary); 84 | } 85 | 86 | //Register these dynamic types to model 87 | sourceParseContext.Model.AddElement(edmEntityType); 88 | sourceParseContext.Model.AddElement(edmComplexType); 89 | ((EdmEntityContainer)sourceParseContext.Model.EntityContainer).AddEntitySet("Groups", edmEntityType); 90 | 91 | var fields = transform.GroupingProperties.Select(p => p.Name).ToList(); 92 | var groups = sourceParseContext.QueryableSourceEntities 93 | .GroupBy(r => fields.ToDictionary(c => c, c => r[c]), new CustomEqualityComparer()); 94 | 95 | var entityRef = new EdmEntityTypeReference(edmEntityType, true); 96 | var collectionRef = new EdmCollectionTypeReference(new EdmCollectionType(entityRef)); 97 | var collection = new EdmEntityObjectCollection(collectionRef); 98 | latestStateDictionary.Add(RequestFilterConstants.GetEntityTypeKeyName(ApplyParser, StepIndex), entityRef); 99 | foreach (var group in groups) 100 | { 101 | var targetQueryableDictionary = new Dictionary(); 102 | var obj = new EdmEntityObject(edmEntityType); 103 | foreach (var prop in fields) 104 | { 105 | var value = group.Key[prop]; 106 | obj.TrySetPropertyValue(prop, value); 107 | targetQueryableDictionary.Add(prop, value); 108 | } 109 | AddAggregationPropertyValuesToModel(targetQueryableDictionary, obj, group, edmComplexType, aggregatePropList); 110 | collection.Add(obj); 111 | targetQueryableSourceEntities.Add(targetQueryableDictionary); 112 | } 113 | targetParseContext.Result = collection; 114 | targetParseContext.Model = sourceParseContext.Model; 115 | targetParseContext.QueryableSourceEntities = targetQueryableSourceEntities; 116 | targetParseContext.EdmEntityTypeSettings = new List { targetEdmSetting }; 117 | targetParseContext.LatestStateDictionary = latestStateDictionary; 118 | return targetParseContext; 119 | } 120 | else if (transformation.Kind == TransformationNodeKind.Aggregate) 121 | { 122 | var targetQueryableDictionary = new Dictionary(); 123 | var obj = new EdmEntityObject(edmEntityType); 124 | var aggregationProperties = (AggregateTransformationNode)transformation; 125 | AddAggregationPropertiesToModel(aggregationProperties 126 | , sourceEdmSetting, edmEntityType, aggregatePropList, targetEdmSetting 127 | , edmComplexType, latestStateDictionary); 128 | //Register these dynamic types to model 129 | sourceParseContext.Model.AddElement(edmEntityType); 130 | sourceParseContext.Model.AddElement(edmComplexType); 131 | var entityRef = new EdmEntityTypeReference(edmEntityType, true); 132 | var collectionRef = new EdmCollectionTypeReference(new EdmCollectionType(entityRef)); 133 | var collection = new EdmEntityObjectCollection(collectionRef); 134 | latestStateDictionary.Add(RequestFilterConstants.GetEntityTypeKeyName(ApplyParser, StepIndex), entityRef); 135 | AddAggregationPropertyValuesToModel(targetQueryableDictionary, obj, sourceParseContext.QueryableSourceEntities, edmComplexType, aggregatePropList); 136 | collection.Add(obj); 137 | targetQueryableSourceEntities.Add(targetQueryableDictionary); 138 | targetParseContext.Result = collection; 139 | targetParseContext.Model = sourceParseContext.Model; 140 | targetParseContext.QueryableSourceEntities = targetQueryableSourceEntities; 141 | targetParseContext.EdmEntityTypeSettings = new List { targetEdmSetting }; 142 | targetParseContext.LatestStateDictionary = latestStateDictionary; 143 | return targetParseContext; 144 | } 145 | else 146 | { 147 | throw new FeatureNotSupportedException(ApplyParser, $"Transformation Kind {transformation.Kind} is not supported"); 148 | } 149 | } 150 | throw new FeatureNotSupportedException(ApplyParser, "Invalid Apply Clause"); 151 | } 152 | 153 | private void AddAggregationPropertyValuesToModel(Dictionary targetQueryableDictionary 154 | , EdmEntityObject obj 155 | , IEnumerable> group 156 | , EdmComplexType edmComplexType 157 | , Dictionary aggregatePropList) 158 | { 159 | foreach (var property in aggregatePropList) 160 | { 161 | var val = aggregatePropList[property.Key]; 162 | var primitiveKind = val.Expression.TypeReference.PrimitiveKind(); 163 | if (val.Method != AggregationMethod.Custom) 164 | { 165 | var sourcePropertyName = val.Method != AggregationMethod.VirtualPropertyCount ? 166 | ((SingleValuePropertyAccessNode)val.Expression).Property.Name : null; 167 | var value = GetAggregatedValue(sourcePropertyName, val, group, primitiveKind); 168 | obj.TrySetPropertyValue(val.Alias, value); 169 | targetQueryableDictionary.Add(val.Alias, value); 170 | } 171 | else 172 | { 173 | object value; 174 | if (val.MethodDefinition.MethodLabel.Contains(ODataFilterConstants.AggregationMethod_Custom_List, StringComparison.OrdinalIgnoreCase)) 175 | { 176 | value = GetAggregatedValue(property.Key, val, group, EdmPrimitiveTypeKind.None, edmComplexType); 177 | var subcollectionContext = (SubCollectionContext)value; 178 | obj.TrySetPropertyValue(property.Key, subcollectionContext.Result); 179 | targetQueryableDictionary.Add(property.Key, subcollectionContext.QueryAbleResult); 180 | } 181 | else if (val.MethodDefinition.MethodLabel.Contains(ODataFilterConstants.AggregationMethod_Custom_CountDistinct, StringComparison.OrdinalIgnoreCase) 182 | || val.MethodDefinition.MethodLabel.Contains(ODataFilterConstants.AggregationMethod_Custom_Count, StringComparison.OrdinalIgnoreCase)) 183 | { 184 | var sourcePropertyName = ((SingleValuePropertyAccessNode)val.Expression).Property.Name; 185 | value = GetAggregatedValue(sourcePropertyName, val, group, EdmPrimitiveTypeKind.None); 186 | obj.TrySetPropertyValue(val.Alias, value); 187 | targetQueryableDictionary.Add(val.Alias, value); 188 | } 189 | } 190 | } 191 | } 192 | 193 | private void AddAggregationPropertiesToModel(AggregateTransformationNode aggregationProperties 194 | , EdmEntityTypeSettings sourceEdmSetting 195 | , EdmEntityType edmEntityType 196 | , Dictionary aggregatePropList 197 | , EdmEntityTypeSettings targetEdmSetting 198 | , EdmComplexType edmComplexType 199 | , Dictionary latestStateDictionary) 200 | { 201 | foreach (var aggregationExpression in aggregationProperties.AggregateExpressions) 202 | { 203 | var expr = (AggregateExpression)aggregationExpression; 204 | if (expr.Method != AggregationMethod.Custom) 205 | { 206 | bool? isNullable = null; 207 | string propertyAlias = ""; 208 | var primitiveKind = expr.Expression.TypeReference.PrimitiveKind(); 209 | if (expr.Method == AggregationMethod.VirtualPropertyCount) 210 | { 211 | var sourceProperty = (CountVirtualPropertyNode)expr.Expression; 212 | isNullable = sourceProperty.TypeReference.IsNullable; 213 | propertyAlias = !string.IsNullOrWhiteSpace(expr.Alias) ? expr.Alias : sourceProperty.Kind.ToString(); 214 | primitiveKind = EdmPrimitiveTypeKind.Int32; 215 | } 216 | else 217 | { 218 | var sourceProperty = (SingleValuePropertyAccessNode)expr.Expression; 219 | var sourceEdmProperty = sourceEdmSetting.Properties.FirstOrDefault(predicate => predicate.PropertyName.Equals(sourceProperty.Property.Name)); 220 | isNullable = sourceEdmProperty.IsNullable; 221 | propertyAlias = !string.IsNullOrWhiteSpace(expr.Alias) ? expr.Alias : sourceProperty.Property.Name; 222 | if (expr.Method == AggregationMethod.Average) 223 | primitiveKind = EdmPrimitiveTypeKind.Double; 224 | if (expr.Method == AggregationMethod.CountDistinct) 225 | primitiveKind = EdmPrimitiveTypeKind.Int32; 226 | } 227 | edmEntityType.AddStructuralProperty(propertyAlias, primitiveKind); 228 | aggregatePropList.Add(propertyAlias, expr); 229 | targetEdmSetting.Properties.Add(new EdmEntityTypePropertySetting 230 | { 231 | PropertyName = propertyAlias, 232 | PropertyType = GetStringTypeFromEdmPrimitiveType(primitiveKind), 233 | IsNullable = isNullable 234 | }); 235 | } 236 | else 237 | { 238 | //Create a list of source type 239 | if (expr.MethodDefinition.MethodLabel.Contains(ODataFilterConstants.AggregationMethod_Custom_List, StringComparison.OrdinalIgnoreCase)) 240 | { 241 | 242 | foreach (var property in sourceEdmSetting.Properties) 243 | { 244 | edmComplexType.AddStructuralProperty(property.PropertyName, property.GetEdmPrimitiveTypeKind()); 245 | } 246 | var groupItemsPropertyName = !string.IsNullOrWhiteSpace(expr.Alias) ? expr.Alias : "Items"; 247 | var complexTypeReference = new EdmComplexTypeReference(edmComplexType, true); 248 | edmEntityType.AddStructuralProperty(groupItemsPropertyName, new EdmCollectionTypeReference(new EdmCollectionType(complexTypeReference))); 249 | aggregatePropList.Add(groupItemsPropertyName, expr); 250 | targetEdmSetting.Properties.Add(new EdmEntityTypePropertySetting 251 | { 252 | PropertyName = groupItemsPropertyName, 253 | PropertyType = "List", 254 | IsNullable = null 255 | }); 256 | latestStateDictionary.Add(RequestFilterConstants.GetComplexTypeKeyName(ApplyParser, StepIndex), edmComplexType); 257 | 258 | } 259 | else if (expr.MethodDefinition.MethodLabel.Contains(ODataFilterConstants.AggregationMethod_Custom_CountDistinct, StringComparison.OrdinalIgnoreCase) 260 | || expr.MethodDefinition.MethodLabel.Contains(ODataFilterConstants.AggregationMethod_Custom_Count, StringComparison.OrdinalIgnoreCase)) 261 | { 262 | var sourceProperty = (SingleValuePropertyAccessNode)expr.Expression; 263 | var primitiveKind = EdmPrimitiveTypeKind.Int32; 264 | var countDistinctPropName = !string.IsNullOrWhiteSpace(expr.Alias) ? expr.Alias : sourceProperty.Property.Name; 265 | edmEntityType.AddStructuralProperty(countDistinctPropName, primitiveKind); 266 | aggregatePropList.Add(countDistinctPropName, expr); 267 | targetEdmSetting.Properties.Add(new EdmEntityTypePropertySetting 268 | { 269 | PropertyName = countDistinctPropName, 270 | PropertyType = GetStringTypeFromEdmPrimitiveType(primitiveKind), 271 | IsNullable = null 272 | }); 273 | } 274 | else 275 | { 276 | throw new FeatureNotSupportedException($"{ApplyParser}-Custom Aggregation-{expr.MethodDefinition.MethodLabel}", "Invalid Custom Aggregation"); 277 | } 278 | } 279 | } 280 | } 281 | 282 | 283 | private object GetAggregatedValue(string key, AggregateExpression expression, IEnumerable> group, EdmPrimitiveTypeKind edmPrimitiveType = EdmPrimitiveTypeKind.None, EdmComplexType edmComplexType = null) 284 | { 285 | var clrType = GetCLRTypeFromEdmType(edmPrimitiveType); 286 | var method = expression.Method; 287 | switch (method) 288 | { 289 | case AggregationMethod.Max: return Convert.ChangeType(group.Max(r => r.GetValueOrDefault(key)), clrType); 290 | case AggregationMethod.Min: return Convert.ChangeType(group.Min(r => r.GetValueOrDefault(key)), clrType); 291 | case AggregationMethod.Average: 292 | switch (edmPrimitiveType) 293 | { 294 | case EdmPrimitiveTypeKind.Double: return group.Average(r => (double)r.GetValueOrDefault(key)); 295 | case EdmPrimitiveTypeKind.Int16: return group.Average(r => (short)r.GetValueOrDefault(key)); 296 | case EdmPrimitiveTypeKind.Int32: return group.Average(r => (int)r.GetValueOrDefault(key)); 297 | case EdmPrimitiveTypeKind.Int64: return group.Average(r => (long)r.GetValueOrDefault(key)); 298 | case EdmPrimitiveTypeKind.Decimal: return group.Average(r => (decimal)r.GetValueOrDefault(key)); 299 | default: return group.Average(r => (int)r.GetValueOrDefault(key)); 300 | } 301 | case AggregationMethod.CountDistinct: return group.Select(r => r.GetValueOrDefault(key)).Distinct().Count(); 302 | case AggregationMethod.VirtualPropertyCount: return group.Select(r => r).Count(); 303 | case AggregationMethod.Sum: 304 | switch (edmPrimitiveType) 305 | { 306 | case EdmPrimitiveTypeKind.Double: return group.Sum(r => (double)r.GetValueOrDefault(key)); 307 | case EdmPrimitiveTypeKind.Int16: return group.Sum(r => (short)r.GetValueOrDefault(key)); 308 | case EdmPrimitiveTypeKind.Int32: return group.Sum(r => (int)r.GetValueOrDefault(key)); 309 | case EdmPrimitiveTypeKind.Int64: return group.Sum(r => (long)r.GetValueOrDefault(key)); 310 | case EdmPrimitiveTypeKind.Decimal: return group.Sum(r => (decimal)r.GetValueOrDefault(key)); 311 | default: return Convert.ChangeType(group.Sum(r => (int)r.GetValueOrDefault(key)), clrType); 312 | } 313 | case AggregationMethod.Custom: return ProcessCustomAggregationValue(expression.MethodDefinition.MethodLabel, group, edmComplexType, key); 314 | } 315 | return null; 316 | } 317 | 318 | private object ProcessCustomAggregationValue(string customAggregationMethod, IEnumerable> group, EdmComplexType edmComplexType, string key) 319 | { 320 | var customMethodName = ParseMethodName(customAggregationMethod); 321 | switch (customMethodName.Name) 322 | { 323 | case ODataFilterConstants.AggregationMethod_Custom_List: return GetList(group, edmComplexType, customMethodName.Count); 324 | case ODataFilterConstants.AggregationMethod_Custom_Count: return GetFilteredResult(group, customMethodName.FilterQuery, SourceParseContext).Count(); 325 | case ODataFilterConstants.AggregationMethod_Custom_CountDistinct: return GetFilteredResult(group, customMethodName.FilterQuery, SourceParseContext).Select(p => GetPropertyValue(p, key)).Distinct().Count(); ; 326 | } 327 | return null; 328 | } 329 | 330 | /// 331 | /// Gets CLR Primitive Type from an EDM Primitive Type 332 | /// 333 | /// 334 | /// 335 | private Type GetCLRTypeFromEdmType(EdmPrimitiveTypeKind edmPrimitiveType) 336 | { 337 | switch (edmPrimitiveType) 338 | { 339 | case EdmPrimitiveTypeKind.DateTimeOffset: return typeof(DateTime); 340 | case EdmPrimitiveTypeKind.Date: return typeof(Date); 341 | case EdmPrimitiveTypeKind.TimeOfDay: return typeof(TimeOfDay); 342 | case EdmPrimitiveTypeKind.Int16: return typeof(short); 343 | case EdmPrimitiveTypeKind.Int32: return typeof(int); 344 | case EdmPrimitiveTypeKind.Int64: return typeof(long); 345 | case EdmPrimitiveTypeKind.Double: return typeof(double); 346 | case EdmPrimitiveTypeKind.Boolean: return typeof(bool); 347 | case EdmPrimitiveTypeKind.Guid: return typeof(Guid); 348 | case EdmPrimitiveTypeKind.Decimal: return typeof(decimal); 349 | default: return typeof(string); 350 | } 351 | } 352 | private CustomMethodName ParseMethodName(string methodName) 353 | { 354 | CustomMethodName cus = new CustomMethodName(); 355 | var name = methodName.Split('_'); 356 | cus.Name = name[0]; 357 | if (name.Length > 1) 358 | { 359 | bool isInt = int.TryParse(name[1], out int value); 360 | if (isInt) 361 | cus.Count = value; 362 | else 363 | cus.FilterQuery = name[1]; 364 | } 365 | return cus; 366 | } 367 | } 368 | 369 | class CustomMethodName 370 | { 371 | public string Name { get; set; } 372 | public int Count { get; set; } 373 | public string FilterQuery { get; set; } 374 | } 375 | 376 | public class SubCollectionContext 377 | { 378 | public object Result { get; set; } 379 | public List> QueryAbleResult { get; set; } 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /src/Dynamic.OData/PredicateParsers/ODataFilterPredicateParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Dynamic.OData.Models; 5 | using Dynamic.OData.PredicateParsers.Interface; 6 | using Microsoft.AspNet.OData; 7 | using Microsoft.OData.Edm; 8 | using Microsoft.OData.UriParser; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Globalization; 12 | using System.Linq; 13 | 14 | namespace Dynamic.OData.PredicateParsers 15 | { 16 | public class ODataFilterPredicateParser : BaseODataPredicateParser, IODataPredicateParser 17 | { 18 | private const string FilterParser = "Filter"; 19 | private const string IdProperty = "id"; 20 | 21 | public ParseContext Parse(ODataUriParser parser, ParseContext parseContext) 22 | { 23 | var rootfilter = parser.ParseFilter(); 24 | var sourceEdmSetting = parseContext.EdmEntityTypeSettings.FirstOrDefault(); 25 | 26 | 27 | var collectionEntityTypeKey = parseContext.LatestStateDictionary 28 | .Keys.FirstOrDefault(p => p.Contains("collectionentitytype")); 29 | 30 | var entityRef = (EdmEntityTypeReference)parseContext.LatestStateDictionary[collectionEntityTypeKey]; 31 | var collectionRef = new EdmCollectionTypeReference(new EdmCollectionType(entityRef)); 32 | var collection = new EdmEntityObjectCollection(collectionRef); 33 | 34 | 35 | var filterdata = ApplyFilter(sourceEdmSetting, parseContext.Result, rootfilter.Expression); 36 | var filteredResults = parseContext.Result.Intersect(filterdata); 37 | 38 | 39 | 40 | foreach (var entity in filteredResults) 41 | collection.Add(entity); 42 | 43 | var idList = filteredResults 44 | .Select(p => p.TryGetPropertyValue(IdProperty, out object id) ? (Guid)id : Guid.Empty) 45 | .ToDictionary(p => p, q => !q.Equals(Guid.Empty)); 46 | 47 | var targetParseContext = new ParseContext 48 | { 49 | Result = collection, 50 | QueryableSourceEntities = parseContext.QueryableSourceEntities.Where(p => idList.ContainsKey((Guid)p[IdProperty])).ToList(), 51 | Model = parseContext.Model, 52 | EdmEntityTypeSettings = parseContext.EdmEntityTypeSettings, 53 | LatestStateDictionary = parseContext.LatestStateDictionary 54 | }; 55 | return targetParseContext; 56 | 57 | 58 | 59 | } 60 | 61 | public IEnumerable ApplyFilter(EdmEntityTypeSettings sourceEdmSetting, IEnumerable items, SingleValueNode filter, string commandPrefix = "") 62 | { 63 | 64 | //var typeSetting = sourceEdmSetting.Properties.FirstOrDefault(predicate => predicate.PropertyName == attributeName); 65 | //if (typeSetting == null) 66 | // throw new InvalidPropertyException("Filter", attributeName); 67 | 68 | SingleValueNode right = null; 69 | SingleValueNode left = null; 70 | BinaryOperatorKind opr = BinaryOperatorKind.Equal; 71 | QueryNodeKind kind = QueryNodeKind.None; 72 | 73 | QueryNode param1 = null; 74 | QueryNode param2 = null; 75 | string function = string.Empty; 76 | if (filter.Kind == QueryNodeKind.UnaryOperator) 77 | { 78 | if (((UnaryOperatorNode)filter).OperatorKind == UnaryOperatorKind.Not) 79 | { 80 | commandPrefix = "not"; 81 | kind = ((UnaryOperatorNode)filter).Kind; 82 | right = ((UnaryOperatorNode)filter).Operand; 83 | } 84 | } 85 | else if (filter.Kind == QueryNodeKind.Convert && ((ConvertNode)filter).Source.Kind == QueryNodeKind.UnaryOperator) 86 | { 87 | if (((UnaryOperatorNode)((ConvertNode)filter).Source).OperatorKind == UnaryOperatorKind.Not) 88 | { 89 | commandPrefix = "not"; 90 | kind = ((UnaryOperatorNode)((ConvertNode)filter).Source).Kind; 91 | right = ((UnaryOperatorNode)((ConvertNode)filter).Source).Operand; 92 | } 93 | } 94 | else if (filter.Kind == QueryNodeKind.BinaryOperator) 95 | { 96 | right = ((BinaryOperatorNode)filter).Right; 97 | left = ((BinaryOperatorNode)filter).Left; 98 | opr = ((BinaryOperatorNode)filter).OperatorKind; 99 | kind = ((BinaryOperatorNode)filter).Kind; 100 | } 101 | else if (filter.Kind == QueryNodeKind.Convert && ((ConvertNode)filter).Source.Kind == QueryNodeKind.BinaryOperator) 102 | { 103 | right = ((BinaryOperatorNode)((ConvertNode)filter).Source).Right; 104 | left = ((BinaryOperatorNode)((ConvertNode)filter).Source).Left; 105 | opr = ((BinaryOperatorNode)((ConvertNode)filter).Source).OperatorKind; 106 | kind = ((ConvertNode)filter).Source.Kind; 107 | } 108 | else if (filter.Kind == QueryNodeKind.SingleValueFunctionCall) 109 | { 110 | kind = filter.Kind; 111 | param1 = ((SingleValueFunctionCallNode)filter).Parameters.ElementAt(0); 112 | param2 = ((SingleValueFunctionCallNode)filter).Parameters.ElementAt(1); 113 | function = ((SingleValueFunctionCallNode)filter).Name; 114 | } 115 | else if (filter.Kind == QueryNodeKind.Convert) 116 | { 117 | if (((ConvertNode)filter).Source.Kind == QueryNodeKind.SingleValueFunctionCall) 118 | { 119 | kind = ((ConvertNode)filter).Source.Kind; 120 | param1 = ((SingleValueFunctionCallNode)((ConvertNode)filter).Source).Parameters.ElementAt(0); 121 | param2 = ((SingleValueFunctionCallNode)((ConvertNode)filter).Source).Parameters.ElementAt(1); 122 | function = ((SingleValueFunctionCallNode)((ConvertNode)filter).Source).Name; 123 | } 124 | } 125 | 126 | if (kind == QueryNodeKind.BinaryOperator || kind == QueryNodeKind.UnaryOperator || kind == QueryNodeKind.SingleValueFunctionCall) 127 | { 128 | if (opr == BinaryOperatorKind.And) 129 | { 130 | items = ApplyFilter(sourceEdmSetting, items, left).Intersect(ApplyFilter(sourceEdmSetting, items, right)); 131 | 132 | } 133 | else if (opr == BinaryOperatorKind.Or) 134 | { 135 | items = ApplyFilter(sourceEdmSetting, items, left).Union(ApplyFilter(sourceEdmSetting, items, right)); 136 | 137 | } 138 | else if (function == "startswith" || function == "endswith" || function == "contains") 139 | { 140 | var param1AttributeName = GetParamAttributeName(param1); 141 | var param2AttributeName = GetParamAttributeName(param2); 142 | var param1AttributeValue = GetPropertyValue(param1); 143 | var param2AttributeValue = GetPropertyValue(param2); 144 | var param1AttributeType = GetDataType(param1); 145 | var param2AttributeType = GetDataType(param2); 146 | 147 | switch (commandPrefix + function) 148 | { 149 | case "startswith": 150 | items = items.Where(x => GetPropertyValue(x, param1AttributeName, param1AttributeValue).ToString().StartsWith(GetPropertyValue(x, param2AttributeName, param2AttributeValue).ToString())); 151 | break; 152 | case "endswith": 153 | items = items.Where(x => GetPropertyValue(x, param1AttributeName, param1AttributeValue).ToString().EndsWith(GetPropertyValue(x, param2AttributeName, param2AttributeValue).ToString())); 154 | break; 155 | case "contains": 156 | items = items.Where(x => GetPropertyValue(x, param1AttributeName, param1AttributeValue).ToString().Contains(GetPropertyValue(x, param2AttributeName, param2AttributeValue).ToString())); 157 | break; 158 | case "notstartswith": 159 | items = items.Where(x => !GetPropertyValue(x, param1AttributeName, param1AttributeValue).ToString().StartsWith(GetPropertyValue(x, param2AttributeName, param2AttributeValue).ToString())); 160 | break; 161 | case "notendswith": 162 | items = items.Where(x => !GetPropertyValue(x, param1AttributeName, param1AttributeValue).ToString().EndsWith(GetPropertyValue(x, param2AttributeName, param2AttributeValue).ToString())); 163 | break; 164 | case "notcontains": 165 | items = items.Where(x => !GetPropertyValue(x, param1AttributeName, param1AttributeValue).ToString().Contains(GetPropertyValue(x, param2AttributeName, param2AttributeValue).ToString())); 166 | break; 167 | } 168 | } 169 | else if (commandPrefix == "not") 170 | { 171 | items = ApplyFilter(sourceEdmSetting, items, right, commandPrefix); 172 | 173 | } 174 | else if (opr == BinaryOperatorKind.Equal || opr == BinaryOperatorKind.NotEqual || opr == BinaryOperatorKind.GreaterThan || opr == BinaryOperatorKind.GreaterThanOrEqual || opr == BinaryOperatorKind.LessThan || opr == BinaryOperatorKind.LessThanOrEqual) 175 | { 176 | var leftAttributeName = GetAttributeName(left); 177 | var rightAttributeName = GetAttributeName(right); 178 | var leftAttributeValue = GetPropertyValue(left); 179 | var rightAttributeValue = GetPropertyValue(right); 180 | var leftAttributeType = GetDataType(left); 181 | var rightAttributeType = GetDataType(right); 182 | 183 | switch (opr) 184 | { 185 | case BinaryOperatorKind.Equal: 186 | if (leftAttributeType == EdmPrimitiveTypeKind.DateTimeOffset) 187 | items = DateEquals(items, leftAttributeName, rightAttributeName, leftAttributeValue, rightAttributeValue, leftAttributeType, left); 188 | else 189 | items = items.Where(x => GetPropertyValue(x, leftAttributeName, leftAttributeValue).Equals(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 190 | break; 191 | case BinaryOperatorKind.NotEqual: 192 | if (leftAttributeType == EdmPrimitiveTypeKind.DateTimeOffset) 193 | items = DateNotEquals(items, leftAttributeName, rightAttributeName, leftAttributeValue, rightAttributeValue, leftAttributeType, left); 194 | else 195 | items = items.Where(x => !(GetPropertyValue(x, leftAttributeName, leftAttributeValue).Equals(GetPropertyValue(x, rightAttributeName, rightAttributeValue)))); 196 | break; 197 | case BinaryOperatorKind.GreaterThan: 198 | items = GreaterThan(items, leftAttributeName, rightAttributeName, leftAttributeValue, rightAttributeValue, leftAttributeType); 199 | break; 200 | case BinaryOperatorKind.GreaterThanOrEqual: 201 | items = GreaterThanOrEqual(items, leftAttributeName, rightAttributeName, leftAttributeValue, rightAttributeValue, leftAttributeType); 202 | break; 203 | case BinaryOperatorKind.LessThan: 204 | items = LesserThan(items, leftAttributeName, rightAttributeName, leftAttributeValue, rightAttributeValue, leftAttributeType); 205 | break; 206 | case BinaryOperatorKind.LessThanOrEqual: 207 | items = LesserThanOrEqual(items, leftAttributeName, rightAttributeName, leftAttributeValue, rightAttributeValue, leftAttributeType); 208 | break; 209 | } 210 | 211 | } 212 | 213 | 214 | } 215 | 216 | return items; 217 | } 218 | 219 | private IEnumerable GreaterThan(IEnumerable items, string leftAttributeName, string rightAttributeName, object leftAttributeValue, object rightAttributeValue, EdmPrimitiveTypeKind leftAttributeType) 220 | { 221 | switch (leftAttributeType) 222 | { 223 | case EdmPrimitiveTypeKind.Int32: 224 | items = items.Where(x => Convert.ToInt32(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) > Convert.ToInt32(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 225 | break; 226 | case EdmPrimitiveTypeKind.Int64: 227 | items = items.Where(x => Convert.ToInt64(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) > Convert.ToInt64(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 228 | break; 229 | case EdmPrimitiveTypeKind.Double: 230 | items = items.Where(x => Convert.ToDouble(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) > Convert.ToDouble(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 231 | break; 232 | case EdmPrimitiveTypeKind.Decimal: 233 | items = items.Where(x => Convert.ToDecimal(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) > Convert.ToDecimal(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 234 | break; 235 | case EdmPrimitiveTypeKind.DateTimeOffset: 236 | items = items.Where((x) => 237 | { 238 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue).ToString()); 239 | var rightAttribute = Convert.ToDateTime(GetPropertyValue(x, rightAttributeName, rightAttributeValue).ToString()); 240 | return leftAttribute > rightAttribute; 241 | }); 242 | break; 243 | } 244 | 245 | return items; 246 | } 247 | 248 | private IEnumerable GreaterThanOrEqual(IEnumerable items, string leftAttributeName, string rightAttributeName, object leftAttributeValue, object rightAttributeValue, EdmPrimitiveTypeKind leftAttributeType) 249 | { 250 | switch (leftAttributeType) 251 | { 252 | case EdmPrimitiveTypeKind.Int32: 253 | items = items.Where(x => Convert.ToInt32(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) >= Convert.ToInt32(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 254 | break; 255 | case EdmPrimitiveTypeKind.Int64: 256 | items = items.Where(x => Convert.ToInt64(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) >= Convert.ToInt64(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 257 | break; 258 | case EdmPrimitiveTypeKind.Double: 259 | items = items.Where(x => Convert.ToDouble(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) >= Convert.ToDouble(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 260 | break; 261 | case EdmPrimitiveTypeKind.Decimal: 262 | items = items.Where(x => Convert.ToDecimal(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) >= Convert.ToDecimal(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 263 | break; 264 | case EdmPrimitiveTypeKind.DateTimeOffset: 265 | items = items.Where((x) => 266 | { 267 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue).ToString()); 268 | var rightAttribute = Convert.ToDateTime(GetPropertyValue(x, rightAttributeName, rightAttributeValue).ToString()); 269 | return leftAttribute >= rightAttribute; 270 | }); 271 | break; 272 | } 273 | 274 | return items; 275 | } 276 | 277 | 278 | 279 | private IEnumerable LesserThan(IEnumerable items, string leftAttributeName, string rightAttributeName, object leftAttributeValue, object rightAttributeValue, EdmPrimitiveTypeKind leftAttributeType) 280 | { 281 | switch (leftAttributeType) 282 | { 283 | case EdmPrimitiveTypeKind.Int32: 284 | items = items.Where(x => Convert.ToInt32(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) < Convert.ToInt32(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 285 | break; 286 | case EdmPrimitiveTypeKind.Int64: 287 | items = items.Where(x => Convert.ToInt64(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) < Convert.ToInt64(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 288 | break; 289 | case EdmPrimitiveTypeKind.Double: 290 | items = items.Where(x => Convert.ToDouble(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) < Convert.ToDouble(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 291 | break; 292 | case EdmPrimitiveTypeKind.Decimal: 293 | items = items.Where(x => Convert.ToDecimal(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) < Convert.ToDecimal(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 294 | break; 295 | case EdmPrimitiveTypeKind.DateTimeOffset: 296 | items = items.Where((x) => 297 | { 298 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue).ToString()); 299 | var rightAttribute = Convert.ToDateTime(GetPropertyValue(x, rightAttributeName, rightAttributeValue).ToString()); 300 | return leftAttribute < rightAttribute; 301 | } 302 | ); 303 | break; 304 | } 305 | 306 | return items; 307 | } 308 | private IEnumerable LesserThanOrEqual(IEnumerable items, string leftAttributeName, string rightAttributeName, object leftAttributeValue, object rightAttributeValue, EdmPrimitiveTypeKind leftAttributeType) 309 | { 310 | switch (leftAttributeType) 311 | { 312 | case EdmPrimitiveTypeKind.Int32: 313 | items = items.Where(x => Convert.ToInt32(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) <= Convert.ToInt32(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 314 | break; 315 | case EdmPrimitiveTypeKind.Int64: 316 | items = items.Where(x => Convert.ToInt64(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) <= Convert.ToInt64(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 317 | break; 318 | case EdmPrimitiveTypeKind.Double: 319 | items = items.Where(x => Convert.ToDouble(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) <= Convert.ToDouble(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 320 | break; 321 | case EdmPrimitiveTypeKind.Decimal: 322 | items = items.Where(x => Convert.ToDecimal(GetPropertyValue(x, leftAttributeName, leftAttributeValue)) <= Convert.ToDecimal(GetPropertyValue(x, rightAttributeName, rightAttributeValue))); 323 | break; 324 | case EdmPrimitiveTypeKind.DateTimeOffset: 325 | items = items.Where((x) => 326 | { 327 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue).ToString()); 328 | var rightAttribute = Convert.ToDateTime(GetPropertyValue(x, rightAttributeName, rightAttributeValue).ToString()); 329 | return leftAttribute <= rightAttribute; 330 | } 331 | ); 332 | break; 333 | 334 | } 335 | 336 | return items; 337 | } 338 | 339 | private IEnumerable DateEquals(IEnumerable items, string leftAttributeName, string rightAttributeName, object leftAttributeValue, object rightAttributeValue, EdmPrimitiveTypeKind leftAttributeType, SingleValueNode left) 340 | { 341 | var typename = ""; 342 | if (left.GetType().Name == "SingleValueFunctionCallNode") 343 | { 344 | typename = ((SingleValueFunctionCallNode)left).Name.ToString(); 345 | } 346 | else 347 | { 348 | typename = "default"; 349 | } 350 | switch (leftAttributeType) 351 | { 352 | case EdmPrimitiveTypeKind.DateTimeOffset: 353 | if (typename == "year") 354 | { 355 | items = items.Where((x) => 356 | { 357 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue)); 358 | var rightAttribute = GetPropertyValue(x, rightAttributeName, rightAttributeValue); 359 | return string.Equals(leftAttribute.Year.ToString(), rightAttribute.ToString()); 360 | }); 361 | } 362 | else if (typename == "month") 363 | { 364 | items = items.Where((x) => 365 | { 366 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue)); 367 | var rightAttribute = GetPropertyValue(x, rightAttributeName, rightAttributeValue); 368 | return string.Equals(leftAttribute.Month.ToString(), rightAttribute.ToString()); 369 | }); 370 | } 371 | else if (typename == "day") 372 | { 373 | items = items.Where((x) => 374 | { 375 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue)); 376 | var rightAttribute = GetPropertyValue(x, rightAttributeName, rightAttributeValue); 377 | return string.Equals(leftAttribute.Day.ToString(), rightAttribute.ToString()); 378 | }); 379 | } 380 | else if (typename == "default") 381 | { 382 | items = items.Where((x) => 383 | { 384 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue).ToString(), new CultureInfo("en-US")); 385 | var rightAttribute = Convert.ToDateTime(GetPropertyValue(x, rightAttributeName, rightAttributeValue).ToString(), new CultureInfo("en-US")); 386 | return string.Equals(leftAttribute.AddMilliseconds(-leftAttribute.Millisecond), rightAttribute.AddMilliseconds(-rightAttribute.Millisecond)); 387 | }); 388 | } 389 | break; 390 | 391 | } 392 | 393 | return items; 394 | } 395 | 396 | private IEnumerable DateNotEquals(IEnumerable items, string leftAttributeName, string rightAttributeName, object leftAttributeValue, object rightAttributeValue, EdmPrimitiveTypeKind leftAttributeType, SingleValueNode left) 397 | { 398 | var typename = ""; 399 | if (left.GetType().Name == "SingleValueFunctionCallNode") 400 | { 401 | typename = ((SingleValueFunctionCallNode)left).Name.ToString(); 402 | } 403 | else 404 | { 405 | typename = "default"; 406 | } 407 | switch (leftAttributeType) 408 | { 409 | case EdmPrimitiveTypeKind.DateTimeOffset: 410 | if (typename == "year") 411 | { 412 | items = items.Where((x) => 413 | { 414 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue)); 415 | var rightAttribute = GetPropertyValue(x, rightAttributeName, rightAttributeValue); 416 | return !string.Equals(leftAttribute.Year.ToString(), rightAttribute.ToString()); 417 | }); 418 | } 419 | else if (typename == "month") 420 | { 421 | items = items.Where((x) => 422 | { 423 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue)); 424 | var rightAttribute = GetPropertyValue(x, rightAttributeName, rightAttributeValue); 425 | return !string.Equals(leftAttribute.Month.ToString(), rightAttribute.ToString()); 426 | }); 427 | } 428 | else if (typename == "day") 429 | { 430 | items = items.Where((x) => 431 | { 432 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue)); 433 | var rightAttribute = GetPropertyValue(x, rightAttributeName, rightAttributeValue); 434 | return !string.Equals(leftAttribute.Day.ToString(), rightAttribute.ToString()); 435 | }); 436 | } 437 | else if (typename == "default") 438 | { 439 | items = items.Where((x) => 440 | { 441 | var leftAttribute = Convert.ToDateTime(GetPropertyValue(x, leftAttributeName, leftAttributeValue).ToString(), new CultureInfo("en-US")); 442 | var rightAttribute = Convert.ToDateTime(GetPropertyValue(x, rightAttributeName, rightAttributeValue).ToString(), new CultureInfo("en-US")); 443 | return !string.Equals(leftAttribute.AddMilliseconds(-leftAttribute.Millisecond), rightAttribute.AddMilliseconds(-rightAttribute.Millisecond)); 444 | 445 | }); 446 | } 447 | break; 448 | 449 | } 450 | 451 | return items; 452 | } 453 | 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /src/Dynamic.OData/PredicateParsers/ODataOrderByPredicateParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Dynamic.OData.Exceptions; 5 | using Dynamic.OData.PredicateParsers.Interface; 6 | using Microsoft.AspNet.OData; 7 | using Microsoft.OData.Edm; 8 | using Microsoft.OData.UriParser; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | 13 | namespace Dynamic.OData.PredicateParsers 14 | { 15 | public class ODataOrderByPredicateParser : BaseODataPredicateParser, IODataPredicateParser 16 | { 17 | private const string OrderByParser = "OrderBy"; 18 | public ParseContext Parse(ODataUriParser parser, ParseContext parseContext) 19 | { 20 | var sourceEdmSetting = parseContext.EdmEntityTypeSettings.FirstOrDefault(); 21 | 22 | // Get primary collection type and setup collection 23 | var collectionEntityTypeKey = parseContext.LatestStateDictionary 24 | .Keys.FirstOrDefault(p => p.Contains("collectionentitytype")); 25 | var entityRef = (EdmEntityTypeReference)parseContext.LatestStateDictionary[collectionEntityTypeKey]; 26 | var collectionRef = new EdmCollectionTypeReference(new EdmCollectionType(entityRef)); 27 | var collection = new EdmEntityObjectCollection(collectionRef); 28 | 29 | // Get orderby clause 30 | var orderByClause = parser.ParseOrderBy(); 31 | 32 | bool isFirstIter = true; 33 | IEnumerable resultStart = parseContext.Result; 34 | IEnumerable> queryableStart = parseContext.QueryableSourceEntities; 35 | IOrderedEnumerable result = null; 36 | IOrderedEnumerable> queryable = null; 37 | while (orderByClause != null) 38 | { 39 | // Get attribute name 40 | string attributeName = ""; 41 | var kind = orderByClause.Expression.Kind; 42 | if (kind == QueryNodeKind.SingleValueOpenPropertyAccess) 43 | attributeName = ((SingleValueOpenPropertyAccessNode)orderByClause.Expression).Name; 44 | else if (kind == QueryNodeKind.SingleValuePropertyAccess) 45 | attributeName = ((SingleValuePropertyAccessNode)orderByClause.Expression).Property.Name; 46 | else 47 | throw new FeatureNotSupportedException(OrderByParser, $"QueryNodeKind: {kind} Not Supported"); 48 | // Check if attribute name in model 49 | var typeSetting = sourceEdmSetting.Properties.FirstOrDefault(predicate => predicate.PropertyName == attributeName); 50 | // Get direction 51 | var direction = orderByClause.Direction.ToString(); 52 | // Perform ordering 53 | Func resultExpression = item => GetPropertyValue(item, attributeName); 54 | Func, object> queryableExpression = item => item[attributeName]; 55 | if (string.Compare(direction, "Ascending") == 0) 56 | { 57 | result = (isFirstIter) ? resultStart.OrderBy(resultExpression) : 58 | result.ThenBy(resultExpression); 59 | queryable = (isFirstIter) ? queryableStart.OrderBy(queryableExpression) : 60 | queryable.ThenBy(queryableExpression); 61 | } 62 | else 63 | { 64 | result = (isFirstIter) ? resultStart.OrderByDescending(resultExpression) : 65 | result.ThenByDescending(resultExpression); 66 | queryable = (isFirstIter) ? queryableStart.OrderByDescending(queryableExpression) : 67 | queryable.ThenByDescending(queryableExpression); 68 | } 69 | isFirstIter = false; 70 | // Go to next ordering clause 71 | orderByClause = orderByClause.ThenBy; 72 | } 73 | // Create collection 74 | foreach (var entity in result) 75 | collection.Add(entity); 76 | 77 | var targetParseContext = new ParseContext 78 | { 79 | Result = collection, 80 | QueryableSourceEntities = queryable, 81 | Model = parseContext.Model, 82 | EdmEntityTypeSettings = parseContext.EdmEntityTypeSettings, 83 | LatestStateDictionary = parseContext.LatestStateDictionary 84 | }; 85 | return targetParseContext; 86 | } 87 | 88 | private object GetPropertyValue(IEdmEntityObject p, string attributeName) 89 | { 90 | p.TryGetPropertyValue(attributeName, out object value); 91 | return value; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Dynamic.OData/PredicateParsers/ODataSelectPredicateParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Dynamic.OData.Exceptions; 5 | using Dynamic.OData.Models; 6 | using Dynamic.OData.PredicateParsers.Interface; 7 | using Microsoft.AspNet.OData; 8 | using Microsoft.AspNet.OData.Extensions; 9 | using Microsoft.OData.Edm; 10 | using Microsoft.OData.UriParser; 11 | using System.Collections.Generic; 12 | using System.Data; 13 | using System.Linq; 14 | 15 | namespace Dynamic.OData.PredicateParsers 16 | { 17 | /// 18 | /// Select clause parser and data generator. If this clause is present in query, it's always 2nd 19 | /// The error behavior is boolean. Either we parse the query completely, or not at all. There is no fallback and this is intentional. 20 | /// 21 | public class ODataSelectPredicateParser : BaseODataPredicateParser, IODataPredicateParser 22 | { 23 | private const string SelectParser = "Select"; 24 | private const int StepIndex = 10; 25 | public ParseContext Parse(ODataUriParser parser, ParseContext sourceParseContext) 26 | { 27 | //Select implementation 28 | var targetParseContext = new ParseContext(); 29 | var targetQueryableSourceEntities = new List>(); 30 | var sourceEdmSetting = sourceParseContext.EdmEntityTypeSettings.FirstOrDefault(); 31 | var targetEdmSetting = new EdmEntityTypeSettings() 32 | { 33 | RouteName = SelectParser, 34 | Personas = sourceEdmSetting.Personas, 35 | Properties = new List() 36 | }; 37 | var selectExpandClause = parser.ParseSelectAndExpand(); 38 | var edmEntityType = new EdmEntityType(EdmNamespaceName, SelectParser); 39 | var latestStateDictionary = new Dictionary(); 40 | 41 | //Construct the types. For now we support non-nested primitive types only. Everything else is an exception for now. 42 | var propertyList = new List(); 43 | foreach (var item in selectExpandClause.SelectedItems) 44 | { 45 | switch (item) 46 | { 47 | case PathSelectItem pathSelectItem: 48 | IEnumerable segments = pathSelectItem.SelectedPath; 49 | var firstPropertySegment = segments.FirstOrDefault(); 50 | if (firstPropertySegment != null) 51 | { 52 | var typeSetting = sourceEdmSetting.Properties.FirstOrDefault(predicate => predicate.PropertyName == firstPropertySegment.Identifier); 53 | 54 | propertyList.Add(firstPropertySegment.Identifier); 55 | 56 | if (typeSetting.GetEdmPrimitiveTypeKind() != EdmPrimitiveTypeKind.None) 57 | { 58 | var edmPrimitiveType = typeSetting.GetEdmPrimitiveTypeKind(); 59 | if (typeSetting.IsNullable.HasValue) 60 | edmEntityType.AddStructuralProperty(firstPropertySegment.Identifier, edmPrimitiveType, typeSetting.IsNullable.Value); 61 | else 62 | edmEntityType.AddStructuralProperty(firstPropertySegment.Identifier, edmPrimitiveType); 63 | targetEdmSetting.Properties.Add(new EdmEntityTypePropertySetting 64 | { 65 | PropertyName = typeSetting.PropertyName, 66 | PropertyType = typeSetting.PropertyType, 67 | IsNullable = typeSetting.IsNullable 68 | }); 69 | } 70 | else 71 | { 72 | //We are doing $select on a property which is of type list. Which means 73 | if (typeSetting.PropertyType == "List") 74 | { 75 | var edmComplexType = GetEdmComplexTypeReference(sourceParseContext); 76 | edmEntityType.AddStructuralProperty(typeSetting.PropertyName, new EdmCollectionTypeReference(new EdmCollectionType(new EdmComplexTypeReference(edmComplexType, true)))); 77 | targetEdmSetting.Properties.Add(new EdmEntityTypePropertySetting 78 | { 79 | PropertyName = typeSetting.PropertyName, 80 | PropertyType = typeSetting.PropertyType, 81 | IsNullable = typeSetting.IsNullable 82 | }); 83 | } 84 | else 85 | { 86 | throw new FeatureNotSupportedException(SelectParser, $"Invalid Custom Selection-{typeSetting.PropertyName}-{typeSetting.PropertyType}"); 87 | } 88 | } 89 | } 90 | else 91 | { 92 | throw new FeatureNotSupportedException(SelectParser, "Empty Path Segments"); 93 | } 94 | break; 95 | case WildcardSelectItem wildcardSelectItem: throw new FeatureNotSupportedException(SelectParser, "WildcardSelect"); 96 | case ExpandedNavigationSelectItem expandedNavigationSelectItem: throw new FeatureNotSupportedException(SelectParser, "ExpandedNavigation"); 97 | case ExpandedReferenceSelectItem expandedReferenceSelectItem: throw new FeatureNotSupportedException(SelectParser, "ExpandedReference"); 98 | case NamespaceQualifiedWildcardSelectItem namespaceQualifiedWildcardSelectItem: throw new FeatureNotSupportedException(SelectParser, "NamespaceQualifiedWildcard"); 99 | } 100 | } 101 | 102 | //Register these dynamic types to model 103 | sourceParseContext.Model.AddElement(edmEntityType); 104 | ((EdmEntityContainer)sourceParseContext.Model.EntityContainer).AddEntitySet("Select", edmEntityType); 105 | 106 | 107 | //Construct the data 108 | var entityReferenceType = new EdmEntityTypeReference(edmEntityType, true); 109 | var collectionRef = new EdmCollectionTypeReference(new EdmCollectionType(entityReferenceType)); 110 | var collection = new EdmEntityObjectCollection(collectionRef); 111 | var filteredQueryableEntityList = sourceParseContext.QueryableSourceEntities.Select(p => p.Where(p => propertyList.Contains(p.Key))); 112 | latestStateDictionary.Add(RequestFilterConstants.GetEntityTypeKeyName(SelectParser, StepIndex), entityReferenceType); 113 | 114 | foreach (var entity in filteredQueryableEntityList) 115 | { 116 | var entityDictionary = new Dictionary(); 117 | var obj = new EdmEntityObject(edmEntityType); 118 | foreach (var propertyKey in propertyList) 119 | { 120 | var setting = targetEdmSetting.Properties.FirstOrDefault(predicate => predicate.PropertyName.Equals(propertyKey)); 121 | var data = entity.FirstOrDefault(property => property.Key.Equals(propertyKey)); 122 | 123 | //This condition is when the type of selected property is a primitive type 124 | if (setting.GetEdmPrimitiveTypeKind() != EdmPrimitiveTypeKind.None) 125 | { 126 | var propertyValue = !data.Equals(default(KeyValuePair)) ? data.Value : null; 127 | obj.TrySetPropertyValue(propertyKey, propertyValue); 128 | entityDictionary.Add(propertyKey, propertyValue); 129 | } 130 | else 131 | { 132 | switch (setting.PropertyType) 133 | { 134 | case "List": 135 | //TODO: There is scope for perf improvement 136 | //We can re-use the previous constructed list instead of constructing one from scratch. 137 | var subList = (List>)data.Value; 138 | var subListContext = GetList(subList, GetEdmComplexTypeReference(sourceParseContext)); 139 | obj.TrySetPropertyValue(propertyKey, subListContext.Result); 140 | entityDictionary.Add(propertyKey, subListContext.QueryAbleResult); 141 | break; 142 | } 143 | } 144 | 145 | } 146 | collection.Add(obj); 147 | targetQueryableSourceEntities.Add(entityDictionary); 148 | } 149 | 150 | targetParseContext.Result = collection; 151 | targetParseContext.QueryableSourceEntities = targetQueryableSourceEntities; 152 | targetParseContext.Model = sourceParseContext.Model; 153 | targetParseContext.EdmEntityTypeSettings = new List { targetEdmSetting }; 154 | targetParseContext.LatestStateDictionary = latestStateDictionary; 155 | return targetParseContext; 156 | } 157 | 158 | private EdmComplexType GetEdmComplexTypeReference(ParseContext parseContext) 159 | { 160 | var complexTypeKey = parseContext.LatestStateDictionary.Keys.FirstOrDefault(predicate => predicate.Contains("complexentitytype")); 161 | var edmComplexTypeRef = (EdmComplexType)parseContext.LatestStateDictionary[complexTypeKey]; 162 | return edmComplexTypeRef; 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Dynamic.OData/PredicateParsers/ODataSkipPredicateParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Dynamic.OData.Exceptions; 5 | using Dynamic.OData.PredicateParsers.Interface; 6 | using Microsoft.AspNet.OData; 7 | using Microsoft.OData.Edm; 8 | using Microsoft.OData.UriParser; 9 | using System.Linq; 10 | 11 | namespace Dynamic.OData.PredicateParsers 12 | { 13 | public class ODataSkipPredicateParser : BaseODataPredicateParser, IODataPredicateParser 14 | { 15 | private const string SkipParser = "Skip"; 16 | public ParseContext Parse(ODataUriParser parser, ParseContext parseContext) 17 | { 18 | var skip = parser.ParseSkip(); 19 | if (skip.Value > 0) 20 | { 21 | bool isParsed = int.TryParse(skip.Value.ToString(), out int count); 22 | 23 | var collectionEntityTypeKey = parseContext.LatestStateDictionary 24 | .Keys.FirstOrDefault(p => p.Contains("collectionentitytype")); 25 | 26 | var entityRef = (EdmEntityTypeReference)parseContext.LatestStateDictionary[collectionEntityTypeKey]; 27 | var collectionRef = new EdmCollectionTypeReference(new EdmCollectionType(entityRef)); 28 | var collection = new EdmEntityObjectCollection(collectionRef); 29 | 30 | var filteredResults = parseContext.Result.Skip(count); 31 | 32 | foreach (var entity in filteredResults) 33 | collection.Add(entity); 34 | 35 | var targetParseContext = new ParseContext 36 | { 37 | Result = collection, 38 | QueryableSourceEntities = parseContext.QueryableSourceEntities.Skip(count), 39 | Model = parseContext.Model, 40 | EdmEntityTypeSettings = parseContext.EdmEntityTypeSettings, 41 | LatestStateDictionary = parseContext.LatestStateDictionary 42 | }; 43 | return targetParseContext; 44 | 45 | } 46 | else 47 | { 48 | throw new InvalidPropertyException("Skip", string.Empty); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Dynamic.OData/PredicateParsers/ODataTopPredicateParser.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Dynamic.OData.Exceptions; 5 | using Dynamic.OData.PredicateParsers.Interface; 6 | using Microsoft.AspNet.OData; 7 | using Microsoft.OData.Edm; 8 | using Microsoft.OData.UriParser; 9 | using System.Linq; 10 | 11 | namespace Dynamic.OData.PredicateParsers 12 | { 13 | public class ODataTopPredicateParser : BaseODataPredicateParser, IODataPredicateParser 14 | { 15 | private const string TopParser = "Top"; 16 | private const int StepIndex = 8; 17 | public ParseContext Parse(ODataUriParser parser, ParseContext parseContext) 18 | { 19 | var top = parser.ParseTop(); 20 | if (top.Value > 0) 21 | { 22 | bool isParsed = int.TryParse(top.Value.ToString(), out int count); 23 | 24 | var collectionEntityTypeKey = parseContext.LatestStateDictionary 25 | .Keys.FirstOrDefault(p => p.Contains("collectionentitytype")); 26 | 27 | var entityRef = (EdmEntityTypeReference)parseContext.LatestStateDictionary[collectionEntityTypeKey]; 28 | var collectionRef = new EdmCollectionTypeReference(new EdmCollectionType(entityRef)); 29 | var collection = new EdmEntityObjectCollection(collectionRef); 30 | 31 | var filteredResults = parseContext.Result.Take(count); 32 | 33 | foreach (var entity in filteredResults) 34 | collection.Add(entity); 35 | 36 | var targetParseContext = new ParseContext 37 | { 38 | Result = collection, 39 | QueryableSourceEntities = parseContext.QueryableSourceEntities.Take(count), 40 | Model = parseContext.Model, 41 | EdmEntityTypeSettings = parseContext.EdmEntityTypeSettings, 42 | LatestStateDictionary = parseContext.LatestStateDictionary 43 | }; 44 | return targetParseContext; 45 | 46 | } 47 | else 48 | { 49 | throw new InvalidPropertyException("Top", string.Empty); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Dynamic.OData/RequestFilterConstants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | namespace Dynamic.OData 5 | { 6 | public class RequestFilterConstants 7 | { 8 | public static string GetEntityTypeKeyName(string step, int stepIndex) 9 | { 10 | return $"{step}_collectionentitytype_{stepIndex}".ToLowerInvariant(); 11 | } 12 | 13 | public static string GetComplexTypeKeyName(string step, int stepIndex) 14 | { 15 | return $"{step}_complexentitytype_{stepIndex}".ToLowerInvariant(); 16 | } 17 | 18 | public const string ContextSuffixKey = "contextsuffixkey"; 19 | 20 | public const string ODataResponseMiddleware = "ODataResponseMiddleware"; 21 | public const string ODataContextParsingMiddleware = "ODataContextParsingMiddleware"; 22 | public const string ODataServiceRoot = "https://services.odata.org/V4/OData/OData.svc"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/UnitTest/Dynamic.OData.Tests/Data/EntityTypeSettings.json: -------------------------------------------------------------------------------- 1 | //Copyright (c) Microsoft Corporation. All rights reserved.// Licensed under the MIT License. See License.txt in the project root for license information. 2 | { 3 | "RouteName": "user", 4 | "Properties": [ 5 | { 6 | "PropertyName": "id", 7 | "PropertyType": "Guid", 8 | "IsNullable": false 9 | }, 10 | { 11 | "PropertyName": "Title", 12 | "PropertyType": "String" 13 | }, 14 | { 15 | "PropertyName": "FirstName", 16 | "PropertyType": "String" 17 | }, 18 | { 19 | "PropertyName": "LastName", 20 | "PropertyType": "String" 21 | }, 22 | { 23 | "PropertyName": "Age", 24 | "PropertyType": "Int32" 25 | }, 26 | { 27 | "PropertyName": "Salary", 28 | "PropertyType": "Decimal" 29 | }, 30 | { 31 | "PropertyName": "BornOn", 32 | "PropertyType": "DateTime" 33 | }, 34 | { 35 | "PropertyName": "Department", 36 | "PropertyType": "String" 37 | }, 38 | { 39 | "PropertyName": "EmailAddress", 40 | "PropertyType": "String" 41 | }, 42 | { 43 | "PropertyName": "EmployeeNumber", 44 | "PropertyType": "Int16" 45 | }, 46 | { 47 | "PropertyName": "UniversalId", 48 | "PropertyType": "Int64" 49 | }, 50 | { 51 | "PropertyName": "VacationDaysInHours", 52 | "PropertyType": "Double" 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /test/UnitTest/Dynamic.OData.Tests/Data/sampledata.json: -------------------------------------------------------------------------------- 1 | //Copyright (c) Microsoft Corporation. All rights reserved.// Licensed under the MIT License. See License.txt in the project root for license information. 2 | { 3 | "@odata.context": "https://localhost:44312/odata/entities/user/$metadata#Collection(Microsoft.Contoso.Models.user)", 4 | "value": [ 5 | { 6 | "@odata.type": "#Microsoft.Contoso.Models.user", 7 | "id": "03bd9cf8-2f56-4d63-ad77-a5f5e23ddced", 8 | "Title": "Project Manager", 9 | "FirstName": "Aditya", 10 | "LastName": "Sharma", 11 | "Age": 28, 12 | "Salary": 150000, 13 | "BornOn": "1993-02-12T14:43:28.8860376+05:30", 14 | "Department": "Office", 15 | "EmailAddress": "adisharma@microsoft.com" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /test/UnitTest/Dynamic.OData.Tests/Dynamic.OData.Tests.csproj: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | netcoreapp3.1 8 | 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Always 19 | true 20 | PreserveNewest 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | all 33 | runtime; build; native; contentfiles; analyzers; buildtransitive 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Always 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /test/UnitTest/Dynamic.OData.Tests/Tests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. All rights reserved. 2 | // Licensed under the MIT License. See License.txt in the project root for license information. 3 | 4 | using Xunit; 5 | 6 | namespace Dynamic.OData.Tests 7 | { 8 | public class ParserTests 9 | { 10 | 11 | [Fact] 12 | public void TestSelect() 13 | { 14 | 15 | Assert.True(1 == 1); 16 | } 17 | 18 | } 19 | 20 | 21 | } 22 | --------------------------------------------------------------------------------