diff --git a/.gitignore b/.gitignore
index 1e7e35a..74e917a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,90 +1,369 @@
-# Created by https://www.toptal.com/developers/gitignore/api/dotnetcore,vscode,macos,linux,windows
-# Edit at https://www.toptal.com/developers/gitignore?templates=dotnetcore,vscode,macos,linux,windows
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
-### DotnetCore ###
-# .NET Core build folders
-bin/
-obj/
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
-# Common node modules locations
-/node_modules
-/wwwroot/node_modules
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
-### Linux ###
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# MacOS file systems
+**/.DS_STORE
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JetBrains Rider
+.idea/
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*[.json, .xml, .info]
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
-# temporary files which can be created if a process still has a handle open of a deleted file
-.fuse_hidden*
-
-# KDE directory preferences
-.directory
-
-# Linux trash folder which might appear on any partition or disk
-.Trash-*
-
-# .nfs files are created when an open file is removed but is still being accessed
-.nfs*
-
-### macOS ###
-# General
-.DS_Store
-.AppleDouble
-.LSOverride
-
-# Icon must end with two \r
-Icon
-
-# Thumbnails
-._*
-
-# Files that might appear in the root of a volume
-.DocumentRevisions-V100
-.fseventsd
-.Spotlight-V100
-.TemporaryItems
-.Trashes
-.VolumeIcon.icns
-.com.apple.timemachine.donotpresent
-
-# Directories potentially created on remote AFP share
-.AppleDB
-.AppleDesktop
-Network Trash Folder
-Temporary Items
-.apdisk
-
-### vscode ###
-.vscode/*
-!.vscode/settings.json
-!.vscode/tasks.json
-!.vscode/launch.json
-!.vscode/extensions.json
-*.code-workspace
-
-### Windows ###
-# Windows thumbnail cache files
-Thumbs.db
-Thumbs.db:encryptable
-ehthumbs.db
-ehthumbs_vista.db
-
-# Dump file
-*.stackdump
-
-# Folder config file
-[Dd]esktop.ini
-
-# Recycle Bin used on file shares
-$RECYCLE.BIN/
-
-# Windows Installer files
-*.cab
-*.msi
-*.msix
-*.msm
-*.msp
-
-# Windows shortcuts
-*.lnk
-
-# End of https://www.toptal.com/developers/gitignore/api/dotnetcore,vscode,macos,linux,windows
+# Sqlite example databases
+*.db
diff --git a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/contentModel.xml b/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/contentModel.xml
deleted file mode 100644
index 701e3d0..0000000
--- a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/contentModel.xml
+++ /dev/null
@@ -1,109 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/encodings.xml b/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/encodings.xml
deleted file mode 100644
index df87cf9..0000000
--- a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/encodings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/indexLayout.xml b/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/indexLayout.xml
deleted file mode 100644
index 27ba142..0000000
--- a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/indexLayout.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/modules.xml b/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/modules.xml
deleted file mode 100644
index f01f435..0000000
--- a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/projectSettingsUpdater.xml b/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/projectSettingsUpdater.xml
deleted file mode 100644
index 4bb9f4d..0000000
--- a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/projectSettingsUpdater.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/vcs.xml b/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/vcs.xml
deleted file mode 100644
index 94a25f7..0000000
--- a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/workspace.xml b/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/workspace.xml
deleted file mode 100644
index 2afb3ca..0000000
--- a/.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/workspace.xml
+++ /dev/null
@@ -1,314 +0,0 @@
-
-
-
- src/Example/Example.csproj
- test/JsonApiDotNetCore.MongoDb.IntegrationTests/JsonApiDotNetCore.MongoDb.IntegrationTests.csproj
- test/JsonApiDotNetCore.MongoDb.Example.Tests/JsonApiDotNetCore.MongoDb.Example.Tests.csproj
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1604674714591
-
-
- 1604674714591
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/.idea.JsonApiDotNetCore.MongoDb/riderModule.iml b/.idea/.idea.JsonApiDotNetCore.MongoDb/riderModule.iml
deleted file mode 100644
index d233658..0000000
--- a/.idea/.idea.JsonApiDotNetCore.MongoDb/riderModule.iml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 1c08e62..6ae2058 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,11 +2,9 @@ language: csharp
mono: none
dotnet: 3.1.100
solution: JsonApiDotNetCore.MongoDb.sln
-services:
- - mongodb
script:
- dotnet restore
- - dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage/lcov-coverage
+ - dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=./coverage/lcov-coverage /p:Include="[JsonApiDotNetCore.MongoDb]*"
after_success:
- bash <(curl -s https://codecov.io/bash)
before_deploy:
diff --git a/Directory.Build.props b/Directory.Build.props
index b427d93..7f73136 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -2,8 +2,8 @@
netcoreapp3.1
3.1.*
- 4.0.0-beta1
- 2.11.2
+ 4.0.*
+ 2.11.*
diff --git a/JsonApiDotNetCore.MongoDb.sln b/JsonApiDotNetCore.MongoDb.sln
index e63cbca..5994f4a 100644
--- a/JsonApiDotNetCore.MongoDb.sln
+++ b/JsonApiDotNetCore.MongoDb.sln
@@ -5,16 +5,16 @@ VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "src\Example\Example.csproj", "{600A3E66-E63F-427D-A991-4CD2067041F9}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb.GettingStarted", "src\JsonApiDotNetCore.MongoDb.GettingStarted\JsonApiDotNetCore.MongoDb.GettingStarted.csproj", "{600A3E66-E63F-427D-A991-4CD2067041F9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb", "src\JsonApiDotNetCore.MongoDb\JsonApiDotNetCore.MongoDb.csproj", "{FD312677-2A62-4B8F-A965-879B059F1755}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{19A533AA-E006-496D-A476-364DF2B637A1}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb.IntegrationTests", "test\JsonApiDotNetCore.MongoDb.IntegrationTests\JsonApiDotNetCore.MongoDb.IntegrationTests.csproj", "{5C59FDFE-F079-4015-9975-FEFA766F0787}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb.Example.Tests", "test\JsonApiDotNetCore.MongoDb.Example.Tests\JsonApiDotNetCore.MongoDb.Example.Tests.csproj", "{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.MongoDb.Example", "src\JsonApiDotNetCore.MongoDb.Example\JsonApiDotNetCore.MongoDb.Example.csproj", "{743C32A5-2584-4FA0-987B-B4E97CDAADE8}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -52,18 +52,6 @@ Global
{FD312677-2A62-4B8F-A965-879B059F1755}.Release|x64.Build.0 = Release|Any CPU
{FD312677-2A62-4B8F-A965-879B059F1755}.Release|x86.ActiveCfg = Release|Any CPU
{FD312677-2A62-4B8F-A965-879B059F1755}.Release|x86.Build.0 = Release|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Debug|x64.ActiveCfg = Debug|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Debug|x64.Build.0 = Debug|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Debug|x86.ActiveCfg = Debug|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Debug|x86.Build.0 = Debug|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Release|Any CPU.Build.0 = Release|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Release|x64.ActiveCfg = Release|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Release|x64.Build.0 = Release|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Release|x86.ActiveCfg = Release|Any CPU
- {5C59FDFE-F079-4015-9975-FEFA766F0787}.Release|x86.Build.0 = Release|Any CPU
{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -76,11 +64,23 @@ Global
{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x64.Build.0 = Release|Any CPU
{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x86.ActiveCfg = Release|Any CPU
{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1}.Release|x86.Build.0 = Release|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x64.Build.0 = Debug|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Debug|x86.Build.0 = Debug|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x64.ActiveCfg = Release|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x64.Build.0 = Release|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x86.ActiveCfg = Release|Any CPU
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{600A3E66-E63F-427D-A991-4CD2067041F9} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784}
{FD312677-2A62-4B8F-A965-879B059F1755} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784}
- {5C59FDFE-F079-4015-9975-FEFA766F0787} = {19A533AA-E006-496D-A476-364DF2B637A1}
{24CE53FA-9C49-4E20-A060-4A43DFB8C8F1} = {19A533AA-E006-496D-A476-364DF2B637A1}
+ {743C32A5-2584-4FA0-987B-B4E97CDAADE8} = {7E29AA10-F938-4CF8-9CAB-7ACD2D6DC784}
EndGlobalSection
EndGlobal
diff --git a/JsonApiDotNetCore.MongoDb.sln.DotSettings.user b/JsonApiDotNetCore.MongoDb.sln.DotSettings.user
deleted file mode 100644
index 0829a84..0000000
--- a/JsonApiDotNetCore.MongoDb.sln.DotSettings.user
+++ /dev/null
@@ -1,4 +0,0 @@
-
- <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
- <Solution />
-</SessionState>
\ No newline at end of file
diff --git a/README.md b/README.md
index bfdc70a..c7b5ac1 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,8 @@ public sealed class BooksController : JsonApiController
```cs
public class Startup
{
- public IServiceProvider ConfigureServices(IServiceCollection services) {
+ public IServiceProvider ConfigureServices(IServiceCollection services)
+ {
services.AddSingleton(sp =>
{
var client = new MongoClient(Configuration.GetSection("DatabaseSettings:ConnectionString").Value);
@@ -71,14 +72,33 @@ public class Startup
// ...
}
- public void Configure(IApplicationBuilder app) {
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseRouting();
app.UseJsonApi();
+ app.UseEndpoints(endpoints => endpoints.MapControllers());
// ...
}
}
```
+## Running tests and examples
+
+Integration tests use the [`Mongo2Go`](https://github.com/Mongo2Go/Mongo2Go) package so they don't require a running instance of MongoDb on your machine.
+
+Just run the following command to run all tests:
+
+```bash
+dotnet test
+```
+
+To run the examples you are indeed going to want to have a running instance of MongoDb on your device. Fastest way to get one running is using docker:
+
+```bash
+docker run -p 27017:27017 -d mongo:latest
+dotnet run
+```
+
## Limitations
- Relations are not supported (yet)
-- Projections are not supported (yet)
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/ArticlesController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/ArticlesController.cs
new file mode 100644
index 0000000..0aa0aa1
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/ArticlesController.cs
@@ -0,0 +1,18 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ public sealed class ArticlesController : JsonApiController
+ {
+ public ArticlesController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/AuthorsController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/AuthorsController.cs
new file mode 100644
index 0000000..73b9962
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/AuthorsController.cs
@@ -0,0 +1,18 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ public sealed class AuthorsController : JsonApiController
+ {
+ public AuthorsController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/BlogsController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/BlogsController.cs
new file mode 100644
index 0000000..6805d16
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/BlogsController.cs
@@ -0,0 +1,18 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ public sealed class BlogsController : JsonApiController
+ {
+ public BlogsController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/CountriesController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/CountriesController.cs
new file mode 100644
index 0000000..78ef4af
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/CountriesController.cs
@@ -0,0 +1,21 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Controllers.Annotations;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.QueryStrings;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ [DisableQueryString(StandardQueryStringParameters.Sort | StandardQueryStringParameters.Page)]
+ public sealed class CountriesController : JsonApiController
+ {
+ public CountriesController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PassportsController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PassportsController.cs
new file mode 100644
index 0000000..b61ee4b
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PassportsController.cs
@@ -0,0 +1,16 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ public sealed class PassportsController : JsonApiController
+ {
+ public PassportsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ {
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PeopleController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PeopleController.cs
new file mode 100644
index 0000000..8710d52
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PeopleController.cs
@@ -0,0 +1,18 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ public sealed class PeopleController : JsonApiController
+ {
+ public PeopleController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PersonRolesController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PersonRolesController.cs
new file mode 100644
index 0000000..9bf10f1
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/PersonRolesController.cs
@@ -0,0 +1,18 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ public sealed class PersonRolesController : JsonApiController
+ {
+ public PersonRolesController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/Restricted/ReadOnlyController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/Restricted/ReadOnlyController.cs
new file mode 100644
index 0000000..31c120f
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/Restricted/ReadOnlyController.cs
@@ -0,0 +1,106 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Controllers.Annotations;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers.Restricted
+{
+ [DisableRoutingConvention, Route("[controller]")]
+ [HttpReadOnly]
+ public class ReadOnlyController : BaseJsonApiController
+ {
+ public ReadOnlyController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+
+ [HttpGet]
+ public IActionResult Get() => Ok();
+
+ [HttpPost]
+ public IActionResult Post() => Ok();
+
+ [HttpPatch]
+ public IActionResult Patch() => Ok();
+
+ [HttpDelete]
+ public IActionResult Delete() => Ok();
+ }
+
+ [DisableRoutingConvention, Route("[controller]")]
+ [NoHttpPost]
+ public class NoHttpPostController : BaseJsonApiController
+ {
+ public NoHttpPostController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+
+ [HttpGet]
+ public IActionResult Get() => Ok();
+
+ [HttpPost]
+ public IActionResult Post() => Ok();
+
+ [HttpPatch]
+ public IActionResult Patch() => Ok();
+
+ [HttpDelete]
+ public IActionResult Delete() => Ok();
+ }
+
+ [DisableRoutingConvention, Route("[controller]")]
+ [NoHttpPatch]
+ public class NoHttpPatchController : BaseJsonApiController
+ {
+ public NoHttpPatchController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+
+ [HttpGet]
+ public IActionResult Get() => Ok();
+
+ [HttpPost]
+ public IActionResult Post() => Ok();
+
+ [HttpPatch]
+ public IActionResult Patch() => Ok();
+
+ [HttpDelete]
+ public IActionResult Delete() => Ok();
+ }
+
+ [DisableRoutingConvention, Route("[controller]")]
+ [NoHttpDelete]
+ public class NoHttpDeleteController : BaseJsonApiController
+ {
+ public NoHttpDeleteController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+
+ [HttpGet]
+ public IActionResult Get() => Ok();
+
+ [HttpPost]
+ public IActionResult Post() => Ok();
+
+ [HttpPatch]
+ public IActionResult Patch() => Ok();
+
+ [HttpDelete]
+ public IActionResult Delete() => Ok();
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TagsController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TagsController.cs
new file mode 100644
index 0000000..16c958d
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TagsController.cs
@@ -0,0 +1,20 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Controllers.Annotations;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ [DisableQueryString("skipCache")]
+ public sealed class TagsController : JsonApiController
+ {
+ public TagsController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TestValuesController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TestValuesController.cs
new file mode 100644
index 0000000..9bd4cec
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TestValuesController.cs
@@ -0,0 +1,35 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ [Route("[controller]")]
+ public class TestValuesController : ControllerBase
+ {
+ [HttpGet]
+ public IActionResult Get()
+ {
+ var result = new[] { "value" };
+ return Ok(result);
+ }
+
+ [HttpPost]
+ public IActionResult Post(string name)
+ {
+ var result = "Hello, " + name;
+ return Ok(result);
+ }
+
+ [HttpPatch]
+ public IActionResult Patch(string name)
+ {
+ var result = "Hello, " + name;
+ return Ok(result);
+ }
+
+ [HttpDelete]
+ public IActionResult Delete()
+ {
+ return Ok("Deleted");
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/ThrowingResourcesController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/ThrowingResourcesController.cs
new file mode 100644
index 0000000..2fcda52
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/ThrowingResourcesController.cs
@@ -0,0 +1,18 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ public sealed class ThrowingResourcesController : JsonApiController
+ {
+ public ThrowingResourcesController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoCollectionsController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoCollectionsController.cs
new file mode 100644
index 0000000..13804a9
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoCollectionsController.cs
@@ -0,0 +1,37 @@
+using System.Threading;
+using System.Threading.Tasks;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ public sealed class TodoCollectionsController : JsonApiController
+ {
+
+ public TodoCollectionsController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ {
+ }
+
+ [HttpPatch("{id}")]
+ public override async Task PatchAsync(string id, [FromBody] TodoItemCollection resource, CancellationToken cancellationToken)
+ {
+ // if (resource.Name == "PRE-ATTACH-TEST")
+ // {
+ // var targetTodoId = resource.TodoItems.First().Id;
+ // var todoItemContext = _dbResolver.GetContext().Set();
+ // await todoItemContext.Where(ti => ti.Id == targetTodoId).FirstOrDefaultAsync(cancellationToken);
+ // }
+
+ return await base.PatchAsync(id, resource, cancellationToken);
+ }
+
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsController.cs
new file mode 100644
index 0000000..a147a20
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsController.cs
@@ -0,0 +1,18 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ public sealed class TodoItemsController : JsonApiController
+ {
+ public TodoItemsController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsCustomController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsCustomController.cs
new file mode 100644
index 0000000..36ad9c5
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsCustomController.cs
@@ -0,0 +1,149 @@
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers.Annotations;
+using JsonApiDotNetCore.Errors;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ [ApiController]
+ [DisableRoutingConvention, Route("custom/route/todoItems")]
+ public class TodoItemsCustomController : CustomJsonApiController
+ {
+ public TodoItemsCustomController(
+ IJsonApiOptions options,
+ IResourceService resourceService)
+ : base(options, resourceService)
+ { }
+ }
+
+ public class CustomJsonApiController
+ : CustomJsonApiController where T : class, IIdentifiable
+ {
+ public CustomJsonApiController(
+ IJsonApiOptions options,
+ IResourceService resourceService)
+ : base(options, resourceService)
+ {
+ }
+ }
+
+ public class CustomJsonApiController
+ : ControllerBase where T : class, IIdentifiable
+ {
+ private readonly IJsonApiOptions _options;
+ private readonly IResourceService _resourceService;
+
+ private IActionResult Forbidden()
+ {
+ return new StatusCodeResult((int)HttpStatusCode.Forbidden);
+ }
+
+ public CustomJsonApiController(
+ IJsonApiOptions options,
+ IResourceService resourceService)
+ {
+ _options = options;
+ _resourceService = resourceService;
+ }
+
+ public CustomJsonApiController(
+ IResourceService resourceService)
+ {
+ _resourceService = resourceService;
+ }
+
+ [HttpGet]
+ public async Task GetAsync(CancellationToken cancellationToken)
+ {
+ var resources = await _resourceService.GetAsync(cancellationToken);
+ return Ok(resources);
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetAsync(TId id, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var resource = await _resourceService.GetAsync(id, cancellationToken);
+ return Ok(resource);
+ }
+ catch (ResourceNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+
+ [HttpGet("{id}/relationships/{relationshipName}")]
+ public async Task GetRelationshipsAsync(TId id, string relationshipName, CancellationToken cancellationToken)
+ {
+ try
+ {
+ var relationship = await _resourceService.GetRelationshipAsync(id, relationshipName, cancellationToken);
+ return Ok(relationship);
+ }
+ catch (ResourceNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+
+ [HttpGet("{id}/{relationshipName}")]
+ public async Task GetRelationshipAsync(TId id, string relationshipName, CancellationToken cancellationToken)
+ {
+ var relationship = await _resourceService.GetSecondaryAsync(id, relationshipName, cancellationToken);
+ return Ok(relationship);
+ }
+
+ [HttpPost]
+ public async Task PostAsync([FromBody] T resource, CancellationToken cancellationToken)
+ {
+ if (resource == null)
+ return UnprocessableEntity();
+
+ if (_options.AllowClientGeneratedIds && !string.IsNullOrEmpty(resource.StringId))
+ return Forbidden();
+
+ resource = await _resourceService.CreateAsync(resource, cancellationToken);
+
+ return Created($"{HttpContext.Request.Path}/{resource.Id}", resource);
+ }
+
+ [HttpPatch("{id}")]
+ public async Task PatchAsync(TId id, [FromBody] T resource, CancellationToken cancellationToken)
+ {
+ if (resource == null)
+ return UnprocessableEntity();
+
+ try
+ {
+ var updated = await _resourceService.UpdateAsync(id, resource, cancellationToken);
+ return Ok(updated);
+ }
+ catch (ResourceNotFoundException)
+ {
+ return NotFound();
+ }
+ }
+
+ [HttpPatch("{id}/relationships/{relationshipName}")]
+ public async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object secondaryResourceIds, CancellationToken cancellationToken)
+ {
+ await _resourceService.SetRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken);
+
+ return Ok();
+ }
+
+ [HttpDelete("{id}")]
+ public async Task DeleteAsync(TId id, CancellationToken cancellationToken)
+ {
+ await _resourceService.DeleteAsync(id, cancellationToken);
+ return NoContent();
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsTestController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsTestController.cs
new file mode 100644
index 0000000..518b34f
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/TodoItemsTestController.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Controllers.Annotations;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Serialization.Objects;
+using JsonApiDotNetCore.Services;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ public abstract class AbstractTodoItemsController
+ : BaseJsonApiController where T : class, IIdentifiable
+ {
+ protected AbstractTodoItemsController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService service)
+ : base(options, loggerFactory, service)
+ { }
+ }
+
+ [DisableRoutingConvention]
+ [Route("/abstract")]
+ public class TodoItemsTestController : AbstractTodoItemsController
+ {
+ public TodoItemsTestController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService service)
+ : base(options, loggerFactory, service)
+ { }
+
+ [HttpGet]
+ public override async Task GetAsync(CancellationToken cancellationToken)
+ {
+ return await base.GetAsync(cancellationToken);
+ }
+
+ [HttpGet("{id}")]
+ public override async Task GetAsync(string id, CancellationToken cancellationToken)
+ {
+ return await base.GetAsync(id, cancellationToken);
+ }
+
+ [HttpGet("{id}/{relationshipName}")]
+ public override async Task GetSecondaryAsync(string id, string relationshipName, CancellationToken cancellationToken)
+ {
+ return await base.GetSecondaryAsync(id, relationshipName, cancellationToken);
+ }
+
+ [HttpGet("{id}/relationships/{relationshipName}")]
+ public override async Task GetRelationshipAsync(string id, string relationshipName, CancellationToken cancellationToken)
+ {
+ return await base.GetRelationshipAsync(id, relationshipName, cancellationToken);
+ }
+
+ [HttpPost]
+ public override async Task PostAsync([FromBody] TodoItem resource, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+
+ return NotFound(new Error(HttpStatusCode.NotFound)
+ {
+ Title = "NotFound ActionResult with explicit error object."
+ });
+ }
+
+ [HttpPost("{id}/relationships/{relationshipName}")]
+ public override async Task PostRelationshipAsync(
+ string id, string relationshipName, [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken)
+ {
+ return await base.PostRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken);
+ }
+
+ [HttpPatch("{id}")]
+ public override async Task PatchAsync(string id, [FromBody] TodoItem resource, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+
+ return Conflict("Something went wrong");
+ }
+
+ [HttpPatch("{id}/relationships/{relationshipName}")]
+ public override async Task PatchRelationshipAsync(
+ string id, string relationshipName, [FromBody] object secondaryResourceIds, CancellationToken cancellationToken)
+ {
+ return await base.PatchRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken);
+ }
+
+ [HttpDelete("{id}")]
+ public override async Task DeleteAsync(string id, CancellationToken cancellationToken)
+ {
+ await Task.Yield();
+
+ return NotFound();
+ }
+
+ [HttpDelete("{id}/relationships/{relationshipName}")]
+ public override async Task DeleteRelationshipAsync(string id, string relationshipName, [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken)
+ {
+ return await base.DeleteRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken);
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Controllers/UsersController.cs b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/UsersController.cs
new file mode 100644
index 0000000..78b962b
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Controllers/UsersController.cs
@@ -0,0 +1,28 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Controllers
+{
+ public sealed class UsersController : JsonApiController
+ {
+ public UsersController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+ }
+
+ public sealed class SuperUsersController : JsonApiController
+ {
+ public SuperUsersController(
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/ArticleHooksDefinition.cs b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/ArticleHooksDefinition.cs
new file mode 100644
index 0000000..76b2a18
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/ArticleHooksDefinition.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Errors;
+using JsonApiDotNetCore.Hooks.Internal.Execution;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Serialization.Objects;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Definitions
+{
+ public class ArticleHooksDefinition : ResourceHooksDefinition
+ {
+ public ArticleHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
+
+ public override IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline)
+ {
+ if (pipeline == ResourcePipeline.GetSingle && resources.Any(r => r.Caption == "Classified"))
+ {
+ throw new JsonApiException(new Error(HttpStatusCode.Forbidden)
+ {
+ Title = "You are not allowed to see this article."
+ });
+ }
+
+ return resources.Where(t => t.Caption != "This should not be included");
+ }
+ }
+}
+
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/LockableHooksDefinition.cs b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/LockableHooksDefinition.cs
new file mode 100644
index 0000000..662cf82
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/LockableHooksDefinition.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Errors;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Serialization.Objects;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Definitions
+{
+ public abstract class LockableHooksDefinition : ResourceHooksDefinition where T : class, IIsLockable, IIdentifiable
+ {
+ protected LockableHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
+
+ protected void DisallowLocked(IEnumerable resources)
+ {
+ foreach (var e in resources ?? Enumerable.Empty())
+ {
+ if (e.IsLocked)
+ {
+ throw new JsonApiException(new Error(HttpStatusCode.Forbidden)
+ {
+ Title = "You are not allowed to update fields or relationships of locked todo items."
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/PassportHooksDefinition.cs b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/PassportHooksDefinition.cs
new file mode 100644
index 0000000..6318fbc
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/PassportHooksDefinition.cs
@@ -0,0 +1,49 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Errors;
+using JsonApiDotNetCore.Hooks.Internal.Execution;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Serialization.Objects;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Definitions
+{
+ public class PassportHooksDefinition : ResourceHooksDefinition
+ {
+ public PassportHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
+ {
+ }
+
+ public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null)
+ {
+ if (pipeline == ResourcePipeline.GetSingle && isIncluded)
+ {
+ throw new JsonApiException(new Error(HttpStatusCode.Forbidden)
+ {
+ Title = "You are not allowed to include passports on individual persons."
+ });
+ }
+ }
+
+ public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline)
+ {
+ resourcesByRelationship.GetByRelationship().ToList().ForEach(kvp => DoesNotTouchLockedPassports(kvp.Value));
+ }
+
+ private void DoesNotTouchLockedPassports(IEnumerable resources)
+ {
+ foreach (var passport in resources ?? Enumerable.Empty())
+ {
+ if (passport.IsLocked)
+ {
+ throw new JsonApiException(new Error(HttpStatusCode.Forbidden)
+ {
+ Title = "You are not allowed to update fields or relationships of locked persons."
+ });
+ }
+ }
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/PersonHooksDefinition.cs b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/PersonHooksDefinition.cs
new file mode 100644
index 0000000..c2b618b
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/PersonHooksDefinition.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.Linq;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Hooks.Internal.Execution;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Definitions
+{
+ public class PersonHooksDefinition : LockableHooksDefinition
+ {
+ public PersonHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
+
+ public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline)
+ {
+ BeforeImplicitUpdateRelationship(resourcesByRelationship, pipeline);
+ return ids;
+ }
+
+ public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline)
+ {
+ resourcesByRelationship.GetByRelationship().ToList().ForEach(kvp => DisallowLocked(kvp.Value));
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TagHooksDefinition.cs b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TagHooksDefinition.cs
new file mode 100644
index 0000000..3742c70
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TagHooksDefinition.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.Linq;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Hooks.Internal.Execution;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Resources;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Definitions
+{
+ public class TagHooksDefinition : ResourceHooksDefinition
+ {
+ public TagHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
+
+ public override IEnumerable BeforeCreate(IResourceHashSet affected, ResourcePipeline pipeline)
+ {
+ return base.BeforeCreate(affected, pipeline);
+ }
+
+ public override IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline)
+ {
+ return resources.Where(t => t.Name != "This should not be included");
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoHooksDefinition.cs b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoHooksDefinition.cs
new file mode 100644
index 0000000..b1ccad4
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoHooksDefinition.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Errors;
+using JsonApiDotNetCore.Hooks.Internal.Execution;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Serialization.Objects;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Definitions
+{
+ public class TodoHooksDefinition : LockableHooksDefinition
+ {
+ public TodoHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
+
+ public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null)
+ {
+ if (stringId == "1337")
+ {
+ throw new JsonApiException(new Error(HttpStatusCode.Forbidden)
+ {
+ Title = "You are not allowed to update the author of todo items."
+ });
+ }
+ }
+
+ public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline)
+ {
+ List todos = resourcesByRelationship.GetByRelationship().SelectMany(kvp => kvp.Value).ToList();
+ DisallowLocked(todos);
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoItemDefinition.cs b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoItemDefinition.cs
new file mode 100644
index 0000000..c5b9706
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Definitions/TodoItemDefinition.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Resources;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Definitions
+{
+ public sealed class TodoItemDefinition : JsonApiResourceDefinition
+ {
+ public TodoItemDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
+ {
+ }
+
+ public override IDictionary GetMeta(TodoItem resource)
+ {
+ if (resource.Description != null && resource.Description.StartsWith("Important:"))
+ {
+ return new Dictionary
+ {
+ ["hasHighPriority"] = true
+ };
+ }
+
+ return base.GetMeta(resource);
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Dockerfile b/src/JsonApiDotNetCore.MongoDb.Example/Dockerfile
new file mode 100644
index 0000000..c5a5d90
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Dockerfile
@@ -0,0 +1,13 @@
+FROM microsoft/dotnet:latest
+
+COPY . /app
+
+WORKDIR /app
+
+RUN ["dotnet", "restore"]
+
+RUN ["dotnet", "build"]
+
+EXPOSE 14140/tcp
+
+CMD ["dotnet", "run", "--server.urls", "http://*:14140"]
diff --git a/src/Example/Example.csproj b/src/JsonApiDotNetCore.MongoDb.Example/JsonApiDotNetCore.MongoDb.Example.csproj
similarity index 100%
rename from src/Example/Example.csproj
rename to src/JsonApiDotNetCore.MongoDb.Example/JsonApiDotNetCore.MongoDb.Example.csproj
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Address.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Address.cs
new file mode 100644
index 0000000..686e488
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Address.cs
@@ -0,0 +1,27 @@
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public sealed class Address : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [Attr]
+ public string Street { get; set; }
+
+ [Attr]
+ public string ZipCode { get; set; }
+
+ [HasOne]
+ public Country Country { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Article.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Article.cs
new file mode 100644
index 0000000..525ccad
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Article.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public sealed class Article : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [Attr]
+ public string Caption { get; set; }
+
+ [Attr]
+ public string Url { get; set; }
+
+ // [HasOne]
+ // public Author Author { get; set; }
+ //
+ // [BsonIgnore]
+ // [HasManyThrough(nameof(ArticleTags))]
+ // public ISet Tags { get; set; }
+ // public ISet ArticleTags { get; set; }
+ //
+ // [BsonIgnore]
+ // [HasManyThrough(nameof(IdentifiableArticleTags))]
+ // public ICollection IdentifiableTags { get; set; }
+ // public ICollection IdentifiableArticleTags { get; set; }
+ //
+ // [HasMany]
+ // public ICollection Revisions { get; set; }
+ //
+ // [HasOne]
+ // public Blog Blog { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/ArticleTag.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/ArticleTag.cs
new file mode 100644
index 0000000..6593c3e
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/ArticleTag.cs
@@ -0,0 +1,11 @@
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public sealed class ArticleTag
+ {
+ public int ArticleId { get; set; }
+ public Article Article { get; set; }
+
+ public int TagId { get; set; }
+ public Tag Tag { get; set; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Author.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Author.cs
new file mode 100644
index 0000000..651dd2e
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Author.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public sealed class Author : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [Attr]
+ public string FirstName { get; set; }
+
+ [Attr]
+ public string LastName { get; set; }
+
+ [Attr]
+ public DateTime? DateOfBirth { get; set; }
+
+ [Attr]
+ public string BusinessEmail { get; set; }
+
+ // [HasOne]
+ // public Address LivingAddress { get; set; }
+ //
+ // [HasMany]
+ // public IList Articles { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Blog.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Blog.cs
new file mode 100644
index 0000000..9f80e75
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Blog.cs
@@ -0,0 +1,30 @@
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public sealed class Blog : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [Attr]
+ public string Title { get; set; }
+
+ [Attr]
+ public string CompanyName { get; set; }
+
+ // [HasMany]
+ // public IList Articles { get; set; }
+
+ // [HasOne]
+ // public Author Owner { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/test/JsonApiDotNetCore.MongoDb.IntegrationTests/Models/Book.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Country.cs
similarity index 59%
rename from test/JsonApiDotNetCore.MongoDb.IntegrationTests/Models/Book.cs
rename to src/JsonApiDotNetCore.MongoDb.Example/Models/Country.cs
index 15a90d9..28e83d7 100644
--- a/test/JsonApiDotNetCore.MongoDb.IntegrationTests/Models/Book.cs
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Country.cs
@@ -3,26 +3,17 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
-namespace JsonApiDotNetCore.MongoDb.IntegrationTests.Models
+namespace JsonApiDotNetCore.MongoDb.Example.Models
{
- public sealed class Book : IIdentifiable
+ public class Country : IIdentifiable
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[Attr]
public string Id { get; set; }
-
+
[Attr]
public string Name { get; set; }
-
- [Attr]
- public decimal Price { get; set; }
-
- [Attr]
- public string Category { get; set; }
-
- [Attr]
- public string Author { get; set; }
[BsonIgnore]
public string StringId { get => Id; set => Id = value; }
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Gender.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Gender.cs
new file mode 100644
index 0000000..c8545b9
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Gender.cs
@@ -0,0 +1,9 @@
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public enum Gender
+ {
+ Unknown,
+ Male,
+ Female
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/IIsLockable.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/IIsLockable.cs
new file mode 100644
index 0000000..35f8c4e
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/IIsLockable.cs
@@ -0,0 +1,7 @@
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public interface IIsLockable
+ {
+ bool IsLocked { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/IdentifiableArticleTag.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/IdentifiableArticleTag.cs
new file mode 100644
index 0000000..d4304d0
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/IdentifiableArticleTag.cs
@@ -0,0 +1,28 @@
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public class IdentifiableArticleTag : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ public int ArticleId { get; set; }
+ [HasOne]
+ public Article Article { get; set; }
+
+ public int TagId { get; set; }
+ [HasOne]
+ public Tag Tag { get; set; }
+
+ public string SomeMetaData { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/KebabCasedModel.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/KebabCasedModel.cs
new file mode 100644
index 0000000..a72c7b1
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/KebabCasedModel.cs
@@ -0,0 +1,21 @@
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public class KebabCasedModel : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [Attr]
+ public string CompoundAttr { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Passport.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Passport.cs
new file mode 100644
index 0000000..e16fe40
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Passport.cs
@@ -0,0 +1,65 @@
+using System;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public class Passport : IIdentifiable
+ {
+ // private readonly ISystemClock _systemClock;
+ private int? _socialSecurityNumber;
+
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [Attr]
+ public int? SocialSecurityNumber
+ {
+ get => _socialSecurityNumber;
+ set
+ {
+ if (value != _socialSecurityNumber)
+ {
+ LastSocialSecurityNumberChange = DateTime.UtcNow.ToLocalTime();
+ _socialSecurityNumber = value;
+ }
+ }
+ }
+
+ [Attr]
+ public DateTime LastSocialSecurityNumberChange { get; set; }
+
+ [Attr]
+ public bool IsLocked { get; set; }
+
+ // [HasOne]
+ // public Person Person { get; set; }
+
+ // [Attr]
+ // [NotMapped]
+ // public string BirthCountryName
+ // {
+ // get => BirthCountry?.Name;
+ // set
+ // {
+ // BirthCountry ??= new Country();
+ // BirthCountry.Name = value;
+ // }
+ // }
+
+ // [EagerLoad]
+ // public Country BirthCountry { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+
+ // public Passport(AppDbContext appDbContext)
+ // {
+ // _systemClock = appDbContext.SystemClock;
+ // }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Person.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Person.cs
new file mode 100644
index 0000000..ae66ab6
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Person.cs
@@ -0,0 +1,90 @@
+using System.Linq;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public sealed class PersonRole : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [HasOne]
+ public Person Person { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+
+ public sealed class Person : IIdentifiable, IIsLockable
+ {
+ private string _firstName;
+
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ public bool IsLocked { get; set; }
+
+ [Attr]
+ public string FirstName
+ {
+ get => _firstName;
+ set
+ {
+ if (value != _firstName)
+ {
+ _firstName = value;
+ Initials = string.Concat(value.Split(' ').Select(x => char.ToUpperInvariant(x[0])));
+ }
+ }
+ }
+
+ [Attr]
+ public string Initials { get; set; }
+
+ [Attr]
+ public string LastName { get; set; }
+
+ [Attr(PublicName = "the-Age")]
+ public int Age { get; set; }
+
+ [Attr]
+ public Gender Gender { get; set; }
+
+ [Attr]
+ public string Category { get; set; }
+
+ // [HasMany]
+ // public ISet TodoItems { get; set; }
+ //
+ // [HasMany]
+ // public ISet AssignedTodoItems { get; set; }
+ //
+ // [HasMany]
+ // public HashSet TodoCollections { get; set; }
+ //
+ // [HasOne]
+ // public PersonRole Role { get; set; }
+ //
+ // [HasOne]
+ // public TodoItem OneToOneTodoItem { get; set; }
+ //
+ // [HasOne]
+ // public TodoItem StakeHolderTodoItem { get; set; }
+ //
+ // [HasOne(Links = LinkTypes.All, CanInclude = false)]
+ // public TodoItem UnIncludeableItem { get; set; }
+ //
+ // [HasOne]
+ // public Passport Passport { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Revision.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Revision.cs
new file mode 100644
index 0000000..ad5d763
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Revision.cs
@@ -0,0 +1,28 @@
+using System;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public sealed class Revision : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [Attr]
+ public DateTime PublishTime { get; set; }
+
+ [HasOne]
+ public Author Author { get; set; }
+
+ [HasOne]
+ public Article Article { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/Tag.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/Tag.cs
new file mode 100644
index 0000000..be3c99b
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/Tag.cs
@@ -0,0 +1,29 @@
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public class Tag : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [Attr]
+ public string Name { get; set; }
+
+ [Attr]
+ public TagColor Color { get; set; }
+
+ // [NotMapped]
+ // [HasManyThrough(nameof(ArticleTags))]
+ // public ISet Articles { get; set; }
+ // public ISet ArticleTags { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/TagColor.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/TagColor.cs
new file mode 100644
index 0000000..bab8ade
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/TagColor.cs
@@ -0,0 +1,9 @@
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public enum TagColor
+ {
+ Red,
+ Green,
+ Blue
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/ThrowingResource.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/ThrowingResource.cs
new file mode 100644
index 0000000..1af8d68
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/ThrowingResource.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using JsonApiDotNetCore.Serialization;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public sealed class ThrowingResource : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [Attr]
+ public string FailsOnSerialize
+ {
+ get
+ {
+ var isSerializingResponse = new StackTrace().GetFrames()
+ .Any(frame => frame.GetMethod().DeclaringType == typeof(JsonApiWriter));
+
+ if (isSerializingResponse)
+ {
+ throw new InvalidOperationException($"The value for the '{nameof(FailsOnSerialize)}' property is currently unavailable.");
+ }
+
+ return string.Empty;
+ }
+ set { }
+ }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItem.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItem.cs
new file mode 100644
index 0000000..5ed4ba6
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItem.cs
@@ -0,0 +1,72 @@
+using System;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public class TodoItem : IIdentifiable, IIsLockable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ public bool IsLocked { get; set; }
+
+ [Attr]
+ public string Description { get; set; }
+
+ [Attr]
+ public long Ordinal { get; set; }
+
+ [Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowCreate)]
+ public string AlwaysChangingValue
+ {
+ get => Guid.NewGuid().ToString();
+ set { }
+ }
+
+ [Attr]
+ public DateTime CreatedDate { get; set; }
+
+ [Attr(Capabilities = AttrCapabilities.All & ~(AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort))]
+ public DateTime? AchievedDate { get; set; }
+
+ [Attr(Capabilities = AttrCapabilities.All & ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))]
+ public string CalculatedValue => "calculated";
+
+ [Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowChange)]
+ public DateTimeOffset? OffsetDate { get; set; }
+
+ // [HasOne]
+ // public Person Owner { get; set; }
+ //
+ // [HasOne]
+ // public Person Assignee { get; set; }
+ //
+ // [HasOne]
+ // public Person OneToOnePerson { get; set; }
+ //
+ // [HasMany]
+ // public ISet StakeHolders { get; set; }
+ //
+ // [HasOne]
+ // public TodoItemCollection Collection { get; set; }
+ //
+ // // cyclical to-one structure
+ // [HasOne]
+ // public TodoItem DependentOnTodo { get; set; }
+ //
+ // // cyclical to-many structure
+ // [HasOne]
+ // public TodoItem ParentTodo { get; set; }
+ //
+ // [HasMany]
+ // public IList ChildrenTodos { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItemCollection.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItemCollection.cs
new file mode 100644
index 0000000..9300816
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/TodoItemCollection.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ [Resource("todoCollections")]
+ public sealed class TodoItemCollection : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [Attr]
+ public string Name { get; set; }
+
+ // [HasMany]
+ // public ISet TodoItems { get; set; }
+ //
+ // [HasOne]
+ // public Person Owner { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Models/User.cs b/src/JsonApiDotNetCore.MongoDb.Example/Models/User.cs
new file mode 100644
index 0000000..412d415
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Models/User.cs
@@ -0,0 +1,54 @@
+using System;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Models
+{
+ public class User : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ // private readonly ISystemClock _systemClock;
+ private string _password;
+
+ [Attr] public string UserName { get; set; }
+
+ [Attr(Capabilities = AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)]
+ public string Password
+ {
+ get => _password;
+ set
+ {
+ if (value != _password)
+ {
+ _password = value;
+ LastPasswordChange = DateTime.UtcNow.ToLocalTime();
+ }
+ }
+ }
+
+ [Attr] public DateTime LastPasswordChange { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+
+ // public User(AppDbContext appDbContext)
+ // {
+ // _systemClock = appDbContext.SystemClock;
+ // }
+ }
+
+ public sealed class SuperUser : User
+ {
+ [Attr] public int SecurityLevel { get; set; }
+
+ // public SuperUser(AppDbContext appDbContext) : base(appDbContext)
+ // {
+ // }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Program.cs b/src/JsonApiDotNetCore.MongoDb.Example/Program.cs
new file mode 100644
index 0000000..d718b50
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Program.cs
@@ -0,0 +1,20 @@
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
+
+namespace JsonApiDotNetCore.MongoDb.Example
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ CreateHostBuilder(args).Build().Run();
+ }
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Properties/launchSettings.json b/src/JsonApiDotNetCore.MongoDb.Example/Properties/launchSettings.json
new file mode 100644
index 0000000..1e3998e
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Properties/launchSettings.json
@@ -0,0 +1,30 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:14140",
+ "sslPort": 44340
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": false,
+ "launchUrl": "api/v1/todoItems",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Kestrel": {
+ "commandName": "Project",
+ "launchBrowser": false,
+ "launchUrl": "api/v1/todoItems",
+ "applicationUrl": "https://localhost:44340;http://localhost:14140",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Services/CustomArticleService.cs b/src/JsonApiDotNetCore.MongoDb.Example/Services/CustomArticleService.cs
new file mode 100644
index 0000000..4f1eed2
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Services/CustomArticleService.cs
@@ -0,0 +1,37 @@
+using System.Threading;
+using System.Threading.Tasks;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Hooks;
+using JsonApiDotNetCore.Middleware;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.Queries;
+using JsonApiDotNetCore.Repositories;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Services
+{
+ public class CustomArticleService : JsonApiResourceService
+ {
+ public CustomArticleService(
+ IResourceRepositoryAccessor repositoryAccessor,
+ IQueryLayerComposer queryLayerComposer,
+ IPaginationContext paginationContext,
+ IJsonApiOptions options,
+ ILoggerFactory loggerFactory,
+ IJsonApiRequest request,
+ IResourceChangeTracker resourceChangeTracker,
+ IResourceHookExecutorFacade hookExecutor)
+ : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request,
+ resourceChangeTracker, hookExecutor)
+ { }
+
+ public override async Task GetAsync(string id, CancellationToken cancellationToken)
+ {
+ var resource = await base.GetAsync(id, cancellationToken);
+ resource.Caption = "None for you Glen Coco";
+ return resource;
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Services/SkipCacheQueryStringParameterReader.cs b/src/JsonApiDotNetCore.MongoDb.Example/Services/SkipCacheQueryStringParameterReader.cs
new file mode 100644
index 0000000..8af3c32
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Services/SkipCacheQueryStringParameterReader.cs
@@ -0,0 +1,36 @@
+using System.Linq;
+using JsonApiDotNetCore.Controllers.Annotations;
+using JsonApiDotNetCore.Errors;
+using JsonApiDotNetCore.QueryStrings;
+using Microsoft.Extensions.Primitives;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Services
+{
+ public class SkipCacheQueryStringParameterReader : IQueryStringParameterReader
+ {
+ private const string _skipCacheParameterName = "skipCache";
+
+ public bool SkipCache { get; private set; }
+
+ public bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute)
+ {
+ return !disableQueryStringAttribute.ParameterNames.Contains(_skipCacheParameterName);
+ }
+
+ public bool CanRead(string parameterName)
+ {
+ return parameterName == _skipCacheParameterName;
+ }
+
+ public void Read(string parameterName, StringValues parameterValue)
+ {
+ if (!bool.TryParse(parameterValue, out bool skipCache))
+ {
+ throw new InvalidQueryStringParameterException(parameterName, "Boolean value required.",
+ $"The value {parameterValue} is not a valid boolean.");
+ }
+
+ SkipCache = skipCache;
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Startups/EmptyStartup.cs b/src/JsonApiDotNetCore.MongoDb.Example/Startups/EmptyStartup.cs
new file mode 100644
index 0000000..2ff1251
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Startups/EmptyStartup.cs
@@ -0,0 +1,26 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JsonApiDotNetCore.MongoDb.Example
+{
+ ///
+ /// Empty startup class, required for integration tests.
+ /// Changes in ASP.NET Core 3 no longer allow Startup class to be defined in test projects. See https://github.com/aspnet/AspNetCore/issues/15373.
+ ///
+ public abstract class EmptyStartup
+ {
+ protected EmptyStartup(IConfiguration configuration)
+ {
+ }
+
+ public virtual void ConfigureServices(IServiceCollection services)
+ {
+ }
+
+ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
+ {
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Startups/Startup.cs b/src/JsonApiDotNetCore.MongoDb.Example/Startups/Startup.cs
new file mode 100644
index 0000000..a2d7854
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Startups/Startup.cs
@@ -0,0 +1,107 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.MongoDb;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using JsonApiDotNetCore.MongoDb.Example.Services;
+using JsonApiDotNetCore.QueryStrings;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using MongoDB.Driver;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace JsonApiDotNetCore.MongoDb.Example
+{
+ public class Startup : EmptyStartup
+ {
+ private IConfiguration Configuration { get; }
+
+ public Startup(IConfiguration configuration) : base(configuration)
+ {
+ Configuration = configuration;
+ }
+
+ public override void ConfigureServices(IServiceCollection services)
+ {
+ ConfigureClock(services);
+
+ services.AddScoped();
+ services.AddScoped(sp => sp.GetRequiredService());
+
+ // TryAddSingleton will only register the IMongoDatabase if there is no
+ // previously registered instance - will make tests use individual dbs
+ services.TryAddSingleton(sp =>
+ {
+ var client = new MongoClient(Configuration.GetSection("DatabaseSettings:ConnectionString").Value);
+ return client.GetDatabase(Configuration.GetSection("DatabaseSettings:Database").Value);
+ });
+
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+ services.AddResourceRepository>();
+
+
+ services.AddJsonApi(
+ ConfigureJsonApiOptions,
+ resources: builder =>
+ {
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ builder.Add();
+ });
+
+ // once all tests have been moved to WebApplicationFactory format we can get rid of this line below
+ services.AddClientSerialization();
+ }
+
+ private void ConfigureClock(IServiceCollection services)
+ {
+ services.AddSingleton();
+ }
+
+ protected virtual void ConfigureJsonApiOptions(JsonApiOptions options)
+ {
+ options.IncludeExceptionStackTraceInErrors = true;
+ options.Namespace = "api/v1";
+ options.DefaultPageSize = new PageSize(5);
+ options.IncludeTotalResourceCount = true;
+ options.ValidateModelState = true;
+ options.SerializerSettings.Formatting = Formatting.Indented;
+ options.SerializerSettings.Converters.Add(new StringEnumConverter());
+ }
+
+ public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
+ {
+ app.UseRouting();
+ app.UseJsonApi();
+ app.UseEndpoints(endpoints => endpoints.MapControllers());
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/Startups/TestStartup.cs b/src/JsonApiDotNetCore.MongoDb.Example/Startups/TestStartup.cs
new file mode 100644
index 0000000..cf99c40
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/Startups/TestStartup.cs
@@ -0,0 +1,26 @@
+using JsonApiDotNetCore.Configuration;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JsonApiDotNetCore.MongoDb.Example
+{
+ public class TestStartup : EmptyStartup
+ {
+ public TestStartup(IConfiguration configuration) : base(configuration)
+ {
+ }
+
+ public override void ConfigureServices(IServiceCollection services)
+ {
+ }
+
+ public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
+ {
+ app.UseRouting();
+ app.UseJsonApi();
+ app.UseEndpoints(endpoints => endpoints.MapControllers());
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.MongoDb.Example/appsettings.json b/src/JsonApiDotNetCore.MongoDb.Example/appsettings.json
new file mode 100644
index 0000000..ff9ab4c
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.Example/appsettings.json
@@ -0,0 +1,14 @@
+{
+ "DatabaseSettings": {
+ "ConnectionString": "mongodb://localhost:27017",
+ "Database": "JsonApiDotNetCoreExample"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning",
+ "Microsoft": "Warning",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/Example/Controllers/BooksController.cs b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Controllers/BooksController.cs
similarity index 79%
rename from src/Example/Controllers/BooksController.cs
rename to src/JsonApiDotNetCore.MongoDb.GettingStarted/Controllers/BooksController.cs
index 5a78d91..e80e46a 100644
--- a/src/Example/Controllers/BooksController.cs
+++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Controllers/BooksController.cs
@@ -1,10 +1,10 @@
-using Example.Models;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.MongoDb.GettingStarted.Models;
using JsonApiDotNetCore.Services;
using Microsoft.Extensions.Logging;
-namespace Example.Controllers
+namespace JsonApiDotNetCore.MongoDb.GettingStarted.Controllers
{
public sealed class BooksController : JsonApiController
{
diff --git a/src/JsonApiDotNetCore.MongoDb.GettingStarted/JsonApiDotNetCore.MongoDb.GettingStarted.csproj b/src/JsonApiDotNetCore.MongoDb.GettingStarted/JsonApiDotNetCore.MongoDb.GettingStarted.csproj
new file mode 100644
index 0000000..68f4699
--- /dev/null
+++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/JsonApiDotNetCore.MongoDb.GettingStarted.csproj
@@ -0,0 +1,14 @@
+
+
+ $(NetCoreAppVersion)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Example/Models/Book.cs b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Models/Book.cs
similarity index 92%
rename from src/Example/Models/Book.cs
rename to src/JsonApiDotNetCore.MongoDb.GettingStarted/Models/Book.cs
index 022cf17..89d7a74 100644
--- a/src/Example/Models/Book.cs
+++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Models/Book.cs
@@ -3,7 +3,7 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
-namespace Example.Models
+namespace JsonApiDotNetCore.MongoDb.GettingStarted.Models
{
public sealed class Book : IIdentifiable
{
diff --git a/src/Example/Program.cs b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Program.cs
similarity index 90%
rename from src/Example/Program.cs
rename to src/JsonApiDotNetCore.MongoDb.GettingStarted/Program.cs
index 1a94e50..87d567e 100644
--- a/src/Example/Program.cs
+++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Program.cs
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
-namespace Example
+namespace JsonApiDotNetCore.MongoDb.GettingStarted
{
public class Program
{
diff --git a/src/Example/Properties/launchSettings.json b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Properties/launchSettings.json
similarity index 96%
rename from src/Example/Properties/launchSettings.json
rename to src/JsonApiDotNetCore.MongoDb.GettingStarted/Properties/launchSettings.json
index 4a6248f..e360e7a 100644
--- a/src/Example/Properties/launchSettings.json
+++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Properties/launchSettings.json
@@ -9,7 +9,7 @@
}
},
"profiles": {
- "Test": {
+ "Kestrel": {
"commandName": "Project",
"launchBrowser": false,
"launchUrl": "api/books",
@@ -20,4 +20,4 @@
}
}
}
-
\ No newline at end of file
+
diff --git a/src/Example/README.md b/src/JsonApiDotNetCore.MongoDb.GettingStarted/README.md
similarity index 87%
rename from src/Example/README.md
rename to src/JsonApiDotNetCore.MongoDb.GettingStarted/README.md
index d79afbd..833ce87 100644
--- a/src/Example/README.md
+++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/README.md
@@ -11,4 +11,4 @@ For further documentation and implementation of a JsonApiDotnetCore Application
Repository: https://github.com/json-api-dotnet/JsonApiDotNetCore
-Documentation: https://json-api-dotnet.github.io/
+Documentation: http://www.jsonapi.net
diff --git a/src/Example/Startup.cs b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Startup.cs
similarity index 82%
rename from src/Example/Startup.cs
rename to src/JsonApiDotNetCore.MongoDb.GettingStarted/Startup.cs
index d3a2b96..908bd90 100644
--- a/src/Example/Startup.cs
+++ b/src/JsonApiDotNetCore.MongoDb.GettingStarted/Startup.cs
@@ -1,15 +1,13 @@
-using Example.Models;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.MongoDb;
-using JsonApiDotNetCore.Repositories;
+using JsonApiDotNetCore.MongoDb.GettingStarted.Models;
using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using Newtonsoft.Json;
-namespace Example
+namespace JsonApiDotNetCore.MongoDb.GettingStarted
{
public sealed class Startup
{
@@ -29,7 +27,7 @@ public void ConfigureServices(IServiceCollection services)
return client.GetDatabase(Configuration.GetSection("DatabaseSettings:Database").Value);
});
- services.AddScoped, MongoEntityRepository>();
+ services.AddResourceRepository>();
services.AddJsonApi(options =>
{
options.Namespace = "api";
@@ -43,7 +41,7 @@ public void ConfigureServices(IServiceCollection services)
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ public void Configure(IApplicationBuilder app)
{
app.UseHttpsRedirection();
app.UseRouting();
diff --git a/src/Example/appsettings.json b/src/JsonApiDotNetCore.MongoDb.GettingStarted/appsettings.json
similarity index 100%
rename from src/Example/appsettings.json
rename to src/JsonApiDotNetCore.MongoDb.GettingStarted/appsettings.json
diff --git a/src/JsonApiDotNetCore.MongoDb/JsonApiDotNetCore.MongoDb.csproj b/src/JsonApiDotNetCore.MongoDb/JsonApiDotNetCore.MongoDb.csproj
index 13281f3..5c843db 100644
--- a/src/JsonApiDotNetCore.MongoDb/JsonApiDotNetCore.MongoDb.csproj
+++ b/src/JsonApiDotNetCore.MongoDb/JsonApiDotNetCore.MongoDb.csproj
@@ -7,9 +7,9 @@
- jsonapi;json:api;dotnet;core;mongodb
+ jsonapi;json:api;dotnet;core;MongoDB
Persistence layer implementation for use of mongodb in applications using JsonApiDotNetCore
- https://github.com/mrnkr/JsonApiDotNetCore.MongoDb
+ https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb
MIT
false
true
diff --git a/src/JsonApiDotNetCore.MongoDb/MongoEntityRepository.cs b/src/JsonApiDotNetCore.MongoDb/MongoEntityRepository.cs
index 2c23f5f..f49f2ad 100644
--- a/src/JsonApiDotNetCore.MongoDb/MongoEntityRepository.cs
+++ b/src/JsonApiDotNetCore.MongoDb/MongoEntityRepository.cs
@@ -1,17 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.MongoDb.Extensions;
-using JsonApiDotNetCore.MongoDb.Queries.Internal.QueryableBuilding;
+using JsonApiDotNetCore.Queries.Internal.QueryableBuilding;
using JsonApiDotNetCore.Repositories;
using JsonApiDotNetCore.Resources;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Expressions;
-using JsonApiDotNetCore.Resources.Annotations;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Infrastructure;
namespace JsonApiDotNetCore.MongoDb
{
@@ -21,74 +23,160 @@ public class MongoEntityRepository
{
private readonly IMongoDatabase _db;
private readonly ITargetedFields _targetedFields;
- private readonly IResourceGraph _resourceGraph;
+ private readonly IResourceContextProvider _resourceContextProvider;
private readonly IResourceFactory _resourceFactory;
- private readonly IEnumerable _constraintProviders;
-
+
public MongoEntityRepository(
IMongoDatabase db,
ITargetedFields targetedFields,
- IResourceGraph resourceGraph,
- IResourceFactory resourceFactory,
- IEnumerable constraintProviders)
+ IResourceContextProvider resourceContextProvider,
+ IResourceFactory resourceFactory)
{
_db = db;
_targetedFields = targetedFields;
- _resourceGraph = resourceGraph;
+ _resourceContextProvider = resourceContextProvider;
_resourceFactory = resourceFactory;
- _constraintProviders = constraintProviders;
}
private IMongoCollection Collection => _db.GetCollection(typeof(TResource).Name);
private IMongoQueryable Entities => Collection.AsQueryable();
- public virtual Task CountAsync(FilterExpression topFilter)
+ public virtual async Task> GetAsync(QueryLayer layer, CancellationToken cancellationToken) =>
+ await ApplyQueryLayer(layer).ToListAsync();
+
+ public virtual Task CountAsync(FilterExpression topFilter, CancellationToken cancellationToken)
{
- var resourceContext = _resourceGraph.GetResourceContext();
+ var resourceContext = _resourceContextProvider.GetResourceContext();
var layer = new QueryLayer(resourceContext)
{
Filter = topFilter
};
var query = ApplyQueryLayer(layer);
- return query.CountAsync();
+ return query.CountAsync(cancellationToken);
}
- public virtual Task CreateAsync(TResource resource) =>
- Collection.InsertOneAsync(resource);
-
- public virtual async Task DeleteAsync(TId id)
+ public virtual Task GetForCreateAsync(TId id, CancellationToken cancellationToken)
{
- var result = await Collection.DeleteOneAsync(Builders.Filter.Eq(e => e.Id, id));
- return result.IsAcknowledged && result.DeletedCount > 0;
+ var resource = _resourceFactory.CreateInstance();
+ resource.Id = id;
+
+ return Task.FromResult(resource);
}
- public virtual void FlushFromCache(TResource resource) =>
- throw new NotImplementedException();
+ public virtual Task CreateAsync(TResource resourceFromRequest, TResource resourceForDatabase,
+ CancellationToken cancellationToken)
+ {
+ if (resourceFromRequest == null) throw new ArgumentNullException(nameof(resourceFromRequest));
+ if (resourceForDatabase == null) throw new ArgumentNullException(nameof(resourceForDatabase));
+
+ foreach (var attribute in _targetedFields.Attributes)
+ {
+ attribute.SetValue(resourceForDatabase, attribute.GetValue(resourceFromRequest));
+ }
- public virtual async Task> GetAsync(QueryLayer layer) =>
- await ApplyQueryLayer(layer).ToListAsync();
+ return Collection.InsertOneAsync(resourceForDatabase, new InsertOneOptions(), cancellationToken);
+ }
- public virtual async Task UpdateAsync(TResource requestResource, TResource databaseResource)
+ public virtual async Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken)
+ {
+ var resources = await GetAsync(queryLayer, cancellationToken);
+ return resources.FirstOrDefault();
+ }
+
+ public virtual async Task UpdateAsync(TResource requestResource, TResource databaseResource, CancellationToken cancellationToken)
{
foreach (var attr in _targetedFields.Attributes)
attr.SetValue(databaseResource, attr.GetValue(requestResource));
- await Collection.ReplaceOneAsync(Builders.Filter.Eq(e => e.Id, databaseResource.Id), databaseResource);
+ await Collection.ReplaceOneAsync(
+ Builders.Filter.Eq(e => e.Id, databaseResource.Id),
+ databaseResource,
+ new ReplaceOptions(),
+ cancellationToken);
+ }
+
+ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToken)
+ {
+ var result = await Collection.DeleteOneAsync(
+ Builders.Filter.Eq(e => e.Id, id),
+ new DeleteOptions(),
+ cancellationToken);
+
+ if (!result.IsAcknowledged || result.DeletedCount == 0)
+ {
+ throw new DataStoreUpdateException(new Exception());
+ }
+ }
+
+ public virtual Task SetRelationshipAsync(TResource primaryResource, object secondaryResourceIds, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
+ }
+
+ public virtual Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken)
+ {
+ throw new NotImplementedException();
}
- public virtual Task UpdateRelationshipAsync(object parent, RelationshipAttribute relationship, IReadOnlyCollection relationshipIds) =>
+ public virtual Task RemoveFromToManyRelationshipAsync(TResource primaryResource, ISet secondaryResourceIds,
+ CancellationToken cancellationToken)
+ {
throw new NotImplementedException();
+ }
protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer layer)
{
var source = Entities;
- var nameFactory = new JsonApiDotNetCore.Queries.Internal.QueryableBuilding.LambdaParameterNameFactory();
- var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, _resourceGraph);
+ var nameFactory = new LambdaParameterNameFactory();
+ var builder = new QueryableBuilder(
+ source.Expression,
+ source.ElementType,
+ typeof(Queryable),
+ nameFactory,
+ _resourceFactory,
+ _resourceContextProvider,
+ DummyModel.Instance);
var expression = builder.ApplyQuery(layer);
return (IMongoQueryable)source.Provider.CreateQuery(expression);
}
}
+
+ internal sealed class DummyModel : IModel
+ {
+ public static IModel Instance { get; } = new DummyModel();
+
+ private DummyModel()
+ {
+ }
+
+ public IAnnotation FindAnnotation(string name)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IEnumerable GetAnnotations()
+ {
+ throw new NotImplementedException();
+ }
+
+ public object this[string name] => throw new NotImplementedException();
+
+ public IEnumerable GetEntityTypes()
+ {
+ throw new NotImplementedException();
+ }
+
+ public IEntityType FindEntityType(string name)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IEntityType FindEntityType(string name, string definingNavigationName, IEntityType definingEntityType)
+ {
+ throw new NotImplementedException();
+ }
+ }
}
diff --git a/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/QueryableBuilder.cs b/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/QueryableBuilder.cs
deleted file mode 100644
index 0b70588..0000000
--- a/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/QueryableBuilder.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using System;
-using System.Linq.Expressions;
-using JsonApiDotNetCore.Configuration;
-using JsonApiDotNetCore.Queries;
-using JsonApiDotNetCore.Queries.Expressions;
-using JsonApiDotNetCore.Queries.Internal.QueryableBuilding;
-using JsonApiDotNetCore.Resources;
-
-namespace JsonApiDotNetCore.MongoDb.Queries.Internal.QueryableBuilding
-{
- ///
- /// Drives conversion from into system trees.
- ///
- public sealed class QueryableBuilder
- {
- private readonly Expression _source;
- private readonly Type _elementType;
- private readonly Type _extensionType;
- private readonly LambdaParameterNameFactory _nameFactory;
- private readonly IResourceFactory _resourceFactory;
- private readonly IResourceContextProvider _resourceContextProvider;
- private readonly LambdaScopeFactory _lambdaScopeFactory;
-
- public QueryableBuilder(Expression source, Type elementType, Type extensionType, LambdaParameterNameFactory nameFactory,
- IResourceFactory resourceFactory, IResourceContextProvider resourceContextProvider,
- LambdaScopeFactory lambdaScopeFactory = null)
- {
- _source = source ?? throw new ArgumentNullException(nameof(source));
- _elementType = elementType ?? throw new ArgumentNullException(nameof(elementType));
- _extensionType = extensionType ?? throw new ArgumentNullException(nameof(extensionType));
- _nameFactory = nameFactory ?? throw new ArgumentNullException(nameof(nameFactory));
- _resourceFactory = resourceFactory ?? throw new ArgumentNullException(nameof(resourceFactory));
- _resourceContextProvider = resourceContextProvider ?? throw new ArgumentNullException(nameof(resourceContextProvider));
- _lambdaScopeFactory = lambdaScopeFactory ?? new LambdaScopeFactory(_nameFactory);
- }
-
- public Expression ApplyQuery(QueryLayer layer)
- {
- var expression = _source;
-
- if (layer.Filter != null)
- {
- expression = ApplyFilter(expression, layer.Filter);
- }
-
- if (layer.Sort != null)
- {
- expression = ApplySort(expression, layer.Sort);
- }
-
- if (layer.Pagination != null)
- {
- expression = ApplyPagination(expression, layer.Pagination);
- }
-
- return expression;
- }
-
- private Expression ApplyFilter(Expression source, FilterExpression filter)
- {
- using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType);
-
- var builder = new WhereClauseBuilder(source, lambdaScope, _extensionType);
- return builder.ApplyWhere(filter);
- }
-
- private Expression ApplySort(Expression source, SortExpression sort)
- {
- using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType);
-
- var builder = new OrderClauseBuilder(source, lambdaScope, _extensionType);
- return builder.ApplyOrderBy(sort);
- }
-
- private Expression ApplyPagination(Expression source, PaginationExpression pagination)
- {
- using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType);
-
- var builder = new SkipTakeClauseBuilder(source, lambdaScope, _extensionType);
- return builder.ApplySkipTake(pagination);
- }
- }
-}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Factories/IntegrationTestWebApplicationFactory.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Factories/IntegrationTestWebApplicationFactory.cs
deleted file mode 100644
index b0f4109..0000000
--- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Factories/IntegrationTestWebApplicationFactory.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Mvc.Testing;
-using Microsoft.AspNetCore.TestHost;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-
-namespace JsonApiDotNetCore.MongoDb.Example.Tests.Factories
-{
- internal sealed class IntegrationTestWebApplicationFactory : WebApplicationFactory
- where TStartup : class
- {
- private Action _beforeServicesConfiguration;
- private Action _afterServicesConfiguration;
-
- public void ConfigureServicesBeforeStartup(Action servicesConfiguration)
- {
- _beforeServicesConfiguration = servicesConfiguration;
- }
-
- public void ConfigureServicesAfterStartup(Action servicesConfiguration)
- {
- _afterServicesConfiguration = servicesConfiguration;
- }
-
- protected override IHostBuilder CreateHostBuilder()
- {
- return Host.CreateDefaultBuilder(null)
- .ConfigureWebHostDefaults(webBuilder =>
- {
- webBuilder.ConfigureTestServices(services =>
- {
- _beforeServicesConfiguration?.Invoke(services);
- });
-
- webBuilder.UseStartup();
-
- webBuilder.ConfigureTestServices(services =>
- {
- _afterServicesConfiguration?.Invoke(services);
- });
- });
- }
- }
-}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Extensions/StringExtensions.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..6ff22de
--- /dev/null
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Extensions/StringExtensions.cs
@@ -0,0 +1,10 @@
+namespace JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Extensions
+{
+ public static class StringExtensions
+ {
+ public static string NormalizeLineEndings(this string text)
+ {
+ return text.Replace("\r\n", "\n").Replace("\r", "\n");
+ }
+ }
+}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/TodoItemClient.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/TodoItemClient.cs
new file mode 100644
index 0000000..b7e5123
--- /dev/null
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/Helpers/Models/TodoItemClient.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Tests.Helpers.Models
+{
+ ///
+ /// this "client" version of the is required because the
+ /// base property that is overridden here does not have a setter. For a model
+ /// defined on a JSON:API client, it would not make sense to have an exposed attribute
+ /// without a setter.
+ ///
+ public class TodoItemClient : TodoItem
+ {
+ [Attr]
+ public new string CalculatedValue { get; set; }
+ }
+
+ [Resource("todoCollections")]
+ public sealed class TodoItemCollectionClient : Identifiable
+ {
+ [Attr]
+ public string Name { get; set; }
+ public int OwnerId { get; set; }
+
+ [HasMany]
+ public ISet TodoItems { get; set; }
+
+ [HasOne]
+ public Person Owner { get; set; }
+ }
+}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/HttpResponseMessageExtensions.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/HttpResponseMessageExtensions.cs
new file mode 100644
index 0000000..14aa16d
--- /dev/null
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/HttpResponseMessageExtensions.cs
@@ -0,0 +1,59 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using FluentAssertions;
+using FluentAssertions.Primitives;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Tests
+{
+ public static class HttpResponseMessageExtensions
+ {
+ public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance)
+ {
+ return new HttpResponseMessageAssertions(instance);
+ }
+
+ public sealed class HttpResponseMessageAssertions
+ : ReferenceTypeAssertions
+ {
+ protected override string Identifier => "response";
+
+ public HttpResponseMessageAssertions(HttpResponseMessage instance)
+ {
+ Subject = instance;
+ }
+
+ public AndConstraint HaveStatusCode(HttpStatusCode statusCode)
+ {
+ if (Subject.StatusCode != statusCode)
+ {
+ string responseText = GetFormattedContentAsync(Subject).Result;
+ Subject.StatusCode.Should().Be(statusCode, "response body returned was:\n" + responseText);
+ }
+
+ return new AndConstraint(this);
+ }
+
+ private static async Task GetFormattedContentAsync(HttpResponseMessage responseMessage)
+ {
+ string text = await responseMessage.Content.ReadAsStringAsync();
+
+ try
+ {
+ if (text.Length > 0)
+ {
+ return JsonConvert.DeserializeObject(text).ToString();
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+
+ return text;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTestContext.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTestContext.cs
index 2e3aafd..3841a4c 100644
--- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTestContext.cs
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTestContext.cs
@@ -2,46 +2,71 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
-using Example;
+using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
-using JsonApiDotNetCore.MongoDb.Example.Tests.Factories;
+using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Mongo2Go;
using MongoDB.Driver;
using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
namespace JsonApiDotNetCore.MongoDb.Example.Tests
{
public sealed class IntegrationTestContext : IDisposable
where TStartup : class
{
- private readonly Lazy> _lazyFactory;
+ private readonly Lazy> _lazyFactory;
private Action _beforeServicesConfiguration;
private Action _afterServicesConfiguration;
+ private Action _registerResources;
+ private readonly MongoDbRunner _runner;
- private WebApplicationFactory Factory => _lazyFactory.Value;
+ public WebApplicationFactory Factory => _lazyFactory.Value;
public IntegrationTestContext()
{
- _lazyFactory = new Lazy>(CreateFactory);
+ _lazyFactory = new Lazy>(CreateFactory);
+ _runner = MongoDbRunner.Start();
}
- private WebApplicationFactory CreateFactory()
+ private WebApplicationFactory CreateFactory()
{
- var factory = new IntegrationTestWebApplicationFactory();
+ var factory = new IntegrationTestWebApplicationFactory();
- factory.ConfigureServicesBeforeStartup(_beforeServicesConfiguration);
-
- factory.ConfigureServicesAfterStartup(services =>
+ factory.ConfigureServicesBeforeStartup(services =>
{
- _afterServicesConfiguration?.Invoke(services);
+ _beforeServicesConfiguration?.Invoke(services);
+
+ services.AddSingleton(sp =>
+ {
+ var client = new MongoClient(_runner.ConnectionString);
+ return client.GetDatabase($"JsonApiDotNetCore_MongoDb_{new Random().Next()}_Test");
+ });
+
+ services.AddJsonApi(
+ options =>
+ {
+ options.IncludeExceptionStackTraceInErrors = true;
+ options.SerializerSettings.Formatting = Formatting.Indented;
+ options.SerializerSettings.Converters.Add(new StringEnumConverter());
+ }, resources: _registerResources);
});
+ factory.ConfigureServicesAfterStartup(_afterServicesConfiguration);
+
return factory;
}
- public void Dispose() => Factory.Dispose();
+ public void Dispose()
+ {
+ _runner.Dispose();
+ Factory.Dispose();
+ }
public void ConfigureServicesBeforeStartup(Action servicesConfiguration) =>
_beforeServicesConfiguration = servicesConfiguration;
@@ -49,6 +74,9 @@ public void ConfigureServicesBeforeStartup(Action servicesCo
public void ConfigureServicesAfterStartup(Action servicesConfiguration) =>
_afterServicesConfiguration = servicesConfiguration;
+ public void RegisterResources(Action resources) =>
+ _registerResources = resources;
+
public async Task RunOnDatabaseAsync(Func asyncAction)
{
using var scope = Factory.Services.CreateScope();
@@ -110,5 +138,40 @@ private TResponseDocument DeserializeResponse(string response
throw new FormatException($"Failed to deserialize response body to JSON:\n{responseText}", exception);
}
}
+
+ private sealed class IntegrationTestWebApplicationFactory : WebApplicationFactory
+ {
+ private Action _beforeServicesConfiguration;
+ private Action _afterServicesConfiguration;
+
+ public void ConfigureServicesBeforeStartup(Action servicesConfiguration)
+ {
+ _beforeServicesConfiguration = servicesConfiguration;
+ }
+
+ public void ConfigureServicesAfterStartup(Action servicesConfiguration)
+ {
+ _afterServicesConfiguration = servicesConfiguration;
+ }
+
+ protected override IHostBuilder CreateHostBuilder()
+ {
+ return Host.CreateDefaultBuilder(null)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.ConfigureTestServices(services =>
+ {
+ _beforeServicesConfiguration?.Invoke(services);
+ });
+
+ webBuilder.UseStartup();
+
+ webBuilder.ConfigureTestServices(services =>
+ {
+ _afterServicesConfiguration?.Invoke(services);
+ });
+ });
+ }
+ }
}
}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/CreatingResourcesTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/CreatingResourcesTests.cs
deleted file mode 100644
index 6326cf1..0000000
--- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/CreatingResourcesTests.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System.Net;
-using System.Threading.Tasks;
-using Bogus;
-using Example;
-using Example.Models;
-using JsonApiDotNetCore.Serialization.Objects;
-using Microsoft.Extensions.DependencyInjection;
-using MongoDB.Driver;
-using Xunit;
-
-namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests
-{
- public sealed class CreatingResourcesTests : IClassFixture>, IAsyncLifetime
- {
- private readonly IntegrationTestContext _testContext;
- private readonly Faker _bookFaker;
-
- private string _createdBookId;
-
- public CreatingResourcesTests(IntegrationTestContext testContext)
- {
- _testContext = testContext;
- _bookFaker = new Faker()
- .RuleFor(b => b.Name, f => f.Lorem.Sentence())
- .RuleFor(b => b.Author, f => f.Name.FindName())
- .RuleFor(b => b.Category, f => f.Commerce.ProductAdjective())
- .RuleFor(b => b.Price, f => f.Random.Decimal(1.00M, 50.00M));
-
- _testContext.ConfigureServicesAfterStartup(services =>
- {
- services.AddSingleton(sp =>
- {
- var client = new MongoClient("mongodb://localhost:27017");
- return client.GetDatabase("JsonApiDotNetCore_MongoDb_Resource_Creation_Tests");
- });
- });
- }
-
- public Task InitializeAsync() => Task.CompletedTask;
-
- public Task DisposeAsync() => _testContext.RunOnDatabaseAsync(db => db.DropCollectionAsync(nameof(Book)));
-
- [Fact]
- public async Task ShouldCreateResource()
- {
- var route = "/api/Books";
- var book = _bookFaker.Generate();
- var resource = new
- {
- data = new
- {
- type = "books",
- attributes = new
- {
- name = book.Name,
- price = book.Price,
- category = book.Category,
- author = book.Author
- }
- }
- };
-
- var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, resource);
- _createdBookId = responseDocument.Data is ResourceObject resourceObject ? resourceObject.Id : null;
-
- Assert.Equal(HttpStatusCode.Created, httpResponse.StatusCode);
- }
- }
-}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/DeletingResourcesTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/DeletingResourcesTests.cs
deleted file mode 100644
index 3a4ecce..0000000
--- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/DeletingResourcesTests.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using System.Net;
-using System.Threading.Tasks;
-using Bogus;
-using Example;
-using Example.Models;
-using JsonApiDotNetCore.Serialization.Objects;
-using Microsoft.Extensions.DependencyInjection;
-using MongoDB.Driver;
-using Xunit;
-
-namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests
-{
- public sealed class DeletingResourcesTests : IClassFixture>, IAsyncLifetime
- {
- private readonly IntegrationTestContext _testContext;
- private readonly Faker _bookFaker;
-
- public DeletingResourcesTests(IntegrationTestContext testContext)
- {
- _testContext = testContext;
- _bookFaker = new Faker()
- .RuleFor(b => b.Name, f => f.Lorem.Sentence())
- .RuleFor(b => b.Author, f => f.Name.FindName())
- .RuleFor(b => b.Category, f => f.Commerce.ProductAdjective())
- .RuleFor(b => b.Price, f => f.Random.Decimal(1.00M, 50.00M));
-
- _testContext.ConfigureServicesAfterStartup(services =>
- {
- services.AddSingleton(sp =>
- {
- var client = new MongoClient("mongodb://localhost:27017");
- return client.GetDatabase("JsonApiDotNetCore_MongoDb_Resource_Deletion_Tests");
- });
- });
- }
-
- public Task InitializeAsync() => Task.CompletedTask;
-
- public Task DisposeAsync() => _testContext.RunOnDatabaseAsync(db => db.DropCollectionAsync(nameof(Book)));
-
- [Fact]
- public async Task ShouldDeleteCreatedResource()
- {
- var deleteStatusCode = HttpStatusCode.InternalServerError;
-
- var book = _bookFaker.Generate();
- var resource = new
- {
- data = new
- {
- type = "books",
- attributes = new
- {
- name = book.Name,
- price = book.Price,
- category = book.Category,
- author = book.Author
- }
- }
- };
-
- var (_, responseDocument) = await _testContext.ExecutePostAsync("/api/Books", resource);
-
- if (responseDocument.Data is ResourceObject resourceObject)
- {
- var (httpResponse, _) = await _testContext.ExecuteDeleteAsync($"/api/Books/{resourceObject.Id}");
- deleteStatusCode = httpResponse.StatusCode;
- }
-
- Assert.Equal(HttpStatusCode.NoContent, deleteStatusCode);
- }
-
- [Fact]
- public async Task ShouldReturnNotFound()
- {
- var (httpResponse, _) = await _testContext.ExecuteDeleteAsync($"/api/Books/5fa6eff63d1508a204d1b161");
- Assert.Equal(HttpStatusCode.NotFound, httpResponse.StatusCode);
- }
- }
-}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FakerContainer.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FakerContainer.cs
new file mode 100644
index 0000000..049bac2
--- /dev/null
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FakerContainer.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using Xunit;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests
+{
+ internal abstract class FakerContainer
+ {
+ protected static int GetFakerSeed()
+ {
+ // The goal here is to have stable data over multiple test runs, but at the same time different data per test case.
+
+ MethodBase testMethod = GetTestMethod();
+ var testName = testMethod.DeclaringType?.FullName + "." + testMethod.Name;
+
+ return GetDeterministicHashCode(testName);
+ }
+
+ private static MethodBase GetTestMethod()
+ {
+ var stackTrace = new StackTrace();
+
+ var testMethod = stackTrace.GetFrames()
+ .Select(stackFrame => stackFrame?.GetMethod())
+ .FirstOrDefault(IsTestMethod);
+
+ if (testMethod == null)
+ {
+ // If called after the first await statement, the test method is no longer on the stack,
+ // but has been replaced with the compiler-generated async/wait state machine.
+ throw new InvalidOperationException("Fakers can only be used from within (the start of) a test method.");
+ }
+
+ return testMethod;
+ }
+
+ private static bool IsTestMethod(MethodBase method)
+ {
+ if (method == null)
+ {
+ return false;
+ }
+
+ return method.GetCustomAttribute(typeof(FactAttribute)) != null || method.GetCustomAttribute(typeof(TheoryAttribute)) != null;
+ }
+
+ private static int GetDeterministicHashCode(string source)
+ {
+ // https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/
+ unchecked
+ {
+ int hash1 = (5381 << 16) + 5381;
+ int hash2 = hash1;
+
+ for (int i = 0; i < source.Length; i += 2)
+ {
+ hash1 = ((hash1 << 5) + hash1) ^ source[i];
+
+ if (i == source.Length - 1)
+ {
+ break;
+ }
+
+ hash2 = ((hash2 << 5) + hash2) ^ source[i + 1];
+ }
+
+ return hash1 + hash2 * 1566083941;
+ }
+ }
+ }
+}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FetchingResourcesTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FetchingResourcesTests.cs
deleted file mode 100644
index b383111..0000000
--- a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/FetchingResourcesTests.cs
+++ /dev/null
@@ -1,160 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net;
-using System.Threading.Tasks;
-using Bogus;
-using Example;
-using Example.Models;
-using Humanizer;
-using JsonApiDotNetCore.Serialization.Objects;
-using Microsoft.Extensions.DependencyInjection;
-using MongoDB.Driver;
-using Xunit;
-
-namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests
-{
- public sealed class FetchingResourcesTests : IClassFixture>, IAsyncLifetime
- {
- private readonly Faker _bookFaker;
-
- private readonly IntegrationTestContext _testContext;
- private readonly IEnumerable _books;
-
- public FetchingResourcesTests(IntegrationTestContext testContext)
- {
- _testContext = testContext;
-
- _bookFaker = new Faker()
- .RuleFor(b => b.Name, f => f.Lorem.Sentence())
- .RuleFor(b => b.Author, f => f.Name.FindName())
- .RuleFor(b => b.Category, f => f.Commerce.ProductAdjective())
- .RuleFor(b => b.Price, f => f.Random.Decimal(1.00M, 50.00M));
-
- _books = GenerateBooks().ToList();
-
- _testContext.ConfigureServicesAfterStartup(services =>
- {
- services.AddSingleton(sp =>
- {
- var client = new MongoClient("mongodb://localhost:27017");
- return client.GetDatabase("JsonApiDotNetCore_MongoDb_Example_Tests");
- });
- });
- }
-
- private IEnumerable GenerateBooks()
- {
- for (var i = 0; i < 30; i++)
- {
- yield return _bookFaker.Generate();
- }
- }
-
- public Task InitializeAsync() => _testContext.RunOnDatabaseAsync(
- db => db.GetCollection(nameof(Book))
- .InsertManyAsync(_books));
-
- public Task DisposeAsync() => _testContext.RunOnDatabaseAsync(db => db.DropCollectionAsync(nameof(Book)));
-
- [Fact]
- public async Task ShouldGetAllResources()
- {
- var route = "/api/Books";
- var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
-
- var expected = _books
- .Select(b => b.StringId)
- .ToArray();
- var actual = responseDocument.ManyData?.Select(x => x.Id).ToArray();
-
- Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
- Assert.Equal(expected.Length, (Int64)responseDocument.Meta["totalResources"]);
- Assert.Equal(expected.Take(10).ToArray(), actual);
- }
-
- [Theory]
- [InlineData("12.99")]
- [InlineData("23.45")]
- [InlineData("12.00")]
- public async Task ShouldGetBooksWithPriceEqualTo(string priceStr)
- {
- var price = Convert.ToDecimal(priceStr);
- var route = $"/api/Books?filter=equals(price,'{price}')";
- var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
-
- var expected = _books
- .Where(b => b.Price == price)
- .Take(10)
- .Select(b => b.StringId)
- .ToArray();
- var actual = responseDocument.ManyData?.Select(x => x.Id).ToArray();
-
- Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
- Assert.Equal(expected.Length, (Int64)responseDocument.Meta["totalResources"]);
- Assert.Equal(expected, actual);
- }
-
- [Theory]
- [InlineData("12.99", "29.34")]
- [InlineData("23.45", "47.22")]
- [InlineData("2.00", "11.32")]
- public async Task ShouldGetBooksWithPriceBetween(string minPriceStr, string maxPriceStr)
- {
- var minPrice = Convert.ToDecimal(minPriceStr);
- var maxPrice = Convert.ToDecimal(maxPriceStr);
-
- var route = $"/api/Books?filter=and(greaterOrEqual(price,'{minPrice}'),lessOrEqual(price,'{maxPrice}'))";
- var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
-
- var expected = _books
- .Where(b => b.Price >= minPrice && b.Price <= maxPrice)
- .Select(b => b.StringId)
- .ToArray();
- var actual = responseDocument.ManyData?.Select(x => x.Id).ToArray();
-
- Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
- Assert.Equal(expected.Length, (Int64)responseDocument.Meta["totalResources"]);
- Assert.Equal(expected.Take(10).ToArray(), actual);
- }
-
- [Theory]
- [InlineData(3, 4)]
- [InlineData(10, 2)]
- [InlineData(20, 1)]
- public async Task ShouldPaginate(int pageSize, int pageNumber)
- {
- var route = $"/api/Books?page[size]={pageSize}&page[number]={pageNumber}";
- var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
-
- var expected = _books
- .Skip((pageNumber - 1) * pageSize)
- .Take(pageSize)
- .Select(b => b.StringId)
- .ToArray();
- var actual = responseDocument.ManyData?.Select(x => x.Id).ToArray();
-
- Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
- Assert.Equal(expected, actual);
- }
-
- [Theory]
- [InlineData("name")]
- [InlineData("price")]
- public async Task ShouldSortByField(string field)
- {
- var route = $"/api/Books?sort={field}";
- var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
-
- var expected = _books
- .OrderBy(b => b.GetType().GetProperty(field.Pascalize())?.GetValue(b))
- .Take(10)
- .Select(b => b.StringId)
- .ToArray();
- var actual = responseDocument.ManyData.Select(x => x.Id).ToArray();
-
- Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
- Assert.Equal(expected, actual);
- }
- }
-}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDataTypeTests.cs
new file mode 100644
index 0000000..b244796
--- /dev/null
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDataTypeTests.cs
@@ -0,0 +1,338 @@
+using System;
+using System.Net;
+using System.Threading.Tasks;
+using FluentAssertions;
+using FluentAssertions.Extensions;
+using Humanizer;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Serialization.Objects;
+using Microsoft.Extensions.DependencyInjection;
+using MongoDB.Driver;
+using Newtonsoft.Json;
+using Xunit;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering
+{
+ public sealed class FilterDataTypeTests : IClassFixture>
+ {
+ private readonly IntegrationTestContext _testContext;
+
+ public FilterDataTypeTests(IntegrationTestContext testContext)
+ {
+ _testContext = testContext;
+
+ _testContext.RegisterResources(builder =>
+ {
+ builder.Add();
+ });
+
+ _testContext.ConfigureServicesAfterStartup(services =>
+ {
+ services.AddResourceRepository>();
+ });
+
+ var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService();
+ options.EnableLegacyFilterNotation = false;
+ options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
+ }
+
+ [Theory]
+ [InlineData(nameof(FilterableResource.SomeString), "text")]
+ [InlineData(nameof(FilterableResource.SomeBoolean), true)]
+ [InlineData(nameof(FilterableResource.SomeNullableBoolean), true)]
+ [InlineData(nameof(FilterableResource.SomeInt32), 1)]
+ [InlineData(nameof(FilterableResource.SomeNullableInt32), 1)]
+ [InlineData(nameof(FilterableResource.SomeUnsignedInt64), 1ul)]
+ [InlineData(nameof(FilterableResource.SomeNullableUnsignedInt64), 1ul)]
+ [InlineData(nameof(FilterableResource.SomeDouble), 0.5d)]
+ [InlineData(nameof(FilterableResource.SomeNullableDouble), 0.5d)]
+ [InlineData(nameof(FilterableResource.SomeEnum), DayOfWeek.Saturday)]
+ [InlineData(nameof(FilterableResource.SomeNullableEnum), DayOfWeek.Saturday)]
+ public async Task Can_filter_equality_on_type(string propertyName, object value)
+ {
+ // Arrange
+ var resource = new FilterableResource();
+ var property = typeof(FilterableResource).GetProperty(propertyName);
+ property?.SetValue(resource, value);
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] {resource, new FilterableResource()});
+ });
+
+ var attributeName = propertyName.Camelize();
+ var route = $"/filterableResources?filter=equals({attributeName},'{value}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes[attributeName].Should().Be(value is Enum ? value.ToString() : value);
+ }
+
+ [Fact]
+ public async Task Can_filter_equality_on_type_Decimal()
+ {
+ // Arrange
+ var resource = new FilterableResource {SomeDecimal = 0.5m};
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] { resource, new FilterableResource() });
+ });
+
+ var route = $"/filterableResources?filter=equals(someDecimal,'{resource.SomeDecimal}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes["someDecimal"].Should().Be(resource.SomeDecimal);
+ }
+
+ [Fact]
+ public async Task Can_filter_equality_on_type_Guid()
+ {
+ // Arrange
+ var resource = new FilterableResource {SomeGuid = Guid.NewGuid()};
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] { resource, new FilterableResource() });
+ });
+
+ var route = $"/filterableResources?filter=equals(someGuid,'{resource.SomeGuid}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes["someGuid"].Should().Be(resource.SomeGuid.ToString());
+ }
+
+ [Fact]
+ public async Task Can_filter_equality_on_type_DateTime()
+ {
+ // Arrange
+ var resource = new FilterableResource {SomeDateTime = 27.January(2003).At(11, 22, 33, 44).AsUtc()};
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] { resource, new FilterableResource() });
+ });
+
+ var route = $"/filterableResources?filter=equals(someDateTime,'{resource.SomeDateTime:O}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes["someDateTime"].Should().Be(resource.SomeDateTime);
+ }
+
+ [Fact]
+ public async Task Can_filter_equality_on_type_DateTimeOffset()
+ {
+ // Arrange
+ var resource = new FilterableResource
+ {
+ SomeDateTimeOffset = new DateTimeOffset(27.January(2003).At(11, 22, 33, 44), TimeSpan.FromHours(3))
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] { resource, new FilterableResource() });
+ });
+
+ var route = $"/filterableResources?filter=equals(someDateTimeOffset,'{WebUtility.UrlEncode(resource.SomeDateTimeOffset.ToString("O"))}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes["someDateTimeOffset"].Should().Be(resource.SomeDateTimeOffset.LocalDateTime);
+ }
+
+ [Fact]
+ public async Task Can_filter_equality_on_type_TimeSpan()
+ {
+ // Arrange
+ var resource = new FilterableResource {SomeTimeSpan = new TimeSpan(1, 2, 3, 4, 5)};
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] { resource, new FilterableResource() });
+ });
+
+ var route = $"/filterableResources?filter=equals(someTimeSpan,'{resource.SomeTimeSpan}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes["someTimeSpan"].Should().Be(resource.SomeTimeSpan.ToString());
+ }
+
+ [Fact]
+ public async Task Cannot_filter_equality_on_incompatible_value()
+ {
+ // Arrange
+ var resource = new FilterableResource {SomeInt32 = 1};
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] { resource, new FilterableResource() });
+ });
+
+ var route = "/filterableResources?filter=equals(someInt32,'ABC')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest);
+
+ responseDocument.Errors.Should().HaveCount(1);
+ responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
+ responseDocument.Errors[0].Title.Should().Be("Query creation failed due to incompatible types.");
+ responseDocument.Errors[0].Detail.Should().Be("Failed to convert 'ABC' of type 'String' to type 'Int32'.");
+ responseDocument.Errors[0].Source.Parameter.Should().BeNull();
+ }
+
+ [Theory]
+ [InlineData(nameof(FilterableResource.SomeString))]
+ [InlineData(nameof(FilterableResource.SomeNullableBoolean))]
+ [InlineData(nameof(FilterableResource.SomeNullableInt32))]
+ [InlineData(nameof(FilterableResource.SomeNullableUnsignedInt64))]
+ [InlineData(nameof(FilterableResource.SomeNullableDecimal))]
+ [InlineData(nameof(FilterableResource.SomeNullableDouble))]
+ [InlineData(nameof(FilterableResource.SomeNullableGuid))]
+ [InlineData(nameof(FilterableResource.SomeNullableDateTime))]
+ [InlineData(nameof(FilterableResource.SomeNullableDateTimeOffset))]
+ [InlineData(nameof(FilterableResource.SomeNullableTimeSpan))]
+ [InlineData(nameof(FilterableResource.SomeNullableEnum))]
+ public async Task Can_filter_is_null_on_type(string propertyName)
+ {
+ // Arrange
+ var resource = new FilterableResource();
+ var property = typeof(FilterableResource).GetProperty(propertyName);
+ property?.SetValue(resource, null);
+
+ var otherResource = new FilterableResource
+ {
+ SomeString = "X",
+ SomeNullableBoolean = true,
+ SomeNullableInt32 = 1,
+ SomeNullableUnsignedInt64 = 1,
+ SomeNullableDecimal = 1,
+ SomeNullableDouble = 1,
+ SomeNullableGuid = Guid.NewGuid(),
+ SomeNullableDateTime = 1.January(2001),
+ SomeNullableDateTimeOffset = 1.January(2001),
+ SomeNullableTimeSpan = TimeSpan.FromHours(1),
+ SomeNullableEnum = DayOfWeek.Friday
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] { resource, otherResource });
+ });
+
+ var attributeName = propertyName.Camelize();
+ var route = $"/filterableResources?filter=equals({attributeName},null)";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes[attributeName].Should().Be(null);
+ }
+
+ [Theory]
+ [InlineData(nameof(FilterableResource.SomeString))]
+ [InlineData(nameof(FilterableResource.SomeNullableBoolean))]
+ [InlineData(nameof(FilterableResource.SomeNullableInt32))]
+ [InlineData(nameof(FilterableResource.SomeNullableUnsignedInt64))]
+ [InlineData(nameof(FilterableResource.SomeNullableDecimal))]
+ [InlineData(nameof(FilterableResource.SomeNullableDouble))]
+ [InlineData(nameof(FilterableResource.SomeNullableGuid))]
+ [InlineData(nameof(FilterableResource.SomeNullableDateTime))]
+ [InlineData(nameof(FilterableResource.SomeNullableDateTimeOffset))]
+ [InlineData(nameof(FilterableResource.SomeNullableTimeSpan))]
+ [InlineData(nameof(FilterableResource.SomeNullableEnum))]
+ public async Task Can_filter_is_not_null_on_type(string propertyName)
+ {
+ // Arrange
+ var resource = new FilterableResource
+ {
+ SomeString = "X",
+ SomeNullableBoolean = true,
+ SomeNullableInt32 = 1,
+ SomeNullableUnsignedInt64 = 1,
+ SomeNullableDecimal = 1,
+ SomeNullableDouble = 1,
+ SomeNullableGuid = Guid.NewGuid(),
+ SomeNullableDateTime = 1.January(2001),
+ SomeNullableDateTimeOffset = 1.January(2001),
+ SomeNullableTimeSpan = TimeSpan.FromHours(1),
+ SomeNullableEnum = DayOfWeek.Friday
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] { resource, new FilterableResource() });
+ });
+
+ var attributeName = propertyName.Camelize();
+ var route = $"/filterableResources?filter=not(equals({attributeName},null))";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes[attributeName].Should().NotBe(null);
+ }
+ }
+}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDepthTests.cs
new file mode 100644
index 0000000..cd718d7
--- /dev/null
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterDepthTests.cs
@@ -0,0 +1,651 @@
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+using FluentAssertions;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Serialization.Objects;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using Microsoft.Extensions.DependencyInjection;
+using MongoDB.Driver;
+using Xunit;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering
+{
+ public sealed class FilterDepthTests : IClassFixture>
+ {
+ private readonly IntegrationTestContext _testContext;
+
+ public FilterDepthTests(IntegrationTestContext testContext)
+ {
+ _testContext = testContext;
+
+ var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService();
+ options.EnableLegacyFilterNotation = false;
+
+ // options.DisableTopPagination = false;
+ // options.DisableChildrenPagination = false;
+ }
+
+ [Fact]
+ public async Task Can_filter_in_primary_resources()
+ {
+ // Arrange
+ var articles = new List
+ {
+ new Article
+ {
+ Caption = "One"
+ },
+ new Article
+ {
+ Caption = "Two"
+ }
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(Article));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(articles);
+ });
+
+ var route = "/api/v1/articles?filter=equals(caption,'Two')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Id.Should().Be(articles[1].StringId);
+ }
+
+ [Fact]
+ public async Task Cannot_filter_in_single_primary_resource()
+ {
+ // Arrange
+ var article = new Article
+ {
+ Caption = "X"
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(Article)).InsertOneAsync(article));
+
+ var route = $"/api/v1/articles/{article.StringId}?filter=equals(caption,'Two')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest);
+
+ responseDocument.Errors.Should().HaveCount(1);
+ responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
+ responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid.");
+ responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource).");
+ responseDocument.Errors[0].Source.Parameter.Should().Be("filter");
+ }
+
+ // [Fact]
+ // public async Task Can_filter_in_secondary_resources()
+ // {
+ // // Arrange
+ // var blog = new Blog
+ // {
+ // Articles = new List
+ // {
+ // new Article
+ // {
+ // Caption = "One"
+ // },
+ // new Article
+ // {
+ // Caption = "Two"
+ // }
+ // }
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // dbContext.Blogs.Add(blog);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // var route = $"/api/v1/blogs/{blog.StringId}/articles?filter=equals(caption,'Two')";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.ManyData[0].Id.Should().Be(blog.Articles[1].StringId);
+ // }
+
+ // [Fact]
+ // public async Task Cannot_filter_in_single_secondary_resource()
+ // {
+ // // Arrange
+ // var article = new Article
+ // {
+ // Caption = "X"
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // dbContext.Articles.Add(article);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // var route = $"/api/v1/articles/{article.StringId}/author?filter=equals(lastName,'Smith')";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest);
+ //
+ // responseDocument.Errors.Should().HaveCount(1);
+ // responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
+ // responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid.");
+ // responseDocument.Errors[0].Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource).");
+ // responseDocument.Errors[0].Source.Parameter.Should().Be("filter");
+ // }
+
+ // [Fact]
+ // public async Task Can_filter_on_HasOne_relationship()
+ // {
+ // // Arrange
+ // var articles = new List
+ // {
+ // new Article
+ // {
+ // Caption = "X",
+ // Author = new Author
+ // {
+ // LastName = "Conner"
+ // }
+ // },
+ // new Article
+ // {
+ // Caption = "X",
+ // Author = new Author
+ // {
+ // LastName = "Smith"
+ // }
+ // }
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // await dbContext.ClearTableAsync();
+ // dbContext.Articles.AddRange(articles);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // var route = "/api/v1/articles?include=author&filter=equals(author.lastName,'Smith')";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.Included.Should().HaveCount(1);
+ //
+ // responseDocument.Included[0].Id.Should().Be(articles[1].Author.StringId);
+ // }
+
+ // [Fact]
+ // public async Task Can_filter_on_HasMany_relationship()
+ // {
+ // // Arrange
+ // var blogs = new List
+ // {
+ // new Blog(),
+ // new Blog
+ // {
+ // Articles = new List
+ // {
+ // new Article
+ // {
+ // Caption = "X"
+ // }
+ // }
+ // }
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // await dbContext.ClearTableAsync();
+ // dbContext.Blogs.AddRange(blogs);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // var route = "/api/v1/blogs?filter=greaterThan(count(articles),'0')";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId);
+ // }
+ //
+ // [Fact]
+ // public async Task Can_filter_on_HasManyThrough_relationship()
+ // {
+ // // Arrange
+ // var articles = new List
+ // {
+ // new Article
+ // {
+ // Caption = "X"
+ // },
+ // new Article
+ // {
+ // Caption = "X",
+ // ArticleTags = new HashSet
+ // {
+ // new ArticleTag
+ // {
+ // Tag = new Tag
+ // {
+ // Name = "Hot"
+ // }
+ // }
+ // }
+ // }
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // await dbContext.ClearTableAsync();
+ // dbContext.Articles.AddRange(articles);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // var route = "/api/v1/articles?filter=has(tags)";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.ManyData[0].Id.Should().Be(articles[1].StringId);
+ // }
+ //
+ // [Fact]
+ // public async Task Can_filter_in_scope_of_HasMany_relationship()
+ // {
+ // // Arrange
+ // var blog = new Blog
+ // {
+ // Articles = new List
+ // {
+ // new Article
+ // {
+ // Caption = "One"
+ // },
+ // new Article
+ // {
+ // Caption = "Two"
+ // }
+ // }
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // await dbContext.ClearTableAsync();
+ // dbContext.Blogs.Add(blog);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // var route = "/api/v1/blogs?include=articles&filter[articles]=equals(caption,'Two')";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.Included.Should().HaveCount(1);
+ //
+ // responseDocument.Included[0].Id.Should().Be(blog.Articles[1].StringId);
+ // }
+ //
+ // [Fact]
+ // public async Task Can_filter_in_scope_of_HasMany_relationship_on_secondary_resource()
+ // {
+ // // Arrange
+ // var blog = new Blog
+ // {
+ // Owner = new Author
+ // {
+ // LastName = "X",
+ // Articles = new List
+ // {
+ // new Article
+ // {
+ // Caption = "One"
+ // },
+ // new Article
+ // {
+ // Caption = "Two"
+ // }
+ // }
+ // }
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // dbContext.Blogs.Add(blog);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // var route = $"/api/v1/blogs/{blog.StringId}/owner?include=articles&filter[articles]=equals(caption,'Two')";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.SingleData.Should().NotBeNull();
+ // responseDocument.Included.Should().HaveCount(1);
+ //
+ // responseDocument.Included[0].Id.Should().Be(blog.Owner.Articles[1].StringId);
+ // }
+ //
+ // [Fact]
+ // public async Task Can_filter_in_scope_of_HasManyThrough_relationship()
+ // {
+ // // Arrange
+ // var articles = new List
+ // {
+ // new Article
+ // {
+ // Caption = "X",
+ // ArticleTags = new HashSet
+ // {
+ // new ArticleTag
+ // {
+ // Tag = new Tag
+ // {
+ // Name = "Cold"
+ // }
+ // }
+ // }
+ // },
+ // new Article
+ // {
+ // Caption = "X",
+ // ArticleTags = new HashSet
+ // {
+ // new ArticleTag
+ // {
+ // Tag = new Tag
+ // {
+ // Name = "Hot"
+ // }
+ // }
+ // }
+ // }
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // await dbContext.ClearTableAsync();
+ // dbContext.Articles.AddRange(articles);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // // Workaround for https://github.com/dotnet/efcore/issues/21026
+ // var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService();
+ // options.DisableTopPagination = false;
+ // options.DisableChildrenPagination = true;
+ //
+ // var route = "/api/v1/articles?include=tags&filter[tags]=equals(name,'Hot')";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(2);
+ // responseDocument.Included.Should().HaveCount(1);
+ //
+ // responseDocument.Included[0].Id.Should().Be(articles[1].ArticleTags.First().Tag.StringId);
+ // }
+ //
+ // [Fact]
+ // public async Task Can_filter_in_scope_of_relationship_chain()
+ // {
+ // // Arrange
+ // var blog = new Blog
+ // {
+ // Owner = new Author
+ // {
+ // LastName = "Smith",
+ // Articles = new List
+ // {
+ // new Article
+ // {
+ // Caption = "One"
+ // },
+ // new Article
+ // {
+ // Caption = "Two"
+ // }
+ // }
+ // }
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // await dbContext.ClearTableAsync();
+ // dbContext.Blogs.Add(blog);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // var route = "/api/v1/blogs?include=owner.articles&filter[owner.articles]=equals(caption,'Two')";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.Included.Should().HaveCount(2);
+ //
+ // responseDocument.Included[0].Id.Should().Be(blog.Owner.StringId);
+ // responseDocument.Included[1].Id.Should().Be(blog.Owner.Articles[1].StringId);
+ // }
+ //
+ // [Fact]
+ // public async Task Can_filter_in_same_scope_multiple_times()
+ // {
+ // // Arrange
+ // var articles = new List
+ // {
+ // new Article
+ // {
+ // Caption = "One"
+ // },
+ // new Article
+ // {
+ // Caption = "Two"
+ // },
+ // new Article
+ // {
+ // Caption = "Three"
+ // }
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // await dbContext.ClearTableAsync();
+ // dbContext.Articles.AddRange(articles);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // var route = "/api/v1/articles?filter=equals(caption,'One')&filter=equals(caption,'Three')";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(2);
+ // responseDocument.ManyData[0].Id.Should().Be(articles[0].StringId);
+ // responseDocument.ManyData[1].Id.Should().Be(articles[2].StringId);
+ // }
+ //
+ // [Fact]
+ // public async Task Can_filter_in_same_scope_multiple_times_using_legacy_notation()
+ // {
+ // // Arrange
+ // var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService();
+ // options.EnableLegacyFilterNotation = true;
+ //
+ // var articles = new List
+ // {
+ // new Article
+ // {
+ // Caption = "One",
+ // Author = new Author
+ // {
+ // FirstName = "Joe",
+ // LastName = "Smith"
+ // }
+ // },
+ // new Article
+ // {
+ // Caption = "Two",
+ // Author = new Author
+ // {
+ // FirstName = "John",
+ // LastName = "Doe"
+ // }
+ // },
+ // new Article
+ // {
+ // Caption = "Three",
+ // Author = new Author
+ // {
+ // FirstName = "Jack",
+ // LastName = "Miller"
+ // }
+ // }
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // await dbContext.ClearTableAsync();
+ // dbContext.Articles.AddRange(articles);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // var route = "/api/v1/articles?filter[author.firstName]=John&filter[author.lastName]=Smith";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(2);
+ // responseDocument.ManyData[0].Id.Should().Be(articles[0].StringId);
+ // responseDocument.ManyData[1].Id.Should().Be(articles[1].StringId);
+ // }
+ //
+ // [Fact]
+ // public async Task Can_filter_in_multiple_scopes()
+ // {
+ // // Arrange
+ // var blogs = new List
+ // {
+ // new Blog(),
+ // new Blog
+ // {
+ // Title = "Technology",
+ // Owner = new Author
+ // {
+ // LastName = "Smith",
+ // Articles = new List
+ // {
+ // new Article
+ // {
+ // Caption = "One"
+ // },
+ // new Article
+ // {
+ // Caption = "Two",
+ // Revisions = new List
+ // {
+ // new Revision
+ // {
+ // PublishTime = 1.January(2000)
+ // },
+ // new Revision
+ // {
+ // PublishTime = 10.January(2010)
+ // }
+ // }
+ // }
+ // }
+ // }
+ // }
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async dbContext =>
+ // {
+ // await dbContext.ClearTableAsync();
+ // dbContext.Blogs.AddRange(blogs);
+ //
+ // await dbContext.SaveChangesAsync();
+ // });
+ //
+ // var route = "/api/v1/blogs?include=owner.articles.revisions&" +
+ // "filter=and(equals(title,'Technology'),has(owner.articles),equals(owner.lastName,'Smith'))&" +
+ // "filter[owner.articles]=equals(caption,'Two')&" +
+ // "filter[owner.articles.revisions]=greaterThan(publishTime,'2005-05-05')";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId);
+ //
+ // responseDocument.Included.Should().HaveCount(3);
+ // responseDocument.Included[0].Id.Should().Be(blogs[1].Owner.StringId);
+ // responseDocument.Included[1].Id.Should().Be(blogs[1].Owner.Articles[1].StringId);
+ // responseDocument.Included[2].Id.Should().Be(blogs[1].Owner.Articles[1].Revisions.Skip(1).First().StringId);
+ // }
+ }
+}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs
new file mode 100644
index 0000000..c1107c0
--- /dev/null
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterOperatorTests.cs
@@ -0,0 +1,567 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+using System.Web;
+using FluentAssertions;
+using Humanizer;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Queries.Expressions;
+using JsonApiDotNetCore.Serialization.Objects;
+using Microsoft.Extensions.DependencyInjection;
+using MongoDB.Driver;
+using Xunit;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering
+{
+ public sealed class FilterOperatorTests : IClassFixture>
+ {
+ private readonly IntegrationTestContext _testContext;
+
+ public FilterOperatorTests(IntegrationTestContext testContext)
+ {
+ _testContext = testContext;
+
+ _testContext.RegisterResources(builder =>
+ {
+ builder.Add();
+ });
+
+ _testContext.ConfigureServicesAfterStartup(services =>
+ {
+ services.AddResourceRepository>();
+ });
+
+ var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService();
+ options.EnableLegacyFilterNotation = false;
+ options.SerializerSettings.DateFormatString = "yyyy-MM-dd";
+ }
+
+ [Fact]
+ public async Task Can_filter_equality_on_special_characters()
+ {
+ // Arrange
+ var resource = new FilterableResource
+ {
+ SomeString = "This, that & more"
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] {resource, new FilterableResource()});
+ });
+
+ var route = $"/filterableResources?filter=equals(someString,'{HttpUtility.UrlEncode(resource.SomeString)}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString);
+ }
+
+ // [Fact]
+ // public async Task Can_filter_equality_on_two_attributes_of_same_type()
+ // {
+ // // Arrange
+ // var resource = new FilterableResource
+ // {
+ // SomeInt32 = 5,
+ // OtherInt32 = 5
+ // };
+ //
+ // var otherResource = new FilterableResource
+ // {
+ // SomeInt32 = 5,
+ // OtherInt32 = 10
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async db =>
+ // {
+ // var collection = db.GetCollection(nameof(FilterableResource));
+ // await collection.DeleteManyAsync(Builders.Filter.Empty);
+ // await collection.InsertManyAsync(new[] {resource, otherResource});
+ // });
+ //
+ // var route = "/filterableResources?filter=equals(someInt32,otherInt32)";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32);
+ // responseDocument.ManyData[0].Attributes["otherInt32"].Should().Be(resource.OtherInt32);
+ // }
+
+ // [Fact]
+ // public async Task Can_filter_equality_on_two_attributes_of_same_nullable_type()
+ // {
+ // // Arrange
+ // var resource = new FilterableResource
+ // {
+ // SomeNullableInt32 = 5,
+ // OtherNullableInt32 = 5
+ // };
+ //
+ // var otherResource = new FilterableResource
+ // {
+ // SomeNullableInt32 = 5,
+ // OtherNullableInt32 = 10
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async db =>
+ // {
+ // var collection = db.GetCollection(nameof(FilterableResource));
+ // await collection.DeleteManyAsync(Builders.Filter.Empty);
+ // await collection.InsertManyAsync(new[] {resource, otherResource});
+ // });
+ //
+ // var route = "/filterableResources?filter=equals(someNullableInt32,otherNullableInt32)";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.ManyData[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32);
+ // responseDocument.ManyData[0].Attributes["otherNullableInt32"].Should().Be(resource.OtherNullableInt32);
+ // }
+
+ // [Fact]
+ // public async Task Can_filter_equality_on_two_attributes_with_nullable_at_start()
+ // {
+ // // Arrange
+ // var resource = new FilterableResource
+ // {
+ // SomeInt32 = 5,
+ // SomeNullableInt32 = 5
+ // };
+ //
+ // var otherResource = new FilterableResource
+ // {
+ // SomeInt32 = 5,
+ // SomeNullableInt32 = 10
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async db =>
+ // {
+ // var collection = db.GetCollection(nameof(FilterableResource));
+ // await collection.DeleteManyAsync(Builders.Filter.Empty);
+ // await collection.InsertManyAsync(new[] {resource, otherResource});
+ // });
+ //
+ // var route = "/filterableResources?filter=equals(someNullableInt32,someInt32)";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32);
+ // responseDocument.ManyData[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32);
+ // }
+
+ // [Fact]
+ // public async Task Can_filter_equality_on_two_attributes_with_nullable_at_end()
+ // {
+ // // Arrange
+ // var resource = new FilterableResource
+ // {
+ // SomeInt32 = 5,
+ // SomeNullableInt32 = 5
+ // };
+ //
+ // var otherResource = new FilterableResource
+ // {
+ // SomeInt32 = 5,
+ // SomeNullableInt32 = 10
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async db =>
+ // {
+ // var collection = db.GetCollection(nameof(FilterableResource));
+ // await collection.DeleteManyAsync(Builders.Filter.Empty);
+ // await collection.InsertManyAsync(new[] {resource, otherResource});
+ // });
+ //
+ // var route = "/filterableResources?filter=equals(someInt32,someNullableInt32)";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32);
+ // responseDocument.ManyData[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32);
+ // }
+
+ // [Fact]
+ // public async Task Can_filter_equality_on_two_attributes_of_compatible_types()
+ // {
+ // // Arrange
+ // var resource = new FilterableResource
+ // {
+ // SomeInt32 = 5,
+ // SomeUnsignedInt64 = 5
+ // };
+ //
+ // var otherResource = new FilterableResource
+ // {
+ // SomeInt32 = 5,
+ // SomeUnsignedInt64 = 10
+ // };
+ //
+ // await _testContext.RunOnDatabaseAsync(async db =>
+ // {
+ // var collection = db.GetCollection(nameof(FilterableResource));
+ // await collection.DeleteManyAsync(Builders.Filter.Empty);
+ // await collection.InsertManyAsync(new[] {resource, otherResource});
+ // });
+ //
+ // var route = "/filterableResources?filter=equals(someInt32,someUnsignedInt64)";
+ //
+ // // Act
+ // var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+ //
+ // // Assert
+ // httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+ //
+ // responseDocument.ManyData.Should().HaveCount(1);
+ // responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32);
+ // responseDocument.ManyData[0].Attributes["someUnsignedInt64"].Should().Be(resource.SomeUnsignedInt64);
+ // }
+
+ [Fact]
+ public async Task Cannot_filter_equality_on_two_attributes_of_incompatible_types()
+ {
+ // Arrange
+ var route = "/filterableResources?filter=equals(someDouble,someTimeSpan)";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest);
+
+ responseDocument.Errors.Should().HaveCount(1);
+ responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
+ responseDocument.Errors[0].Title.Should().Be("Query creation failed due to incompatible types.");
+ responseDocument.Errors[0].Detail.Should().Be("No coercion operator is defined between types 'System.TimeSpan' and 'System.Double'.");
+ responseDocument.Errors[0].Source.Parameter.Should().BeNull();
+ }
+
+ [Theory]
+ [InlineData(19, 21, ComparisonOperator.LessThan, 20)]
+ [InlineData(19, 21, ComparisonOperator.LessThan, 21)]
+ [InlineData(19, 21, ComparisonOperator.LessOrEqual, 20)]
+ [InlineData(19, 21, ComparisonOperator.LessOrEqual, 19)]
+ [InlineData(21, 19, ComparisonOperator.GreaterThan, 20)]
+ [InlineData(21, 19, ComparisonOperator.GreaterThan, 19)]
+ [InlineData(21, 19, ComparisonOperator.GreaterOrEqual, 20)]
+ [InlineData(21, 19, ComparisonOperator.GreaterOrEqual, 21)]
+ public async Task Can_filter_comparison_on_whole_number(int matchingValue, int nonMatchingValue, ComparisonOperator filterOperator, double filterValue)
+ {
+ // Arrange
+ var resource = new FilterableResource
+ {
+ SomeInt32 = matchingValue
+ };
+
+ var otherResource = new FilterableResource
+ {
+ SomeInt32 = nonMatchingValue
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] {resource, otherResource});
+ });
+
+ var route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someInt32,'{filterValue}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32);
+ }
+
+ [Theory]
+ [InlineData(1.9, 2.1, ComparisonOperator.LessThan, 2.0)]
+ [InlineData(1.9, 2.1, ComparisonOperator.LessThan, 2.1)]
+ [InlineData(1.9, 2.1, ComparisonOperator.LessOrEqual, 2.0)]
+ [InlineData(1.9, 2.1, ComparisonOperator.LessOrEqual, 1.9)]
+ [InlineData(2.1, 1.9, ComparisonOperator.GreaterThan, 2.0)]
+ [InlineData(2.1, 1.9, ComparisonOperator.GreaterThan, 1.9)]
+ [InlineData(2.1, 1.9, ComparisonOperator.GreaterOrEqual, 2.0)]
+ [InlineData(2.1, 1.9, ComparisonOperator.GreaterOrEqual, 2.1)]
+ public async Task Can_filter_comparison_on_fractional_number(double matchingValue, double nonMatchingValue, ComparisonOperator filterOperator, double filterValue)
+ {
+ // Arrange
+ var resource = new FilterableResource
+ {
+ SomeDouble = matchingValue
+ };
+
+ var otherResource = new FilterableResource
+ {
+ SomeDouble = nonMatchingValue
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] {resource, otherResource});
+ });
+
+ var route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someDouble,'{filterValue}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes["someDouble"].Should().Be(resource.SomeDouble);
+ }
+
+ [Theory]
+ [InlineData("2001-01-01", "2001-01-09", ComparisonOperator.LessThan, "2001-01-05")]
+ [InlineData("2001-01-01", "2001-01-09", ComparisonOperator.LessThan, "2001-01-09")]
+ [InlineData("2001-01-01", "2001-01-09", ComparisonOperator.LessOrEqual, "2001-01-05")]
+ [InlineData("2001-01-01", "2001-01-09", ComparisonOperator.LessOrEqual, "2001-01-01")]
+ [InlineData("2001-01-09", "2001-01-01", ComparisonOperator.GreaterThan, "2001-01-05")]
+ [InlineData("2001-01-09", "2001-01-01", ComparisonOperator.GreaterThan, "2001-01-01")]
+ [InlineData("2001-01-09", "2001-01-01", ComparisonOperator.GreaterOrEqual, "2001-01-05")]
+ [InlineData("2001-01-09", "2001-01-01", ComparisonOperator.GreaterOrEqual, "2001-01-09")]
+ public async Task Can_filter_comparison_on_DateTime(string matchingDateTime, string nonMatchingDateTime, ComparisonOperator filterOperator, string filterDateTime)
+ {
+ // Arrange
+ var resource = new FilterableResource
+ {
+ SomeDateTime = DateTime.ParseExact(matchingDateTime, "yyyy-MM-dd", null)
+ };
+
+ var otherResource = new FilterableResource
+ {
+ SomeDateTime = DateTime.ParseExact(nonMatchingDateTime, "yyyy-MM-dd", null)
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] {resource, otherResource});
+ });
+
+ var route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someDateTime,'{DateTime.ParseExact(filterDateTime, "yyyy-MM-dd", null)}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes["someDateTime"].Should().Be(resource.SomeDateTime.ToString("yyyy-MM-dd"));
+ }
+
+ [Theory]
+ [InlineData("The fox jumped over the lazy dog", "Other", TextMatchKind.Contains, "jumped")]
+ [InlineData("The fox jumped over the lazy dog", "the fox...", TextMatchKind.Contains, "The")]
+ [InlineData("The fox jumped over the lazy dog", "The fox jumped", TextMatchKind.Contains, "dog")]
+ [InlineData("The fox jumped over the lazy dog", "Yesterday The fox...", TextMatchKind.StartsWith, "The")]
+ [InlineData("The fox jumped over the lazy dog", "over the lazy dog earlier", TextMatchKind.EndsWith, "dog")]
+ public async Task Can_filter_text_match(string matchingText, string nonMatchingText, TextMatchKind matchKind, string filterText)
+ {
+ // Arrange
+ var resource = new FilterableResource
+ {
+ SomeString = matchingText
+ };
+
+ var otherResource = new FilterableResource
+ {
+ SomeString = nonMatchingText
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] {resource, otherResource});
+ });
+
+ var route = $"/filterableResources?filter={matchKind.ToString().Camelize()}(someString,'{filterText}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString);
+ }
+
+ [Theory]
+ [InlineData("two", "one two", "'one','two','three'")]
+ [InlineData("two", "nine", "'one','two','three','four','five'")]
+ public async Task Can_filter_in_set(string matchingText, string nonMatchingText, string filterText)
+ {
+ // Arrange
+ var resource = new FilterableResource
+ {
+ SomeString = matchingText
+ };
+
+ var otherResource = new FilterableResource
+ {
+ SomeString = nonMatchingText
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] {resource, otherResource});
+ });
+
+ var route = $"/filterableResources?filter=any(someString,{filterText})";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString);
+ }
+
+ [Fact]
+ public async Task Can_filter_on_has()
+ {
+ // Arrange
+ var resource = new FilterableResource
+ {
+ Children = new List
+ {
+ new FilterableResource()
+ }
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] {resource, new FilterableResource()});
+ });
+
+ var route = "/filterableResources?filter=has(children)";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Id.Should().Be(resource.StringId);
+ }
+
+ [Fact]
+ public async Task Can_filter_on_count()
+ {
+ // Arrange
+ var resource = new FilterableResource
+ {
+ Children = new List
+ {
+ new FilterableResource(),
+ new FilterableResource()
+ }
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] {resource, new FilterableResource()});
+ });
+
+ var route = "/filterableResources?filter=equals(count(children),'2')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Id.Should().Be(resource.StringId);
+ }
+
+ [Theory]
+ [InlineData("and(equals(someString,'ABC'),equals(someInt32,'11'))")]
+ [InlineData("and(equals(someString,'ABC'),equals(someInt32,'11'),equals(someEnum,'Tuesday'))")]
+ [InlineData("or(equals(someString,'---'),lessThan(someInt32,'33'))")]
+ [InlineData("not(equals(someEnum,'Saturday'))")]
+ public async Task Can_filter_on_logical_functions(string filterExpression)
+ {
+ // Arrange
+ var resource1 = new FilterableResource
+ {
+ SomeString = "ABC",
+ SomeInt32 = 11,
+ SomeEnum = DayOfWeek.Tuesday
+ };
+
+ var resource2 = new FilterableResource
+ {
+ SomeString = "XYZ",
+ SomeInt32 = 99,
+ SomeEnum = DayOfWeek.Saturday
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(FilterableResource));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] {resource1, resource2});
+ });
+
+ var route = $"/filterableResources?filter={filterExpression}";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Id.Should().Be(resource1.StringId);
+ }
+ }
+}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterTests.cs
new file mode 100644
index 0000000..bb68358
--- /dev/null
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterTests.cs
@@ -0,0 +1,111 @@
+using System.Net;
+using System.Threading.Tasks;
+using FluentAssertions;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Serialization.Objects;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using Microsoft.Extensions.DependencyInjection;
+using MongoDB.Driver;
+using Xunit;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering
+{
+ public sealed class FilterTests : IClassFixture>
+ {
+ private readonly IntegrationTestContext _testContext;
+
+ public FilterTests(IntegrationTestContext testContext)
+ {
+ _testContext = testContext;
+
+ var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService();
+ options.EnableLegacyFilterNotation = false;
+ }
+
+ [Fact]
+ public async Task Cannot_filter_in_unknown_scope()
+ {
+ // Arrange
+ var route = "/api/v1/people?filter[doesNotExist]=equals(title,null)";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest);
+
+ responseDocument.Errors.Should().HaveCount(1);
+ responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
+ responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid.");
+ responseDocument.Errors[0].Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'people'.");
+ responseDocument.Errors[0].Source.Parameter.Should().Be("filter[doesNotExist]");
+ }
+
+ [Fact]
+ public async Task Cannot_filter_in_unknown_nested_scope()
+ {
+ // Arrange
+ var route = "/api/v1/people?filter[todoItems.doesNotExist]=equals(title,null)";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest);
+
+ responseDocument.Errors.Should().HaveCount(1);
+ responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
+ responseDocument.Errors[0].Title.Should().Be("The specified filter is invalid.");
+ responseDocument.Errors[0].Detail.Should().Be("Relationship 'todoItems' in 'todoItems.doesNotExist' does not exist on resource 'people'.");
+ responseDocument.Errors[0].Source.Parameter.Should().Be("filter[todoItems.doesNotExist]");
+ }
+
+ [Fact]
+ public async Task Cannot_filter_on_attribute_with_blocked_capability()
+ {
+ // Arrange
+ var route = "/api/v1/todoItems?filter=equals(achievedDate,null)";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest);
+
+ responseDocument.Errors.Should().HaveCount(1);
+ responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest);
+ responseDocument.Errors[0].Title.Should().Be("Filtering on the requested attribute is not allowed.");
+ responseDocument.Errors[0].Detail.Should().Be("Filtering on attribute 'achievedDate' is not allowed.");
+ responseDocument.Errors[0].Source.Parameter.Should().Be("filter");
+ }
+
+ [Fact]
+ public async Task Can_filter_on_ID()
+ {
+ // Arrange
+ var person = new Person
+ {
+ FirstName = "Jane"
+ };
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(Person));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertManyAsync(new[] {person, new Person()});
+ });
+
+ var route = $"/api/v1/people?filter=equals(id,'{person.StringId}')";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.ManyData.Should().HaveCount(1);
+ responseDocument.ManyData[0].Id.Should().Be(person.StringId);
+ responseDocument.ManyData[0].Attributes["firstName"].Should().Be(person.FirstName);
+ }
+ }
+}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs
new file mode 100644
index 0000000..182a312
--- /dev/null
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResource.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using JsonApiDotNetCore.Resources;
+using JsonApiDotNetCore.Resources.Annotations;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization.Attributes;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering
+{
+ public sealed class FilterableResource : IIdentifiable
+ {
+ [BsonId]
+ [BsonRepresentation(BsonType.ObjectId)]
+ [Attr]
+ public string Id { get; set; }
+
+ [Attr] public string SomeString { get; set; }
+
+ [Attr] public bool SomeBoolean { get; set; }
+ [Attr] public bool? SomeNullableBoolean { get; set; }
+
+ [Attr] public int SomeInt32 { get; set; }
+ [Attr] public int? SomeNullableInt32 { get; set; }
+
+ [Attr] public int OtherInt32 { get; set; }
+ [Attr] public int? OtherNullableInt32 { get; set; }
+
+ [Attr] public ulong SomeUnsignedInt64 { get; set; }
+ [Attr] public ulong? SomeNullableUnsignedInt64 { get; set; }
+
+ [Attr] public decimal SomeDecimal { get; set; }
+ [Attr] public decimal? SomeNullableDecimal { get; set; }
+
+ [Attr] public double SomeDouble { get; set; }
+ [Attr] public double? SomeNullableDouble { get; set; }
+
+ [Attr] public Guid SomeGuid { get; set; }
+ [Attr] public Guid? SomeNullableGuid { get; set; }
+
+ [Attr] public DateTime SomeDateTime { get; set; }
+ [Attr] public DateTime? SomeNullableDateTime { get; set; }
+
+ [Attr] public DateTimeOffset SomeDateTimeOffset { get; set; }
+ [Attr] public DateTimeOffset? SomeNullableDateTimeOffset { get; set; }
+
+ [Attr] public TimeSpan SomeTimeSpan { get; set; }
+ [Attr] public TimeSpan? SomeNullableTimeSpan { get; set; }
+
+ [Attr] public DayOfWeek SomeEnum { get; set; }
+ [Attr] public DayOfWeek? SomeNullableEnum { get; set; }
+
+ [HasMany] public ICollection Children { get; set; }
+
+ [BsonIgnore]
+ public string StringId { get => Id; set => Id = value; }
+ }
+}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResourcesController.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResourcesController.cs
new file mode 100644
index 0000000..42eb764
--- /dev/null
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Filtering/FilterableResourcesController.cs
@@ -0,0 +1,16 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Services;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Filtering
+{
+ public sealed class FilterableResourcesController : JsonApiController
+ {
+ public FilterableResourcesController(IJsonApiOptions options, ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ {
+ }
+ }
+}
diff --git a/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Meta/TopLevelCountTests.cs
new file mode 100644
index 0000000..fa5ca00
--- /dev/null
+++ b/test/JsonApiDotNetCore.MongoDb.Example.Tests/IntegrationTests/Meta/TopLevelCountTests.cs
@@ -0,0 +1,127 @@
+using System.Net;
+using System.Threading.Tasks;
+using FluentAssertions;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Serialization.Objects;
+using JsonApiDotNetCore.MongoDb.Example.Models;
+using Microsoft.Extensions.DependencyInjection;
+using MongoDB.Driver;
+using Xunit;
+
+namespace JsonApiDotNetCore.MongoDb.Example.Tests.IntegrationTests.Meta
+{
+ public sealed class TopLevelCountTests : IClassFixture>
+ {
+ private readonly IntegrationTestContext _testContext;
+
+ public TopLevelCountTests(IntegrationTestContext testContext)
+ {
+ _testContext = testContext;
+
+ var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService();
+ options.IncludeTotalResourceCount = true;
+ }
+
+ [Fact]
+ public async Task Total_Resource_Count_Included_For_Collection()
+ {
+ // Arrange
+ var todoItem = new TodoItem();
+
+ await _testContext.RunOnDatabaseAsync(async db =>
+ {
+ var collection = db.GetCollection(nameof(TodoItem));
+ await collection.DeleteManyAsync(Builders.Filter.Empty);
+ await collection.InsertOneAsync(todoItem);
+ });
+
+ var route = "/api/v1/todoItems";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.Meta.Should().NotBeNull();
+ responseDocument.Meta["totalResources"].Should().Be(1);
+ }
+
+ [Fact]
+ public async Task Total_Resource_Count_Included_For_Empty_Collection()
+ {
+ // Arrange
+ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection(nameof(TodoItem)).DeleteManyAsync(Builders.Filter.Empty));
+
+ var route = "/api/v1/todoItems";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
+
+ responseDocument.Meta.Should().NotBeNull();
+ responseDocument.Meta["totalResources"].Should().Be(0);
+ }
+
+ [Fact]
+ public async Task Total_Resource_Count_Excluded_From_POST_Response()
+ {
+ // Arrange
+ var requestBody = new
+ {
+ data = new
+ {
+ type = "todoItems",
+ attributes = new
+ {
+ description = "Something"
+ }
+ }
+ };
+
+ var route = "/api/v1/todoItems";
+
+ // Act
+ var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody);
+
+ // Assert
+ httpResponse.Should().HaveStatusCode(HttpStatusCode.Created);
+
+ responseDocument.Meta.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task Total_Resource_Count_Excluded_From_PATCH_Response()
+ {
+ // Arrange
+ var todoItem = new TodoItem();
+
+ await _testContext.RunOnDatabaseAsync(async db => await db.GetCollection