├── .config └── dotnet-tools.json ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build.yml │ ├── codeql.yml │ └── deps-review.yml ├── .gitignore ├── .idea └── .idea.JsonApiDotNetCore.MongoDb │ └── .idea │ ├── .gitignore │ └── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── Build.ps1 ├── CSharpGuidelinesAnalyzer.config ├── CodingGuidelines.ruleset ├── Directory.Build.props ├── JetBrainsInspectCodeTransform.xslt ├── JsonApiDotNetCore.MongoDb.sln ├── JsonApiDotNetCore.MongoDb.sln.DotSettings ├── LICENSE ├── PackageReadme.md ├── README.md ├── WarningSeverities.DotSettings ├── appveyor.yml ├── cleanupcode.ps1 ├── codecov.yml ├── inspectcode.ps1 ├── package-icon.png ├── package-versions.props ├── run-docker-mongodb.ps1 ├── src ├── Examples │ ├── GettingStarted │ │ ├── GettingStarted.csproj │ │ ├── Models │ │ │ └── Book.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── README.md │ │ └── appsettings.json │ └── JsonApiDotNetCoreMongoDbExample │ │ ├── Controllers │ │ └── OperationsController.cs │ │ ├── Definitions │ │ └── TodoItemDefinition.cs │ │ ├── JsonApiDotNetCoreMongoDbExample.csproj │ │ ├── Models │ │ ├── TodoItem.cs │ │ └── TodoItemPriority.cs │ │ ├── Program.cs │ │ ├── Properties │ │ └── launchSettings.json │ │ └── appsettings.json └── JsonApiDotNetCore.MongoDb │ ├── AtomicOperations │ ├── MongoTransaction.cs │ └── MongoTransactionFactory.cs │ ├── Configuration │ ├── ResourceGraphExtensions.cs │ └── ServiceCollectionExtensions.cs │ ├── Errors │ ├── AttributeComparisonInFilterNotSupportedException.cs │ └── UnsupportedRelationshipException.cs │ ├── JsonApiDotNetCore.MongoDb.csproj │ ├── PolyfillCollectionExtensions.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── Queries │ └── HideRelationshipsSparseFieldSetCache.cs │ ├── ReadOnlySet.cs │ ├── Repositories │ ├── IMongoDataAccess.cs │ ├── MongoDataAccess.cs │ ├── MongoQueryExpressionValidator.cs │ └── MongoRepository.cs │ └── Resources │ ├── FreeStringMongoIdentifiable.cs │ ├── HexStringMongoIdentifiable.cs │ └── IMongoIdentifiable.cs ├── test ├── JsonApiDotNetCoreMongoDbTests │ ├── IntegrationTests │ │ ├── AtomicOperations │ │ │ ├── AtomicOperationsCollectionFixture.cs │ │ │ ├── AtomicOperationsFixture.cs │ │ │ ├── BaseForAtomicOperationsTestsThatChangeOptions.cs │ │ │ ├── Creating │ │ │ │ ├── AtomicCreateResourceTests.cs │ │ │ │ ├── AtomicCreateResourceWithClientGeneratedIdTests.cs │ │ │ │ ├── AtomicCreateResourceWithToManyRelationshipTests.cs │ │ │ │ └── AtomicCreateResourceWithToOneRelationshipTests.cs │ │ │ ├── Deleting │ │ │ │ └── AtomicDeleteResourceTests.cs │ │ │ ├── ImplicitlyChangingTextLanguageDefinition.cs │ │ │ ├── LocalIds │ │ │ │ └── AtomicLocalIdTests.cs │ │ │ ├── Lyric.cs │ │ │ ├── Meta │ │ │ │ ├── AtomicResourceMetaTests.cs │ │ │ │ ├── MusicTrackMetaDefinition.cs │ │ │ │ └── TextLanguageMetaDefinition.cs │ │ │ ├── Mixed │ │ │ │ └── MaximumOperationsPerRequestTests.cs │ │ │ ├── MusicTrack.cs │ │ │ ├── OperationsController.cs │ │ │ ├── OperationsDbContext.cs │ │ │ ├── OperationsFakers.cs │ │ │ ├── Performer.cs │ │ │ ├── Playlist.cs │ │ │ ├── RecordCompany.cs │ │ │ ├── TextLanguage.cs │ │ │ ├── Transactions │ │ │ │ ├── AtomicRollbackTests.cs │ │ │ │ ├── AtomicTransactionConsistencyTests.cs │ │ │ │ ├── LyricRepository.cs │ │ │ │ ├── MusicTrackRepository.cs │ │ │ │ └── PerformerRepository.cs │ │ │ └── Updating │ │ │ │ ├── Relationships │ │ │ │ ├── AtomicAddToToManyRelationshipTests.cs │ │ │ │ ├── AtomicRemoveFromToManyRelationshipTests.cs │ │ │ │ ├── AtomicReplaceToManyRelationshipTests.cs │ │ │ │ └── AtomicUpdateToOneRelationshipTests.cs │ │ │ │ └── Resources │ │ │ │ ├── AtomicReplaceToManyRelationshipTests.cs │ │ │ │ ├── AtomicUpdateResourceTests.cs │ │ │ │ └── AtomicUpdateToOneRelationshipTests.cs │ │ ├── HitCountingResourceDefinition.cs │ │ ├── Meta │ │ │ ├── MetaDbContext.cs │ │ │ ├── MetaFakers.cs │ │ │ ├── ResourceMetaTests.cs │ │ │ ├── SupportTicket.cs │ │ │ ├── SupportTicketDefinition.cs │ │ │ └── TopLevelCountTests.cs │ │ ├── QueryStrings │ │ │ ├── AccountPreferences.cs │ │ │ ├── Blog.cs │ │ │ ├── BlogPost.cs │ │ │ ├── Comment.cs │ │ │ ├── Filtering │ │ │ │ ├── FilterDataTypeTests.cs │ │ │ │ ├── FilterDbContext.cs │ │ │ │ ├── FilterDepthTests.cs │ │ │ │ ├── FilterOperatorTests.cs │ │ │ │ ├── FilterTests.cs │ │ │ │ └── FilterableResource.cs │ │ │ ├── Includes │ │ │ │ └── IncludeTests.cs │ │ │ ├── Label.cs │ │ │ ├── LabelColor.cs │ │ │ ├── LoginAttempt.cs │ │ │ ├── Pagination │ │ │ │ ├── PaginationWithTotalCountTests.cs │ │ │ │ └── RangeValidationTests.cs │ │ │ ├── QueryStringDbContext.cs │ │ │ ├── QueryStringFakers.cs │ │ │ ├── Sorting │ │ │ │ └── SortTests.cs │ │ │ ├── SparseFieldSets │ │ │ │ ├── ResourceCaptureStore.cs │ │ │ │ ├── ResultCapturingRepository.cs │ │ │ │ └── SparseFieldSetTests.cs │ │ │ └── WebAccount.cs │ │ ├── ReadWrite │ │ │ ├── Creating │ │ │ │ ├── CreateResourceTests.cs │ │ │ │ ├── CreateResourceWithClientGeneratedIdTests.cs │ │ │ │ ├── CreateResourceWithToManyRelationshipTests.cs │ │ │ │ └── CreateResourceWithToOneRelationshipTests.cs │ │ │ ├── Deleting │ │ │ │ └── DeleteResourceTests.cs │ │ │ ├── Fetching │ │ │ │ ├── FetchRelationshipTests.cs │ │ │ │ └── FetchResourceTests.cs │ │ │ ├── ImplicitlyChangingWorkItemDefinition.cs │ │ │ ├── ImplicitlyChangingWorkItemGroupDefinition.cs │ │ │ ├── ModelWithIntId.cs │ │ │ ├── ReadWriteDbContext.cs │ │ │ ├── ReadWriteFakers.cs │ │ │ ├── RgbColor.cs │ │ │ ├── Updating │ │ │ │ ├── Relationships │ │ │ │ │ ├── AddToToManyRelationshipTests.cs │ │ │ │ │ ├── RemoveFromToManyRelationshipTests.cs │ │ │ │ │ ├── ReplaceToManyRelationshipTests.cs │ │ │ │ │ └── UpdateToOneRelationshipTests.cs │ │ │ │ └── Resources │ │ │ │ │ ├── ReplaceToManyRelationshipTests.cs │ │ │ │ │ ├── UpdateResourceTests.cs │ │ │ │ │ └── UpdateToOneRelationshipTests.cs │ │ │ ├── UserAccount.cs │ │ │ ├── WorkItem.cs │ │ │ ├── WorkItemGroup.cs │ │ │ ├── WorkItemPriority.cs │ │ │ └── WorkTag.cs │ │ ├── ResourceDefinitionExtensibilityPoints.cs │ │ ├── ResourceDefinitionHitCounter.cs │ │ ├── ResourceDefinitions │ │ │ └── Reading │ │ │ │ ├── IClientSettingsProvider.cs │ │ │ │ ├── Moon.cs │ │ │ │ ├── MoonDefinition.cs │ │ │ │ ├── Planet.cs │ │ │ │ ├── PlanetDefinition.cs │ │ │ │ ├── ResourceDefinitionReadTests.cs │ │ │ │ ├── Star.cs │ │ │ │ ├── StarDefinition.cs │ │ │ │ ├── StarKind.cs │ │ │ │ ├── TestClientSettingsProvider.cs │ │ │ │ ├── UniverseDbContext.cs │ │ │ │ └── UniverseFakers.cs │ │ └── TestableStartup.cs │ └── JsonApiDotNetCoreMongoDbTests.csproj └── TestBuildingBlocks │ ├── DateTimeExtensions.cs │ ├── DummyTest.cs │ ├── FakerExtensions.cs │ ├── FluentExtensions.cs │ ├── FluentMetaExtensions.cs │ ├── HttpResponseMessageExtensions.cs │ ├── IntegrationTest.cs │ ├── IntegrationTestContext.cs │ ├── MarkedText.cs │ ├── MongoDbContextShim.cs │ ├── MongoDbSetShim.cs │ ├── MongoRunnerProvider.cs │ ├── NeverSameResourceChangeTracker.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── ResourceTypeFinder.cs │ ├── ServiceCollectionExtensions.cs │ ├── TestBuildingBlocks.csproj │ ├── TestControllerProvider.cs │ ├── Unknown.cs │ └── appsettings.json └── tests.runsettings /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "jetbrains.resharper.globaltools": { 6 | "version": "2024.3.6", 7 | "commands": [ 8 | "jb" 9 | ], 10 | "rollForward": false 11 | }, 12 | "regitlint": { 13 | "version": "6.3.13", 14 | "commands": [ 15 | "regitlint" 16 | ], 17 | "rollForward": false 18 | }, 19 | "dotnet-reportgenerator-globaltool": { 20 | "version": "5.4.5", 21 | "commands": [ 22 | "reportgenerator" 23 | ], 24 | "rollForward": false 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: json-api-dotnet 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve. 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | #### DESCRIPTION 13 | 14 | 15 | #### STEPS TO REPRODUCE 16 | 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 22 | #### EXPECTED BEHAVIOR 23 | 24 | 25 | #### ACTUAL BEHAVIOR 26 | 27 | 28 | #### VERSIONS USED 29 | - JsonApiDotNetCore.MongoDb version: 30 | - JsonApiDotNetCore version: 31 | - ASP.NET Core version: 32 | - MongoDB version: 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Documentation 4 | url: https://www.jsonapi.net/usage/resources/index.html 5 | about: Read our comprehensive documentation. 6 | - name: Sponsor JsonApiDotNetCore 7 | url: https://github.com/sponsors/json-api-dotnet 8 | about: Help the continued development. 9 | - name: Ask on Gitter 10 | url: https://gitter.im/json-api-dotnet-core/Lobby 11 | about: Get in touch with the whole community. 12 | - name: Ask on Stack Overflow 13 | url: https://stackoverflow.com/questions/tagged/json-api 14 | about: The best place for asking general-purpose questions. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project. 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | 14 | 15 | **Describe the solution you'd like** 16 | 17 | 18 | **Describe alternatives you've considered** 19 | 20 | 21 | **Additional context** 22 | 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question. 4 | title: '' 5 | labels: 'question' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 13 | 14 | #### SUMMARY 15 | 18 | 19 | #### DETAILS 20 | 24 | 25 | 26 | #### STEPS TO REPRODUCE 27 | 30 | 31 | 1. 32 | 2. 33 | 3. 34 | 35 | #### VERSIONS USED 36 | - JsonApiDotNetCore.MongoDb version: 37 | - JsonApiDotNetCore version: 38 | - ASP.NET Core version: 39 | - MongoDB version: 40 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | Closes #{ISSUE_NUMBER} 4 | 5 | #### QUALITY CHECKLIST 6 | - [ ] Changes implemented in code 7 | - [ ] Complies with our [contributing guidelines](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/.github/CONTRIBUTING.md) 8 | - [ ] Adapted tests 9 | - [ ] Documentation updated 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | pull-request-branch-name: 8 | separator: "-" 9 | - package-ecosystem: nuget 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | pull-request-branch-name: 14 | separator: "-" 15 | open-pull-requests-limit: 25 16 | ignore: 17 | # Block updates to all exposed dependencies of the NuGet packages we produce, as updating them would be a breaking change. 18 | - dependency-name: "JsonApiDotNetCore*" 19 | - dependency-name: "MongoDB.Driver*" 20 | # Block major updates of packages that require a matching .NET version. 21 | - dependency-name: "Microsoft.AspNetCore*" 22 | update-types: ["version-update:semver-major"] 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ 'master', 'release/**' ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ 'master', 'release/**' ] 9 | schedule: 10 | - cron: '0 0 * * 5' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: 'ubuntu-latest' 16 | timeout-minutes: 60 17 | permissions: 18 | actions: read 19 | contents: read 20 | security-events: write 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'csharp' ] 25 | steps: 26 | - name: Setup .NET 27 | uses: actions/setup-dotnet@v4 28 | with: 29 | dotnet-version: | 30 | 8.0.* 31 | 9.0.* 32 | - name: Git checkout 33 | uses: actions/checkout@v4 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | with: 37 | languages: ${{ matrix.language }} 38 | - name: Autobuild 39 | uses: github/codeql-action/autobuild@v3 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@v3 42 | with: 43 | category: "/language:${{matrix.language}}" 44 | -------------------------------------------------------------------------------- /.github/workflows/deps-review.yml: -------------------------------------------------------------------------------- 1 | name: 'Dependency Review' 2 | on: [pull_request] 3 | 4 | permissions: 5 | contents: read 6 | 7 | jobs: 8 | dependency-review: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 'Checkout Repository' 12 | uses: actions/checkout@v4 13 | - name: 'Dependency Review' 14 | uses: actions/dependency-review-action@v4 15 | -------------------------------------------------------------------------------- /.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Empty .gitignore file to prevent Rider from adding one 2 | -------------------------------------------------------------------------------- /.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 8 | 9 | 11 | 12 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/.idea.JsonApiDotNetCore.MongoDb/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /Build.ps1: -------------------------------------------------------------------------------- 1 | function VerifySuccessExitCode { 2 | if ($LastExitCode -ne 0) { 3 | throw "Command failed with exit code $LastExitCode." 4 | } 5 | } 6 | 7 | Write-Host "$(pwsh --version)" 8 | Write-Host "Active .NET SDK: $(dotnet --version)" 9 | Write-Host "Using version suffix: $versionSuffix" 10 | 11 | Remove-Item -Recurse -Force artifacts -ErrorAction SilentlyContinue 12 | Remove-Item -Recurse -Force * -Include coverage.cobertura.xml 13 | 14 | dotnet tool restore 15 | VerifySuccessExitCode 16 | 17 | dotnet build --configuration Release 18 | VerifySuccessExitCode 19 | 20 | dotnet test --no-build --configuration Release --verbosity quiet --collect:"XPlat Code Coverage" 21 | VerifySuccessExitCode 22 | 23 | dotnet reportgenerator -reports:**\coverage.cobertura.xml -targetdir:artifacts\coverage -filefilters:-*.g.cs 24 | VerifySuccessExitCode 25 | 26 | dotnet pack --no-build --configuration Release --output artifacts/packages 27 | VerifySuccessExitCode 28 | -------------------------------------------------------------------------------- /CSharpGuidelinesAnalyzer.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CodingGuidelines.ruleset: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | enable 4 | latest 5 | enable 6 | false 7 | false 8 | true 9 | Recommended 10 | $(MSBuildThisFileDirectory)CodingGuidelines.ruleset 11 | $(MSBuildThisFileDirectory)tests.runsettings 12 | 5.7.2 13 | pre 14 | direct 15 | 16 | 17 | 18 | 25 | IDE0028;IDE0300;IDE0301;IDE0302;IDE0303;IDE0304;IDE0305;IDE0306 26 | $(NoWarn);$(UseCollectionExpressionRules) 27 | 28 | 29 | 30 | $(NoWarn);AV2210 31 | 32 | 33 | 34 | $(NoWarn);1591 35 | true 36 | true 37 | 38 | 39 | 40 | true 41 | 42 | 43 | 44 | $(NoWarn);CA1707;CA1062 45 | 46 | 47 | 48 | $(NoWarn);CA1062 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /JetBrainsInspectCodeTransform.xslt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | JetBrains Inspect Code Report 10 | 11 | 16 | 17 | 18 |

JetBrains InspectCode Report

19 | 20 | 21 |

22 | : 23 |

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | 39 | 42 | 45 | 46 | 47 |
FileLine NumberTypeMessage
34 | 35 | 37 | 38 | 40 | 41 | 43 | 44 |
48 |
49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Alvaro Nicoli 2 | Copyright (c) 2021 Bart Koelman 3 | 4 | MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /PackageReadme.md: -------------------------------------------------------------------------------- 1 | [MongoDB](https://www.mongodb.com/) persistence for [JsonApiDotNetCore](https://www.jsonapi.net/), which is a framework for building JSON:API compliant REST APIs using ASP.NET Core. 2 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2022 2 | 3 | version: '{build}' 4 | 5 | branches: 6 | only: 7 | - master 8 | - /release\/.+/ 9 | 10 | build: off 11 | test: off 12 | deploy: off 13 | -------------------------------------------------------------------------------- /cleanupcode.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 7.0 2 | 3 | # This script reformats (part of) the codebase to make it compliant with our coding guidelines. 4 | 5 | param( 6 | # Git branch name or base commit hash to reformat only the subset of changed files. Omit for all files. 7 | [string] $revision 8 | ) 9 | 10 | function VerifySuccessExitCode { 11 | if ($LastExitCode -ne 0) { 12 | throw "Command failed with exit code $LastExitCode." 13 | } 14 | } 15 | 16 | dotnet tool restore 17 | VerifySuccessExitCode 18 | 19 | dotnet restore 20 | VerifySuccessExitCode 21 | 22 | if ($revision) { 23 | $headCommitHash = git rev-parse HEAD 24 | VerifySuccessExitCode 25 | 26 | $baseCommitHash = git rev-parse $revision 27 | VerifySuccessExitCode 28 | 29 | if ($baseCommitHash -eq $headCommitHash) { 30 | Write-Output "Running code cleanup on staged/unstaged files." 31 | dotnet regitlint -s JsonApiDotNetCore.MongoDb.sln --print-command --skip-tool-check --max-runs=5 --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="JADNC Full Cleanup" --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --verbosity=WARN -f staged,modified 32 | VerifySuccessExitCode 33 | } 34 | else { 35 | Write-Output "Running code cleanup on commit range $baseCommitHash..$headCommitHash, including staged/unstaged files." 36 | dotnet regitlint -s JsonApiDotNetCore.MongoDb.sln --print-command --skip-tool-check --max-runs=5 --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="JADNC Full Cleanup" --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --verbosity=WARN -f staged,modified,commits -a $headCommitHash -b $baseCommitHash 37 | VerifySuccessExitCode 38 | } 39 | } 40 | else { 41 | Write-Output "Running code cleanup on all files." 42 | dotnet regitlint -s JsonApiDotNetCore.MongoDb.sln --print-command --skip-tool-check --jb --dotnetcoresdk=$(dotnet --version) --jb-profile="JADNC Full Cleanup" --jb --properties:Configuration=Release --jb --properties:RunAnalyzers=false --jb --verbosity=WARN 43 | VerifySuccessExitCode 44 | } 45 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 10% 7 | informational: true 8 | patch: 9 | default: 10 | informational: true 11 | 12 | github_checks: 13 | annotations: false 14 | -------------------------------------------------------------------------------- /inspectcode.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 7.0 2 | 3 | # This script runs code inspection and opens the results in a web browser. 4 | 5 | dotnet tool restore 6 | 7 | if ($LastExitCode -ne 0) { 8 | throw "Tool restore failed with exit code $LastExitCode" 9 | } 10 | 11 | $outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml') 12 | $resultPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.html') 13 | dotnet jb inspectcode JsonApiDotNetCore.MongoDb.sln --dotnetcoresdk=$(dotnet --version) --build --output="$outputPath" --format="xml" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --properties:RunAnalyzers=false --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=GlobalPerProduct -dsl=SolutionPersonal -dsl=ProjectPersonal 14 | 15 | if ($LastExitCode -ne 0) { 16 | throw "Code inspection failed with exit code $LastExitCode" 17 | } 18 | 19 | [xml]$xml = Get-Content "$outputPath" 20 | if ($xml.report.Issues -and $xml.report.Issues.Project) { 21 | $xslt = new-object System.Xml.Xsl.XslCompiledTransform; 22 | $xslt.Load("$pwd/JetBrainsInspectCodeTransform.xslt"); 23 | $xslt.Transform($outputPath, $resultPath); 24 | 25 | Write-Output "Opening results in browser" 26 | Invoke-Item "$resultPath" 27 | } 28 | -------------------------------------------------------------------------------- /package-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/48e257c4b915304512ec6c692107329e4ca4d3a2/package-icon.png -------------------------------------------------------------------------------- /package-versions.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5.7.1 5 | 3.3.0 6 | 7 | 8 | 35.6.* 9 | 6.0.* 10 | 3.1.* 11 | 7.2.* 12 | 2.4.* 13 | 2.0.* 14 | 3.3.* 15 | 17.13.* 16 | 2.9.* 17 | 2.8.* 18 | 19 | 20 | 21 | 22 | 9.0.* 23 | 24 | 25 | 26 | 27 | 8.0.* 28 | 29 | 30 | -------------------------------------------------------------------------------- /run-docker-mongodb.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 7.0 2 | 3 | # This script starts a MongoDB database in a docker container, which is required for running examples locally. 4 | 5 | docker container stop jsonapi-mongo-db 6 | docker run --pull always --rm --detach --name jsonapi-mongo-db -p 27017:27017 mongo:latest 7 | -------------------------------------------------------------------------------- /src/Examples/GettingStarted/GettingStarted.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0;net8.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Examples/GettingStarted/Models/Book.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.MongoDb.Resources; 3 | using JsonApiDotNetCore.Resources.Annotations; 4 | 5 | namespace GettingStarted.Models; 6 | 7 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 8 | [Resource] 9 | public sealed class Book : HexStringMongoIdentifiable 10 | { 11 | [Attr] 12 | public string Title { get; set; } = null!; 13 | 14 | [Attr] 15 | public string Author { get; set; } = null!; 16 | 17 | [Attr] 18 | public int PublishYear { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/Examples/GettingStarted/Program.cs: -------------------------------------------------------------------------------- 1 | using GettingStarted.Models; 2 | using JsonApiDotNetCore.Configuration; 3 | using JsonApiDotNetCore.MongoDb.Configuration; 4 | using JsonApiDotNetCore.MongoDb.Repositories; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | using MongoDB.Driver; 7 | 8 | WebApplicationBuilder builder = WebApplication.CreateBuilder(args); 9 | 10 | // Add services to the container. 11 | 12 | builder.Services.TryAddSingleton(_ => 13 | { 14 | var client = new MongoClient(builder.Configuration.GetSection("DatabaseSettings:ConnectionString").Value); 15 | return client.GetDatabase(builder.Configuration.GetSection("DatabaseSettings:Database").Value); 16 | }); 17 | 18 | builder.Services.AddJsonApi(options => 19 | { 20 | options.Namespace = "api"; 21 | options.UseRelativeLinks = true; 22 | options.IncludeTotalResourceCount = true; 23 | 24 | #if DEBUG 25 | options.IncludeExceptionStackTraceInErrors = true; 26 | options.IncludeRequestBodyInErrors = true; 27 | options.SerializerOptions.WriteIndented = true; 28 | #endif 29 | }, resources: resourceGraphBuilder => resourceGraphBuilder.Add()); 30 | 31 | builder.Services.AddJsonApiMongoDb(); 32 | 33 | builder.Services.AddResourceRepository>(); 34 | 35 | WebApplication app = builder.Build(); 36 | 37 | // Configure the HTTP request pipeline. 38 | 39 | app.UseRouting(); 40 | app.UseJsonApi(); 41 | app.MapControllers(); 42 | 43 | var database = app.Services.GetRequiredService(); 44 | await CreateSampleDataAsync(database); 45 | 46 | await app.RunAsync(); 47 | 48 | static async Task CreateSampleDataAsync(IMongoDatabase database) 49 | { 50 | await database.DropCollectionAsync(nameof(Book)); 51 | 52 | await database.GetCollection(nameof(Book)).InsertManyAsync(new[] 53 | { 54 | new Book 55 | { 56 | Title = "Frankenstein", 57 | PublishYear = 1818, 58 | Author = "Mary Shelley" 59 | }, 60 | new Book 61 | { 62 | Title = "Robinson Crusoe", 63 | PublishYear = 1719, 64 | Author = "Daniel Defoe" 65 | }, 66 | new Book 67 | { 68 | Title = "Gulliver's Travels", 69 | PublishYear = 1726, 70 | Author = "Jonathan Swift" 71 | } 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /src/Examples/GettingStarted/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:24141" 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "api/books", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "Kestrel": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "api/books", 23 | "applicationUrl": "http://localhost:24141", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Examples/GettingStarted/README.md: -------------------------------------------------------------------------------- 1 | ## Sample project 2 | 3 | ## Usage 4 | 5 | `dotnet run` to run the project 6 | 7 | You can verify the project is running by checking this endpoint: 8 | `localhost:24141/api/people` 9 | 10 | For further documentation and implementation of a JsonApiDotNetCore application, see the documentation or GitHub page: 11 | 12 | Repository: https://github.com/json-api-dotnet/JsonApiDotNetCore 13 | Documentation: https://www.jsonapi.net 14 | -------------------------------------------------------------------------------- /src/Examples/GettingStarted/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "DatabaseSettings": { 3 | "ConnectionString": "mongodb://localhost:27017", 4 | "Database": "JsonApiDotNetCoreMongoDbGettingStarted" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Warning", 9 | // Include server startup and incoming requests. 10 | "Microsoft.Hosting.Lifetime": "Information", 11 | "Microsoft.AspNetCore.Hosting.Diagnostics": "Information", 12 | "Microsoft.EntityFrameworkCore": "Critical" 13 | } 14 | }, 15 | "AllowedHosts": "*" 16 | } 17 | -------------------------------------------------------------------------------- /src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/OperationsController.cs: -------------------------------------------------------------------------------- 1 | using JsonApiDotNetCore.AtomicOperations; 2 | using JsonApiDotNetCore.Configuration; 3 | using JsonApiDotNetCore.Controllers; 4 | using JsonApiDotNetCore.Middleware; 5 | using JsonApiDotNetCore.Resources; 6 | 7 | namespace JsonApiDotNetCoreMongoDbExample.Controllers; 8 | 9 | public sealed class OperationsController( 10 | IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request, 11 | ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) 12 | : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields, operationFilter); 13 | -------------------------------------------------------------------------------- /src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using JetBrains.Annotations; 3 | using JsonApiDotNetCore.Configuration; 4 | using JsonApiDotNetCore.Middleware; 5 | using JsonApiDotNetCore.Queries.Expressions; 6 | using JsonApiDotNetCore.Resources; 7 | using JsonApiDotNetCoreMongoDbExample.Models; 8 | 9 | namespace JsonApiDotNetCoreMongoDbExample.Definitions; 10 | 11 | [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] 12 | public sealed class TodoItemDefinition(IResourceGraph resourceGraph, TimeProvider timeProvider) 13 | : JsonApiResourceDefinition(resourceGraph) 14 | { 15 | private readonly TimeProvider _timeProvider = timeProvider; 16 | 17 | public override SortExpression OnApplySort(SortExpression? existingSort) 18 | { 19 | return existingSort ?? GetDefaultSortOrder(); 20 | } 21 | 22 | private SortExpression GetDefaultSortOrder() 23 | { 24 | return CreateSortExpressionFromLambda([ 25 | (todoItem => todoItem.Priority, ListSortDirection.Ascending), 26 | (todoItem => todoItem.LastModifiedAt, ListSortDirection.Descending) 27 | ]); 28 | } 29 | 30 | public override Task OnWritingAsync(TodoItem resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) 31 | { 32 | if (writeOperation == WriteOperationKind.CreateResource) 33 | { 34 | resource.CreatedAt = _timeProvider.GetUtcNow(); 35 | } 36 | else if (writeOperation == WriteOperationKind.UpdateResource) 37 | { 38 | resource.LastModifiedAt = _timeProvider.GetUtcNow(); 39 | } 40 | 41 | return Task.CompletedTask; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Examples/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net9.0;net8.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using JetBrains.Annotations; 3 | using JsonApiDotNetCore.MongoDb.Resources; 4 | using JsonApiDotNetCore.Resources.Annotations; 5 | 6 | namespace JsonApiDotNetCoreMongoDbExample.Models; 7 | 8 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 9 | [Resource] 10 | public sealed class TodoItem : HexStringMongoIdentifiable 11 | { 12 | [Attr] 13 | public string Description { get; set; } = null!; 14 | 15 | [Attr] 16 | [Required] 17 | public TodoItemPriority? Priority { get; set; } 18 | 19 | [Attr] 20 | public long? DurationInHours { get; set; } 21 | 22 | [Attr(Capabilities = AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort | AttrCapabilities.AllowView)] 23 | public DateTimeOffset CreatedAt { get; set; } 24 | 25 | [Attr(PublicName = "modifiedAt", Capabilities = AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort | AttrCapabilities.AllowView)] 26 | public DateTimeOffset? LastModifiedAt { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItemPriority.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | 3 | namespace JsonApiDotNetCoreMongoDbExample.Models; 4 | 5 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 6 | public enum TodoItemPriority 7 | { 8 | High = 1, 9 | Medium = 2, 10 | Low = 3 11 | } 12 | -------------------------------------------------------------------------------- /src/Examples/JsonApiDotNetCoreMongoDbExample/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Text.Json.Serialization; 3 | using JsonApiDotNetCore.Configuration; 4 | using JsonApiDotNetCore.MongoDb.Configuration; 5 | using JsonApiDotNetCore.MongoDb.Repositories; 6 | using JsonApiDotNetCore.Repositories; 7 | using Microsoft.Extensions.DependencyInjection.Extensions; 8 | using MongoDB.Driver; 9 | 10 | [assembly: ExcludeFromCodeCoverage] 11 | 12 | WebApplicationBuilder builder = WebApplication.CreateBuilder(args); 13 | 14 | // Add services to the container. 15 | 16 | builder.Services.TryAddSingleton(TimeProvider.System); 17 | 18 | builder.Services.TryAddSingleton(_ => 19 | { 20 | var client = new MongoClient(builder.Configuration.GetValue("DatabaseSettings:ConnectionString")); 21 | return client.GetDatabase(builder.Configuration.GetValue("DatabaseSettings:Database")); 22 | }); 23 | 24 | builder.Services.TryAddScoped(typeof(IResourceReadRepository<,>), typeof(MongoRepository<,>)); 25 | builder.Services.TryAddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoRepository<,>)); 26 | builder.Services.TryAddScoped(typeof(IResourceRepository<,>), typeof(MongoRepository<,>)); 27 | 28 | builder.Services.AddJsonApi(ConfigureJsonApiOptions, facade => facade.AddCurrentAssembly()); 29 | builder.Services.AddJsonApiMongoDb(); 30 | 31 | WebApplication app = builder.Build(); 32 | 33 | // Configure the HTTP request pipeline. 34 | 35 | app.UseRouting(); 36 | app.UseJsonApi(); 37 | app.MapControllers(); 38 | 39 | await app.RunAsync(); 40 | 41 | static void ConfigureJsonApiOptions(JsonApiOptions options) 42 | { 43 | options.Namespace = "api"; 44 | options.UseRelativeLinks = true; 45 | options.IncludeTotalResourceCount = true; 46 | options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); 47 | #if DEBUG 48 | options.IncludeExceptionStackTraceInErrors = true; 49 | options.IncludeRequestBodyInErrors = true; 50 | options.SerializerOptions.WriteIndented = true; 51 | #endif 52 | } 53 | -------------------------------------------------------------------------------- /src/Examples/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:24140", 8 | "sslPort": 44340 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "api/todoItems", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "Kestrel": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "api/todoItems", 24 | "applicationUrl": "https://localhost:44340;http://localhost:24140", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Examples/JsonApiDotNetCoreMongoDbExample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "DatabaseSettings": { 3 | "ConnectionString": "mongodb://localhost:27017", 4 | "Database": "JsonApiDotNetCoreMongoDbExample" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Warning", 9 | // Include server startup and incoming requests. 10 | "Microsoft.Hosting.Lifetime": "Information", 11 | "Microsoft.AspNetCore.Hosting.Diagnostics": "Information", 12 | "Microsoft.EntityFrameworkCore": "Critical" 13 | } 14 | }, 15 | "AllowedHosts": "*" 16 | } 17 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/AtomicOperations/MongoTransaction.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.AtomicOperations; 3 | using JsonApiDotNetCore.MongoDb.Repositories; 4 | 5 | namespace JsonApiDotNetCore.MongoDb.AtomicOperations; 6 | 7 | /// 8 | [PublicAPI] 9 | public sealed class MongoTransaction : IOperationsTransaction 10 | { 11 | private readonly IMongoDataAccess _mongoDataAccess; 12 | private readonly bool _ownsTransaction; 13 | 14 | /// 15 | public string TransactionId => _mongoDataAccess.TransactionId!; 16 | 17 | public MongoTransaction(IMongoDataAccess mongoDataAccess, bool ownsTransaction) 18 | { 19 | ArgumentNullException.ThrowIfNull(mongoDataAccess); 20 | 21 | _mongoDataAccess = mongoDataAccess; 22 | _ownsTransaction = ownsTransaction; 23 | } 24 | 25 | /// 26 | public Task BeforeProcessOperationAsync(CancellationToken cancellationToken) 27 | { 28 | return Task.CompletedTask; 29 | } 30 | 31 | /// 32 | public Task AfterProcessOperationAsync(CancellationToken cancellationToken) 33 | { 34 | return Task.CompletedTask; 35 | } 36 | 37 | /// 38 | public async Task CommitAsync(CancellationToken cancellationToken) 39 | { 40 | if (_ownsTransaction && _mongoDataAccess.ActiveSession != null) 41 | { 42 | await _mongoDataAccess.ActiveSession.CommitTransactionAsync(cancellationToken); 43 | } 44 | } 45 | 46 | /// 47 | public async ValueTask DisposeAsync() 48 | { 49 | if (_ownsTransaction) 50 | { 51 | await _mongoDataAccess.DisposeAsync(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/AtomicOperations/MongoTransactionFactory.cs: -------------------------------------------------------------------------------- 1 | using JsonApiDotNetCore.AtomicOperations; 2 | using JsonApiDotNetCore.MongoDb.Repositories; 3 | 4 | namespace JsonApiDotNetCore.MongoDb.AtomicOperations; 5 | 6 | /// 7 | /// Provides transaction support for atomic:operation requests using MongoDB. 8 | /// 9 | public sealed class MongoTransactionFactory : IOperationsTransactionFactory 10 | { 11 | private readonly IMongoDataAccess _mongoDataAccess; 12 | 13 | public MongoTransactionFactory(IMongoDataAccess mongoDataAccess) 14 | { 15 | ArgumentNullException.ThrowIfNull(mongoDataAccess); 16 | 17 | _mongoDataAccess = mongoDataAccess; 18 | } 19 | 20 | /// 21 | public async Task BeginTransactionAsync(CancellationToken cancellationToken) 22 | { 23 | bool transactionCreated = await CreateOrJoinTransactionAsync(cancellationToken); 24 | return new MongoTransaction(_mongoDataAccess, transactionCreated); 25 | } 26 | 27 | private async Task CreateOrJoinTransactionAsync(CancellationToken cancellationToken) 28 | { 29 | _mongoDataAccess.ActiveSession ??= await _mongoDataAccess.MongoDatabase.Client.StartSessionAsync(cancellationToken: cancellationToken); 30 | 31 | if (_mongoDataAccess.ActiveSession.IsInTransaction) 32 | { 33 | return false; 34 | } 35 | 36 | _mongoDataAccess.ActiveSession.StartTransaction(); 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Configuration/ResourceGraphExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using JsonApiDotNetCore.Configuration; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | using MongoDB.Bson.Serialization.Attributes; 7 | 8 | namespace JsonApiDotNetCore.MongoDb.Configuration; 9 | 10 | internal static class ResourceGraphExtensions 11 | { 12 | public static IReadOnlyModel ToEntityModel(this IResourceGraph resourceGraph) 13 | { 14 | ArgumentNullException.ThrowIfNull(resourceGraph); 15 | 16 | var modelBuilder = new ModelBuilder(); 17 | 18 | foreach (ResourceType resourceType in resourceGraph.GetResourceTypes()) 19 | { 20 | IncludeResourceType(resourceType, modelBuilder); 21 | } 22 | 23 | return modelBuilder.Model; 24 | } 25 | 26 | private static void IncludeResourceType(ResourceType resourceType, ModelBuilder builder) 27 | { 28 | EntityTypeBuilder entityTypeBuilder = builder.Entity(resourceType.ClrType); 29 | 30 | foreach (PropertyInfo property in resourceType.ClrType.GetProperties().Where(property => !IsIgnored(property))) 31 | { 32 | entityTypeBuilder.Property(property.PropertyType, property.Name); 33 | } 34 | } 35 | 36 | private static bool IsIgnored(PropertyInfo property) 37 | { 38 | return property.GetCustomAttribute() != null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Configuration/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.AtomicOperations; 3 | using JsonApiDotNetCore.Configuration; 4 | using JsonApiDotNetCore.MongoDb.AtomicOperations; 5 | using JsonApiDotNetCore.MongoDb.Queries; 6 | using JsonApiDotNetCore.MongoDb.Repositories; 7 | using JsonApiDotNetCore.Queries; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.DependencyInjection.Extensions; 10 | 11 | namespace JsonApiDotNetCore.MongoDb.Configuration; 12 | 13 | public static class ServiceCollectionExtensions 14 | { 15 | /// 16 | /// Expands JsonApiDotNetCore configuration for usage with MongoDB. 17 | /// 18 | [PublicAPI] 19 | public static IServiceCollection AddJsonApiMongoDb(this IServiceCollection services) 20 | { 21 | ArgumentNullException.ThrowIfNull(services); 22 | 23 | services.TryAddSingleton(serviceProvider => 24 | { 25 | var resourceGraph = serviceProvider.GetRequiredService(); 26 | return resourceGraph.ToEntityModel(); 27 | }); 28 | 29 | services.TryAddScoped(); 30 | 31 | // Replace the built-in implementations from JsonApiDotNetCore. 32 | services.AddScoped(); 33 | services.AddScoped(); 34 | 35 | return services; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Errors/AttributeComparisonInFilterNotSupportedException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using JetBrains.Annotations; 3 | using JsonApiDotNetCore.Errors; 4 | using JsonApiDotNetCore.Serialization.Objects; 5 | 6 | namespace JsonApiDotNetCore.MongoDb.Errors; 7 | 8 | /// 9 | /// The error that is thrown when a filter compares two attributes. This is not supported by MongoDB.Driver. See 10 | /// https://jira.mongodb.org/browse/CSHARP-1592. 11 | /// 12 | [PublicAPI] 13 | public sealed class AttributeComparisonInFilterNotSupportedException() 14 | : JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) 15 | { 16 | Title = "Comparing attributes against each other is not supported when using MongoDB." 17 | }); 18 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using JetBrains.Annotations; 3 | using JsonApiDotNetCore.Errors; 4 | using JsonApiDotNetCore.Serialization.Objects; 5 | 6 | namespace JsonApiDotNetCore.MongoDb.Errors; 7 | 8 | /// 9 | /// The error that is thrown when the user attempts to fetch, create or update a relationship. 10 | /// 11 | [PublicAPI] 12 | public sealed class UnsupportedRelationshipException() 13 | : JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) 14 | { 15 | Title = "Relationships are not supported when using MongoDB." 16 | }); 17 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/JsonApiDotNetCore.MongoDb.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | true 5 | true 6 | 7 | 8 | 9 | 10 | 11 | jsonapi;json:api;dotnet;asp.net;rest;web-api;MongoDB 12 | MongoDB persistence for JsonApiDotNetCore, which is a framework for building JSON:API compliant REST APIs using ASP.NET Core. 13 | json-api-dotnet 14 | https://www.jsonapi.net/ 15 | MIT 16 | false 17 | See https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/releases. 18 | package-icon.png 19 | PackageReadme.md 20 | true 21 | embedded 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/PolyfillCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace JsonApiDotNetCore.MongoDb; 4 | 5 | // These methods provide polyfills for lower .NET versions. 6 | internal static class PolyfillCollectionExtensions 7 | { 8 | public static IReadOnlySet AsReadOnly(this HashSet source) 9 | { 10 | return new ReadOnlySet(source); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("TestBuildingBlocks")] 4 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Queries/HideRelationshipsSparseFieldSetCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using JsonApiDotNetCore.Configuration; 3 | using JsonApiDotNetCore.MongoDb.Resources; 4 | using JsonApiDotNetCore.Queries; 5 | using JsonApiDotNetCore.Resources; 6 | using JsonApiDotNetCore.Resources.Annotations; 7 | 8 | namespace JsonApiDotNetCore.MongoDb.Queries; 9 | 10 | /// 11 | public sealed class HideRelationshipsSparseFieldSetCache : ISparseFieldSetCache 12 | { 13 | private readonly SparseFieldSetCache _innerCache; 14 | 15 | public HideRelationshipsSparseFieldSetCache(IEnumerable constraintProviders, 16 | IResourceDefinitionAccessor resourceDefinitionAccessor) 17 | { 18 | ArgumentNullException.ThrowIfNull(constraintProviders); 19 | ArgumentNullException.ThrowIfNull(resourceDefinitionAccessor); 20 | 21 | _innerCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); 22 | } 23 | 24 | /// 25 | public IImmutableSet GetSparseFieldSetForQuery(ResourceType resourceType) 26 | { 27 | ArgumentNullException.ThrowIfNull(resourceType); 28 | 29 | return _innerCache.GetSparseFieldSetForQuery(resourceType); 30 | } 31 | 32 | /// 33 | public IImmutableSet GetIdAttributeSetForRelationshipQuery(ResourceType resourceType) 34 | { 35 | ArgumentNullException.ThrowIfNull(resourceType); 36 | 37 | return _innerCache.GetIdAttributeSetForRelationshipQuery(resourceType); 38 | } 39 | 40 | /// 41 | public IImmutableSet GetSparseFieldSetForSerializer(ResourceType resourceType) 42 | { 43 | ArgumentNullException.ThrowIfNull(resourceType); 44 | 45 | IImmutableSet fieldSet = _innerCache.GetSparseFieldSetForSerializer(resourceType); 46 | 47 | return resourceType.ClrType.IsAssignableTo(typeof(IMongoIdentifiable)) ? RemoveRelationships(fieldSet) : fieldSet; 48 | } 49 | 50 | private static IImmutableSet RemoveRelationships(IImmutableSet fieldSet) 51 | { 52 | ResourceFieldAttribute[] relationships = fieldSet.Where(field => field is RelationshipAttribute).ToArray(); 53 | return fieldSet.Except(relationships); 54 | } 55 | 56 | /// 57 | public void Reset() 58 | { 59 | _innerCache.Reset(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Repositories/IMongoDataAccess.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata; 2 | using MongoDB.Driver; 3 | 4 | namespace JsonApiDotNetCore.MongoDb.Repositories; 5 | 6 | /// 7 | /// Provides access to the MongoDB Driver and the optionally active session. 8 | /// 9 | public interface IMongoDataAccess : IAsyncDisposable 10 | { 11 | /// 12 | /// Provides access to the entity model, which is built at startup. 13 | /// 14 | IReadOnlyModel EntityModel { get; } 15 | 16 | /// 17 | /// Provides access to the underlying MongoDB database, which data changes can be applied on. 18 | /// 19 | IMongoDatabase MongoDatabase { get; } 20 | 21 | /// 22 | /// Provides access to the active session, if any. 23 | /// 24 | IClientSessionHandle? ActiveSession { get; set; } 25 | 26 | /// 27 | /// Identifies the current transaction, if any. 28 | /// 29 | string? TransactionId { get; } 30 | } 31 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Repositories/MongoDataAccess.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata; 2 | using MongoDB.Driver; 3 | 4 | namespace JsonApiDotNetCore.MongoDb.Repositories; 5 | 6 | /// 7 | public sealed class MongoDataAccess : IMongoDataAccess 8 | { 9 | /// 10 | public IReadOnlyModel EntityModel { get; } 11 | 12 | /// 13 | public IMongoDatabase MongoDatabase { get; } 14 | 15 | /// 16 | public IClientSessionHandle? ActiveSession { get; set; } 17 | 18 | /// 19 | public string? TransactionId => ActiveSession is { IsInTransaction: true } ? ActiveSession.GetHashCode().ToString() : null; 20 | 21 | public MongoDataAccess(IReadOnlyModel entityModel, IMongoDatabase mongoDatabase) 22 | { 23 | ArgumentNullException.ThrowIfNull(entityModel); 24 | ArgumentNullException.ThrowIfNull(mongoDatabase); 25 | 26 | EntityModel = entityModel; 27 | MongoDatabase = mongoDatabase; 28 | } 29 | 30 | /// 31 | public async ValueTask DisposeAsync() 32 | { 33 | if (ActiveSession != null) 34 | { 35 | if (ActiveSession.IsInTransaction) 36 | { 37 | await ActiveSession.AbortTransactionAsync(); 38 | } 39 | 40 | ActiveSession.Dispose(); 41 | ActiveSession = null; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Repositories/MongoQueryExpressionValidator.cs: -------------------------------------------------------------------------------- 1 | using JsonApiDotNetCore.Configuration; 2 | using JsonApiDotNetCore.MongoDb.Errors; 3 | using JsonApiDotNetCore.Queries; 4 | using JsonApiDotNetCore.Queries.Expressions; 5 | using JsonApiDotNetCore.Resources.Annotations; 6 | 7 | namespace JsonApiDotNetCore.MongoDb.Repositories; 8 | 9 | internal sealed class MongoQueryExpressionValidator : QueryExpressionRewriter 10 | { 11 | public void Validate(QueryLayer layer) 12 | { 13 | ArgumentNullException.ThrowIfNull(layer); 14 | 15 | bool hasIncludes = layer.Include?.Elements.Count > 0; 16 | 17 | if (hasIncludes || HasSparseRelationshipSets(layer.Selection)) 18 | { 19 | throw new UnsupportedRelationshipException(); 20 | } 21 | 22 | ValidateExpression(layer.Filter); 23 | ValidateExpression(layer.Sort); 24 | ValidateExpression(layer.Pagination); 25 | } 26 | 27 | private static bool HasSparseRelationshipSets(FieldSelection? selection) 28 | { 29 | if (selection is { IsEmpty: false }) 30 | { 31 | foreach (ResourceType resourceType in selection.GetResourceTypes()) 32 | { 33 | FieldSelectors selectors = selection.GetOrCreateSelectors(resourceType); 34 | 35 | if (selectors.Any(pair => pair.Key is RelationshipAttribute)) 36 | { 37 | return true; 38 | } 39 | } 40 | } 41 | 42 | return false; 43 | } 44 | 45 | private void ValidateExpression(QueryExpression? expression) 46 | { 47 | if (expression != null) 48 | { 49 | Visit(expression, null); 50 | } 51 | } 52 | 53 | public override QueryExpression VisitResourceFieldChain(ResourceFieldChainExpression expression, object? argument) 54 | { 55 | if (expression.Fields.Count > 1 || expression.Fields[0] is RelationshipAttribute) 56 | { 57 | throw new UnsupportedRelationshipException(); 58 | } 59 | 60 | return base.VisitResourceFieldChain(expression, argument); 61 | } 62 | 63 | public override QueryExpression? VisitComparison(ComparisonExpression expression, object? argument) 64 | { 65 | if (expression is { Left: ResourceFieldChainExpression, Right: ResourceFieldChainExpression }) 66 | { 67 | throw new AttributeComparisonInFilterNotSupportedException(); 68 | } 69 | 70 | return base.VisitComparison(expression, argument); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Resources/FreeStringMongoIdentifiable.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson.Serialization.Attributes; 2 | using MongoDB.Bson.Serialization.IdGenerators; 3 | 4 | namespace JsonApiDotNetCore.MongoDb.Resources; 5 | 6 | /// 7 | /// Basic implementation of a JSON:API resource whose Id is stored as a free-format string in MongoDB. Useful for resources that are created using 8 | /// client-generated IDs. 9 | /// 10 | public abstract class FreeStringMongoIdentifiable : IMongoIdentifiable 11 | { 12 | /// 13 | [BsonId(IdGenerator = typeof(StringObjectIdGenerator))] 14 | public virtual string? Id { get; set; } 15 | 16 | /// 17 | [BsonIgnore] 18 | public string? StringId 19 | { 20 | get => Id; 21 | set => Id = value; 22 | } 23 | 24 | /// 25 | [BsonIgnore] 26 | public string? LocalId { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Resources/HexStringMongoIdentifiable.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson; 2 | using MongoDB.Bson.Serialization.Attributes; 3 | 4 | namespace JsonApiDotNetCore.MongoDb.Resources; 5 | 6 | /// 7 | /// Basic implementation of a JSON:API resource whose Id is stored as a 12-byte hexadecimal ObjectId in MongoDB. 8 | /// 9 | public abstract class HexStringMongoIdentifiable : IMongoIdentifiable 10 | { 11 | /// 12 | [BsonId] 13 | [BsonRepresentation(BsonType.ObjectId)] 14 | public virtual string? Id { get; set; } 15 | 16 | /// 17 | [BsonIgnore] 18 | public string? StringId 19 | { 20 | get => Id; 21 | set => Id = value; 22 | } 23 | 24 | /// 25 | [BsonIgnore] 26 | public string? LocalId { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /src/JsonApiDotNetCore.MongoDb/Resources/IMongoIdentifiable.cs: -------------------------------------------------------------------------------- 1 | using JsonApiDotNetCore.Resources; 2 | 3 | namespace JsonApiDotNetCore.MongoDb.Resources; 4 | 5 | /// 6 | /// Marker interface to indicate a resource that is stored in MongoDB. 7 | /// 8 | public interface IMongoIdentifiable : IIdentifiable; 9 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsCollectionFixture.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 4 | 5 | [CollectionDefinition("AtomicOperationsFixture")] 6 | public sealed class AtomicOperationsCollectionFixture : ICollectionFixture 7 | { 8 | // Starting MongoDB in Single Node Replica Set mode is required to enable transactions. 9 | // Starting in this mode requires about 10 seconds, which is normally repeated for each test class. 10 | // So to improve test runtime performance, we reuse a single MongoDB instance for all atomic:operations tests. 11 | } 12 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsFixture.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.Configuration; 3 | using JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Meta; 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | using TestBuildingBlocks; 6 | using Xunit; 7 | 8 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 9 | 10 | [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] 11 | public sealed class AtomicOperationsFixture : IAsyncLifetime 12 | { 13 | internal IntegrationTestContext TestContext { get; } = new(); 14 | 15 | public AtomicOperationsFixture() 16 | { 17 | TestContext.UseResourceTypesInNamespace(typeof(MusicTrack).Namespace); 18 | 19 | TestContext.UseController(); 20 | 21 | TestContext.ConfigureServices(services => 22 | { 23 | services.TryAddSingleton(); 24 | 25 | services.AddResourceDefinition(); 26 | services.AddResourceDefinition(); 27 | }); 28 | } 29 | 30 | public Task InitializeAsync() 31 | { 32 | return Task.CompletedTask; 33 | } 34 | 35 | public async Task DisposeAsync() 36 | { 37 | await TestContext.DisposeAsync(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/BaseForAtomicOperationsTestsThatChangeOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using JsonApiDotNetCore.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 6 | 7 | public abstract class BaseForAtomicOperationsTestsThatChangeOptions : IDisposable 8 | { 9 | private readonly JsonApiOptionsScope _optionsScope; 10 | 11 | protected BaseForAtomicOperationsTestsThatChangeOptions(AtomicOperationsFixture fixture) 12 | { 13 | var options = (JsonApiOptions)fixture.TestContext.Factory.Services.GetRequiredService(); 14 | _optionsScope = new JsonApiOptionsScope(options); 15 | } 16 | 17 | public void Dispose() 18 | { 19 | Dispose(true); 20 | GC.SuppressFinalize(this); 21 | } 22 | 23 | #pragma warning disable CA1063 // Implement IDisposable Correctly 24 | private void Dispose(bool disposing) 25 | #pragma warning restore CA1063 // Implement IDisposable Correctly 26 | { 27 | if (disposing) 28 | { 29 | _optionsScope.Dispose(); 30 | } 31 | } 32 | 33 | private sealed class JsonApiOptionsScope : IDisposable 34 | { 35 | private static readonly PropertyInfo[] PropertyCache = typeof(JsonApiOptions).GetProperties().Where(IsAccessibleProperty).ToArray(); 36 | 37 | private readonly JsonApiOptions _options; 38 | private readonly JsonApiOptions _backupValues; 39 | 40 | public JsonApiOptionsScope(JsonApiOptions options) 41 | { 42 | _options = options; 43 | _backupValues = new JsonApiOptions(); 44 | 45 | CopyPropertyValues(_options, _backupValues); 46 | } 47 | 48 | private static bool IsAccessibleProperty(PropertyInfo property) 49 | { 50 | return property.GetMethod != null && property.SetMethod != null && property.GetCustomAttribute() == null; 51 | } 52 | 53 | public void Dispose() 54 | { 55 | CopyPropertyValues(_backupValues, _options); 56 | } 57 | 58 | private static void CopyPropertyValues(JsonApiOptions source, JsonApiOptions destination) 59 | { 60 | foreach (PropertyInfo property in PropertyCache) 61 | { 62 | property.SetMethod!.Invoke(destination, [property.GetMethod!.Invoke(source, null)]); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using FluentAssertions; 3 | using JsonApiDotNetCore.Serialization.Objects; 4 | using TestBuildingBlocks; 5 | using Xunit; 6 | 7 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Creating; 8 | 9 | [Collection("AtomicOperationsFixture")] 10 | public sealed class AtomicCreateResourceWithToManyRelationshipTests(AtomicOperationsFixture fixture) 11 | { 12 | private readonly IntegrationTestContext _testContext = fixture.TestContext; 13 | private readonly OperationsFakers _fakers = new(); 14 | 15 | [Fact] 16 | public async Task Cannot_create_ToMany_relationship() 17 | { 18 | // Arrange 19 | Performer existingPerformer = _fakers.Performer.GenerateOne(); 20 | 21 | string newTitle = _fakers.MusicTrack.GenerateOne().Title; 22 | 23 | await _testContext.RunOnDatabaseAsync(async dbContext => 24 | { 25 | dbContext.Performers.Add(existingPerformer); 26 | await dbContext.SaveChangesAsync(); 27 | }); 28 | 29 | var requestBody = new 30 | { 31 | atomic__operations = new[] 32 | { 33 | new 34 | { 35 | op = "add", 36 | data = new 37 | { 38 | type = "musicTracks", 39 | attributes = new 40 | { 41 | title = newTitle 42 | }, 43 | relationships = new 44 | { 45 | performers = new 46 | { 47 | data = new[] 48 | { 49 | new 50 | { 51 | type = "performers", 52 | id = existingPerformer.StringId 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | }; 61 | 62 | const string route = "/operations"; 63 | 64 | // Act 65 | (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); 66 | 67 | // Assert 68 | httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); 69 | 70 | responseDocument.Errors.Should().HaveCount(1); 71 | 72 | ErrorObject error = responseDocument.Errors[0]; 73 | error.StatusCode.Should().Be(HttpStatusCode.BadRequest); 74 | error.Title.Should().Be("Relationships are not supported when using MongoDB."); 75 | error.Detail.Should().BeNull(); 76 | error.Source.Should().NotBeNull(); 77 | error.Source.Pointer.Should().Be("/atomic:operations[0]"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using FluentAssertions; 3 | using JsonApiDotNetCore.Serialization.Objects; 4 | using TestBuildingBlocks; 5 | using Xunit; 6 | 7 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Creating; 8 | 9 | [Collection("AtomicOperationsFixture")] 10 | public sealed class AtomicCreateResourceWithToOneRelationshipTests(AtomicOperationsFixture fixture) 11 | { 12 | private readonly IntegrationTestContext _testContext = fixture.TestContext; 13 | private readonly OperationsFakers _fakers = new(); 14 | 15 | [Fact] 16 | public async Task Cannot_create_ToOne_relationship() 17 | { 18 | // Arrange 19 | MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); 20 | 21 | string newLyricText = _fakers.Lyric.GenerateOne().Text; 22 | 23 | await _testContext.RunOnDatabaseAsync(async dbContext => 24 | { 25 | dbContext.MusicTracks.Add(existingTrack); 26 | await dbContext.SaveChangesAsync(); 27 | }); 28 | 29 | var requestBody = new 30 | { 31 | atomic__operations = new[] 32 | { 33 | new 34 | { 35 | op = "add", 36 | data = new 37 | { 38 | type = "lyrics", 39 | attributes = new 40 | { 41 | text = newLyricText 42 | }, 43 | relationships = new 44 | { 45 | track = new 46 | { 47 | data = new 48 | { 49 | type = "musicTracks", 50 | id = existingTrack.StringId 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | }; 58 | 59 | const string route = "/operations"; 60 | 61 | // Act 62 | (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); 63 | 64 | // Assert 65 | httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); 66 | 67 | responseDocument.Errors.Should().HaveCount(1); 68 | 69 | ErrorObject error = responseDocument.Errors[0]; 70 | error.StatusCode.Should().Be(HttpStatusCode.BadRequest); 71 | error.Title.Should().Be("Relationships are not supported when using MongoDB."); 72 | error.Detail.Should().BeNull(); 73 | error.Source.Should().NotBeNull(); 74 | error.Source.Pointer.Should().Be("/atomic:operations[0]"); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.Configuration; 3 | using JsonApiDotNetCore.Middleware; 4 | using JsonApiDotNetCore.MongoDb.Repositories; 5 | using MongoDB.Driver; 6 | 7 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 8 | 9 | /// 10 | /// Used to simulate side effects that occur in the database while saving, typically caused by database triggers. 11 | /// 12 | [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] 13 | public abstract class ImplicitlyChangingTextLanguageDefinition( 14 | IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter, IMongoDataAccess mongoDataAccess) 15 | : HitCountingResourceDefinition(resourceGraph, hitCounter) 16 | { 17 | internal const string Suffix = " (changed)"; 18 | 19 | private readonly IMongoDataAccess _mongoDataAccess = mongoDataAccess; 20 | 21 | public override async Task OnWriteSucceededAsync(TextLanguage resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) 22 | { 23 | await base.OnWriteSucceededAsync(resource, writeOperation, cancellationToken); 24 | 25 | if (writeOperation is not WriteOperationKind.DeleteResource) 26 | { 27 | resource.IsoCode += Suffix; 28 | 29 | FilterDefinition filter = Builders.Filter.Eq(item => item.Id, resource.Id); 30 | 31 | IMongoCollection collection = _mongoDataAccess.MongoDatabase.GetCollection(nameof(TextLanguage)); 32 | await collection.ReplaceOneAsync(_mongoDataAccess.ActiveSession, filter, resource, cancellationToken: cancellationToken); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Lyric.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.MongoDb.Resources; 3 | using JsonApiDotNetCore.Resources.Annotations; 4 | using MongoDB.Bson.Serialization.Attributes; 5 | 6 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 7 | 8 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 9 | [Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")] 10 | public sealed class Lyric : HexStringMongoIdentifiable 11 | { 12 | [Attr] 13 | public string? Format { get; set; } 14 | 15 | [Attr] 16 | public string Text { get; set; } = null!; 17 | 18 | [Attr(Capabilities = AttrCapabilities.None)] 19 | public DateTimeOffset CreatedAt { get; set; } 20 | 21 | [HasOne] 22 | [BsonIgnore] 23 | public TextLanguage? Language { get; set; } 24 | 25 | [HasOne] 26 | [BsonIgnore] 27 | public MusicTrack? Track { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.Configuration; 3 | 4 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Meta; 5 | 6 | [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] 7 | public sealed class MusicTrackMetaDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) 8 | : HitCountingResourceDefinition(resourceGraph, hitCounter) 9 | { 10 | protected override ResourceDefinitionExtensibilityPoints ExtensibilityPointsToTrack => ResourceDefinitionExtensibilityPoints.GetMeta; 11 | 12 | public override IDictionary GetMeta(MusicTrack resource) 13 | { 14 | base.GetMeta(resource); 15 | 16 | return new Dictionary 17 | { 18 | ["Copyright"] = $"(C) {resource.ReleasedAt.Year}. All rights reserved." 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.Configuration; 3 | using JsonApiDotNetCore.MongoDb.Repositories; 4 | 5 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Meta; 6 | 7 | [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] 8 | public sealed class TextLanguageMetaDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter, IMongoDataAccess mongoDataAccess) 9 | : ImplicitlyChangingTextLanguageDefinition(resourceGraph, hitCounter, mongoDataAccess) 10 | { 11 | internal const string NoticeText = "See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes for ISO 639-1 language codes."; 12 | 13 | protected override ResourceDefinitionExtensibilityPoints ExtensibilityPointsToTrack => ResourceDefinitionExtensibilityPoints.GetMeta; 14 | 15 | public override IDictionary GetMeta(TextLanguage resource) 16 | { 17 | base.GetMeta(resource); 18 | 19 | return new Dictionary 20 | { 21 | ["Notice"] = NoticeText 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using JsonApiDotNetCore.Configuration; 3 | using JsonApiDotNetCore.Serialization.Objects; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using TestBuildingBlocks; 6 | using Xunit; 7 | 8 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Mixed; 9 | 10 | [Collection("AtomicOperationsFixture")] 11 | public sealed class MaximumOperationsPerRequestTests(AtomicOperationsFixture fixture) 12 | : BaseForAtomicOperationsTestsThatChangeOptions(fixture) 13 | { 14 | private readonly IntegrationTestContext _testContext = fixture.TestContext; 15 | 16 | [Fact] 17 | public async Task Can_process_high_number_of_operations_when_unconstrained() 18 | { 19 | // Arrange 20 | var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); 21 | options.MaximumOperationsPerRequest = null; 22 | 23 | const int elementCount = 100; 24 | 25 | var operationElements = new List(elementCount); 26 | 27 | for (int index = 0; index < elementCount; index++) 28 | { 29 | operationElements.Add(new 30 | { 31 | op = "add", 32 | data = new 33 | { 34 | type = "performers", 35 | attributes = new 36 | { 37 | } 38 | } 39 | }); 40 | } 41 | 42 | var requestBody = new 43 | { 44 | atomic__operations = operationElements 45 | }; 46 | 47 | const string route = "/operations"; 48 | 49 | // Act 50 | (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); 51 | 52 | // Assert 53 | httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/MusicTrack.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using JetBrains.Annotations; 3 | using JsonApiDotNetCore.MongoDb.Resources; 4 | using JsonApiDotNetCore.Resources.Annotations; 5 | using MongoDB.Bson.Serialization.Attributes; 6 | 7 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 8 | 9 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 10 | [Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")] 11 | public sealed class MusicTrack : HexStringMongoIdentifiable 12 | { 13 | [RegularExpression(@"^[a-fA-F\d]{24}$")] 14 | public override string? Id { get; set; } 15 | 16 | [Attr] 17 | public string Title { get; set; } = null!; 18 | 19 | [Attr] 20 | [Range(1, 24 * 60)] 21 | public decimal? LengthInSeconds { get; set; } 22 | 23 | [Attr] 24 | public string? Genre { get; set; } 25 | 26 | [Attr] 27 | public DateTimeOffset ReleasedAt { get; set; } 28 | 29 | [HasOne] 30 | [BsonIgnore] 31 | public Lyric? Lyric { get; set; } 32 | 33 | [HasOne] 34 | [BsonIgnore] 35 | public RecordCompany? OwnedBy { get; set; } 36 | 37 | [HasMany] 38 | [BsonIgnore] 39 | public IList Performers { get; set; } = new List(); 40 | 41 | [HasMany] 42 | [BsonIgnore] 43 | public IList OccursIn { get; set; } = new List(); 44 | } 45 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsController.cs: -------------------------------------------------------------------------------- 1 | using JsonApiDotNetCore.AtomicOperations; 2 | using JsonApiDotNetCore.Configuration; 3 | using JsonApiDotNetCore.Controllers; 4 | using JsonApiDotNetCore.Middleware; 5 | using JsonApiDotNetCore.Resources; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 9 | 10 | public sealed class OperationsController( 11 | IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request, 12 | ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) 13 | : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields, operationFilter); 14 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using MongoDB.Driver; 3 | using TestBuildingBlocks; 4 | 5 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 6 | 7 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 8 | public sealed class OperationsDbContext(IMongoDatabase database) 9 | : MongoDbContextShim(database) 10 | { 11 | public MongoDbSetShim Playlists => Set(); 12 | public MongoDbSetShim MusicTracks => Set(); 13 | public MongoDbSetShim Lyrics => Set(); 14 | public MongoDbSetShim TextLanguages => Set(); 15 | public MongoDbSetShim Performers => Set(); 16 | public MongoDbSetShim RecordCompanies => Set(); 17 | } 18 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsFakers.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using Bogus; 3 | using TestBuildingBlocks; 4 | 5 | // @formatter:wrap_chained_method_calls chop_if_long 6 | // @formatter:wrap_before_first_method_call true 7 | 8 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 9 | 10 | internal sealed class OperationsFakers 11 | { 12 | private static readonly Lazy LazyLanguageIsoCodes = new(() => CultureInfo 13 | .GetCultures(CultureTypes.NeutralCultures) 14 | .Where(culture => !string.IsNullOrEmpty(culture.Name)) 15 | .Select(culture => culture.Name) 16 | .ToArray()); 17 | 18 | private readonly Lazy> _lazyPlaylistFaker = new(() => new Faker() 19 | .MakeDeterministic() 20 | .RuleFor(playlist => playlist.Name, faker => faker.Lorem.Sentence())); 21 | 22 | private readonly Lazy> _lazyMusicTrackFaker = new(() => new Faker() 23 | .MakeDeterministic() 24 | .RuleFor(musicTrack => musicTrack.Title, faker => faker.Lorem.Word()) 25 | .RuleFor(musicTrack => musicTrack.LengthInSeconds, faker => faker.Random.Decimal(3 * 60, 5 * 60)) 26 | .RuleFor(musicTrack => musicTrack.Genre, faker => faker.Lorem.Word()) 27 | .RuleFor(musicTrack => musicTrack.ReleasedAt, faker => faker.Date.PastOffset().TruncateToWholeMilliseconds())); 28 | 29 | private readonly Lazy> _lazyLyricFaker = new(() => new Faker() 30 | .MakeDeterministic() 31 | .RuleFor(lyric => lyric.Text, faker => faker.Lorem.Text()) 32 | .RuleFor(lyric => lyric.Format, "LRC")); 33 | 34 | private readonly Lazy> _lazyTextLanguageFaker = new(() => new Faker() 35 | .MakeDeterministic() 36 | .RuleFor(textLanguage => textLanguage.IsoCode, faker => faker.PickRandom(LazyLanguageIsoCodes.Value))); 37 | 38 | private readonly Lazy> _lazyPerformerFaker = new(() => new Faker() 39 | .MakeDeterministic() 40 | .RuleFor(performer => performer.ArtistName, faker => faker.Name.FullName()) 41 | .RuleFor(performer => performer.BornAt, faker => faker.Date.PastOffset().TruncateToWholeMilliseconds())); 42 | 43 | private readonly Lazy> _lazyRecordCompanyFaker = new(() => new Faker() 44 | .MakeDeterministic() 45 | .RuleFor(recordCompany => recordCompany.Name, faker => faker.Company.CompanyName()) 46 | .RuleFor(recordCompany => recordCompany.CountryOfResidence, faker => faker.Address.Country())); 47 | 48 | public Faker Playlist => _lazyPlaylistFaker.Value; 49 | public Faker MusicTrack => _lazyMusicTrackFaker.Value; 50 | public Faker Lyric => _lazyLyricFaker.Value; 51 | public Faker TextLanguage => _lazyTextLanguageFaker.Value; 52 | public Faker Performer => _lazyPerformerFaker.Value; 53 | public Faker RecordCompany => _lazyRecordCompanyFaker.Value; 54 | } 55 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Performer.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.MongoDb.Resources; 3 | using JsonApiDotNetCore.Resources.Annotations; 4 | 5 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 6 | 7 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 8 | [Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")] 9 | public sealed class Performer : HexStringMongoIdentifiable 10 | { 11 | [Attr] 12 | public string? ArtistName { get; set; } 13 | 14 | [Attr] 15 | public DateTimeOffset BornAt { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Playlist.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.MongoDb.Resources; 3 | using JsonApiDotNetCore.Resources.Annotations; 4 | using MongoDB.Bson.Serialization.Attributes; 5 | 6 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 7 | 8 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 9 | [Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")] 10 | public sealed class Playlist : FreeStringMongoIdentifiable 11 | { 12 | [Attr] 13 | public string Name { get; set; } = null!; 14 | 15 | [Attr] 16 | [BsonIgnore] 17 | public bool IsArchived => false; 18 | 19 | [HasMany] 20 | [BsonIgnore] 21 | public IList Tracks { get; set; } = new List(); 22 | } 23 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/RecordCompany.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.MongoDb.Resources; 3 | using JsonApiDotNetCore.Resources.Annotations; 4 | using MongoDB.Bson.Serialization.Attributes; 5 | 6 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 7 | 8 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 9 | [Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")] 10 | public sealed class RecordCompany : HexStringMongoIdentifiable 11 | { 12 | [Attr] 13 | public string Name { get; set; } = null!; 14 | 15 | [Attr] 16 | public string? CountryOfResidence { get; set; } 17 | 18 | [HasMany] 19 | [BsonIgnore] 20 | public IList Tracks { get; set; } = new List(); 21 | 22 | [HasOne] 23 | [BsonIgnore] 24 | public RecordCompany? Parent { get; set; } 25 | } 26 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/TextLanguage.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.MongoDb.Resources; 3 | using JsonApiDotNetCore.Resources.Annotations; 4 | using MongoDB.Bson.Serialization.Attributes; 5 | 6 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; 7 | 8 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 9 | [Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations")] 10 | public sealed class TextLanguage : FreeStringMongoIdentifiable 11 | { 12 | [Attr] 13 | public string? IsoCode { get; set; } 14 | 15 | [Attr(Capabilities = AttrCapabilities.None)] 16 | public bool IsRightToLeft { get; set; } 17 | 18 | [HasMany] 19 | [BsonIgnore] 20 | public ICollection Lyrics { get; set; } = new List(); 21 | } 22 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.AtomicOperations; 3 | using JsonApiDotNetCore.Configuration; 4 | using JsonApiDotNetCore.MongoDb.AtomicOperations; 5 | using JsonApiDotNetCore.MongoDb.Repositories; 6 | using JsonApiDotNetCore.Queries; 7 | using JsonApiDotNetCore.Queries.QueryableBuilding; 8 | using JsonApiDotNetCore.Resources; 9 | 10 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Transactions; 11 | 12 | [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] 13 | public sealed class LyricRepository : MongoRepository, IAsyncDisposable 14 | { 15 | private readonly MongoDataAccess _otherDataAccess; 16 | private readonly IOperationsTransaction _transaction; 17 | 18 | public override string TransactionId => _transaction.TransactionId; 19 | 20 | public LyricRepository(IMongoDataAccess mongoDataAccess, ITargetedFields targetedFields, IResourceGraph resourceGraph, IResourceFactory resourceFactory, 21 | IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, IQueryableBuilder queryableBuilder) 22 | : base(mongoDataAccess, targetedFields, resourceGraph, resourceFactory, constraintProviders, resourceDefinitionAccessor, queryableBuilder) 23 | { 24 | _otherDataAccess = new MongoDataAccess(mongoDataAccess.EntityModel, mongoDataAccess.MongoDatabase); 25 | 26 | var factory = new MongoTransactionFactory(_otherDataAccess); 27 | _transaction = factory.BeginTransactionAsync(CancellationToken.None).Result; 28 | } 29 | 30 | public async ValueTask DisposeAsync() 31 | { 32 | await _transaction.DisposeAsync(); 33 | await _otherDataAccess.DisposeAsync(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.Configuration; 3 | using JsonApiDotNetCore.MongoDb.Repositories; 4 | using JsonApiDotNetCore.Queries; 5 | using JsonApiDotNetCore.Queries.QueryableBuilding; 6 | using JsonApiDotNetCore.Resources; 7 | 8 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Transactions; 9 | 10 | [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] 11 | public sealed class MusicTrackRepository( 12 | IMongoDataAccess mongoDataAccess, ITargetedFields targetedFields, IResourceGraph resourceGraph, IResourceFactory resourceFactory, 13 | IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, IQueryableBuilder queryableBuilder) 14 | : MongoRepository(mongoDataAccess, targetedFields, resourceGraph, resourceFactory, constraintProviders, resourceDefinitionAccessor, 15 | queryableBuilder) 16 | { 17 | public override string? TransactionId => null; 18 | } 19 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.Queries; 3 | using JsonApiDotNetCore.Queries.Expressions; 4 | using JsonApiDotNetCore.Repositories; 5 | using JsonApiDotNetCore.Resources; 6 | 7 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Transactions; 8 | 9 | [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] 10 | public sealed class PerformerRepository : IResourceRepository 11 | { 12 | public Task> GetAsync(QueryLayer queryLayer, CancellationToken cancellationToken) 13 | { 14 | throw new NotImplementedException(); 15 | } 16 | 17 | public Task CountAsync(FilterExpression? filter, CancellationToken cancellationToken) 18 | { 19 | throw new NotImplementedException(); 20 | } 21 | 22 | public Task GetForCreateAsync(Type resourceClrType, string? id, CancellationToken cancellationToken) 23 | { 24 | throw new NotImplementedException(); 25 | } 26 | 27 | public Task CreateAsync(Performer resourceFromRequest, Performer resourceForDatabase, CancellationToken cancellationToken) 28 | { 29 | throw new NotImplementedException(); 30 | } 31 | 32 | public Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken) 33 | { 34 | throw new NotImplementedException(); 35 | } 36 | 37 | public Task UpdateAsync(Performer resourceFromRequest, Performer resourceFromDatabase, CancellationToken cancellationToken) 38 | { 39 | throw new NotImplementedException(); 40 | } 41 | 42 | public Task DeleteAsync(Performer? resourceFromDatabase, string? id, CancellationToken cancellationToken) 43 | { 44 | throw new NotImplementedException(); 45 | } 46 | 47 | public Task SetRelationshipAsync(Performer leftResource, object? rightValue, CancellationToken cancellationToken) 48 | { 49 | throw new NotImplementedException(); 50 | } 51 | 52 | public Task AddToToManyRelationshipAsync(Performer? leftResource, string? leftId, ISet rightResourceIds, CancellationToken cancellationToken) 53 | { 54 | throw new NotImplementedException(); 55 | } 56 | 57 | public Task RemoveFromToManyRelationshipAsync(Performer leftResource, ISet rightResourceIds, CancellationToken cancellationToken) 58 | { 59 | throw new NotImplementedException(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using FluentAssertions; 3 | using JsonApiDotNetCore.Serialization.Objects; 4 | using TestBuildingBlocks; 5 | using Xunit; 6 | 7 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Updating.Relationships; 8 | 9 | [Collection("AtomicOperationsFixture")] 10 | public sealed class AtomicUpdateToOneRelationshipTests(AtomicOperationsFixture fixture) 11 | { 12 | private readonly IntegrationTestContext _testContext = fixture.TestContext; 13 | private readonly OperationsFakers _fakers = new(); 14 | 15 | [Fact] 16 | public async Task Cannot_create_ManyToOne_relationship() 17 | { 18 | // Arrange 19 | MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); 20 | RecordCompany existingCompany = _fakers.RecordCompany.GenerateOne(); 21 | 22 | await _testContext.RunOnDatabaseAsync(async dbContext => 23 | { 24 | dbContext.RecordCompanies.Add(existingCompany); 25 | dbContext.MusicTracks.Add(existingTrack); 26 | await dbContext.SaveChangesAsync(); 27 | }); 28 | 29 | var requestBody = new 30 | { 31 | atomic__operations = new[] 32 | { 33 | new 34 | { 35 | op = "update", 36 | @ref = new 37 | { 38 | type = "musicTracks", 39 | id = existingTrack.StringId, 40 | relationship = "ownedBy" 41 | }, 42 | data = new 43 | { 44 | type = "recordCompanies", 45 | id = existingCompany.StringId 46 | } 47 | } 48 | } 49 | }; 50 | 51 | const string route = "/operations"; 52 | 53 | // Act 54 | (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); 55 | 56 | // Assert 57 | httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); 58 | 59 | responseDocument.Errors.Should().HaveCount(1); 60 | 61 | ErrorObject error = responseDocument.Errors[0]; 62 | error.StatusCode.Should().Be(HttpStatusCode.BadRequest); 63 | error.Title.Should().Be("Relationships are not supported when using MongoDB."); 64 | error.Detail.Should().BeNull(); 65 | error.Source.Should().NotBeNull(); 66 | error.Source.Pointer.Should().Be("/atomic:operations[0]"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using FluentAssertions; 3 | using JsonApiDotNetCore.Serialization.Objects; 4 | using TestBuildingBlocks; 5 | using Xunit; 6 | 7 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Updating.Resources; 8 | 9 | [Collection("AtomicOperationsFixture")] 10 | public sealed class AtomicReplaceToManyRelationshipTests(AtomicOperationsFixture fixture) 11 | { 12 | private readonly IntegrationTestContext _testContext = fixture.TestContext; 13 | private readonly OperationsFakers _fakers = new(); 14 | 15 | [Fact] 16 | public async Task Cannot_replace_ToMany_relationship() 17 | { 18 | // Arrange 19 | MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); 20 | Performer existingPerformer = _fakers.Performer.GenerateOne(); 21 | 22 | await _testContext.RunOnDatabaseAsync(async dbContext => 23 | { 24 | dbContext.Performers.Add(existingPerformer); 25 | dbContext.MusicTracks.Add(existingTrack); 26 | await dbContext.SaveChangesAsync(); 27 | }); 28 | 29 | var requestBody = new 30 | { 31 | atomic__operations = new[] 32 | { 33 | new 34 | { 35 | op = "update", 36 | data = new 37 | { 38 | type = "musicTracks", 39 | id = existingTrack.StringId, 40 | relationships = new 41 | { 42 | performers = new 43 | { 44 | data = new[] 45 | { 46 | new 47 | { 48 | type = "performers", 49 | id = existingPerformer.StringId 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | }; 58 | 59 | const string route = "/operations"; 60 | 61 | // Act 62 | (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); 63 | 64 | // Assert 65 | httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); 66 | 67 | responseDocument.Errors.Should().HaveCount(1); 68 | 69 | ErrorObject error = responseDocument.Errors[0]; 70 | error.StatusCode.Should().Be(HttpStatusCode.BadRequest); 71 | error.Title.Should().Be("Relationships are not supported when using MongoDB."); 72 | error.Detail.Should().BeNull(); 73 | error.Source.Should().NotBeNull(); 74 | error.Source.Pointer.Should().Be("/atomic:operations[0]"); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using FluentAssertions; 3 | using JsonApiDotNetCore.Serialization.Objects; 4 | using TestBuildingBlocks; 5 | using Xunit; 6 | 7 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Updating.Resources; 8 | 9 | [Collection("AtomicOperationsFixture")] 10 | public sealed class AtomicUpdateToOneRelationshipTests(AtomicOperationsFixture fixture) 11 | { 12 | private readonly IntegrationTestContext _testContext = fixture.TestContext; 13 | private readonly OperationsFakers _fakers = new(); 14 | 15 | [Fact] 16 | public async Task Cannot_create_ToOne_relationship() 17 | { 18 | // Arrange 19 | Lyric existingLyric = _fakers.Lyric.GenerateOne(); 20 | MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); 21 | 22 | await _testContext.RunOnDatabaseAsync(async dbContext => 23 | { 24 | dbContext.MusicTracks.Add(existingTrack); 25 | dbContext.Lyrics.Add(existingLyric); 26 | await dbContext.SaveChangesAsync(); 27 | }); 28 | 29 | var requestBody = new 30 | { 31 | atomic__operations = new[] 32 | { 33 | new 34 | { 35 | op = "update", 36 | data = new 37 | { 38 | type = "lyrics", 39 | id = existingLyric.StringId, 40 | relationships = new 41 | { 42 | track = new 43 | { 44 | data = new 45 | { 46 | type = "musicTracks", 47 | id = existingTrack.StringId 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | }; 55 | 56 | const string route = "/operations"; 57 | 58 | // Act 59 | (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); 60 | 61 | // Assert 62 | httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); 63 | 64 | responseDocument.Errors.Should().HaveCount(1); 65 | 66 | ErrorObject error = responseDocument.Errors[0]; 67 | error.StatusCode.Should().Be(HttpStatusCode.BadRequest); 68 | error.Title.Should().Be("Relationships are not supported when using MongoDB."); 69 | error.Detail.Should().BeNull(); 70 | error.Source.Should().NotBeNull(); 71 | error.Source.Pointer.Should().Be("/atomic:operations[0]"); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/MetaDbContext.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using MongoDB.Driver; 3 | using TestBuildingBlocks; 4 | 5 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta; 6 | 7 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 8 | public sealed class MetaDbContext(IMongoDatabase database) 9 | : MongoDbContextShim(database) 10 | { 11 | public MongoDbSetShim SupportTickets => Set(); 12 | } 13 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/MetaFakers.cs: -------------------------------------------------------------------------------- 1 | using Bogus; 2 | using TestBuildingBlocks; 3 | 4 | // @formatter:wrap_chained_method_calls chop_if_long 5 | // @formatter:wrap_before_first_method_call true 6 | 7 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta; 8 | 9 | internal sealed class MetaFakers 10 | { 11 | private readonly Lazy> _lazySupportTicketFaker = new(() => new Faker() 12 | .MakeDeterministic() 13 | .RuleFor(supportTicket => supportTicket.Description, faker => faker.Lorem.Paragraph())); 14 | 15 | public Faker SupportTicket => _lazySupportTicketFaker.Value; 16 | } 17 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/ResourceMetaTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using FluentAssertions; 3 | using JsonApiDotNetCore.Configuration; 4 | using JsonApiDotNetCore.Serialization.Objects; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using TestBuildingBlocks; 7 | using Xunit; 8 | 9 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta; 10 | 11 | public sealed class ResourceMetaTests : IClassFixture> 12 | { 13 | private readonly IntegrationTestContext _testContext; 14 | private readonly MetaFakers _fakers = new(); 15 | 16 | public ResourceMetaTests(IntegrationTestContext testContext) 17 | { 18 | _testContext = testContext; 19 | 20 | testContext.UseResourceTypesInNamespace(typeof(SupportTicket).Namespace); 21 | 22 | testContext.UseController(); 23 | 24 | testContext.ConfigureServices(services => 25 | { 26 | services.AddResourceDefinition(); 27 | 28 | services.AddSingleton(); 29 | }); 30 | 31 | var hitCounter = _testContext.Factory.Services.GetRequiredService(); 32 | hitCounter.Reset(); 33 | } 34 | 35 | [Fact] 36 | public async Task Returns_resource_meta_from_ResourceDefinition() 37 | { 38 | // Arrange 39 | var hitCounter = _testContext.Factory.Services.GetRequiredService(); 40 | 41 | List tickets = _fakers.SupportTicket.GenerateList(3); 42 | tickets[0].Description = $"Critical: {tickets[0].Description}"; 43 | tickets[2].Description = $"Critical: {tickets[2].Description}"; 44 | 45 | await _testContext.RunOnDatabaseAsync(async dbContext => 46 | { 47 | await dbContext.ClearTableAsync(); 48 | dbContext.SupportTickets.AddRange(tickets); 49 | await dbContext.SaveChangesAsync(); 50 | }); 51 | 52 | const string route = "/supportTickets"; 53 | 54 | // Act 55 | (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); 56 | 57 | // Assert 58 | httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); 59 | 60 | responseDocument.Data.ManyValue.Should().HaveCount(3); 61 | responseDocument.Data.ManyValue[0].Meta.Should().ContainKey("hasHighPriority"); 62 | responseDocument.Data.ManyValue[1].Meta.Should().BeNull(); 63 | responseDocument.Data.ManyValue[2].Meta.Should().ContainKey("hasHighPriority"); 64 | 65 | hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] 66 | { 67 | (typeof(SupportTicket), ResourceDefinitionExtensibilityPoints.GetMeta), 68 | (typeof(SupportTicket), ResourceDefinitionExtensibilityPoints.GetMeta), 69 | (typeof(SupportTicket), ResourceDefinitionExtensibilityPoints.GetMeta) 70 | }, options => options.WithStrictOrdering()); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/SupportTicket.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.MongoDb.Resources; 3 | using JsonApiDotNetCore.Resources.Annotations; 4 | 5 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta; 6 | 7 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 8 | [Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta")] 9 | public sealed class SupportTicket : HexStringMongoIdentifiable 10 | { 11 | [Attr] 12 | public string Description { get; set; } = null!; 13 | } 14 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/SupportTicketDefinition.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.Configuration; 3 | 4 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta; 5 | 6 | [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] 7 | public sealed class SupportTicketDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) 8 | : HitCountingResourceDefinition(resourceGraph, hitCounter) 9 | { 10 | protected override ResourceDefinitionExtensibilityPoints ExtensibilityPointsToTrack => ResourceDefinitionExtensibilityPoints.GetMeta; 11 | 12 | public override IDictionary? GetMeta(SupportTicket resource) 13 | { 14 | base.GetMeta(resource); 15 | 16 | if (!string.IsNullOrEmpty(resource.Description) && resource.Description.StartsWith("Critical:", StringComparison.Ordinal)) 17 | { 18 | return new Dictionary 19 | { 20 | ["hasHighPriority"] = true 21 | }; 22 | } 23 | 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/AccountPreferences.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.MongoDb.Resources; 3 | using JsonApiDotNetCore.Resources.Annotations; 4 | 5 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings; 6 | 7 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 8 | [Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")] 9 | public sealed class AccountPreferences : HexStringMongoIdentifiable 10 | { 11 | [Attr] 12 | public bool UseDarkTheme { get; set; } 13 | } 14 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Blog.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.MongoDb.Resources; 3 | using JsonApiDotNetCore.Resources.Annotations; 4 | using MongoDB.Bson.Serialization.Attributes; 5 | 6 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings; 7 | 8 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 9 | [Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")] 10 | public sealed class Blog : HexStringMongoIdentifiable 11 | { 12 | [Attr] 13 | public string Title { get; set; } = null!; 14 | 15 | [Attr] 16 | public string PlatformName { get; set; } = null!; 17 | 18 | [Attr(Capabilities = AttrCapabilities.All & ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))] 19 | public bool ShowAdvertisements => PlatformName.EndsWith("(using free account)", StringComparison.Ordinal); 20 | 21 | public bool IsPublished { get; set; } 22 | 23 | [HasMany] 24 | [BsonIgnore] 25 | public IList Posts { get; set; } = new List(); 26 | 27 | [HasOne] 28 | [BsonIgnore] 29 | public WebAccount? Owner { get; set; } 30 | } 31 | -------------------------------------------------------------------------------- /test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/BlogPost.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using JsonApiDotNetCore.MongoDb.Resources; 3 | using JsonApiDotNetCore.Resources.Annotations; 4 | using MongoDB.Bson.Serialization.Attributes; 5 | 6 | namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings; 7 | 8 | [UsedImplicitly(ImplicitUseTargetFlags.Members)] 9 | [Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")] 10 | public sealed class BlogPost : HexStringMongoIdentifiable 11 | { 12 | [Attr] 13 | public string Caption { get; set; } = null!; 14 | 15 | [Attr] 16 | public string Url { get; set; } = null!; 17 | 18 | [HasOne] 19 | [BsonIgnore] 20 | public WebAccount? Author { get; set; } 21 | 22 | [HasOne] 23 | [BsonIgnore] 24 | public WebAccount? Reviewer { get; set; } 25 | 26 | [HasMany] 27 | [BsonIgnore] 28 | public ISet