diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index d914fc3..197960a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,28 +3,25 @@ "isRoot": true, "tools": { "jetbrains.resharper.globaltools": { - "version": "2022.2.3", + "version": "2024.3.6", "commands": [ "jb" - ] + ], + "rollForward": false }, "regitlint": { - "version": "6.1.1", + "version": "6.3.13", "commands": [ "regitlint" - ] - }, - "codecov.tool": { - "version": "1.13.0", - "commands": [ - "codecov" - ] + ], + "rollForward": false }, "dotnet-reportgenerator-globaltool": { - "version": "5.1.3", + "version": "5.4.5", "commands": [ "reportgenerator" - ] + ], + "rollForward": false } } -} +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig index ca191cf..2e5c106 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,60 +4,125 @@ root = true [*] indent_style = space indent_size = 4 +tab-width = 4 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.{config,csproj,css,js,json,props,ruleset,xslt}] +[*.{config,csproj,css,js,json,props,targets,xml,ruleset,xsd,xslt,html,yml,yaml}] indent_size = 2 +tab-width = 2 +max_line_length = 160 + +[*.{cs,cshtml,ascx,aspx}] -[*.{cs}] #### C#/.NET Coding Conventions #### +# Default severity for IDE* analyzers with category 'Style' +# Note: specific rules below use severity silent, because Resharper code cleanup auto-fixes them. +dotnet_analyzer_diagnostic.category-Style.severity = warning + # 'using' directive preferences dotnet_sort_system_directives_first = true -csharp_using_directive_placement = outside_namespace:suggestion +csharp_using_directive_placement = outside_namespace:silent +# IDE0005: Remove unnecessary import +dotnet_diagnostic.IDE0005.severity = silent # Namespace declarations -csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_namespace_declarations = file_scoped:silent +# IDE0160: Use block-scoped namespace +dotnet_diagnostic.IDE0160.severity = silent +# IDE0161: Use file-scoped namespace +dotnet_diagnostic.IDE0161.severity = silent # this. preferences -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_property = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_event = false:suggestion +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent +# IDE0003: Remove this or Me qualification +dotnet_diagnostic.IDE0003.severity = silent +# IDE0009: Add this or Me qualification +dotnet_diagnostic.IDE0009.severity = silent # Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +# IDE0049: Use language keywords instead of framework type names for type references +dotnet_diagnostic.IDE0049.severity = silent # Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion -csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion -csharp_style_pattern_local_over_anonymous_function = false:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +# IDE0040: Add accessibility modifiers +dotnet_diagnostic.IDE0040.severity = silent +csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:silent +# IDE0036: Order modifiers +dotnet_diagnostic.IDE0036.severity = silent # Expression-level preferences dotnet_style_operator_placement_when_wrapping = end_of_line -dotnet_style_prefer_auto_properties = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_return = true:suggestion -csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion +dotnet_style_prefer_auto_properties = true:silent +# IDE0032: Use auto property +dotnet_diagnostic.IDE0032.severity = silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +# IDE0045: Use conditional expression for assignment +dotnet_diagnostic.IDE0045.severity = silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +# IDE0046: Use conditional expression for return +dotnet_diagnostic.IDE0046.severity = silent +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +# IDE0058: Remove unused expression value +dotnet_diagnostic.IDE0058.severity = silent + +# Collection expression preferences (note: partially turned off in Directory.Build.props) +dotnet_style_prefer_collection_expression = when_types_exactly_match # Parameter preferences -dotnet_code_quality_unused_parameters = non_public:suggestion +dotnet_code_quality_unused_parameters = non_public + +# Local functions vs lambdas +csharp_style_prefer_local_over_anonymous_function = false:silent +# IDE0039: Use local function instead of lambda +dotnet_diagnostic.IDE0039.severity = silent # Expression-bodied members -csharp_style_expression_bodied_accessors = true:suggestion -csharp_style_expression_bodied_constructors = false:suggestion -csharp_style_expression_bodied_indexers = true:suggestion -csharp_style_expression_bodied_lambdas = true:suggestion -csharp_style_expression_bodied_local_functions = false:suggestion -csharp_style_expression_bodied_methods = false:suggestion -csharp_style_expression_bodied_operators = false:suggestion -csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_accessors = true:silent +# IDE0027: Use expression body for accessors +dotnet_diagnostic.IDE0027.severity = silent +csharp_style_expression_bodied_constructors = false:silent +# IDE0021: Use expression body for constructors +dotnet_diagnostic.IDE0021.severity = silent +csharp_style_expression_bodied_indexers = true:silent +# IDE0026: Use expression body for indexers +dotnet_diagnostic.IDE0026.severity = silent +csharp_style_expression_bodied_lambdas = true:silent +# IDE0053: Use expression body for lambdas +dotnet_diagnostic.IDE0053.severity = silent +csharp_style_expression_bodied_local_functions = false:silent +# IDE0061: Use expression body for local functions +dotnet_diagnostic.IDE0061.severity = silent +csharp_style_expression_bodied_methods = false:silent +# IDE0022: Use expression body for methods +dotnet_diagnostic.IDE0022.severity = silent +csharp_style_expression_bodied_operators = false:silent +# IDE0023: Use expression body for conversion operators +dotnet_diagnostic.IDE0023.severity = silent +# IDE0024: Use expression body for operators +dotnet_diagnostic.IDE0024.severity = silent +csharp_style_expression_bodied_properties = true:silent +# IDE0025: Use expression body for properties +dotnet_diagnostic.IDE0025.severity = silent + +# Member preferences (these analyzers are unreliable) +# IDE0051: Remove unused private member +dotnet_diagnostic.IDE0051.severity = silent +# IDE0052: Remove unread private member +dotnet_diagnostic.IDE0052.severity = silent # Code-block preferences -csharp_prefer_braces = true:suggestion +csharp_prefer_braces = true:silent +# IDE0011: Add braces +dotnet_diagnostic.IDE0011.severity = silent # Indentation preferences csharp_indent_case_contents_when_block = false @@ -66,16 +131,44 @@ csharp_indent_case_contents_when_block = false csharp_preserve_single_line_statements = false # 'var' usage preferences -csharp_style_var_for_built_in_types = false:suggestion -csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_var_elsewhere = false:suggestion +csharp_style_var_for_built_in_types = false:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = false:silent +# IDE0007: Use var instead of explicit type +dotnet_diagnostic.IDE0007.severity = silent +# IDE0008: Use explicit type instead of var +dotnet_diagnostic.IDE0008.severity = silent # Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:suggestion -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion -dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:suggestion - -#### Naming Style #### +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:silent +# IDE0047: Remove unnecessary parentheses +dotnet_diagnostic.IDE0047.severity = silent +# IDE0048: Add parentheses for clarity +dotnet_diagnostic.IDE0048.severity = silent + +# Switch preferences +# IDE0010: Add missing cases to switch statement +dotnet_diagnostic.IDE0010.severity = silent +# IDE0072: Add missing cases to switch expression +dotnet_diagnostic.IDE0072.severity = silent + +# Null check preferences +# IDE0029: Null check can be simplified +dotnet_diagnostic.IDE0029.severity = silent +# IDE0030: Null check can be simplified +dotnet_diagnostic.IDE0030.severity = silent +# IDE0270: Null check can be simplified +dotnet_diagnostic.IDE0270.severity = silent + +# JSON002: Probable JSON string detected +dotnet_diagnostic.JSON002.severity = silent + +# CA1062: Validate arguments of public methods +dotnet_code_quality.CA1062.excluded_symbol_names = Accept|DefaultVisit|Visit*|Apply* + +#### .NET Naming Style #### dotnet_diagnostic.IDE1006.severity = warning diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b065d30 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: json-api-dotnet diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 103a247..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,21 +0,0 @@ -#### DESCRIPTION - -_Describe your bug or feature request here._ - -#### STEPS TO REPRODUCE - -_Consider to include your code here, such as models, DbContext, controllers, resource services, repositories, resource definitions etc. Please also include the request URL with body (if applicable) and the full exception stack trace (set `options.IncludeExceptionStackTraceInErrors` to `true`) in case of errors._ - -1. -2. -3. - -#### EXPECTED BEHAVIOR - -#### ACTUAL BEHAVIOR - -#### VERSIONS USED -- JsonApiDotNetCore.MongoDb version: -- JsonApiDotNetCore version: -- ASP.NET Core version: -- MongoDB version: diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d204896 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve. +title: '' +labels: 'bug' +assignees: '' + +--- + + + +#### DESCRIPTION + + +#### STEPS TO REPRODUCE + + +1. +2. +3. + +#### EXPECTED BEHAVIOR + + +#### ACTUAL BEHAVIOR + + +#### VERSIONS USED +- JsonApiDotNetCore.MongoDb version: +- JsonApiDotNetCore version: +- ASP.NET Core version: +- MongoDB version: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8bf0a91 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: true +contact_links: +- name: Documentation + url: https://www.jsonapi.net/usage/resources/index.html + about: Read our comprehensive documentation. +- name: Sponsor JsonApiDotNetCore + url: https://github.com/sponsors/json-api-dotnet + about: Help the continued development. +- name: Ask on Gitter + url: https://gitter.im/json-api-dotnet-core/Lobby + about: Get in touch with the whole community. +- name: Ask on Stack Overflow + url: https://stackoverflow.com/questions/tagged/json-api + about: The best place for asking general-purpose questions. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..019f7a9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for this project. +title: '' +labels: 'enhancement' +assignees: '' + +--- + + + +**Is your feature request related to a problem? Please describe.** + + +**Describe the solution you'd like** + + +**Describe alternatives you've considered** + + +**Additional context** + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..fcff5ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,39 @@ +--- +name: Question +about: Ask a question. +title: '' +labels: 'question' +assignees: '' + +--- + + + +#### SUMMARY + + +#### DETAILS + + + +#### STEPS TO REPRODUCE + + +1. +2. +3. + +#### VERSIONS USED +- JsonApiDotNetCore.MongoDb version: +- JsonApiDotNetCore version: +- ASP.NET Core version: +- MongoDB version: diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8bd1514 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + pull-request-branch-name: + separator: "-" + - package-ecosystem: nuget + directory: "/" + schedule: + interval: daily + pull-request-branch-name: + separator: "-" + open-pull-requests-limit: 25 + ignore: + # Block updates to all exposed dependencies of the NuGet packages we produce, as updating them would be a breaking change. + - dependency-name: "JsonApiDotNetCore*" + - dependency-name: "MongoDB.Driver*" + # Block major updates of packages that require a matching .NET version. + - dependency-name: "Microsoft.AspNetCore*" + update-types: ["version-update:semver-major"] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e6a9569 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,251 @@ +# General links +# https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables +# https://docs.github.com/en/actions/learn-github-actions/contexts#github-context +# https://docs.github.com/en/webhooks-and-events/webhooks/webhook-events-and-payloads +# https://docs.github.com/en/actions/learn-github-actions/expressions +# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions +# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs + +name: Build + +on: + push: + branches: [ 'master', 'release/**' ] + pull_request: + branches: [ 'master', 'release/**' ] + release: + types: [published] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +jobs: + build-and-test: + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + permissions: + contents: read + steps: + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.* + 9.0.* + - name: Show installed versions + shell: pwsh + run: | + Write-Host "$(pwsh --version) is installed at $PSHOME" + Write-Host "Active .NET SDK: $(dotnet --version)" + - name: Git checkout + uses: actions/checkout@v4 + - name: Restore tools + run: | + dotnet tool restore + - name: Restore packages + run: | + dotnet restore + - name: Calculate version suffix + shell: pwsh + run: | + if ($env:GITHUB_REF_TYPE -eq 'tag') { + # Get the version prefix/suffix from the git tag. For example: 'v1.0.0-preview1-final' => '1.0.0' and 'preview1-final' + $segments = $env:GITHUB_REF_NAME -split "-" + $versionPrefix = $segments[0].TrimStart('v') + $versionSuffix = $segments.Length -eq 1 ? '' : $segments[1..$($segments.Length - 1)] -join '-' + + [xml]$xml = Get-Content Directory.Build.props + $configuredVersionPrefix = $xml.Project.PropertyGroup.VersionPrefix | Select-Object -First 1 + + if ($configuredVersionPrefix -ne $versionPrefix) { + Write-Error "Version prefix from git release tag '$versionPrefix' does not match version prefix '$configuredVersionPrefix' stored in Directory.Build.props." + # To recover from this: + # - Delete the GitHub release + # - Run: git push --delete origin the-invalid-tag-name + # - Adjust VersionPrefix in Directory.Build.props, commit and push + # - Recreate the GitHub release + } + } + else { + # Get the version suffix from the auto-incrementing build number. For example: '123' => 'master-00123' + $revision = "{0:D5}" -f [convert]::ToInt32($env:GITHUB_RUN_NUMBER, 10) + $branchName = ![string]::IsNullOrEmpty($env:GITHUB_HEAD_REF) ? $env:GITHUB_HEAD_REF : $env:GITHUB_REF_NAME + $safeName = $branchName.Replace('/', '-').Replace('_', '-') + $versionSuffix = "$safeName-$revision" + } + Write-Output "Using version suffix: $versionSuffix" + Write-Output "PACKAGE_VERSION_SUFFIX=$versionSuffix" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + - name: Build + shell: pwsh + run: | + dotnet build --no-restore --configuration Release /p:VersionSuffix=$env:PACKAGE_VERSION_SUFFIX + - name: Test + run: | + dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --logger "GitHubActions;summary.includeSkippedTests=true" + - name: Upload coverage to codecov.io + if: matrix.os == 'ubuntu-latest' + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: true + verbose: true + - name: Generate packages + shell: pwsh + run: | + dotnet pack --no-build --configuration Release --output $env:GITHUB_WORKSPACE/artifacts/packages /p:VersionSuffix=$env:PACKAGE_VERSION_SUFFIX + - name: Upload packages to artifacts + if: matrix.os == 'ubuntu-latest' + uses: actions/upload-artifact@v4 + with: + name: packages + path: artifacts/packages + + inspect-code: + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + permissions: + contents: read + steps: + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.* + 9.0.* + - name: Git checkout + uses: actions/checkout@v4 + - name: Restore tools + run: | + dotnet tool restore + - name: InspectCode + shell: pwsh + run: | + $inspectCodeOutputPath = Join-Path $env:RUNNER_TEMP 'jetbrains-inspectcode-results.xml' + Write-Output "INSPECT_CODE_OUTPUT_PATH=$inspectCodeOutputPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + dotnet jb inspectcode JsonApiDotNetCore.MongoDb.sln --build --dotnetcoresdk=$(dotnet --version) --output="$inspectCodeOutputPath" --format="xml" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --properties:ContinuousIntegrationBuild=false --properties:RunAnalyzers=false --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=GlobalPerProduct -dsl=SolutionPersonal -dsl=ProjectPersonal + - name: Verify outcome + shell: pwsh + run: | + [xml]$xml = Get-Content $env:INSPECT_CODE_OUTPUT_PATH + if ($xml.report.Issues -and $xml.report.Issues.Project) { + foreach ($project in $xml.report.Issues.Project) { + if ($project.Issue.Count -gt 0) { + $project.ForEach({ + Write-Output "`nProject $($project.Name)" + $failed = $true + + $_.Issue.ForEach({ + $issueType = $xml.report.IssueTypes.SelectSingleNode("IssueType[@Id='$($_.TypeId)']") + $severity = $_.Severity ?? $issueType.Severity + + Write-Output "[$severity] $($_.File):$($_.Line) $($_.TypeId): $($_.Message)" + }) + }) + } + } + + if ($failed) { + Write-Error "One or more projects failed code inspection." + } + } + + cleanup-code: + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + permissions: + contents: read + steps: + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.* + 9.0.* + - name: Git checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: Restore tools + run: | + dotnet tool restore + - name: Restore packages + run: | + dotnet restore + - name: CleanupCode (on PR diff) + if: github.event_name == 'pull_request' + shell: pwsh + run: | + # Not using the environment variables for SHAs, because they may be outdated. This may happen on force-push after the build is queued, but before it starts. + # The below works because HEAD is detached (at the merge commit), so HEAD~1 is at the base branch. When a PR contains no commits, this job will not run. + $headCommitHash = git rev-parse HEAD + $baseCommitHash = git rev-parse HEAD~1 + + Write-Output "Running code cleanup on commit range $baseCommitHash..$headCommitHash in pull request." + 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 commits -a $headCommitHash -b $baseCommitHash --fail-on-diff --print-diff + - name: CleanupCode (on branch) + if: github.event_name == 'push' || github.event_name == 'release' + shell: pwsh + run: | + Write-Output "Running code cleanup on all files." + 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 --fail-on-diff --print-diff + + publish: + timeout-minutes: 60 + runs-on: ubuntu-latest + needs: [ build-and-test, inspect-code, cleanup-code ] + if: ${{ !github.event.pull_request.head.repo.fork }} + permissions: + packages: write + contents: write + steps: + - name: Tune GitHub-hosted runner network + uses: smorimoto/tune-github-hosted-runner-network@v1 + - name: Download artifacts + uses: actions/download-artifact@v4 + - name: Publish to GitHub Packages + if: github.event_name == 'push' || github.event_name == 'release' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: pwsh + run: | + dotnet nuget add source --username 'json-api-dotnet' --password "$env:GITHUB_TOKEN" --store-password-in-clear-text --name 'github' 'https://nuget.pkg.github.com/json-api-dotnet/index.json' + dotnet nuget push "$env:GITHUB_WORKSPACE/packages/*.nupkg" --api-key "$env:GITHUB_TOKEN" --source 'github' + - name: Publish to feedz.io + if: github.event_name == 'push' || github.event_name == 'release' + env: + FEEDZ_IO_API_KEY: ${{ secrets.FEEDZ_IO_API_KEY }} + shell: pwsh + run: | + dotnet nuget add source --name 'feedz-io' 'https://f.feedz.io/json-api-dotnet/jsonapidotnetcore/nuget/index.json' + dotnet nuget push "$env:GITHUB_WORKSPACE/packages/*.nupkg" --api-key "$env:FEEDZ_IO_API_KEY" --source 'feedz-io' + - name: Publish to NuGet + if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') + env: + NUGET_ORG_API_KEY: ${{ secrets.NUGET_ORG_API_KEY }} + shell: pwsh + run: | + dotnet nuget push "$env:GITHUB_WORKSPACE/packages/*.nupkg" --api-key "$env:NUGET_ORG_API_KEY" --source 'nuget.org' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..d3d4db6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,43 @@ +name: "CodeQL" + +on: + push: + branches: [ 'master', 'release/**' ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ 'master', 'release/**' ] + schedule: + - cron: '0 0 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: 'ubuntu-latest' + timeout-minutes: 60 + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.* + 9.0.* + - name: Git checkout + uses: actions/checkout@v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/deps-review.yml b/.github/workflows/deps-review.yml new file mode 100644 index 0000000..b9d6d20 --- /dev/null +++ b/.github/workflows/deps-review.yml @@ -0,0 +1,14 @@ +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 diff --git a/Build.ps1 b/Build.ps1 index ce11718..6c6ff9c 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,118 +1,27 @@ -function CheckLastExitCode { - param ([int[]]$SuccessCodes = @(0), [scriptblock]$CleanupScript=$null) - - if ($SuccessCodes -notcontains $LastExitCode) { - throw "Executable returned exit code $LastExitCode" - } -} - -function RunInspectCode { - $outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml') - dotnet jb inspectcode JsonApiDotNetCore.MongoDb.sln --no-build --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=GlobalPerProduct -dsl=SolutionPersonal -dsl=ProjectPersonal - CheckLastExitCode - - [xml]$xml = Get-Content "$outputPath" - if ($xml.report.Issues -and $xml.report.Issues.Project) { - foreach ($project in $xml.report.Issues.Project) { - if ($project.Issue.Count -gt 0) { - $project.ForEach({ - Write-Output "`nProject $($project.Name)" - $failed = $true - - $_.Issue.ForEach({ - $issueType = $xml.report.IssueTypes.SelectSingleNode("IssueType[@Id='$($_.TypeId)']") - $severity = $_.Severity ?? $issueType.Severity - - Write-Output "[$severity] $($_.File):$($_.Line) $($_.Message)" - }) - }) - } - } - - if ($failed) { - throw "One or more projects failed code inspection."; - } - } -} - -function RunCleanupCode { - # When running in cibuild for a pull request, this reformats only the files changed in the PR and fails if the reformat produces changes. - - if ($env:APPVEYOR_PULL_REQUEST_HEAD_COMMIT) { - # In the past, we used $env:APPVEYOR_PULL_REQUEST_HEAD_COMMIT for the merge commit hash. That is the pinned hash at the time the build is enqueued. - # When a force-push happens after that, while the build hasn't yet started, this hash becomes invalid during the build, resulting in a lookup error. - # To prevent failing the build for unobvious reasons we use HEAD, which is always a detached head (the PR merge result). - - $headCommitHash = git rev-parse HEAD - CheckLastExitCode - - $baseCommitHash = git rev-parse "$env:APPVEYOR_REPO_BRANCH" - CheckLastExitCode - - if ($baseCommitHash -ne $headCommitHash) { - Write-Output "Running code cleanup on commit range $baseCommitHash..$headCommitHash in pull request." - dotnet regitlint -s JsonApiDotNetCore.MongoDb.sln --print-command --skip-tool-check --jb-profile="JADNC Full Cleanup" --jb --properties:Configuration=Release --jb --verbosity=WARN -f commits -a $headCommitHash -b $baseCommitHash --fail-on-diff --print-diff - CheckLastExitCode - } +function VerifySuccessExitCode { + if ($LastExitCode -ne 0) { + throw "Command failed with exit code $LastExitCode." } } -function ReportCodeCoverage { - if ($env:APPVEYOR) { - if ($IsWindows) { - dotnet codecov -f "**\coverage.cobertura.xml" - } - } - else { - dotnet reportgenerator -reports:**\coverage.cobertura.xml -targetdir:artifacts\coverage - } - - CheckLastExitCode -} - -function CreateNuGetPackage { - if ($env:APPVEYOR_REPO_TAG -eq $true) { - # Get the version suffix from the repo tag. Example: v1.0.0-preview1-final => preview1-final - $segments = $env:APPVEYOR_REPO_TAG_NAME -split "-" - $suffixSegments = $segments[1..2] - $versionSuffix = $suffixSegments -join "-" - } - else { - # Get the version suffix from the auto-incrementing build number. Example: "123" => "master-0123". - if ($env:APPVEYOR_BUILD_NUMBER) { - $revision = "{0:D4}" -f [convert]::ToInt32($env:APPVEYOR_BUILD_NUMBER, 10) - $versionSuffix = "$($env:APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH ?? $env:APPVEYOR_REPO_BRANCH)-$revision" - } - else { - $versionSuffix = "pre-0001" - } - } - - if ([string]::IsNullOrWhitespace($versionSuffix)) { - dotnet pack --no-restore --no-build --configuration Release --output .\artifacts - } - else { - dotnet pack --no-restore --no-build --configuration Release --output .\artifacts --version-suffix=$versionSuffix - } +Write-Host "$(pwsh --version)" +Write-Host "Active .NET SDK: $(dotnet --version)" +Write-Host "Using version suffix: $versionSuffix" - CheckLastExitCode -} +Remove-Item -Recurse -Force artifacts -ErrorAction SilentlyContinue +Remove-Item -Recurse -Force * -Include coverage.cobertura.xml dotnet tool restore -CheckLastExitCode - -dotnet build -c Release -CheckLastExitCode +VerifySuccessExitCode -# https://youtrack.jetbrains.com/issue/RSRP-488628/Breaking-InspectCode-fails-with-Roslyn-Worker-process-exited-unexpectedly-after-update -if ($IsWindows) { - RunInspectCode - RunCleanupCode -} +dotnet build --configuration Release +VerifySuccessExitCode -dotnet test -c Release --no-build --collect:"XPlat Code Coverage" -CheckLastExitCode +dotnet test --no-build --configuration Release --verbosity quiet --collect:"XPlat Code Coverage" +VerifySuccessExitCode -ReportCodeCoverage +dotnet reportgenerator -reports:**\coverage.cobertura.xml -targetdir:artifacts\coverage -filefilters:-*.g.cs +VerifySuccessExitCode -CreateNuGetPackage +dotnet pack --no-build --configuration Release --output artifacts/packages +VerifySuccessExitCode diff --git a/CSharpGuidelinesAnalyzer.config b/CSharpGuidelinesAnalyzer.config index acd0856..89b568e 100644 --- a/CSharpGuidelinesAnalyzer.config +++ b/CSharpGuidelinesAnalyzer.config @@ -1,5 +1,5 @@ - + diff --git a/CodingGuidelines.ruleset b/CodingGuidelines.ruleset index e647ad9..b29d742 100644 --- a/CodingGuidelines.ruleset +++ b/CodingGuidelines.ruleset @@ -1,32 +1,54 @@  - + + + - - - - - - + + - - - + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index a9d5421..9619270 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,38 +1,56 @@ - net6.0 - 6.0.* - 5.1.2 - 2.15.0 - 5.1.2 - $(MSBuildThisFileDirectory)CodingGuidelines.ruleset - 9999 enable + latest enable false false + true + Recommended + $(MSBuildThisFileDirectory)CodingGuidelines.ruleset + $(MSBuildThisFileDirectory)tests.runsettings + 5.7.2 + pre + direct - - - - - + + + IDE0028;IDE0300;IDE0301;IDE0302;IDE0303;IDE0304;IDE0305;IDE0306 + $(NoWarn);$(UseCollectionExpressionRules) + + + + $(NoWarn);AV2210 + - - $(NoWarn);1591;NU5104 + + $(NoWarn);1591 true true - - $(NoWarn);AV2210 + + true - - - 3.2.0 - 4.16.1 - 17.4.0 + + $(NoWarn);CA1707;CA1062 + + + + $(NoWarn);CA1062 + + + + + + diff --git a/JetBrainsInspectCodeTransform.xslt b/JetBrainsInspectCodeTransform.xslt index 098821f..28fa772 100644 --- a/JetBrainsInspectCodeTransform.xslt +++ b/JetBrainsInspectCodeTransform.xslt @@ -25,6 +25,7 @@ File Line Number + Type Message @@ -35,6 +36,9 @@ + + + diff --git a/JsonApiDotNetCore.MongoDb.sln b/JsonApiDotNetCore.MongoDb.sln index 0f6c7b7..83efaf6 100644 --- a/JsonApiDotNetCore.MongoDb.sln +++ b/JsonApiDotNetCore.MongoDb.sln @@ -23,9 +23,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .gitignore = .gitignore + .github\workflows\build.yml = .github\workflows\build.yml CodingGuidelines.ruleset = CodingGuidelines.ruleset CSharpGuidelinesAnalyzer.config = CSharpGuidelinesAnalyzer.config Directory.Build.props = Directory.Build.props + package-versions.props = package-versions.props + tests.runsettings = tests.runsettings EndProjectSection EndProject Global diff --git a/JsonApiDotNetCore.MongoDb.sln.DotSettings b/JsonApiDotNetCore.MongoDb.sln.DotSettings index 8e80299..cf02da1 100644 --- a/JsonApiDotNetCore.MongoDb.sln.DotSettings +++ b/JsonApiDotNetCore.MongoDb.sln.DotSettings @@ -1,19 +1,9 @@  - // Use the following placeholders: -// $EXPR$ -- source expression -// $NAME$ -- source name (string literal or 'nameof' expression) -// $MESSAGE$ -- string literal in the form of "$NAME$ != null" -JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($EXPR$); - 199 - 5000 - 99 - 100 - 200 - 1000 - 500 + 5000 + 2000 3000 - 50 False + FD312677-2A62-4B8F-A965-879B059F1755/f:ReadOnlySet.cs SOLUTION True True @@ -28,6 +18,7 @@ JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($EXPR$); SUGGESTION SUGGESTION WARNING + WARNING SUGGESTION SUGGESTION SUGGESTION @@ -53,18 +44,21 @@ JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($EXPR$); WARNING WARNING WARNING + DO_NOT_SHOW WARNING + SUGGESTION HINT WARNING + SUGGESTION DO_NOT_SHOW HINT SUGGESTION - WARNING - WARNING + SUGGESTION + SUGGESTION WARNING WARNING - SUGGESTION WARNING + SUGGESTION SUGGESTION SUGGESTION DO_NOT_SHOW @@ -76,6 +70,8 @@ JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($EXPR$); SUGGESTION SUGGESTION SUGGESTION + WARNING + DO_NOT_SHOW WARNING WARNING WARNING @@ -88,8 +84,14 @@ JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($EXPR$); WARNING WARNING WARNING + SUGGESTION + SUGGESTION + SUGGESTION WARNING - <?xml version="1.0" encoding="utf-16"?><Profile name="JADNC Full Cleanup"><XMLReformatCode>True</XMLReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="True" ArrangeVarStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" /><CssAlphabetizeProperties>True</CssAlphabetizeProperties><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><HtmlReformatCode>True</HtmlReformatCode><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><CSReorderTypeMembers>True</CSReorderTypeMembers><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags></Profile> + True + SUGGESTION + False + <?xml version="1.0" encoding="utf-16"?><Profile name="JADNC Full Cleanup"><XMLReformatCode>True</XMLReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="True" ArrangeVarStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" ArrangeNullCheckingPattern="True" /><CssAlphabetizeProperties>True</CssAlphabetizeProperties><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><HtmlReformatCode>True</HtmlReformatCode><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><CSReorderTypeMembers>True</CSReorderTypeMembers><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><CSReformatInactiveBranches>True</CSReformatInactiveBranches></Profile> JADNC Full Cleanup Required Required @@ -109,6 +111,7 @@ JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($EXPR$); True True True + INDENT 1 1 False @@ -116,14 +119,17 @@ JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($EXPR$); False False False + False False False False True + 1 NEVER NEVER False NEVER + False False False NEVER @@ -134,14 +140,17 @@ JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($EXPR$); False False CHOP_ALWAYS + False True True True WRAP_IF_LONG 160 + CHOP_IF_LONG WRAP_IF_LONG CHOP_ALWAYS CHOP_ALWAYS + WRAP_IF_LONG True True 2 @@ -574,13 +583,17 @@ JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($EXPR$); False <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> True True True + True True True + True True - Replace argument null check using throw expression with Guard clause + Replace argument null check using throw expression with ArgumentNullException.ThrowIfNull True True False @@ -599,13 +612,12 @@ JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($EXPR$); True CSHARP False - Replace argument null check with Guard clause - JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($argument$); + System.ArgumentNullException.ThrowIfNull($argument$); $left$ = $right$; $left$ = $right$ ?? throw new ArgumentNullException(nameof($argument$)); WARNING True - Replace argument == null check with Guard clause + Replace argument == null check with ArgumentNullException.ThrowIfNull True True False @@ -614,8 +626,7 @@ $left$ = $right$; True CSHARP False - Replace argument null check with Guard clause - JsonApiDotNetCore.MongoDb.ArgumentGuard.NotNull($argument$); + System.ArgumentNullException.ThrowIfNull($argument$); if ($argument$ == null) throw new ArgumentNullException(nameof($argument$)); WARNING True @@ -627,12 +638,11 @@ $left$ = $right$; True CSHARP False - Replace collection null/empty check with extension method $collection$.IsNullOrEmpty() $collection$ == null || !$collection$.Any() WARNING True - Replace argument is null check with Guard clause + Replace argument is null check with ArgumentNullException.ThrowIfNull True True False @@ -641,7 +651,7 @@ $left$ = $right$; True CSHARP False - JsonApiDotNetCore.ArgumentGuard.NotNull($argument$); + System.ArgumentNullException.ThrowIfNull($argument$); if ($argument$ is null) throw new ArgumentNullException(nameof($argument$)); WARNING True diff --git a/LICENSE b/LICENSE index 509975f..54cf85b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,23 @@ -The MIT License (MIT) Copyright (c) 2020 Alvaro Nicoli +Copyright (c) 2021 Bart Koelman -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: +MIT License -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/NuGet.config b/NuGet.config deleted file mode 100644 index aacab5d..0000000 --- a/NuGet.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/PackageReadme.md b/PackageReadme.md new file mode 100644 index 0000000..a2889e6 --- /dev/null +++ b/PackageReadme.md @@ -0,0 +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. diff --git a/README.md b/README.md index a1f85f9..b7f6f9c 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,182 @@ # MongoDB support for JsonApiDotNetCore -Plug-n-play implementation of `IResourceRepository` allowing you to use [MongoDB](https://www.mongodb.com/) with your [JsonApiDotNetCore](https://github.com/json-api-dotnet/JsonApiDotNetCore) APIs. - -[![Build](https://ci.appveyor.com/api/projects/status/dadm2kr2y0353mji/branch/master?svg=true)](https://ci.appveyor.com/project/json-api-dotnet/jsonapidotnetcore-mongodb/branch/master) +[![Build](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/actions/workflows/build.yml?query=branch%3Amaster) [![Coverage](https://codecov.io/gh/json-api-dotnet/JsonApiDotNetCore.MongoDb/branch/master/graph/badge.svg?token=QPVf8rii7l)](https://codecov.io/gh/json-api-dotnet/JsonApiDotNetCore.MongoDb) [![NuGet](https://img.shields.io/nuget/v/JsonApiDotNetCore.MongoDb.svg)](https://www.nuget.org/packages/JsonApiDotNetCore.MongoDb/) - -## Installation and Usage - -```bash -dotnet add package JsonApiDotNetCore.MongoDb -``` - -### Models - -```c# -#nullable enable - -[Resource] -public class Book : HexStringMongoIdentifiable -{ - [Attr] - public string Name { get; set; } = null!; -} -``` - -### Middleware - -```c# -// Program.cs - -#nullable enable - -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - -// Add services to the container. - -builder.Services.AddSingleton(_ => -{ - var client = new MongoClient("mongodb://localhost:27017"); - return client.GetDatabase("ExampleDbName"); -}); - -builder.Services.AddJsonApi(resources: resourceGraphBuilder => -{ - resourceGraphBuilder.Add(); -}); - -builder.Services.AddJsonApiMongoDb(); - -builder.Services.AddResourceRepository>(); - -// Configure the HTTP request pipeline. - -app.UseRouting(); -app.UseJsonApi(); -app.MapControllers(); - -app.Run(); -``` - -Note: If your API project uses MongoDB only (so not in combination with EF Core), then instead of -registering all MongoDB resources and repositories individually, you can use: - -```c# -builder.Services.AddJsonApi(facade => facade.AddCurrentAssembly()); -builder.Services.AddJsonApiMongoDb(); - -builder.Services.AddScoped(typeof(IResourceReadRepository<,>), typeof(MongoRepository<,>)); -builder.Services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoRepository<,>)); -builder.Services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoRepository<,>)); -``` +[![GitHub License](https://img.shields.io/github/license/json-api-dotnet/JsonApiDotNetCore.MongoDb)](LICENSE) + +Plug-n-play implementation of `IResourceRepository`, allowing you to use [MongoDB](https://www.mongodb.com/) with your [JsonApiDotNetCore](https://github.com/json-api-dotnet/JsonApiDotNetCore) API projects. + +## Getting started + +The following steps describe how to create a JSON:API project with MongoDB. + +1. Install the JsonApiDotNetCore.MongoDb package: + ```bash + dotnet add package JsonApiDotNetCore.MongoDb + ``` + +1. Declare your entities, annotated with JsonApiDotNetCore attributes: + ```c# + #nullable enable + + [Resource] + public class Person : HexStringMongoIdentifiable + { + [Attr] public string? FirstName { get; set; } + [Attr] public string LastName { get; set; } = null!; + } + ``` + +1. Configure MongoDB and JsonApiDotNetCore in `Program.cs`, seeding the database with sample data: + ```c# + var builder = WebApplication.CreateBuilder(args); + builder.Services.AddSingleton(_ => + new MongoClient("mongodb://localhost:27017").GetDatabase("ExampleDbName")); + builder.Services.AddJsonApi(options => + { + options.UseRelativeLinks = true; + options.IncludeTotalResourceCount = true; + }, resources: resourceGraphBuilder => resourceGraphBuilder.Add()); + builder.Services.AddJsonApiMongoDb(); + builder.Services.AddResourceRepository>(); + + var app = builder.Build(); + app.UseRouting(); + app.UseJsonApi(); + app.MapControllers(); + + var database = app.Services.GetRequiredService(); + await CreateSampleDataAsync(database); + + app.Run(); + + static async Task CreateSampleDataAsync(IMongoDatabase database) + { + await database.DropCollectionAsync(nameof(Person)); + await database.GetCollection(nameof(Person)).InsertManyAsync(new[] + { + new Person + { + FirstName = "John", + LastName = "Doe", + }, + new Person + { + FirstName = "Jane", + LastName = "Doe", + }, + new Person + { + FirstName = "John", + LastName = "Smith", + } + }); + } + ``` + +1. Start your API + ```bash + dotnet run + ``` + +1. Send a GET request to retrieve data: + ```bash + GET http://localhost:5000/people?filter=equals(lastName,'Doe')&fields[people]=firstName HTTP/1.1 + ``` + +
+ Expand to view the JSON response + + ```json + { + "links": { + "self": "/people?filter=equals(lastName,%27Doe%27)&fields[people]=firstName", + "first": "/people?filter=equals(lastName,%27Doe%27)&fields%5Bpeople%5D=firstName", + "last": "/people?filter=equals(lastName,%27Doe%27)&fields%5Bpeople%5D=firstName" + }, + "data": [ + { + "type": "people", + "id": "680cae2e1759666c5c1e988c", + "attributes": { + "firstName": "John" + }, + "links": { + "self": "/people/680cae2e1759666c5c1e988c" + } + }, + { + "type": "people", + "id": "680cae2e1759666c5c1e988d", + "attributes": { + "firstName": "Jane" + }, + "links": { + "self": "/people/680cae2e1759666c5c1e988d" + } + } + ], + "meta": { + "total": 2 + } + } + ``` + +
+ +> [!TIP] +> If your API project uses MongoDB only (so not in combination with EF Core), then instead of +> registering all MongoDB resources and repositories individually, you can use: +> +> ```c# +> builder.Services.AddJsonApi(facade => facade.AddCurrentAssembly()); +> builder.Services.AddJsonApiMongoDb(); +> +> builder.Services.AddScoped(typeof(IResourceReadRepository<,>), typeof(MongoRepository<,>)); +> builder.Services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoRepository<,>)); +> builder.Services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoRepository<,>)); +> ``` ## Using client-generated IDs -Resources that inherit from `HexStringMongoIdentifiable` use auto-generated (performant) 12-byte hexadecimal + +Resources that inherit from `HexStringMongoIdentifiable` use auto-generated (high-performance) 12-byte hexadecimal [Object IDs](https://docs.mongodb.com/manual/reference/bson-types/#objectid). -You can assign an ID manually, but it must match the 12-byte hexadecimal pattern. +You can assign an ID explicitly, but it must match the 12-byte hexadecimal pattern. -To assign free-format string IDs manually, make your resources inherit from `FreeStringMongoIdentifiable` instead. +To use free-format string IDs, make your resources inherit from `FreeStringMongoIdentifiable` instead. When creating a resource without assigning an ID, a 12-byte hexadecimal ID will be auto-generated. -Set `options.AllowClientGeneratedIds` to `true` in Program.cs to allow API clients to assign IDs. This can be combined +Set `options.ClientIdGeneration` to `Allowed` or `Required` from `Program.cs` to enable API clients to assign IDs. This can be combined with both base classes, but `FreeStringMongoIdentifiable` probably makes the most sense. ## Limitations -- JSON:API relationships are currently not supported. You can use complex object graphs though, which are stored in a single document. +- JSON:API relationships are [currently not supported](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/issues/73). You *can* use complex object graphs though, which are stored in a single document. -## Contributing +## Trying out the latest build -Have a question, found a bug or want to submit code changes? See our [contributing guidelines](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/.github/CONTRIBUTING.md). +After each commit to the master branch, a new pre-release NuGet package is automatically published to [feedz.io](https://feedz.io/docs/package-types/nuget). +To try it out, follow the steps below: -## Trying out the latest build +1. Create a `nuget.config` file in the same directory as your .sln file, with the following contents: + ```xml + + + + + + + + ``` + +1. In your IDE, browse the list of packages from the `json-api-dotnet` feed. Make sure pre-release packages are included in the list. -After each commit to the master branch, a new prerelease NuGet package is automatically published to AppVeyor at https://ci.appveyor.com/nuget/jsonapidotnetcore-mongodb. To try it out, follow the next steps: +## Contributing -* In Visual Studio: **Tools**, **NuGet Package Manager**, **Package Manager Settings**, **Package Sources** - * Click **+** - * Name: **AppVeyor JADNC MongoDb**, Source: **https://ci.appveyor.com/nuget/jsonapidotnetcore-mongodb** - * Click **Update**, **Ok** -* Open the NuGet package manager console (**Tools**, **NuGet Package Manager**, **Package Manager Console**) - * Select **AppVeyor JADNC MongoDb** as package source - * Run command: `Install-Package JonApiDotNetCore -pre` +Have a question, found a bug or want to submit code changes? See our [contributing guidelines](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/.github/CONTRIBUTING.md). -## Development +## Build from source To build the code from this repository locally, run: @@ -111,13 +184,14 @@ To build the code from this repository locally, run: dotnet build ``` -You don't need to have a running instance of MongoDB on your machine to run tests. Just type the following command in your terminal: +You can run tests without MongoDB on your machine. The following command runs all tests: ```bash dotnet test ``` -If you want to run the examples and explore them on your own **you are** going to need that running instance of MongoDB. If you have docker installed you can launch it like this: +A running instance of MongoDB is required to run the examples. +If you have docker installed, you can launch MongoDB in a container with the following command: ```bash pwsh run-docker-mongodb.ps1 @@ -129,7 +203,7 @@ And then to run the API: dotnet run --project src/Examples/GettingStarted ``` -Alternatively, to build and validate the code, run all tests, generate code coverage and produce the NuGet package: +Alternatively, to build, run all tests, generate code coverage and NuGet packages: ```bash pwsh Build.ps1 diff --git a/WarningSeverities.DotSettings b/WarningSeverities.DotSettings index 0d4eeba..5b64971 100644 --- a/WarningSeverities.DotSettings +++ b/WarningSeverities.DotSettings @@ -1,4 +1,5 @@  + WARNING WARNING WARNING WARNING @@ -13,6 +14,7 @@ WARNING WARNING WARNING + WARNING WARNING WARNING WARNING @@ -68,8 +70,10 @@ WARNING WARNING WARNING + WARNING WARNING WARNING + WARNING WARNING WARNING WARNING @@ -82,6 +86,7 @@ WARNING WARNING WARNING + WARNING WARNING WARNING WARNING @@ -97,8 +102,13 @@ WARNING WARNING WARNING + WARNING WARNING WARNING + WARNING + WARNING + WARNING + WARNING WARNING WARNING WARNING @@ -109,12 +119,16 @@ WARNING WARNING WARNING + WARNING WARNING WARNING WARNING WARNING + WARNING WARNING WARNING + WARNING + WARNING WARNING WARNING WARNING @@ -135,6 +149,7 @@ WARNING WARNING WARNING + WARNING WARNING WARNING WARNING @@ -152,6 +167,8 @@ WARNING WARNING WARNING + WARNING + WARNING WARNING WARNING WARNING @@ -227,6 +244,7 @@ WARNING WARNING WARNING + WARNING WARNING WARNING WARNING @@ -240,14 +258,17 @@ WARNING WARNING WARNING + WARNING WARNING WARNING WARNING WARNING + WARNING WARNING WARNING WARNING WARNING WARNING WARNING + WARNING \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 2593740..1ad1455 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,54 +1,12 @@ -image: - - Ubuntu2004 - - Visual Studio 2022 +image: Visual Studio 2022 version: '{build}' branches: only: - master - - develop - - unstable - /release\/.+/ -pull_requests: - do_not_increment_build_number: true - -nuget: - disable_publish_on_pr: true - -matrix: - fast_finish: true - -for: -- - matrix: - only: - - image: Visual Studio 2022 - artifacts: - - path: .\**\artifacts\**\*.nupkg - name: NuGet - deploy: - - provider: NuGet - skip_symbols: false - api_key: - secure: S9fkLwmhi7w+DGouXYqYq/1PGocnYo8UBUKwv+BGpWHnzE6yHZEYth3j/XJ9Ydsa - on: - branch: master - appveyor_repo_tag: true - - provider: NuGet - skip_symbols: false - api_key: - secure: S9fkLwmhi7w+DGouXYqYq/1PGocnYo8UBUKwv+BGpWHnzE6yHZEYth3j/XJ9Ydsa - on: - branch: /release\/.+/ - appveyor_repo_tag: true - -build_script: -- pwsh: | - Write-Output ".NET version:" - dotnet --version - - .\Build.ps1 - -test: off \ No newline at end of file +build: off +test: off +deploy: off diff --git a/cleanupcode.ps1 b/cleanupcode.ps1 index 717f369..9a86aba 100644 --- a/cleanupcode.ps1 +++ b/cleanupcode.ps1 @@ -28,17 +28,17 @@ if ($revision) { if ($baseCommitHash -eq $headCommitHash) { Write-Output "Running code cleanup on staged/unstaged files." - dotnet regitlint -s JsonApiDotNetCore.MongoDb.sln --print-command --skip-tool-check --jb-profile="JADNC Full Cleanup" --jb --properties:Configuration=Release --jb --verbosity=WARN -f staged,modified + 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 VerifySuccessExitCode } else { Write-Output "Running code cleanup on commit range $baseCommitHash..$headCommitHash, including staged/unstaged files." - dotnet regitlint -s JsonApiDotNetCore.MongoDb.sln --print-command --skip-tool-check --jb-profile="JADNC Full Cleanup" --jb --properties:Configuration=Release --jb --verbosity=WARN -f staged,modified,commits -a $headCommitHash -b $baseCommitHash + 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 VerifySuccessExitCode } } else { Write-Output "Running code cleanup on all files." - dotnet regitlint -s JsonApiDotNetCore.MongoDb.sln --print-command --skip-tool-check --jb-profile="JADNC Full Cleanup" --jb --properties:Configuration=Release --jb --verbosity=WARN + 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 VerifySuccessExitCode } diff --git a/inspectcode.ps1 b/inspectcode.ps1 index 0c55d7b..25195fe 100644 --- a/inspectcode.ps1 +++ b/inspectcode.ps1 @@ -4,16 +4,16 @@ dotnet tool restore -if ($LASTEXITCODE -ne 0) { - throw "Tool restore failed with exit code $LASTEXITCODE" +if ($LastExitCode -ne 0) { + throw "Tool restore failed with exit code $LastExitCode" } $outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml') $resultPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.html') -dotnet jb inspectcode JsonApiDotNetCore.MongoDb.sln --build --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=GlobalPerProduct -dsl=SolutionPersonal -dsl=ProjectPersonal +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 -if ($LASTEXITCODE -ne 0) { - throw "Code inspection failed with exit code $LASTEXITCODE" +if ($LastExitCode -ne 0) { + throw "Code inspection failed with exit code $LastExitCode" } [xml]$xml = Get-Content "$outputPath" diff --git a/logo.png b/logo.png deleted file mode 100644 index 78f1acd..0000000 Binary files a/logo.png and /dev/null differ diff --git a/package-icon.png b/package-icon.png new file mode 100644 index 0000000..f95eb77 Binary files /dev/null and b/package-icon.png differ diff --git a/package-versions.props b/package-versions.props new file mode 100644 index 0000000..94750ca --- /dev/null +++ b/package-versions.props @@ -0,0 +1,29 @@ + + + + 5.7.1 + 3.3.0 + + + 35.6.* + 6.0.* + 3.1.* + 7.2.* + 2.4.* + 2.0.* + 3.3.* + 17.13.* + 2.9.* + 2.8.* + + + + + 9.0.* + + + + + 8.0.* + + diff --git a/run-docker-mongodb.ps1 b/run-docker-mongodb.ps1 index b1092b4..215b048 100644 --- a/run-docker-mongodb.ps1 +++ b/run-docker-mongodb.ps1 @@ -1,9 +1,6 @@ #Requires -Version 7.0 -# This script starts a docker container with MongoDB database, used for running tests. +# This script starts a MongoDB database in a docker container, which is required for running examples locally. -docker container stop jsonapi-dotnet-core-mongodb-testing - -docker run --rm --name jsonapi-dotnet-core-mongodb-testing ` - -p 27017:27017 ` - mongo:latest +docker container stop jsonapi-mongo-db +docker run --pull always --rm --detach --name jsonapi-mongo-db -p 27017:27017 mongo:latest diff --git a/src/Examples/GettingStarted/GettingStarted.csproj b/src/Examples/GettingStarted/GettingStarted.csproj index a5b25ea..408a434 100644 --- a/src/Examples/GettingStarted/GettingStarted.csproj +++ b/src/Examples/GettingStarted/GettingStarted.csproj @@ -1,8 +1,10 @@ - $(TargetFrameworkName) + net9.0;net8.0 + + diff --git a/src/Examples/GettingStarted/Program.cs b/src/Examples/GettingStarted/Program.cs index e28c933..3a20376 100644 --- a/src/Examples/GettingStarted/Program.cs +++ b/src/Examples/GettingStarted/Program.cs @@ -2,22 +2,31 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; +using Microsoft.Extensions.DependencyInjection.Extensions; using MongoDB.Driver; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddSingleton(_ => +builder.Services.TryAddSingleton(_ => { var client = new MongoClient(builder.Configuration.GetSection("DatabaseSettings:ConnectionString").Value); return client.GetDatabase(builder.Configuration.GetSection("DatabaseSettings:Database").Value); }); -builder.Services.AddJsonApi(ConfigureJsonApiOptions, resources: resourceGraphBuilder => +builder.Services.AddJsonApi(options => { - resourceGraphBuilder.Add(); -}); + options.Namespace = "api"; + options.UseRelativeLinks = true; + options.IncludeTotalResourceCount = true; + +#if DEBUG + options.IncludeExceptionStackTraceInErrors = true; + options.IncludeRequestBodyInErrors = true; + options.SerializerOptions.WriteIndented = true; +#endif +}, resources: resourceGraphBuilder => resourceGraphBuilder.Add()); builder.Services.AddJsonApiMongoDb(); @@ -34,15 +43,7 @@ var database = app.Services.GetRequiredService(); await CreateSampleDataAsync(database); -app.Run(); - -static void ConfigureJsonApiOptions(JsonApiOptions options) -{ - options.Namespace = "api"; - options.UseRelativeLinks = true; - options.IncludeTotalResourceCount = true; - options.SerializerOptions.WriteIndented = true; -} +await app.RunAsync(); static async Task CreateSampleDataAsync(IMongoDatabase database) { diff --git a/src/Examples/GettingStarted/Properties/launchSettings.json b/src/Examples/GettingStarted/Properties/launchSettings.json index ad97b55..55041b5 100644 --- a/src/Examples/GettingStarted/Properties/launchSettings.json +++ b/src/Examples/GettingStarted/Properties/launchSettings.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", + "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, @@ -10,7 +10,7 @@ "profiles": { "IIS Express": { "commandName": "IISExpress", - "launchBrowser": false, + "launchBrowser": true, "launchUrl": "api/books", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -18,7 +18,7 @@ }, "Kestrel": { "commandName": "Project", - "launchBrowser": false, + "launchBrowser": true, "launchUrl": "api/books", "applicationUrl": "http://localhost:24141", "environmentVariables": { diff --git a/src/Examples/GettingStarted/README.md b/src/Examples/GettingStarted/README.md index aa8afe0..b076df4 100644 --- a/src/Examples/GettingStarted/README.md +++ b/src/Examples/GettingStarted/README.md @@ -7,8 +7,7 @@ You can verify the project is running by checking this endpoint: `localhost:24141/api/people` -For further documentation and implementation of a JsonApiDotNetCore Application see the documentation or GitHub page: +For further documentation and implementation of a JsonApiDotNetCore application, see the documentation or GitHub page: Repository: https://github.com/json-api-dotnet/JsonApiDotNetCore - -Documentation: http://www.jsonapi.net +Documentation: https://www.jsonapi.net diff --git a/src/Examples/GettingStarted/appsettings.json b/src/Examples/GettingStarted/appsettings.json index 31455b7..4db298e 100644 --- a/src/Examples/GettingStarted/appsettings.json +++ b/src/Examples/GettingStarted/appsettings.json @@ -6,7 +6,9 @@ "Logging": { "LogLevel": { "Default": "Warning", + // Include server startup and incoming requests. "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore.Hosting.Diagnostics": "Information", "Microsoft.EntityFrameworkCore": "Critical" } }, diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/OperationsController.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/OperationsController.cs index 8172459..a2a1ca7 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/OperationsController.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Controllers/OperationsController.cs @@ -6,11 +6,7 @@ namespace JsonApiDotNetCoreMongoDbExample.Controllers; -public sealed class OperationsController : JsonApiOperationsController -{ - public OperationsController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, - IJsonApiRequest request, ITargetedFields targetedFields) - : base(options, resourceGraph, loggerFactory, processor, request, targetedFields) - { - } -} +public sealed class OperationsController( + IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request, + ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) + : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields, operationFilter); diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs index 8079def..96c21ee 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Definitions/TodoItemDefinition.cs @@ -5,20 +5,14 @@ using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Resources; using JsonApiDotNetCoreMongoDbExample.Models; -using Microsoft.AspNetCore.Authentication; namespace JsonApiDotNetCoreMongoDbExample.Definitions; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class TodoItemDefinition : JsonApiResourceDefinition +public sealed class TodoItemDefinition(IResourceGraph resourceGraph, TimeProvider timeProvider) + : JsonApiResourceDefinition(resourceGraph) { - private readonly ISystemClock _systemClock; - - public TodoItemDefinition(IResourceGraph resourceGraph, ISystemClock systemClock) - : base(resourceGraph) - { - _systemClock = systemClock; - } + private readonly TimeProvider _timeProvider = timeProvider; public override SortExpression OnApplySort(SortExpression? existingSort) { @@ -27,22 +21,21 @@ public override SortExpression OnApplySort(SortExpression? existingSort) private SortExpression GetDefaultSortOrder() { - return CreateSortExpressionFromLambda(new PropertySortOrder - { - (todoItem => todoItem.Priority, ListSortDirection.Descending), + return CreateSortExpressionFromLambda([ + (todoItem => todoItem.Priority, ListSortDirection.Ascending), (todoItem => todoItem.LastModifiedAt, ListSortDirection.Descending) - }); + ]); } public override Task OnWritingAsync(TodoItem resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) { if (writeOperation == WriteOperationKind.CreateResource) { - resource.CreatedAt = _systemClock.UtcNow; + resource.CreatedAt = _timeProvider.GetUtcNow(); } else if (writeOperation == WriteOperationKind.UpdateResource) { - resource.LastModifiedAt = _systemClock.UtcNow; + resource.LastModifiedAt = _timeProvider.GetUtcNow(); } return Task.CompletedTask; diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj b/src/Examples/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj index a5b25ea..408a434 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/JsonApiDotNetCoreMongoDbExample.csproj @@ -1,8 +1,10 @@ - $(TargetFrameworkName) + net9.0;net8.0 + + diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs index 7dc654c..9b0e8d6 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItem.cs @@ -16,6 +16,9 @@ public sealed class TodoItem : HexStringMongoIdentifiable [Required] public TodoItemPriority? Priority { get; set; } + [Attr] + public long? DurationInHours { get; set; } + [Attr(Capabilities = AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort | AttrCapabilities.AllowView)] public DateTimeOffset CreatedAt { get; set; } diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItemPriority.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItemPriority.cs index a782897..3bb17ed 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItemPriority.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Models/TodoItemPriority.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCoreMongoDbExample.Models; [UsedImplicitly(ImplicitUseTargetFlags.Members)] public enum TodoItemPriority { - Low, - Medium, - High + High = 1, + Medium = 2, + Low = 3 } diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Program.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Program.cs index 5f4b0dc..ff216b8 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Program.cs +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Program.cs @@ -1,30 +1,33 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Repositories; -using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.DependencyInjection.Extensions; using MongoDB.Driver; +[assembly: ExcludeFromCodeCoverage] + WebApplicationBuilder builder = WebApplication.CreateBuilder(args); // Add services to the container. -builder.Services.AddSingleton(); +builder.Services.TryAddSingleton(TimeProvider.System); -builder.Services.AddSingleton(_ => +builder.Services.TryAddSingleton(_ => { - var client = new MongoClient(builder.Configuration.GetSection("DatabaseSettings:ConnectionString").Value); - return client.GetDatabase(builder.Configuration.GetSection("DatabaseSettings:Database").Value); + var client = new MongoClient(builder.Configuration.GetValue("DatabaseSettings:ConnectionString")); + return client.GetDatabase(builder.Configuration.GetValue("DatabaseSettings:Database")); }); +builder.Services.TryAddScoped(typeof(IResourceReadRepository<,>), typeof(MongoRepository<,>)); +builder.Services.TryAddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoRepository<,>)); +builder.Services.TryAddScoped(typeof(IResourceRepository<,>), typeof(MongoRepository<,>)); + builder.Services.AddJsonApi(ConfigureJsonApiOptions, facade => facade.AddCurrentAssembly()); builder.Services.AddJsonApiMongoDb(); -builder.Services.AddScoped(typeof(IResourceReadRepository<,>), typeof(MongoRepository<,>)); -builder.Services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoRepository<,>)); -builder.Services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoRepository<,>)); - WebApplication app = builder.Build(); // Configure the HTTP request pipeline. @@ -33,17 +36,17 @@ app.UseJsonApi(); app.MapControllers(); -app.Run(); +await app.RunAsync(); static void ConfigureJsonApiOptions(JsonApiOptions options) { - options.Namespace = "api/v1"; + options.Namespace = "api"; options.UseRelativeLinks = true; options.IncludeTotalResourceCount = true; - options.SerializerOptions.WriteIndented = true; options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); #if DEBUG options.IncludeExceptionStackTraceInErrors = true; options.IncludeRequestBodyInErrors = true; + options.SerializerOptions.WriteIndented = true; #endif } diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Properties/AssemblyInfo.cs b/src/Examples/JsonApiDotNetCoreMongoDbExample/Properties/AssemblyInfo.cs deleted file mode 100644 index 82d1291..0000000 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -// https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/MSBuildIntegration.md#excluding-from-coverage -[assembly: ExcludeFromCodeCoverage] diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json b/src/Examples/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json index f155249..efcce2e 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/Properties/launchSettings.json @@ -1,5 +1,5 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", + "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, @@ -11,16 +11,16 @@ "profiles": { "IIS Express": { "commandName": "IISExpress", - "launchBrowser": false, - "launchUrl": "api/v1/todoItems", + "launchBrowser": true, + "launchUrl": "api/todoItems", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Kestrel": { "commandName": "Project", - "launchBrowser": false, - "launchUrl": "api/v1/todoItems", + "launchBrowser": true, + "launchUrl": "api/todoItems", "applicationUrl": "https://localhost:44340;http://localhost:24140", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/src/Examples/JsonApiDotNetCoreMongoDbExample/appsettings.json b/src/Examples/JsonApiDotNetCoreMongoDbExample/appsettings.json index dde4b49..b8ed43e 100644 --- a/src/Examples/JsonApiDotNetCoreMongoDbExample/appsettings.json +++ b/src/Examples/JsonApiDotNetCoreMongoDbExample/appsettings.json @@ -6,7 +6,9 @@ "Logging": { "LogLevel": { "Default": "Warning", + // Include server startup and incoming requests. "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore.Hosting.Diagnostics": "Information", "Microsoft.EntityFrameworkCore": "Critical" } }, diff --git a/src/JsonApiDotNetCore.MongoDb/ArgumentGuard.cs b/src/JsonApiDotNetCore.MongoDb/ArgumentGuard.cs deleted file mode 100644 index c7c9908..0000000 --- a/src/JsonApiDotNetCore.MongoDb/ArgumentGuard.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Runtime.CompilerServices; -using JetBrains.Annotations; -using SysNotNull = System.Diagnostics.CodeAnalysis.NotNullAttribute; - -#pragma warning disable AV1008 // Class should not be static -#pragma warning disable AV1553 // Do not use optional parameters with default value null for strings, collections or tasks - -namespace JsonApiDotNetCore.MongoDb; - -internal static class ArgumentGuard -{ - [AssertionMethod] - public static void NotNull([NoEnumeration] [SysNotNull] T? value, [CallerArgumentExpression("value")] string? parameterName = null) - where T : class - { - ArgumentNullException.ThrowIfNull(value, parameterName); - } -} diff --git a/src/JsonApiDotNetCore.MongoDb/AtomicOperations/MongoTransaction.cs b/src/JsonApiDotNetCore.MongoDb/AtomicOperations/MongoTransaction.cs index 7aa1388..0e19327 100644 --- a/src/JsonApiDotNetCore.MongoDb/AtomicOperations/MongoTransaction.cs +++ b/src/JsonApiDotNetCore.MongoDb/AtomicOperations/MongoTransaction.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCore.MongoDb.AtomicOperations; -/// +/// [PublicAPI] public sealed class MongoTransaction : IOperationsTransaction { @@ -16,7 +16,7 @@ public sealed class MongoTransaction : IOperationsTransaction public MongoTransaction(IMongoDataAccess mongoDataAccess, bool ownsTransaction) { - ArgumentGuard.NotNull(mongoDataAccess); + ArgumentNullException.ThrowIfNull(mongoDataAccess); _mongoDataAccess = mongoDataAccess; _ownsTransaction = ownsTransaction; diff --git a/src/JsonApiDotNetCore.MongoDb/AtomicOperations/MongoTransactionFactory.cs b/src/JsonApiDotNetCore.MongoDb/AtomicOperations/MongoTransactionFactory.cs index b51e1a1..d016781 100644 --- a/src/JsonApiDotNetCore.MongoDb/AtomicOperations/MongoTransactionFactory.cs +++ b/src/JsonApiDotNetCore.MongoDb/AtomicOperations/MongoTransactionFactory.cs @@ -12,7 +12,7 @@ public sealed class MongoTransactionFactory : IOperationsTransactionFactory public MongoTransactionFactory(IMongoDataAccess mongoDataAccess) { - ArgumentGuard.NotNull(mongoDataAccess); + ArgumentNullException.ThrowIfNull(mongoDataAccess); _mongoDataAccess = mongoDataAccess; } diff --git a/src/JsonApiDotNetCore.MongoDb/Configuration/ResourceGraphExtensions.cs b/src/JsonApiDotNetCore.MongoDb/Configuration/ResourceGraphExtensions.cs new file mode 100644 index 0000000..5ad31b2 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Configuration/ResourceGraphExtensions.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using MongoDB.Bson.Serialization.Attributes; + +namespace JsonApiDotNetCore.MongoDb.Configuration; + +internal static class ResourceGraphExtensions +{ + public static IReadOnlyModel ToEntityModel(this IResourceGraph resourceGraph) + { + ArgumentNullException.ThrowIfNull(resourceGraph); + + var modelBuilder = new ModelBuilder(); + + foreach (ResourceType resourceType in resourceGraph.GetResourceTypes()) + { + IncludeResourceType(resourceType, modelBuilder); + } + + return modelBuilder.Model; + } + + private static void IncludeResourceType(ResourceType resourceType, ModelBuilder builder) + { + EntityTypeBuilder entityTypeBuilder = builder.Entity(resourceType.ClrType); + + foreach (PropertyInfo property in resourceType.ClrType.GetProperties().Where(property => !IsIgnored(property))) + { + entityTypeBuilder.Property(property.PropertyType, property.Name); + } + } + + private static bool IsIgnored(PropertyInfo property) + { + return property.GetCustomAttribute() != null; + } +} diff --git a/src/JsonApiDotNetCore.MongoDb/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.MongoDb/Configuration/ServiceCollectionExtensions.cs index f914f91..a49d77e 100644 --- a/src/JsonApiDotNetCore.MongoDb/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.MongoDb/Configuration/ServiceCollectionExtensions.cs @@ -1,10 +1,12 @@ using JetBrains.Annotations; using JsonApiDotNetCore.AtomicOperations; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.AtomicOperations; -using JsonApiDotNetCore.MongoDb.Queries.Internal; +using JsonApiDotNetCore.MongoDb.Queries; using JsonApiDotNetCore.MongoDb.Repositories; -using JsonApiDotNetCore.Queries.Internal; +using JsonApiDotNetCore.Queries; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace JsonApiDotNetCore.MongoDb.Configuration; @@ -16,7 +18,17 @@ public static class ServiceCollectionExtensions [PublicAPI] public static IServiceCollection AddJsonApiMongoDb(this IServiceCollection services) { - services.AddScoped(); + ArgumentNullException.ThrowIfNull(services); + + services.TryAddSingleton(serviceProvider => + { + var resourceGraph = serviceProvider.GetRequiredService(); + return resourceGraph.ToEntityModel(); + }); + + services.TryAddScoped(); + + // Replace the built-in implementations from JsonApiDotNetCore. services.AddScoped(); services.AddScoped(); diff --git a/src/JsonApiDotNetCore.MongoDb/Errors/AttributeComparisonInFilterNotSupportedException.cs b/src/JsonApiDotNetCore.MongoDb/Errors/AttributeComparisonInFilterNotSupportedException.cs index 4b7508b..51c7131 100644 --- a/src/JsonApiDotNetCore.MongoDb/Errors/AttributeComparisonInFilterNotSupportedException.cs +++ b/src/JsonApiDotNetCore.MongoDb/Errors/AttributeComparisonInFilterNotSupportedException.cs @@ -10,13 +10,8 @@ namespace JsonApiDotNetCore.MongoDb.Errors; /// https://jira.mongodb.org/browse/CSHARP-1592. /// [PublicAPI] -public sealed class AttributeComparisonInFilterNotSupportedException : JsonApiException -{ - public AttributeComparisonInFilterNotSupportedException() - : base(new ErrorObject(HttpStatusCode.BadRequest) - { - Title = "Comparing attributes against each other is not supported when using MongoDB." - }) +public sealed class AttributeComparisonInFilterNotSupportedException() + : JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { - } -} + Title = "Comparing attributes against each other is not supported when using MongoDB." + }); diff --git a/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs b/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs index 2526e59..b1401c2 100644 --- a/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs +++ b/src/JsonApiDotNetCore.MongoDb/Errors/UnsupportedRelationshipException.cs @@ -9,13 +9,8 @@ namespace JsonApiDotNetCore.MongoDb.Errors; /// The error that is thrown when the user attempts to fetch, create or update a relationship. /// [PublicAPI] -public sealed class UnsupportedRelationshipException : JsonApiException -{ - public UnsupportedRelationshipException() - : base(new ErrorObject(HttpStatusCode.BadRequest) - { - Title = "Relationships are not supported when using MongoDB." - }) +public sealed class UnsupportedRelationshipException() + : JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { - } -} + Title = "Relationships are not supported when using MongoDB." + }); diff --git a/src/JsonApiDotNetCore.MongoDb/JsonApiDotNetCore.MongoDb.csproj b/src/JsonApiDotNetCore.MongoDb/JsonApiDotNetCore.MongoDb.csproj index 2cc225f..13aa46a 100644 --- a/src/JsonApiDotNetCore.MongoDb/JsonApiDotNetCore.MongoDb.csproj +++ b/src/JsonApiDotNetCore.MongoDb/JsonApiDotNetCore.MongoDb.csproj @@ -1,30 +1,29 @@ - + - $(TargetFrameworkName) + net8.0 true true + + - $(JsonApiDotNetCoreMongoDbVersionPrefix) jsonapi;json:api;dotnet;asp.net;rest;web-api;MongoDB - Persistence layer implementation for use of MongoDB in APIs using JsonApiDotNetCore. + MongoDB persistence for JsonApiDotNetCore, which is a framework for building JSON:API compliant REST APIs using ASP.NET Core. json-api-dotnet - https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb + https://www.jsonapi.net/ MIT false See https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/releases. - logo.png + package-icon.png + PackageReadme.md true - true embedded - - True - - + + @@ -32,9 +31,8 @@ - - - - + + + diff --git a/src/JsonApiDotNetCore.MongoDb/PolyfillCollectionExtensions.cs b/src/JsonApiDotNetCore.MongoDb/PolyfillCollectionExtensions.cs new file mode 100644 index 0000000..06adff6 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/PolyfillCollectionExtensions.cs @@ -0,0 +1,12 @@ +using System.Collections.ObjectModel; + +namespace JsonApiDotNetCore.MongoDb; + +// These methods provide polyfills for lower .NET versions. +internal static class PolyfillCollectionExtensions +{ + public static IReadOnlySet AsReadOnly(this HashSet source) + { + return new ReadOnlySet(source); + } +} diff --git a/src/JsonApiDotNetCore.MongoDb/Properties/AssemblyInfo.cs b/src/JsonApiDotNetCore.MongoDb/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..58b7ea5 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("TestBuildingBlocks")] diff --git a/src/JsonApiDotNetCore.MongoDb/Queries/Internal/HideRelationshipsSparseFieldSetCache.cs b/src/JsonApiDotNetCore.MongoDb/Queries/HideRelationshipsSparseFieldSetCache.cs similarity index 82% rename from src/JsonApiDotNetCore.MongoDb/Queries/Internal/HideRelationshipsSparseFieldSetCache.cs rename to src/JsonApiDotNetCore.MongoDb/Queries/HideRelationshipsSparseFieldSetCache.cs index fcd9d8a..ac5f45c 100644 --- a/src/JsonApiDotNetCore.MongoDb/Queries/Internal/HideRelationshipsSparseFieldSetCache.cs +++ b/src/JsonApiDotNetCore.MongoDb/Queries/HideRelationshipsSparseFieldSetCache.cs @@ -2,13 +2,12 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Internal; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -namespace JsonApiDotNetCore.MongoDb.Queries.Internal; +namespace JsonApiDotNetCore.MongoDb.Queries; -/// +/// public sealed class HideRelationshipsSparseFieldSetCache : ISparseFieldSetCache { private readonly SparseFieldSetCache _innerCache; @@ -16,8 +15,8 @@ public sealed class HideRelationshipsSparseFieldSetCache : ISparseFieldSetCache public HideRelationshipsSparseFieldSetCache(IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor) { - ArgumentGuard.NotNull(constraintProviders); - ArgumentGuard.NotNull(resourceDefinitionAccessor); + ArgumentNullException.ThrowIfNull(constraintProviders); + ArgumentNullException.ThrowIfNull(resourceDefinitionAccessor); _innerCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); } @@ -25,18 +24,24 @@ public HideRelationshipsSparseFieldSetCache(IEnumerable public IImmutableSet GetSparseFieldSetForQuery(ResourceType resourceType) { + ArgumentNullException.ThrowIfNull(resourceType); + return _innerCache.GetSparseFieldSetForQuery(resourceType); } /// public IImmutableSet GetIdAttributeSetForRelationshipQuery(ResourceType resourceType) { + ArgumentNullException.ThrowIfNull(resourceType); + return _innerCache.GetIdAttributeSetForRelationshipQuery(resourceType); } /// public IImmutableSet GetSparseFieldSetForSerializer(ResourceType resourceType) { + ArgumentNullException.ThrowIfNull(resourceType); + IImmutableSet fieldSet = _innerCache.GetSparseFieldSetForSerializer(resourceType); return resourceType.ClrType.IsAssignableTo(typeof(IMongoIdentifiable)) ? RemoveRelationships(fieldSet) : fieldSet; diff --git a/src/JsonApiDotNetCore.MongoDb/ReadOnlySet.cs b/src/JsonApiDotNetCore.MongoDb/ReadOnlySet.cs new file mode 100644 index 0000000..9917af9 --- /dev/null +++ b/src/JsonApiDotNetCore.MongoDb/ReadOnlySet.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if NET8_0 +#pragma warning disable + +// ReadOnlySet was introduced in .NET 9. +// This file was copied from https://github.com/dotnet/runtime/blob/release/9.0/src/libraries/System.Collections/src/System/Collections/Generic/ReadOnlySet.cs +// and made internal to enable usage on lower .NET versions. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Collections.ObjectModel; + +/// Represents a read-only, generic set of values. +/// The type of values in the set. +[DebuggerDisplay("Count = {Count}")] +[ExcludeFromCodeCoverage] +internal class ReadOnlySet : IReadOnlySet, ISet, ICollection +{ + /// The wrapped set. + private readonly ISet _set; + + /// Initializes a new instance of the class that is a wrapper around the specified set. + /// The set to wrap. + public ReadOnlySet(ISet set) + { + ArgumentNullException.ThrowIfNull(set); + _set = set; + } + + /// Gets an empty . + public static ReadOnlySet Empty { get; } = new ReadOnlySet(new HashSet()); + + /// Gets the set that is wrapped by this object. + protected ISet Set => _set; + + /// + public int Count => _set.Count; + + /// + public IEnumerator GetEnumerator() => + _set.Count == 0 ? ((IEnumerable)Array.Empty()).GetEnumerator() : + _set.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + public bool Contains(T item) => _set.Contains(item); + + /// + public bool IsProperSubsetOf(IEnumerable other) => _set.IsProperSubsetOf(other); + + /// + public bool IsProperSupersetOf(IEnumerable other) => _set.IsProperSupersetOf(other); + + /// + public bool IsSubsetOf(IEnumerable other) => _set.IsSubsetOf(other); + + /// + public bool IsSupersetOf(IEnumerable other) => _set.IsSupersetOf(other); + + /// + public bool Overlaps(IEnumerable other) => _set.Overlaps(other); + + /// + public bool SetEquals(IEnumerable other) => _set.SetEquals(other); + + /// + void ICollection.CopyTo(T[] array, int arrayIndex) => _set.CopyTo(array, arrayIndex); + + /// + void ICollection.CopyTo(Array array, int index) => CollectionHelpers.CopyTo(_set, array, index); + + /// + bool ICollection.IsReadOnly => true; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot => _set is ICollection c ? c.SyncRoot : this; + + /// + bool ISet.Add(T item) => throw new NotSupportedException(); + + /// + void ISet.ExceptWith(IEnumerable other) => throw new NotSupportedException(); + + /// + void ISet.IntersectWith(IEnumerable other) => throw new NotSupportedException(); + + /// + void ISet.SymmetricExceptWith(IEnumerable other) => throw new NotSupportedException(); + + /// + void ISet.UnionWith(IEnumerable other) => throw new NotSupportedException(); + + /// + void ICollection.Add(T item) => throw new NotSupportedException(); + + /// + void ICollection.Clear() => throw new NotSupportedException(); + + /// + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + private static class CollectionHelpers + { + private static void ValidateCopyToArguments(int sourceCount, Array array, int index) + { + ArgumentNullException.ThrowIfNull(array); + + if (array.Rank != 1) + { + throw new ArgumentException("Only single dimensional arrays are supported for the requested action.", nameof(array)); + } + + if (array.GetLowerBound(0) != 0) + { + throw new ArgumentException("The lower bound of target array must be zero.", nameof(array)); + } + + ArgumentOutOfRangeException.ThrowIfNegative(index); + ArgumentOutOfRangeException.ThrowIfGreaterThan(index, array.Length); + + if (array.Length - index < sourceCount) + { + throw new ArgumentException("Destination array is not long enough to copy all the items in the collection. Check array index and length."); + } + } + + internal static void CopyTo(ICollection collection, Array array, int index) + { + ValidateCopyToArguments(collection.Count, array, index); + + if (collection is ICollection nonGenericCollection) + { + // Easy out if the ICollection implements the non-generic ICollection + nonGenericCollection.CopyTo(array, index); + } + else if (array is T[] items) + { + collection.CopyTo(items, index); + } + else + { + // We can't cast array of value type to object[], so we don't support widening of primitive types here. + if (array is not object?[] objects) + { + throw new ArgumentException("Target array type is not compatible with the type of items in the collection.", nameof(array)); + } + + try + { + foreach (T item in collection) + { + objects[index++] = item; + } + } + catch (ArrayTypeMismatchException) + { + throw new ArgumentException("Target array type is not compatible with the type of items in the collection.", nameof(array)); + } + } + } + } +} +#endif diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/IMongoDataAccess.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/IMongoDataAccess.cs index ff2d348..c0f5852 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/IMongoDataAccess.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/IMongoDataAccess.cs @@ -1,3 +1,4 @@ +using Microsoft.EntityFrameworkCore.Metadata; using MongoDB.Driver; namespace JsonApiDotNetCore.MongoDb.Repositories; @@ -7,6 +8,11 @@ namespace JsonApiDotNetCore.MongoDb.Repositories; /// public interface IMongoDataAccess : IAsyncDisposable { + /// + /// Provides access to the entity model, which is built at startup. + /// + IReadOnlyModel EntityModel { get; } + /// /// Provides access to the underlying MongoDB database, which data changes can be applied on. /// diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDataAccess.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDataAccess.cs index fcbff6d..a92db8c 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDataAccess.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoDataAccess.cs @@ -1,10 +1,14 @@ +using Microsoft.EntityFrameworkCore.Metadata; using MongoDB.Driver; namespace JsonApiDotNetCore.MongoDb.Repositories; -/// +/// public sealed class MongoDataAccess : IMongoDataAccess { + /// + public IReadOnlyModel EntityModel { get; } + /// public IMongoDatabase MongoDatabase { get; } @@ -14,10 +18,12 @@ public sealed class MongoDataAccess : IMongoDataAccess /// public string? TransactionId => ActiveSession is { IsInTransaction: true } ? ActiveSession.GetHashCode().ToString() : null; - public MongoDataAccess(IMongoDatabase mongoDatabase) + public MongoDataAccess(IReadOnlyModel entityModel, IMongoDatabase mongoDatabase) { - ArgumentGuard.NotNull(mongoDatabase); + ArgumentNullException.ThrowIfNull(entityModel); + ArgumentNullException.ThrowIfNull(mongoDatabase); + EntityModel = entityModel; MongoDatabase = mongoDatabase; } diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoModel.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoModel.cs deleted file mode 100644 index 336a098..0000000 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoModel.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Reflection; -using JsonApiDotNetCore.Configuration; -using Microsoft.EntityFrameworkCore.Metadata; -using MongoDB.Bson.Serialization.Attributes; - -namespace JsonApiDotNetCore.MongoDb.Repositories; - -internal sealed class MongoModel : RuntimeModel -{ - public MongoModel(IResourceGraph resourceGraph) - { - ArgumentGuard.NotNull(resourceGraph); - - foreach (ResourceType resourceType in resourceGraph.GetResourceTypes()) - { - RuntimeEntityType entityType = AddEntityType(resourceType.ClrType.FullName!, resourceType.ClrType); - SetEntityProperties(entityType, resourceType); - } - } - - private static void SetEntityProperties(RuntimeEntityType entityType, ResourceType resourceType) - { - foreach (PropertyInfo property in resourceType.ClrType.GetProperties().Where(property => !IsIgnored(property))) - { - entityType.AddProperty(property.Name, property.PropertyType, property); - } - } - - private static bool IsIgnored(PropertyInfo property) - { - return property.GetCustomAttribute() != null; - } -} diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoQueryExpressionValidator.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoQueryExpressionValidator.cs index 58081c6..c1deeed 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoQueryExpressionValidator.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoQueryExpressionValidator.cs @@ -10,9 +10,9 @@ internal sealed class MongoQueryExpressionValidator : QueryExpressionRewriter 0; if (hasIncludes || HasSparseRelationshipSets(layer.Selection)) { @@ -50,9 +50,9 @@ private void ValidateExpression(QueryExpression? expression) } } - public override QueryExpression? VisitResourceFieldChain(ResourceFieldChainExpression expression, object? argument) + public override QueryExpression VisitResourceFieldChain(ResourceFieldChainExpression expression, object? argument) { - if (expression.Fields.Count > 1 || expression.Fields.First() is RelationshipAttribute) + if (expression.Fields.Count > 1 || expression.Fields[0] is RelationshipAttribute) { throw new UnsupportedRelationshipException(); } @@ -62,7 +62,7 @@ private void ValidateExpression(QueryExpression? expression) public override QueryExpression? VisitComparison(ComparisonExpression expression, object? argument) { - if (expression.Left is ResourceFieldChainExpression && expression.Right is ResourceFieldChainExpression) + if (expression is { Left: ResourceFieldChainExpression, Right: ResourceFieldChainExpression }) { throw new AttributeComparisonInFilterNotSupportedException(); } diff --git a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoRepository.cs b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoRepository.cs index 6bd64e3..3a0edfc 100644 --- a/src/JsonApiDotNetCore.MongoDb/Repositories/MongoRepository.cs +++ b/src/JsonApiDotNetCore.MongoDb/Repositories/MongoRepository.cs @@ -1,3 +1,5 @@ +using System.Data; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -7,7 +9,7 @@ using JsonApiDotNetCore.MongoDb.Resources; using JsonApiDotNetCore.Queries; using JsonApiDotNetCore.Queries.Expressions; -using JsonApiDotNetCore.Queries.Internal.QueryableBuilding; +using JsonApiDotNetCore.Queries.QueryableBuilding; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -19,6 +21,12 @@ namespace JsonApiDotNetCore.MongoDb.Repositories; /// /// Implements the foundational Repository layer in the JsonApiDotNetCore architecture that uses MongoDB. /// +/// +/// The resource type. +/// +/// +/// The resource identifier type. +/// [PublicAPI] public class MongoRepository : IResourceRepository, IRepositorySupportsTransaction where TResource : class, IIdentifiable @@ -27,8 +35,9 @@ public class MongoRepository : IResourceRepository _constraintProviders; + private readonly IQueryConstraintProvider[] _constraintProviders; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; + private readonly IQueryableBuilder _queryableBuilder; protected virtual IMongoCollection Collection => _mongoDataAccess.MongoDatabase.GetCollection(typeof(TResource).Name); @@ -36,21 +45,23 @@ public class MongoRepository : IResourceRepository _mongoDataAccess.TransactionId; public MongoRepository(IMongoDataAccess mongoDataAccess, ITargetedFields targetedFields, IResourceGraph resourceGraph, IResourceFactory resourceFactory, - IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor) + IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, IQueryableBuilder queryableBuilder) { - ArgumentGuard.NotNull(mongoDataAccess); - ArgumentGuard.NotNull(targetedFields); - ArgumentGuard.NotNull(resourceGraph); - ArgumentGuard.NotNull(resourceFactory); - ArgumentGuard.NotNull(constraintProviders); - ArgumentGuard.NotNull(resourceDefinitionAccessor); + ArgumentNullException.ThrowIfNull(mongoDataAccess); + ArgumentNullException.ThrowIfNull(targetedFields); + ArgumentNullException.ThrowIfNull(resourceGraph); + ArgumentNullException.ThrowIfNull(resourceFactory); + ArgumentNullException.ThrowIfNull(constraintProviders); + ArgumentNullException.ThrowIfNull(resourceDefinitionAccessor); + ArgumentNullException.ThrowIfNull(queryableBuilder); _mongoDataAccess = mongoDataAccess; _targetedFields = targetedFields; _resourceGraph = resourceGraph; _resourceFactory = resourceFactory; - _constraintProviders = constraintProviders; + _constraintProviders = constraintProviders as IQueryConstraintProvider[] ?? constraintProviders.ToArray(); _resourceDefinitionAccessor = resourceDefinitionAccessor; + _queryableBuilder = queryableBuilder; if (!typeof(TResource).IsAssignableTo(typeof(IMongoIdentifiable))) { @@ -61,10 +72,11 @@ public MongoRepository(IMongoDataAccess mongoDataAccess, ITargetedFields targete /// public virtual async Task> GetAsync(QueryLayer queryLayer, CancellationToken cancellationToken) { - ArgumentGuard.NotNull(queryLayer); + ArgumentNullException.ThrowIfNull(queryLayer); - IMongoQueryable query = ApplyQueryLayer(queryLayer); - return await query.ToListAsync(cancellationToken); + IQueryable query = ApplyQueryLayer(queryLayer); + List? resources = await query.ToListAsync(cancellationToken); + return resources.AsReadOnly(); } /// @@ -77,15 +89,15 @@ public virtual Task CountAsync(FilterExpression? topFilter, CancellationTok Filter = topFilter }; - IMongoQueryable query = ApplyQueryLayer(layer); + IQueryable query = ApplyQueryLayer(layer); return query.CountAsync(cancellationToken); } #pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection - protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer queryLayer) + protected virtual IQueryable ApplyQueryLayer(QueryLayer queryLayer) #pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection { - ArgumentGuard.NotNull(queryLayer); + ArgumentNullException.ThrowIfNull(queryLayer); var queryExpressionValidator = new MongoQueryExpressionValidator(); queryExpressionValidator.Validate(queryLayer); @@ -95,7 +107,7 @@ protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer queryLay IQueryable source = GetAll(); // @formatter:wrap_chained_method_calls chop_always - // @formatter:keep_existing_linebreaks true + // @formatter:wrap_before_first_method_call true QueryableHandlerExpression[] queryableHandlers = _constraintProviders .SelectMany(provider => provider.GetConstraints()) @@ -104,7 +116,7 @@ protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer queryLay .OfType() .ToArray(); - // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_before_first_method_call restore // @formatter:wrap_chained_method_calls restore foreach (QueryableHandlerExpression queryableHandler in queryableHandlers) @@ -112,13 +124,10 @@ protected virtual IMongoQueryable ApplyQueryLayer(QueryLayer queryLay source = queryableHandler.Apply(source); } - var nameFactory = new LambdaParameterNameFactory(); + var context = QueryableBuilderContext.CreateRoot(source, typeof(Queryable), _mongoDataAccess.EntityModel, null); + Expression expression = _queryableBuilder.ApplyQuery(queryLayer, context); - var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, - new MongoModel(_resourceGraph)); - - Expression expression = builder.ApplyQuery(queryLayer); - return (IMongoQueryable)source.Provider.CreateQuery(expression); + return source.Provider.CreateQuery(expression); } protected virtual IQueryable GetAll() @@ -131,17 +140,16 @@ private void AssertNoRelationshipsInSparseFieldSets() ResourceType resourceType = _resourceGraph.GetResourceType(); // @formatter:wrap_chained_method_calls chop_always - // @formatter:keep_existing_linebreaks true + // @formatter:wrap_before_first_method_call true bool hasRelationshipSelectors = _constraintProviders .SelectMany(provider => provider.GetConstraints()) .Select(expressionInScope => expressionInScope.Expression) .OfType() - .Any(fieldTable => - fieldTable.Table.Keys.Any(targetResourceType => !resourceType.Equals(targetResourceType)) || + .Any(fieldTable => fieldTable.Table.Keys.Any(targetResourceType => !resourceType.Equals(targetResourceType)) || fieldTable.Table.Values.Any(fieldSet => fieldSet.Fields.Any(field => field is RelationshipAttribute))); - // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_before_first_method_call restore // @formatter:wrap_chained_method_calls restore if (hasRelationshipSelectors) @@ -151,8 +159,10 @@ private void AssertNoRelationshipsInSparseFieldSets() } /// - public virtual Task GetForCreateAsync(Type resourceClrType, TId id, CancellationToken cancellationToken) + public virtual Task GetForCreateAsync(Type resourceClrType, [DisallowNull] TId id, CancellationToken cancellationToken) { + ArgumentNullException.ThrowIfNull(resourceClrType); + var resource = (TResource)_resourceFactory.CreateInstance(resourceClrType); resource.Id = id; @@ -162,8 +172,8 @@ public virtual Task GetForCreateAsync(Type resourceClrType, TId id, C /// public virtual async Task CreateAsync(TResource resourceFromRequest, TResource resourceForDatabase, CancellationToken cancellationToken) { - ArgumentGuard.NotNull(resourceFromRequest); - ArgumentGuard.NotNull(resourceForDatabase); + ArgumentNullException.ThrowIfNull(resourceFromRequest); + ArgumentNullException.ThrowIfNull(resourceForDatabase); AssertNoRelationshipsAreTargeted(); @@ -174,19 +184,17 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r await _resourceDefinitionAccessor.OnWritingAsync(resourceForDatabase, WriteOperationKind.CreateResource, cancellationToken); - await SaveChangesAsync(async () => - { - await (_mongoDataAccess.ActiveSession != null + await SaveChangesAsync( + async () => await (_mongoDataAccess.ActiveSession != null ? Collection.InsertOneAsync(_mongoDataAccess.ActiveSession, resourceForDatabase, cancellationToken: cancellationToken) - : Collection.InsertOneAsync(resourceForDatabase, cancellationToken: cancellationToken)); - }, cancellationToken); + : Collection.InsertOneAsync(resourceForDatabase, cancellationToken: cancellationToken)), cancellationToken); await _resourceDefinitionAccessor.OnWriteSucceededAsync(resourceForDatabase, WriteOperationKind.CreateResource, cancellationToken); } private void AssertNoRelationshipsAreTargeted() { - if (_targetedFields.Relationships.Any()) + if (_targetedFields.Relationships.Count > 0) { throw new UnsupportedRelationshipException(); } @@ -195,7 +203,7 @@ private void AssertNoRelationshipsAreTargeted() /// public virtual async Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken) { - ArgumentGuard.NotNull(queryLayer); + ArgumentNullException.ThrowIfNull(queryLayer); IReadOnlyCollection resources = await GetAsync(queryLayer, cancellationToken); return resources.FirstOrDefault(); @@ -204,8 +212,8 @@ private void AssertNoRelationshipsAreTargeted() /// public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource resourceFromDatabase, CancellationToken cancellationToken) { - ArgumentGuard.NotNull(resourceFromRequest); - ArgumentGuard.NotNull(resourceFromDatabase); + ArgumentNullException.ThrowIfNull(resourceFromRequest); + ArgumentNullException.ThrowIfNull(resourceFromDatabase); AssertNoRelationshipsAreTargeted(); @@ -218,18 +226,16 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r FilterDefinition filter = Builders.Filter.Eq(resource => resource.Id, resourceFromDatabase.Id); - await SaveChangesAsync(async () => - { - await (_mongoDataAccess.ActiveSession != null + await SaveChangesAsync( + () => _mongoDataAccess.ActiveSession != null ? Collection.ReplaceOneAsync(_mongoDataAccess.ActiveSession, filter, resourceFromDatabase, cancellationToken: cancellationToken) - : Collection.ReplaceOneAsync(filter, resourceFromDatabase, cancellationToken: cancellationToken)); - }, cancellationToken); + : Collection.ReplaceOneAsync(filter, resourceFromDatabase, cancellationToken: cancellationToken), cancellationToken); await _resourceDefinitionAccessor.OnWriteSucceededAsync(resourceFromDatabase, WriteOperationKind.UpdateResource, cancellationToken); } /// - public virtual async Task DeleteAsync(TResource? resourceFromDatabase, TId id, CancellationToken cancellationToken) + public virtual async Task DeleteAsync(TResource? resourceFromDatabase, [DisallowNull] TId id, CancellationToken cancellationToken) { TResource placeholderResource = resourceFromDatabase ?? _resourceFactory.CreateInstance(); placeholderResource.Id = id; @@ -239,19 +245,19 @@ public virtual async Task DeleteAsync(TResource? resourceFromDatabase, TId id, C FilterDefinition filter = Builders.Filter.Eq(resource => resource.Id, id); DeleteResult result = await SaveChangesAsync( - async () => _mongoDataAccess.ActiveSession != null - ? await Collection.DeleteOneAsync(_mongoDataAccess.ActiveSession, filter, cancellationToken: cancellationToken) - : await Collection.DeleteOneAsync(filter, cancellationToken), cancellationToken); + () => _mongoDataAccess.ActiveSession != null + ? Collection.DeleteOneAsync(_mongoDataAccess.ActiveSession, filter, cancellationToken: cancellationToken) + : Collection.DeleteOneAsync(filter, cancellationToken), cancellationToken); if (!result.IsAcknowledged) { throw new DataStoreUpdateException( - new Exception($"Failed to delete document with id '{id}', because the operation was not acknowledged by MongoDB.")); + new DataException($"Failed to delete document with id '{id}', because the operation was not acknowledged by MongoDB.")); } if (result.DeletedCount == 0) { - throw new DataStoreUpdateException(new Exception($"Failed to delete document with id '{id}', because it does not exist.")); + throw new DataStoreUpdateException(new DataException($"Failed to delete document with id '{id}', because it does not exist.")); } await _resourceDefinitionAccessor.OnWriteSucceededAsync(placeholderResource, WriteOperationKind.DeleteResource, cancellationToken); @@ -264,7 +270,7 @@ public virtual Task SetRelationshipAsync(TResource leftResource, object? rightVa } /// - public virtual Task AddToToManyRelationshipAsync(TResource? leftResource, TId leftId, ISet rightResourceIds, + public virtual Task AddToToManyRelationshipAsync(TResource? leftResource, [DisallowNull] TId leftId, ISet rightResourceIds, CancellationToken cancellationToken) { throw new UnsupportedRelationshipException(); @@ -278,6 +284,8 @@ public virtual Task RemoveFromToManyRelationshipAsync(TResource leftResource, IS protected virtual async Task SaveChangesAsync(Func asyncSaveAction, CancellationToken cancellationToken) { + ArgumentNullException.ThrowIfNull(asyncSaveAction); + _ = await SaveChangesAsync(async () => { await asyncSaveAction(); @@ -287,6 +295,8 @@ protected virtual async Task SaveChangesAsync(Func asyncSaveAction, Cancel protected virtual async Task SaveChangesAsync(Func> asyncSaveAction, CancellationToken cancellationToken) { + ArgumentNullException.ThrowIfNull(asyncSaveAction); + try { return await asyncSaveAction(); diff --git a/src/JsonApiDotNetCore.MongoDb/Resources/IMongoIdentifiable.cs b/src/JsonApiDotNetCore.MongoDb/Resources/IMongoIdentifiable.cs index 11dbfa2..576cdcc 100644 --- a/src/JsonApiDotNetCore.MongoDb/Resources/IMongoIdentifiable.cs +++ b/src/JsonApiDotNetCore.MongoDb/Resources/IMongoIdentifiable.cs @@ -5,6 +5,4 @@ namespace JsonApiDotNetCore.MongoDb.Resources; /// /// Marker interface to indicate a resource that is stored in MongoDB. /// -public interface IMongoIdentifiable : IIdentifiable -{ -} +public interface IMongoIdentifiable : IIdentifiable; diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsTestCollection.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsCollectionFixture.cs similarity index 81% rename from test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsTestCollection.cs rename to test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsCollectionFixture.cs index acd9a96..dd1be80 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsTestCollection.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsCollectionFixture.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; [CollectionDefinition("AtomicOperationsFixture")] -public sealed class AtomicOperationsTestCollection : ICollectionFixture +public sealed class AtomicOperationsCollectionFixture : ICollectionFixture { // Starting MongoDB in Single Node Replica Set mode is required to enable transactions. // Starting in this mode requires about 10 seconds, which is normally repeated for each test class. diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsFixture.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsFixture.cs index e10e17d..262a2b6 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsFixture.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/AtomicOperationsFixture.cs @@ -1,28 +1,39 @@ using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Meta; +using Microsoft.Extensions.DependencyInjection.Extensions; using TestBuildingBlocks; +using Xunit; namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class AtomicOperationsFixture : IDisposable +public sealed class AtomicOperationsFixture : IAsyncLifetime { - internal IntegrationTestContext TestContext { get; } + internal IntegrationTestContext TestContext { get; } = new(); public AtomicOperationsFixture() { - TestContext = new IntegrationTestContext - { - StartMongoDbInSingleNodeReplicaSetMode = true - }; + TestContext.UseResourceTypesInNamespace(typeof(MusicTrack).Namespace); TestContext.UseController(); - TestContext.ConfigureServicesAfterStartup(services => services.AddSingleton()); + TestContext.ConfigureServices(services => + { + services.TryAddSingleton(); + + services.AddResourceDefinition(); + services.AddResourceDefinition(); + }); + } + + public Task InitializeAsync() + { + return Task.CompletedTask; } - public void Dispose() + public async Task DisposeAsync() { - TestContext.Dispose(); + await TestContext.DisposeAsync(); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/BaseForAtomicOperationsTestsThatChangeOptions.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/BaseForAtomicOperationsTestsThatChangeOptions.cs new file mode 100644 index 0000000..e9ffc53 --- /dev/null +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/BaseForAtomicOperationsTestsThatChangeOptions.cs @@ -0,0 +1,66 @@ +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; + +public abstract class BaseForAtomicOperationsTestsThatChangeOptions : IDisposable +{ + private readonly JsonApiOptionsScope _optionsScope; + + protected BaseForAtomicOperationsTestsThatChangeOptions(AtomicOperationsFixture fixture) + { + var options = (JsonApiOptions)fixture.TestContext.Factory.Services.GetRequiredService(); + _optionsScope = new JsonApiOptionsScope(options); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + +#pragma warning disable CA1063 // Implement IDisposable Correctly + private void Dispose(bool disposing) +#pragma warning restore CA1063 // Implement IDisposable Correctly + { + if (disposing) + { + _optionsScope.Dispose(); + } + } + + private sealed class JsonApiOptionsScope : IDisposable + { + private static readonly PropertyInfo[] PropertyCache = typeof(JsonApiOptions).GetProperties().Where(IsAccessibleProperty).ToArray(); + + private readonly JsonApiOptions _options; + private readonly JsonApiOptions _backupValues; + + public JsonApiOptionsScope(JsonApiOptions options) + { + _options = options; + _backupValues = new JsonApiOptions(); + + CopyPropertyValues(_options, _backupValues); + } + + private static bool IsAccessibleProperty(PropertyInfo property) + { + return property.GetMethod != null && property.SetMethod != null && property.GetCustomAttribute() == null; + } + + public void Dispose() + { + CopyPropertyValues(_backupValues, _options); + } + + private static void CopyPropertyValues(JsonApiOptions source, JsonApiOptions destination) + { + foreach (PropertyInfo property in PropertyCache) + { + property.SetMethod!.Invoke(destination, [property.GetMethod!.Invoke(source, null)]); + } + } + } +} diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index a103a42..8d65046 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -1,8 +1,6 @@ using System.Net; using FluentAssertions; -using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.Extensions.DependencyInjection; using MongoDB.Bson; using TestBuildingBlocks; using Xunit; @@ -10,25 +8,17 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Creating; [Collection("AtomicOperationsFixture")] -public sealed class AtomicCreateResourceTests +public sealed class AtomicCreateResourceTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicCreateResourceTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - - var options = (JsonApiOptions)fixture.TestContext.Factory.Services.GetRequiredService(); - options.AllowClientGeneratedIds = false; - } - [Fact] public async Task Can_create_resource() { // Arrange - string newArtistName = _fakers.Performer.Generate().ArtistName!; - DateTimeOffset newBornAt = _fakers.Performer.Generate().BornAt; + string newArtistName = _fakers.Performer.GenerateOne().ArtistName!; + DateTimeOffset newBornAt = _fakers.Performer.GenerateOne().BornAt; var requestBody = new { @@ -58,17 +48,17 @@ public async Task Can_create_resource() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Results.ShouldHaveCount(1); + responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].Data.SingleValue.ShouldNotBeNull().With(resource => + responseDocument.Results[0].Data.SingleValue.RefShould().NotBeNull().And.Subject.With(resource => { resource.Type.Should().Be("performers"); - resource.Attributes.ShouldContainKey("artistName").With(value => value.Should().Be(newArtistName)); - resource.Attributes.ShouldContainKey("bornAt").With(value => value.Should().Be(newBornAt)); + resource.Attributes.Should().ContainKey("artistName").WhoseValue.Should().Be(newArtistName); + resource.Attributes.Should().ContainKey("bornAt").WhoseValue.Should().Be(newBornAt); resource.Relationships.Should().BeNull(); }); - string newPerformerId = responseDocument.Results[0].Data.SingleValue!.Id.ShouldNotBeNull(); + string newPerformerId = responseDocument.Results[0].Data.SingleValue!.Id.Should().NotBeNull().And.Subject; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -85,7 +75,7 @@ public async Task Can_create_resources() // Arrange const int elementCount = 5; - List newTracks = _fakers.MusicTrack.Generate(elementCount); + List newTracks = _fakers.MusicTrack.GenerateList(elementCount); var operationElements = new List(elementCount); @@ -121,33 +111,30 @@ public async Task Can_create_resources() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Results.ShouldHaveCount(elementCount); + responseDocument.Results.Should().HaveCount(elementCount); for (int index = 0; index < elementCount; index++) { - responseDocument.Results[index].Data.SingleValue.ShouldNotBeNull().With(resource => + responseDocument.Results[index].Data.SingleValue.RefShould().NotBeNull().And.Subject.With(resource => { - resource.ShouldNotBeNull(); + resource.Should().NotBeNull(); resource.Type.Should().Be("musicTracks"); - resource.Attributes.ShouldContainKey("title").With(value => value.Should().Be(newTracks[index].Title)); - - resource.Attributes.ShouldContainKey("lengthInSeconds") - .With(value => value.As().Should().BeApproximately(newTracks[index].LengthInSeconds)); - - resource.Attributes.ShouldContainKey("genre").With(value => value.Should().Be(newTracks[index].Genre)); - resource.Attributes.ShouldContainKey("releasedAt").With(value => value.Should().Be(newTracks[index].ReleasedAt)); + resource.Attributes.Should().ContainKey("title").WhoseValue.Should().Be(newTracks[index].Title); + resource.Attributes.Should().ContainKey("lengthInSeconds").WhoseValue.As().Should().BeApproximately(newTracks[index].LengthInSeconds); + resource.Attributes.Should().ContainKey("genre").WhoseValue.Should().Be(newTracks[index].Genre); + resource.Attributes.Should().ContainKey("releasedAt").WhoseValue.Should().Be(newTracks[index].ReleasedAt); resource.Relationships.Should().BeNull(); }); } - string[] newTrackIds = responseDocument.Results.Select(result => result.Data.SingleValue!.Id.ShouldNotBeNull()).ToArray(); + string[] newTrackIds = responseDocument.Results.Select(result => result.Data.SingleValue!.Id.Should().NotBeNull().And.Subject).ToArray(); await _testContext.RunOnDatabaseAsync(async dbContext => { List tracksInDatabase = await dbContext.MusicTracks.ToListWhereAsync(musicTrack => newTrackIds.Contains(musicTrack.Id)); - tracksInDatabase.ShouldHaveCount(elementCount); + tracksInDatabase.Should().HaveCount(elementCount); for (int index = 0; index < elementCount; index++) { @@ -194,17 +181,17 @@ public async Task Can_create_resource_without_attributes_or_relationships() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Results.ShouldHaveCount(1); + responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].Data.SingleValue.ShouldNotBeNull().With(resource => + responseDocument.Results[0].Data.SingleValue.RefShould().NotBeNull().And.Subject.With(resource => { resource.Type.Should().Be("performers"); - resource.Attributes.ShouldContainKey("artistName").With(value => value.Should().BeNull()); - resource.Attributes.ShouldContainKey("bornAt").With(value => value.Should().Be(default(DateTimeOffset))); + resource.Attributes.Should().ContainKey("artistName").WhoseValue.Should().BeNull(); + resource.Attributes.Should().ContainKey("bornAt").WhoseValue.Should().Be(default(DateTimeOffset)); resource.Relationships.Should().BeNull(); }); - string newPerformerId = responseDocument.Results[0].Data.SingleValue!.Id.ShouldNotBeNull(); + string newPerformerId = responseDocument.Results[0].Data.SingleValue!.Id.Should().NotBeNull().And.Subject; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -219,7 +206,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_resource_with_client_generated_ID() { // Arrange - MusicTrack newTrack = _fakers.MusicTrack.Generate(); + MusicTrack newTrack = _fakers.MusicTrack.GenerateOne(); newTrack.Id = ObjectId.GenerateNewId().ToString(); var requestBody = new @@ -250,14 +237,14 @@ public async Task Cannot_create_resource_with_client_generated_ID() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Failed to deserialize request body: The use of client-generated IDs is disabled."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]/data/id"); - error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty()); + error.Meta.Should().HaveRequestBody(); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs index 4f83dfb..6014612 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs @@ -9,24 +9,25 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Creating; [Collection("AtomicOperationsFixture")] -public sealed class AtomicCreateResourceWithClientGeneratedIdTests +public sealed class AtomicCreateResourceWithClientGeneratedIdTests : BaseForAtomicOperationsTestsThatChangeOptions { private readonly IntegrationTestContext _testContext; private readonly OperationsFakers _fakers = new(); public AtomicCreateResourceWithClientGeneratedIdTests(AtomicOperationsFixture fixture) + : base(fixture) { _testContext = fixture.TestContext; var options = (JsonApiOptions)fixture.TestContext.Factory.Services.GetRequiredService(); - options.AllowClientGeneratedIds = true; + options.ClientIdGeneration = ClientIdGenerationMode.Required; } [Fact] public async Task Can_create_resource_with_client_generated_string_ID_having_side_effects() { // Arrange - TextLanguage newLanguage = _fakers.TextLanguage.Generate(); + TextLanguage newLanguage = _fakers.TextLanguage.GenerateOne(); newLanguage.Id = "free-format-client-generated-id"; var requestBody = new @@ -57,14 +58,14 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_sid // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - string isoCode = $"{newLanguage.IsoCode}{ContainerTypeToHideFromAutoDiscovery.ImplicitlyChangingTextLanguageDefinition.Suffix}"; + string isoCode = $"{newLanguage.IsoCode}{ImplicitlyChangingTextLanguageDefinition.Suffix}"; - responseDocument.Results.ShouldHaveCount(1); + responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].Data.SingleValue.ShouldNotBeNull().With(resource => + responseDocument.Results[0].Data.SingleValue.RefShould().NotBeNull().And.Subject.With(resource => { resource.Type.Should().Be("textLanguages"); - resource.Attributes.ShouldContainKey("isoCode").With(value => value.Should().Be(isoCode)); + resource.Attributes.Should().ContainKey("isoCode").WhoseValue.Should().Be(isoCode); resource.Attributes.Should().NotContainKey("isRightToLeft"); resource.Relationships.Should().BeNull(); }); @@ -81,7 +82,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_resource_with_client_generated_string_ID_having_no_side_effects() { // Arrange - Playlist newPlaylist = _fakers.Playlist.Generate(); + Playlist newPlaylist = _fakers.Playlist.GenerateOne(); newPlaylist.Id = "free-format-client-generated-id"; var requestBody = new @@ -126,10 +127,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_resource_for_existing_client_generated_ID() { // Arrange - TextLanguage existingLanguage = _fakers.TextLanguage.Generate(); + TextLanguage existingLanguage = _fakers.TextLanguage.GenerateOne(); existingLanguage.Id = "existing-free-format-client-generated-id"; - string newIsoCode = _fakers.TextLanguage.Generate().IsoCode!; + string newIsoCode = _fakers.TextLanguage.GenerateOne().IsoCode!; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -165,13 +166,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Another resource with the specified ID already exists."); error.Detail.Should().Be($"Another resource of type 'textLanguages' with ID '{existingLanguage.StringId}' already exists."); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); error.Meta.Should().NotContainKey("requestBody"); } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index 94eabe3..4f5f561 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -7,23 +7,18 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Creating; [Collection("AtomicOperationsFixture")] -public sealed class AtomicCreateResourceWithToManyRelationshipTests +public sealed class AtomicCreateResourceWithToManyRelationshipTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicCreateResourceWithToManyRelationshipTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } - [Fact] public async Task Cannot_create_ToMany_relationship() { // Arrange - Performer existingPerformer = _fakers.Performer.Generate(); + Performer existingPerformer = _fakers.Performer.GenerateOne(); - string newTitle = _fakers.MusicTrack.Generate().Title; + string newTitle = _fakers.MusicTrack.GenerateOne().Title; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -72,13 +67,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Relationships are not supported when using MongoDB."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index dd7cf9e..e449068 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -7,23 +7,18 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Creating; [Collection("AtomicOperationsFixture")] -public sealed class AtomicCreateResourceWithToOneRelationshipTests +public sealed class AtomicCreateResourceWithToOneRelationshipTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicCreateResourceWithToOneRelationshipTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } - [Fact] public async Task Cannot_create_ToOne_relationship() { // Arrange - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); - string newLyricText = _fakers.Lyric.Generate().Text; + string newLyricText = _fakers.Lyric.GenerateOne().Text; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -69,13 +64,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Relationships are not supported when using MongoDB."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index 5721c1b..1ea1ac4 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -7,21 +7,16 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Deleting; [Collection("AtomicOperationsFixture")] -public sealed class AtomicDeleteResourceTests +public sealed class AtomicDeleteResourceTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicDeleteResourceTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } - [Fact] public async Task Can_delete_existing_resource() { // Arrange - Performer existingPerformer = _fakers.Performer.Generate(); + Performer existingPerformer = _fakers.Performer.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -69,7 +64,7 @@ public async Task Can_delete_existing_resources() // Arrange const int elementCount = 5; - List existingTracks = _fakers.MusicTrack.Generate(elementCount); + List existingTracks = _fakers.MusicTrack.GenerateList(elementCount); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -146,13 +141,13 @@ public async Task Cannot_delete_resource_for_unknown_ID() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'performers' with ID '{performerId}' does not exist."); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); error.Meta.Should().NotContainKey("requestBody"); } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs index dc5ada1..822cfd4 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs @@ -6,39 +6,30 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; -[UsedImplicitly] -public sealed class ContainerTypeToHideFromAutoDiscovery +/// +/// Used to simulate side effects that occur in the database while saving, typically caused by database triggers. +/// +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public abstract class ImplicitlyChangingTextLanguageDefinition( + IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter, IMongoDataAccess mongoDataAccess) + : HitCountingResourceDefinition(resourceGraph, hitCounter) { - /// - /// Used to simulate side effects that occur in the database while saving, typically caused by database triggers. - /// - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public abstract class ImplicitlyChangingTextLanguageDefinition : HitCountingResourceDefinition - { - internal const string Suffix = " (changed)"; + internal const string Suffix = " (changed)"; - private readonly IMongoDataAccess _mongoDataAccess; + private readonly IMongoDataAccess _mongoDataAccess = mongoDataAccess; - protected ImplicitlyChangingTextLanguageDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter, - IMongoDataAccess mongoDataAccess) - : base(resourceGraph, hitCounter) - { - _mongoDataAccess = mongoDataAccess; - } + public override async Task OnWriteSucceededAsync(TextLanguage resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + { + await base.OnWriteSucceededAsync(resource, writeOperation, cancellationToken); - public override async Task OnWriteSucceededAsync(TextLanguage resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + if (writeOperation is not WriteOperationKind.DeleteResource) { - await base.OnWriteSucceededAsync(resource, writeOperation, cancellationToken); - - if (writeOperation is not WriteOperationKind.DeleteResource) - { - resource.IsoCode += Suffix; + resource.IsoCode += Suffix; - FilterDefinition filter = Builders.Filter.Eq(item => item.Id, resource.Id); + FilterDefinition filter = Builders.Filter.Eq(item => item.Id, resource.Id); - IMongoCollection collection = _mongoDataAccess.MongoDatabase.GetCollection(nameof(TextLanguage)); - await collection.ReplaceOneAsync(_mongoDataAccess.ActiveSession, filter, resource, cancellationToken: cancellationToken); - } + IMongoCollection collection = _mongoDataAccess.MongoDatabase.GetCollection(nameof(TextLanguage)); + await collection.ReplaceOneAsync(_mongoDataAccess.ActiveSession, filter, resource, cancellationToken: cancellationToken); } } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs index b9a2080..61459ed 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs @@ -7,22 +7,17 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.LocalIds; [Collection("AtomicOperationsFixture")] -public sealed class AtomicLocalIdTests +public sealed class AtomicLocalIdTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicLocalIdTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } - [Fact] public async Task Can_update_resource_using_local_ID() { // Arrange - string newTrackTitle = _fakers.MusicTrack.Generate().Title; - string newTrackGenre = _fakers.MusicTrack.Generate().Genre!; + string newTrackTitle = _fakers.MusicTrack.GenerateOne().Title; + string newTrackGenre = _fakers.MusicTrack.GenerateOne().Genre!; const string trackLocalId = "track-1"; @@ -67,19 +62,19 @@ public async Task Can_update_resource_using_local_ID() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Results.ShouldHaveCount(2); + responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].Data.SingleValue.ShouldNotBeNull().With(resource => + responseDocument.Results[0].Data.SingleValue.RefShould().NotBeNull().And.Subject.With(resource => { resource.Type.Should().Be("musicTracks"); resource.Lid.Should().BeNull(); - resource.Attributes.ShouldContainKey("title").With(value => value.Should().Be(newTrackTitle)); - resource.Attributes.ShouldContainKey("genre").With(value => value.Should().BeNull()); + resource.Attributes.Should().ContainKey("title").WhoseValue.Should().Be(newTrackTitle); + resource.Attributes.Should().ContainKey("genre").WhoseValue.Should().BeNull(); }); responseDocument.Results[1].Data.Value.Should().BeNull(); - string newTrackId = responseDocument.Results[0].Data.SingleValue!.Id.ShouldNotBeNull(); + string newTrackId = responseDocument.Results[0].Data.SingleValue!.Id.Should().NotBeNull().And.Subject; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -94,7 +89,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_delete_resource_using_local_ID() { // Arrange - string newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newTrackTitle = _fakers.MusicTrack.GenerateOne().Title; const string trackLocalId = "track-1"; @@ -135,18 +130,18 @@ public async Task Can_delete_resource_using_local_ID() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Results.ShouldHaveCount(2); + responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].Data.SingleValue.ShouldNotBeNull().With(resource => + responseDocument.Results[0].Data.SingleValue.RefShould().NotBeNull().And.Subject.With(resource => { resource.Type.Should().Be("musicTracks"); resource.Lid.Should().BeNull(); - resource.Attributes.ShouldContainKey("title").With(value => value.Should().Be(newTrackTitle)); + resource.Attributes.Should().ContainKey("title").WhoseValue.Should().Be(newTrackTitle); }); responseDocument.Results[1].Data.Value.Should().BeNull(); - string newTrackId = responseDocument.Results[0].Data.SingleValue!.Id.ShouldNotBeNull(); + string newTrackId = responseDocument.Results[0].Data.SingleValue!.Id.Should().NotBeNull().And.Subject; await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs index a701256..988b697 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs @@ -29,8 +29,8 @@ public async Task Returns_resource_meta_in_create_resource_with_side_effects() // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - string newTitle1 = _fakers.MusicTrack.Generate().Title; - string newTitle2 = _fakers.MusicTrack.Generate().Title; + string newTitle1 = _fakers.MusicTrack.GenerateOne().Title; + string newTitle2 = _fakers.MusicTrack.GenerateOne().Title; var requestBody = new { @@ -73,24 +73,24 @@ public async Task Returns_resource_meta_in_create_resource_with_side_effects() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Results.ShouldHaveCount(2); + responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].Data.SingleValue.ShouldNotBeNull().With(resource => + responseDocument.Results[0].Data.SingleValue.RefShould().NotBeNull().And.Subject.With(resource => { - resource.Meta.ShouldHaveCount(1); + resource.Meta.Should().HaveCount(1); - resource.Meta.ShouldContainKey("copyright").With(value => + resource.Meta.Should().ContainKey("copyright").WhoseValue.With(value => { JsonElement element = value.Should().BeOfType().Subject; element.GetString().Should().Be("(C) 2018. All rights reserved."); }); }); - responseDocument.Results[1].Data.SingleValue.ShouldNotBeNull().With(resource => + responseDocument.Results[1].Data.SingleValue.RefShould().NotBeNull().And.Subject.With(resource => { - resource.Meta.ShouldHaveCount(1); + resource.Meta.Should().HaveCount(1); - resource.Meta.ShouldContainKey("copyright").With(value => + resource.Meta.Should().ContainKey("copyright").WhoseValue.With(value => { JsonElement element = value.Should().BeOfType().Subject; element.GetString().Should().Be("(C) 1994. All rights reserved."); @@ -110,7 +110,7 @@ public async Task Returns_resource_meta_in_update_resource_with_side_effects() // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - TextLanguage existingLanguage = _fakers.TextLanguage.Generate(); + TextLanguage existingLanguage = _fakers.TextLanguage.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -145,13 +145,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Results.ShouldHaveCount(1); + responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].Data.SingleValue.ShouldNotBeNull().With(resource => + responseDocument.Results[0].Data.SingleValue.RefShould().NotBeNull().And.Subject.With(resource => { - resource.Meta.ShouldHaveCount(1); + resource.Meta.Should().HaveCount(1); - resource.Meta.ShouldContainKey("notice").With(value => + resource.Meta.Should().ContainKey("notice").WhoseValue.With(value => { JsonElement element = value.Should().BeOfType().Subject; element.GetString().Should().Be(TextLanguageMetaDefinition.NoticeText); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs index 479898d..93cf7d2 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs @@ -4,15 +4,11 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Meta; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class MusicTrackMetaDefinition : HitCountingResourceDefinition +public sealed class MusicTrackMetaDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) + : HitCountingResourceDefinition(resourceGraph, hitCounter) { protected override ResourceDefinitionExtensibilityPoints ExtensibilityPointsToTrack => ResourceDefinitionExtensibilityPoints.GetMeta; - public MusicTrackMetaDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) - : base(resourceGraph, hitCounter) - { - } - public override IDictionary GetMeta(MusicTrack resource) { base.GetMeta(resource); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs index ef3db2a..2d440be 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs @@ -5,17 +5,13 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Meta; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class TextLanguageMetaDefinition : ContainerTypeToHideFromAutoDiscovery.ImplicitlyChangingTextLanguageDefinition +public sealed class TextLanguageMetaDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter, IMongoDataAccess mongoDataAccess) + : ImplicitlyChangingTextLanguageDefinition(resourceGraph, hitCounter, mongoDataAccess) { internal const string NoticeText = "See https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes for ISO 639-1 language codes."; protected override ResourceDefinitionExtensibilityPoints ExtensibilityPointsToTrack => ResourceDefinitionExtensibilityPoints.GetMeta; - public TextLanguageMetaDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter, IMongoDataAccess mongoDataAccess) - : base(resourceGraph, hitCounter, mongoDataAccess) - { - } - public override IDictionary GetMeta(TextLanguage resource) { base.GetMeta(resource); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs index c9205a3..d405a69 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs @@ -8,14 +8,10 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Mixed; [Collection("AtomicOperationsFixture")] -public sealed class MaximumOperationsPerRequestTests +public sealed class MaximumOperationsPerRequestTests(AtomicOperationsFixture fixture) + : BaseForAtomicOperationsTestsThatChangeOptions(fixture) { - private readonly IntegrationTestContext _testContext; - - public MaximumOperationsPerRequestTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } + private readonly IntegrationTestContext _testContext = fixture.TestContext; [Fact] public async Task Can_process_high_number_of_operations_when_unconstrained() diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsController.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsController.cs index f82e32d..1ba04d2 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsController.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsController.cs @@ -7,11 +7,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; -public sealed class OperationsController : JsonApiOperationsController -{ - public OperationsController(IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, - IJsonApiRequest request, ITargetedFields targetedFields) - : base(options, resourceGraph, loggerFactory, processor, request, targetedFields) - { - } -} +public sealed class OperationsController( + IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request, + ITargetedFields targetedFields, IAtomicOperationFilter operationFilter) + : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields, operationFilter); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs index 6b6f6f0..5d443a6 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs @@ -5,7 +5,8 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -public sealed class OperationsDbContext : MongoDbContextShim +public sealed class OperationsDbContext(IMongoDatabase database) + : MongoDbContextShim(database) { public MongoDbSetShim Playlists => Set(); public MongoDbSetShim MusicTracks => Set(); @@ -13,9 +14,4 @@ public sealed class OperationsDbContext : MongoDbContextShim public MongoDbSetShim TextLanguages => Set(); public MongoDbSetShim Performers => Set(); public MongoDbSetShim RecordCompanies => Set(); - - public OperationsDbContext(IMongoDatabase database) - : base(database) - { - } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsFakers.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsFakers.cs index b26135e..6a10744 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsFakers.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/OperationsFakers.cs @@ -2,57 +2,48 @@ using Bogus; using TestBuildingBlocks; -// @formatter:wrap_chained_method_calls chop_always -// @formatter:keep_existing_linebreaks true +// @formatter:wrap_chained_method_calls chop_if_long +// @formatter:wrap_before_first_method_call true namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations; -internal sealed class OperationsFakers : FakerContainer +internal sealed class OperationsFakers { - private static readonly Lazy> LazyLanguageIsoCodes = - new(() => CultureInfo - .GetCultures(CultureTypes.NeutralCultures) - .Where(culture => !string.IsNullOrEmpty(culture.Name)) - .Select(culture => culture.Name) - .ToArray()); - - private readonly Lazy> _lazyPlaylistFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(playlist => playlist.Name, faker => faker.Lorem.Sentence())); - - private readonly Lazy> _lazyMusicTrackFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(musicTrack => musicTrack.Title, faker => faker.Lorem.Word()) - .RuleFor(musicTrack => musicTrack.LengthInSeconds, faker => faker.Random.Decimal(3 * 60, 5 * 60)) - .RuleFor(musicTrack => musicTrack.Genre, faker => faker.Lorem.Word()) - .RuleFor(musicTrack => musicTrack.ReleasedAt, faker => faker.Date.PastOffset() - .TruncateToWholeMilliseconds())); - - private readonly Lazy> _lazyLyricFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(lyric => lyric.Text, faker => faker.Lorem.Text()) - .RuleFor(lyric => lyric.Format, "LRC")); - - private readonly Lazy> _lazyTextLanguageFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(textLanguage => textLanguage.IsoCode, faker => faker.PickRandom(LazyLanguageIsoCodes.Value))); - - private readonly Lazy> _lazyPerformerFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(performer => performer.ArtistName, faker => faker.Name.FullName()) - .RuleFor(performer => performer.BornAt, faker => faker.Date.PastOffset() - .TruncateToWholeMilliseconds())); - - private readonly Lazy> _lazyRecordCompanyFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(recordCompany => recordCompany.Name, faker => faker.Company.CompanyName()) - .RuleFor(recordCompany => recordCompany.CountryOfResidence, faker => faker.Address.Country())); + private static readonly Lazy LazyLanguageIsoCodes = new(() => CultureInfo + .GetCultures(CultureTypes.NeutralCultures) + .Where(culture => !string.IsNullOrEmpty(culture.Name)) + .Select(culture => culture.Name) + .ToArray()); + + private readonly Lazy> _lazyPlaylistFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(playlist => playlist.Name, faker => faker.Lorem.Sentence())); + + private readonly Lazy> _lazyMusicTrackFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(musicTrack => musicTrack.Title, faker => faker.Lorem.Word()) + .RuleFor(musicTrack => musicTrack.LengthInSeconds, faker => faker.Random.Decimal(3 * 60, 5 * 60)) + .RuleFor(musicTrack => musicTrack.Genre, faker => faker.Lorem.Word()) + .RuleFor(musicTrack => musicTrack.ReleasedAt, faker => faker.Date.PastOffset().TruncateToWholeMilliseconds())); + + private readonly Lazy> _lazyLyricFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(lyric => lyric.Text, faker => faker.Lorem.Text()) + .RuleFor(lyric => lyric.Format, "LRC")); + + private readonly Lazy> _lazyTextLanguageFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(textLanguage => textLanguage.IsoCode, faker => faker.PickRandom(LazyLanguageIsoCodes.Value))); + + private readonly Lazy> _lazyPerformerFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(performer => performer.ArtistName, faker => faker.Name.FullName()) + .RuleFor(performer => performer.BornAt, faker => faker.Date.PastOffset().TruncateToWholeMilliseconds())); + + private readonly Lazy> _lazyRecordCompanyFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(recordCompany => recordCompany.Name, faker => faker.Company.CompanyName()) + .RuleFor(recordCompany => recordCompany.CountryOfResidence, faker => faker.Address.Country())); public Faker Playlist => _lazyPlaylistFaker.Value; public Faker MusicTrack => _lazyMusicTrackFaker.Value; diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs index b9db4b1..08bd99a 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs @@ -23,13 +23,10 @@ public AtomicRollbackTests(AtomicOperationsFixture fixture) public async Task Can_rollback_created_resource_on_error() { // Arrange - string newArtistName = _fakers.Performer.Generate().ArtistName!; - DateTimeOffset newBornAt = _fakers.Performer.Generate().BornAt; + string newArtistName = _fakers.Performer.GenerateOne().ArtistName!; + DateTimeOffset newBornAt = _fakers.Performer.GenerateOne().BornAt; - await _testContext.RunOnDatabaseAsync(async dbContext => - { - await dbContext.ClearTableAsync(); - }); + await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.ClearTableAsync()); string unknownPerformerId = Unknown.StringId.For(); @@ -70,13 +67,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'performers' with ID '{unknownPerformerId}' does not exist."); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[1]"); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -90,9 +87,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_rollback_updated_resource_on_error() { // Arrange - Performer existingPerformer = _fakers.Performer.Generate(); + Performer existingPerformer = _fakers.Performer.GenerateOne(); - string newArtistName = _fakers.Performer.Generate().ArtistName!; + string newArtistName = _fakers.Performer.GenerateOne().ArtistName!; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -139,13 +136,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'performers' with ID '{unknownPerformerId}' does not exist."); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[1]"); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -160,7 +157,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_rollback_deleted_resource_on_error() { // Arrange - Performer existingPerformer = _fakers.Performer.Generate(); + Performer existingPerformer = _fakers.Performer.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -204,13 +201,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'performers' with ID '{unknownPerformerId}' does not exist."); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[1]"); await _testContext.RunOnDatabaseAsync(async dbContext => diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs index 6445fe8..b19501b 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs @@ -2,7 +2,6 @@ using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -17,17 +16,15 @@ public AtomicTransactionConsistencyTests(IntegrationTestContext(); + testContext.UseResourceTypesInNamespace(typeof(MusicTrack).Namespace); - testContext.StartMongoDbInSingleNodeReplicaSetMode = true; + testContext.UseController(); - testContext.ConfigureServicesAfterStartup(services => + testContext.ConfigureServices(services => { - services.AddSingleton(); - - services.AddResourceRepository(); - services.AddResourceRepository(); - services.AddResourceRepository(); + services.AddResourceRepository(); + services.AddResourceRepository(); + services.AddResourceRepository(); }); } @@ -61,13 +58,13 @@ public async Task Cannot_use_non_transactional_repository() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported resource type in atomic:operations request."); error.Detail.Should().Be("Operations on resources of type 'performers' cannot be used because transaction support is unavailable."); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -75,7 +72,7 @@ public async Task Cannot_use_non_transactional_repository() public async Task Cannot_use_transactional_repository_without_active_transaction() { // Arrange - string newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newTrackTitle = _fakers.MusicTrack.GenerateOne().Title; var requestBody = new { @@ -104,13 +101,13 @@ public async Task Cannot_use_transactional_repository_without_active_transaction // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of resource types in atomic:operations request."); error.Detail.Should().Be("All operations need to participate in a single shared transaction, which is not the case for this request."); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -118,7 +115,7 @@ public async Task Cannot_use_transactional_repository_without_active_transaction public async Task Cannot_use_distributed_transaction() { // Arrange - string newLyricText = _fakers.Lyric.Generate().Text; + string newLyricText = _fakers.Lyric.GenerateOne().Text; var requestBody = new { @@ -147,13 +144,13 @@ public async Task Cannot_use_distributed_transaction() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of resource types in atomic:operations request."); error.Detail.Should().Be("All operations need to participate in a single shared transaction, which is not the case for this request."); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs index c1275be..90d71d0 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs @@ -4,33 +4,32 @@ using JsonApiDotNetCore.MongoDb.AtomicOperations; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.QueryableBuilding; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Transactions; -[UsedImplicitly] -internal sealed partial class ContainerTypeToHideFromAutoDiscovery +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public sealed class LyricRepository : MongoRepository, IAsyncDisposable { - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class LyricRepository : MongoRepository, IAsyncDisposable - { - private readonly IOperationsTransaction _transaction; + private readonly MongoDataAccess _otherDataAccess; + private readonly IOperationsTransaction _transaction; - public override string TransactionId => _transaction.TransactionId; + public override string TransactionId => _transaction.TransactionId; - public LyricRepository(IMongoDataAccess mongoDataAccess, ITargetedFields targetedFields, IResourceGraph resourceGraph, IResourceFactory resourceFactory, - IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(mongoDataAccess, targetedFields, resourceGraph, resourceFactory, constraintProviders, resourceDefinitionAccessor) - { - IMongoDataAccess otherDataAccess = new MongoDataAccess(mongoDataAccess.MongoDatabase); + public LyricRepository(IMongoDataAccess mongoDataAccess, ITargetedFields targetedFields, IResourceGraph resourceGraph, IResourceFactory resourceFactory, + IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, IQueryableBuilder queryableBuilder) + : base(mongoDataAccess, targetedFields, resourceGraph, resourceFactory, constraintProviders, resourceDefinitionAccessor, queryableBuilder) + { + _otherDataAccess = new MongoDataAccess(mongoDataAccess.EntityModel, mongoDataAccess.MongoDatabase); - var factory = new MongoTransactionFactory(otherDataAccess); - _transaction = factory.BeginTransactionAsync(CancellationToken.None).Result; - } + var factory = new MongoTransactionFactory(_otherDataAccess); + _transaction = factory.BeginTransactionAsync(CancellationToken.None).Result; + } - public async ValueTask DisposeAsync() - { - await _transaction.DisposeAsync(); - } + public async ValueTask DisposeAsync() + { + await _transaction.DisposeAsync(); + await _otherDataAccess.DisposeAsync(); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs index 09164f2..bd73d6c 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs @@ -2,21 +2,17 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.QueryableBuilding; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Transactions; -internal sealed partial class ContainerTypeToHideFromAutoDiscovery +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public sealed class MusicTrackRepository( + IMongoDataAccess mongoDataAccess, ITargetedFields targetedFields, IResourceGraph resourceGraph, IResourceFactory resourceFactory, + IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, IQueryableBuilder queryableBuilder) + : MongoRepository(mongoDataAccess, targetedFields, resourceGraph, resourceFactory, constraintProviders, resourceDefinitionAccessor, + queryableBuilder) { - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class MusicTrackRepository : MongoRepository - { - public override string? TransactionId => null; - - public MusicTrackRepository(IMongoDataAccess mongoDataAccess, ITargetedFields targetedFields, IResourceGraph resourceGraph, - IResourceFactory resourceFactory, IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(mongoDataAccess, targetedFields, resourceGraph, resourceFactory, constraintProviders, resourceDefinitionAccessor) - { - } - } + public override string? TransactionId => null; } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs index c4d2e26..2f73d74 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs @@ -6,60 +6,56 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Transactions; -internal sealed partial class ContainerTypeToHideFromAutoDiscovery +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public sealed class PerformerRepository : IResourceRepository { - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class PerformerRepository : IResourceRepository + public Task> GetAsync(QueryLayer queryLayer, CancellationToken cancellationToken) { - public Task> GetAsync(QueryLayer queryLayer, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); + } - public Task CountAsync(FilterExpression? filter, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public Task CountAsync(FilterExpression? filter, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - public Task GetForCreateAsync(Type resourceClrType, string? id, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public Task GetForCreateAsync(Type resourceClrType, string? id, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - public Task CreateAsync(Performer resourceFromRequest, Performer resourceForDatabase, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public Task CreateAsync(Performer resourceFromRequest, Performer resourceForDatabase, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - public Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - public Task UpdateAsync(Performer resourceFromRequest, Performer resourceFromDatabase, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public Task UpdateAsync(Performer resourceFromRequest, Performer resourceFromDatabase, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - public Task DeleteAsync(Performer? resourceFromDatabase, string? id, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public Task DeleteAsync(Performer? resourceFromDatabase, string? id, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - public Task SetRelationshipAsync(Performer leftResource, object? rightValue, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public Task SetRelationshipAsync(Performer leftResource, object? rightValue, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - public Task AddToToManyRelationshipAsync(Performer? leftResource, string? leftId, ISet rightResourceIds, - CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public Task AddToToManyRelationshipAsync(Performer? leftResource, string? leftId, ISet rightResourceIds, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } - public Task RemoveFromToManyRelationshipAsync(Performer leftResource, ISet rightResourceIds, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } + public Task RemoveFromToManyRelationshipAsync(Performer leftResource, ISet rightResourceIds, CancellationToken cancellationToken) + { + throw new NotImplementedException(); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index bf4edda..e461a70 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -7,22 +7,17 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Updating.Relationships; [Collection("AtomicOperationsFixture")] -public sealed class AtomicAddToToManyRelationshipTests +public sealed class AtomicAddToToManyRelationshipTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicAddToToManyRelationshipTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } - [Fact] public async Task Cannot_add_to_OneToMany_relationship() { // Arrange - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); - Performer existingPerformer = _fakers.Performer.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); + Performer existingPerformer = _fakers.Performer.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -64,13 +59,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Relationships are not supported when using MongoDB."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -78,8 +73,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_add_to_ManyToMany_relationship() { // Arrange - Playlist existingPlaylist = _fakers.Playlist.Generate(); - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + Playlist existingPlaylist = _fakers.Playlist.GenerateOne(); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -121,13 +116,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Relationships are not supported when using MongoDB."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index cb17dcd..e68e8fd 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -7,22 +7,17 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Updating.Relationships; [Collection("AtomicOperationsFixture")] -public sealed class AtomicRemoveFromToManyRelationshipTests +public sealed class AtomicRemoveFromToManyRelationshipTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicRemoveFromToManyRelationshipTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } - [Fact] public async Task Cannot_remove_from_OneToMany_relationship() { // Arrange - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); - existingTrack.Performers = _fakers.Performer.Generate(1); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); + existingTrack.Performers = _fakers.Performer.GenerateList(1); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -64,13 +59,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Relationships are not supported when using MongoDB."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -78,8 +73,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_remove_from_ManyToMany_relationship() { // Arrange - Playlist existingPlaylist = _fakers.Playlist.Generate(); - existingPlaylist.Tracks = _fakers.MusicTrack.Generate(1); + Playlist existingPlaylist = _fakers.Playlist.GenerateOne(); + existingPlaylist.Tracks = _fakers.MusicTrack.GenerateList(1); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -121,13 +116,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Relationships are not supported when using MongoDB."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index 27d679a..e71ef7b 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -7,22 +7,17 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Updating.Relationships; [Collection("AtomicOperationsFixture")] -public sealed class AtomicReplaceToManyRelationshipTests +public sealed class AtomicReplaceToManyRelationshipTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicReplaceToManyRelationshipTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } - [Fact] public async Task Cannot_replace_OneToMany_relationship() { // Arrange - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); - Performer existingPerformer = _fakers.Performer.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); + Performer existingPerformer = _fakers.Performer.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -64,13 +59,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Relationships are not supported when using MongoDB."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -78,8 +73,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_replace_ManyToMany_relationship() { // Arrange - Playlist existingPlaylist = _fakers.Playlist.Generate(); - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + Playlist existingPlaylist = _fakers.Playlist.GenerateOne(); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -121,13 +116,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Relationships are not supported when using MongoDB."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 0f80aee..e51e98d 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -7,22 +7,17 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Updating.Relationships; [Collection("AtomicOperationsFixture")] -public sealed class AtomicUpdateToOneRelationshipTests +public sealed class AtomicUpdateToOneRelationshipTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicUpdateToOneRelationshipTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } - [Fact] public async Task Cannot_create_ManyToOne_relationship() { // Arrange - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); - RecordCompany existingCompany = _fakers.RecordCompany.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); + RecordCompany existingCompany = _fakers.RecordCompany.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -61,13 +56,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Relationships are not supported when using MongoDB."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 162bacb..8ebe878 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -7,22 +7,17 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Updating.Resources; [Collection("AtomicOperationsFixture")] -public sealed class AtomicReplaceToManyRelationshipTests +public sealed class AtomicReplaceToManyRelationshipTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicReplaceToManyRelationshipTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } - [Fact] public async Task Cannot_replace_ToMany_relationship() { // Arrange - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); - Performer existingPerformer = _fakers.Performer.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); + Performer existingPerformer = _fakers.Performer.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -69,13 +64,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Relationships are not supported when using MongoDB."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index a6cec5b..d438d34 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -7,24 +7,19 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Updating.Resources; [Collection("AtomicOperationsFixture")] -public sealed class AtomicUpdateResourceTests +public sealed class AtomicUpdateResourceTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicUpdateResourceTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } - [Fact] public async Task Can_update_resources() { // Arrange const int elementCount = 5; - List existingTracks = _fakers.MusicTrack.Generate(elementCount); - string[] newTrackTitles = _fakers.MusicTrack.Generate(elementCount).Select(musicTrack => musicTrack.Title).ToArray(); + List existingTracks = _fakers.MusicTrack.GenerateList(elementCount); + string[] newTrackTitles = _fakers.MusicTrack.GenerateList(elementCount).Select(musicTrack => musicTrack.Title).ToArray(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -71,7 +66,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); - tracksInDatabase.ShouldHaveCount(elementCount); + tracksInDatabase.Should().HaveCount(elementCount); for (int index = 0; index < elementCount; index++) { @@ -87,7 +82,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_without_attributes_or_relationships() { // Arrange - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -140,9 +135,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_partially_update_resource_without_side_effects() { // Arrange - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); - string newGenre = _fakers.MusicTrack.Generate().Genre!; + string newGenre = _fakers.MusicTrack.GenerateOne().Genre!; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -195,12 +190,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_completely_update_resource_without_side_effects() { // Arrange - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); - string newTitle = _fakers.MusicTrack.Generate().Title; - decimal? newLengthInSeconds = _fakers.MusicTrack.Generate().LengthInSeconds; - string newGenre = _fakers.MusicTrack.Generate().Genre!; - DateTimeOffset newReleasedAt = _fakers.MusicTrack.Generate().ReleasedAt; + string newTitle = _fakers.MusicTrack.GenerateOne().Title; + decimal? newLengthInSeconds = _fakers.MusicTrack.GenerateOne().LengthInSeconds; + string newGenre = _fakers.MusicTrack.GenerateOne().Genre!; + DateTimeOffset newReleasedAt = _fakers.MusicTrack.GenerateOne().ReleasedAt; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -256,8 +251,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_side_effects() { // Arrange - TextLanguage existingLanguage = _fakers.TextLanguage.Generate(); - string newIsoCode = _fakers.TextLanguage.Generate().IsoCode!; + TextLanguage existingLanguage = _fakers.TextLanguage.GenerateOne(); + string newIsoCode = _fakers.TextLanguage.GenerateOne().IsoCode!; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -293,14 +288,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Results.ShouldHaveCount(1); + responseDocument.Results.Should().HaveCount(1); - string isoCode = $"{newIsoCode}{ContainerTypeToHideFromAutoDiscovery.ImplicitlyChangingTextLanguageDefinition.Suffix}"; + string isoCode = $"{newIsoCode}{ImplicitlyChangingTextLanguageDefinition.Suffix}"; - responseDocument.Results[0].Data.SingleValue.ShouldNotBeNull().With(resource => + responseDocument.Results[0].Data.SingleValue.RefShould().NotBeNull().And.Subject.With(resource => { resource.Type.Should().Be("textLanguages"); - resource.Attributes.ShouldContainKey("isoCode").With(value => value.Should().Be(isoCode)); + resource.Attributes.Should().ContainKey("isoCode").WhoseValue.Should().Be(isoCode); resource.Attributes.Should().NotContainKey("isRightToLeft"); resource.Relationships.Should().BeNull(); }); @@ -348,13 +343,13 @@ public async Task Cannot_update_resource_for_unknown_ID() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'performers' with ID '{performerId}' does not exist."); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); error.Meta.Should().NotContainKey("requestBody"); } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index 0e0562a..21967d5 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -7,22 +7,17 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.AtomicOperations.Updating.Resources; [Collection("AtomicOperationsFixture")] -public sealed class AtomicUpdateToOneRelationshipTests +public sealed class AtomicUpdateToOneRelationshipTests(AtomicOperationsFixture fixture) { - private readonly IntegrationTestContext _testContext; + private readonly IntegrationTestContext _testContext = fixture.TestContext; private readonly OperationsFakers _fakers = new(); - public AtomicUpdateToOneRelationshipTests(AtomicOperationsFixture fixture) - { - _testContext = fixture.TestContext; - } - [Fact] public async Task Cannot_create_ToOne_relationship() { // Arrange - Lyric existingLyric = _fakers.Lyric.Generate(); - MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + Lyric existingLyric = _fakers.Lyric.GenerateOne(); + MusicTrack existingTrack = _fakers.MusicTrack.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -66,13 +61,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Relationships are not supported when using MongoDB."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/HitCountingResourceDefinition.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/HitCountingResourceDefinition.cs index fd93e5e..d059689 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/HitCountingResourceDefinition.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/HitCountingResourceDefinition.cs @@ -21,6 +21,8 @@ public abstract class HitCountingResourceDefinition : JsonApiRes protected HitCountingResourceDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) : base(resourceGraph) { + ArgumentNullException.ThrowIfNull(hitCounter); + _hitCounter = hitCounter; } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/MetaDbContext.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/MetaDbContext.cs index fbd2ff5..ec003be 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/MetaDbContext.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/MetaDbContext.cs @@ -5,12 +5,8 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -public sealed class MetaDbContext : MongoDbContextShim +public sealed class MetaDbContext(IMongoDatabase database) + : MongoDbContextShim(database) { public MongoDbSetShim SupportTickets => Set(); - - public MetaDbContext(IMongoDatabase database) - : base(database) - { - } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/MetaFakers.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/MetaFakers.cs index 8c85a0c..9b33ec2 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/MetaFakers.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/MetaFakers.cs @@ -1,17 +1,16 @@ using Bogus; using TestBuildingBlocks; -// @formatter:wrap_chained_method_calls chop_always -// @formatter:keep_existing_linebreaks true +// @formatter:wrap_chained_method_calls chop_if_long +// @formatter:wrap_before_first_method_call true namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta; -internal sealed class MetaFakers : FakerContainer +internal sealed class MetaFakers { - private readonly Lazy> _lazySupportTicketFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(supportTicket => supportTicket.Description, faker => faker.Lorem.Paragraph())); + private readonly Lazy> _lazySupportTicketFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(supportTicket => supportTicket.Description, faker => faker.Lorem.Paragraph())); public Faker SupportTicket => _lazySupportTicketFaker.Value; } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/ResourceMetaTests.cs index 3a3edca..f8eedf2 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/ResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -1,5 +1,6 @@ using System.Net; using FluentAssertions; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; @@ -16,10 +17,14 @@ public ResourceMetaTests(IntegrationTestContext { _testContext = testContext; + testContext.UseResourceTypesInNamespace(typeof(SupportTicket).Namespace); + testContext.UseController(); - testContext.ConfigureServicesAfterStartup(services => + testContext.ConfigureServices(services => { + services.AddResourceDefinition(); + services.AddSingleton(); }); @@ -33,7 +38,7 @@ public async Task Returns_resource_meta_from_ResourceDefinition() // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - List tickets = _fakers.SupportTicket.Generate(3); + List tickets = _fakers.SupportTicket.GenerateList(3); tickets[0].Description = $"Critical: {tickets[0].Description}"; tickets[2].Description = $"Critical: {tickets[2].Description}"; @@ -52,10 +57,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(3); - responseDocument.Data.ManyValue[0].Meta.ShouldContainKey("hasHighPriority"); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Meta.Should().ContainKey("hasHighPriority"); responseDocument.Data.ManyValue[1].Meta.Should().BeNull(); - responseDocument.Data.ManyValue[2].Meta.ShouldContainKey("hasHighPriority"); + responseDocument.Data.ManyValue[2].Meta.Should().ContainKey("hasHighPriority"); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/SupportTicketDefinition.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/SupportTicketDefinition.cs index 2cbff91..704c6c8 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/SupportTicketDefinition.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/SupportTicketDefinition.cs @@ -4,15 +4,11 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.Meta; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class SupportTicketDefinition : HitCountingResourceDefinition +public sealed class SupportTicketDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) + : HitCountingResourceDefinition(resourceGraph, hitCounter) { protected override ResourceDefinitionExtensibilityPoints ExtensibilityPointsToTrack => ResourceDefinitionExtensibilityPoints.GetMeta; - public SupportTicketDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) - : base(resourceGraph, hitCounter) - { - } - public override IDictionary? GetMeta(SupportTicket resource) { base.GetMeta(resource); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/TopLevelCountTests.cs index 6b1c1c7..c85635d 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/TopLevelCountTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/Meta/TopLevelCountTests.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Text.Json; using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; @@ -19,13 +18,11 @@ public TopLevelCountTests(IntegrationTestContext { _testContext = testContext; + testContext.UseResourceTypesInNamespace(typeof(SupportTicket).Namespace); + testContext.UseController(); - testContext.ConfigureServicesAfterStartup(services => - { - services.AddSingleton(); - services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>)); - }); + testContext.ConfigureServices(services => services.AddScoped(typeof(IResourceChangeTracker<>), typeof(NeverSameResourceChangeTracker<>))); var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); options.IncludeTotalResourceCount = true; @@ -35,7 +32,7 @@ public TopLevelCountTests(IntegrationTestContext public async Task Renders_resource_count_for_collection() { // Arrange - SupportTicket ticket = _fakers.SupportTicket.Generate(); + SupportTicket ticket = _fakers.SupportTicket.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -52,23 +49,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Meta.ShouldNotBeNull(); - - responseDocument.Meta.ShouldContainKey("total").With(value => - { - JsonElement element = value.Should().BeOfType().Subject; - element.GetInt32().Should().Be(1); - }); + responseDocument.Meta.Should().ContainTotal(1); } [Fact] public async Task Renders_resource_count_for_empty_collection() { // Arrange - await _testContext.RunOnDatabaseAsync(async dbContext => - { - await dbContext.ClearTableAsync(); - }); + await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.ClearTableAsync()); const string route = "/supportTickets"; @@ -78,20 +66,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Meta.ShouldNotBeNull(); - - responseDocument.Meta.ShouldContainKey("total").With(value => - { - JsonElement element = value.Should().BeOfType().Subject; - element.GetInt32().Should().Be(0); - }); + responseDocument.Meta.Should().ContainTotal(0); } [Fact] public async Task Hides_resource_count_in_create_resource_response() { // Arrange - string newDescription = _fakers.SupportTicket.Generate().Description; + string newDescription = _fakers.SupportTicket.GenerateOne().Description; var requestBody = new { @@ -120,9 +102,9 @@ public async Task Hides_resource_count_in_create_resource_response() public async Task Hides_resource_count_in_update_resource_response() { // Arrange - SupportTicket existingTicket = _fakers.SupportTicket.Generate(); + SupportTicket existingTicket = _fakers.SupportTicket.GenerateOne(); - string newDescription = _fakers.SupportTicket.Generate().Description; + string newDescription = _fakers.SupportTicket.GenerateOne().Description; await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/AccountPreferences.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/AccountPreferences.cs index 0cd5aa2..73ea764 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/AccountPreferences.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/AccountPreferences.cs @@ -5,6 +5,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings; [UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")] public sealed class AccountPreferences : HexStringMongoIdentifiable { [Attr] diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs index 3e97ce3..7027b2a 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs @@ -1,11 +1,14 @@ using System.Globalization; using System.Net; using System.Reflection; +using System.Text.Json.Serialization; using System.Web; using FluentAssertions; using FluentAssertions.Extensions; using Humanizer; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -19,7 +22,16 @@ public FilterDataTypeTests(IntegrationTestContext(); + + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); + + if (!options.SerializerOptions.Converters.Any(converter => converter is JsonStringEnumConverter)) + { + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + } } [Theory] @@ -60,8 +72,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey(attributeName).With(value => value.Should().Be(value)); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey(attributeName).WhoseValue.Should().Be(propertyValue); } [Fact] @@ -88,8 +100,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someDecimal").With(value => value.Should().Be(resource.SomeDecimal)); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someDecimal").WhoseValue.Should().Be(resource.SomeDecimal); } [Fact] @@ -116,8 +128,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someGuid").With(value => value.Should().Be(resource.SomeGuid)); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someGuid").WhoseValue.Should().Be(resource.SomeGuid); } [Fact] @@ -144,10 +156,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someDateTimeInLocalZone") - .With(value => value.Should().Be(resource.SomeDateTimeInLocalZone)); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someDateTimeInLocalZone").WhoseValue.Should().Be(resource.SomeDateTimeInLocalZone); } [Fact] @@ -174,10 +185,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someDateTimeInUtcZone") - .With(value => value.Should().Be(resource.SomeDateTimeInUtcZone)); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someDateTimeInUtcZone").WhoseValue.Should().Be(resource.SomeDateTimeInUtcZone); } [Fact] @@ -204,8 +214,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someDateTimeOffset").With(value => value.Should().Be(resource.SomeDateTimeOffset)); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someDateTimeOffset").WhoseValue.Should().Be(resource.SomeDateTimeOffset); } [Fact] @@ -232,12 +242,68 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someTimeSpan").With(value => value.Should().Be(resource.SomeTimeSpan)); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someTimeSpan").WhoseValue.Should().Be(resource.SomeTimeSpan); + } + + [Fact] + public async Task Can_filter_equality_on_type_DateOnly() + { + // Arrange + var resource = new FilterableResource + { + SomeDateOnly = DateOnly.FromDateTime(27.January(2003)) + }; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.FilterableResources.AddRange(resource, new FilterableResource()); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/filterableResources?filter=equals(someDateOnly,'{resource.SomeDateOnly:O}')"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someDateOnly").WhoseValue.Should().Be(resource.SomeDateOnly); + } + + [Fact] + public async Task Can_filter_equality_on_type_TimeOnly() + { + // Arrange + var resource = new FilterableResource + { + SomeTimeOnly = new TimeOnly(23, 59, 59, 999) + }; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.FilterableResources.AddRange(resource, new FilterableResource()); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/filterableResources?filter=equals(someTimeOnly,'{resource.SomeTimeOnly:O}')"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someTimeOnly").WhoseValue.Should().Be(resource.SomeTimeOnly); } [Fact] - public async Task Cannot_filter_equality_on_incompatible_value() + public async Task Cannot_filter_equality_on_incompatible_values() { // Arrange var resource = new FilterableResource @@ -252,7 +318,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - const string route = "/filterableResources?filter=equals(someInt32,'ABC')"; + var parameterValue = new MarkedText("equals(someInt32,^'ABC')", '^'); + string route = $"/filterableResources?filter={parameterValue.Text}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -260,13 +327,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Query creation failed due to incompatible types."); - error.Detail.Should().Be("Failed to convert 'ABC' of type 'String' to type 'Int32'."); - error.Source.Should().BeNull(); + error.Title.Should().Be("The specified filter is invalid."); + error.Detail.Should().Be($"Failed to convert 'ABC' of type 'String' to type 'Int32'. {parameterValue}"); + error.Source.Should().NotBeNull(); + error.Source.Parameter.Should().Be("filter"); } [Theory] @@ -280,6 +348,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => [InlineData(nameof(FilterableResource.SomeNullableDateTime))] [InlineData(nameof(FilterableResource.SomeNullableDateTimeOffset))] [InlineData(nameof(FilterableResource.SomeNullableTimeSpan))] + [InlineData(nameof(FilterableResource.SomeNullableDateOnly))] + [InlineData(nameof(FilterableResource.SomeNullableTimeOnly))] [InlineData(nameof(FilterableResource.SomeNullableEnum))] public async Task Can_filter_is_null_on_type(string propertyName) { @@ -300,6 +370,8 @@ public async Task Can_filter_is_null_on_type(string propertyName) SomeNullableDateTime = 1.January(2001).AsUtc(), SomeNullableDateTimeOffset = 1.January(2001).AsUtc(), SomeNullableTimeSpan = TimeSpan.FromHours(1), + SomeNullableDateOnly = DateOnly.FromDateTime(1.January(2001)), + SomeNullableTimeOnly = new TimeOnly(1, 0), SomeNullableEnum = DayOfWeek.Friday }; @@ -319,8 +391,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey(attributeName).With(value => value.Should().BeNull()); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey(attributeName).WhoseValue.Should().BeNull(); } [Theory] @@ -334,6 +406,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => [InlineData(nameof(FilterableResource.SomeNullableDateTime))] [InlineData(nameof(FilterableResource.SomeNullableDateTimeOffset))] [InlineData(nameof(FilterableResource.SomeNullableTimeSpan))] + [InlineData(nameof(FilterableResource.SomeNullableDateOnly))] + [InlineData(nameof(FilterableResource.SomeNullableTimeOnly))] [InlineData(nameof(FilterableResource.SomeNullableEnum))] public async Task Can_filter_is_not_null_on_type(string propertyName) { @@ -350,6 +424,8 @@ public async Task Can_filter_is_not_null_on_type(string propertyName) SomeNullableDateTime = 1.January(2001).AsUtc(), SomeNullableDateTimeOffset = 1.January(2001).AsUtc(), SomeNullableTimeSpan = TimeSpan.FromHours(1), + SomeNullableDateOnly = DateOnly.FromDateTime(1.January(2001)), + SomeNullableTimeOnly = new TimeOnly(1, 0), SomeNullableEnum = DayOfWeek.Friday }; @@ -369,7 +445,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey(attributeName).With(value => value.Should().NotBeNull()); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey(attributeName).WhoseValue.Should().NotBeNull(); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDbContext.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDbContext.cs index 356e537..0d02f23 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDbContext.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDbContext.cs @@ -5,12 +5,8 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings.Filtering; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -public sealed class FilterDbContext : MongoDbContextShim +public sealed class FilterDbContext(IMongoDatabase database) + : MongoDbContextShim(database) { public MongoDbSetShim FilterableResources => Set(); - - public FilterDbContext(IMongoDatabase database) - : base(database) - { - } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs index 888cd6c..144fda5 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs @@ -15,6 +15,8 @@ public FilterDepthTests(IntegrationTestContext(); testContext.UseController(); } @@ -23,7 +25,7 @@ public FilterDepthTests(IntegrationTestContext posts = _fakers.BlogPost.Generate(2); + List posts = _fakers.BlogPost.GenerateList(2); posts[0].Caption = "One"; posts[1].Caption = "Two"; @@ -42,7 +44,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); } @@ -58,7 +60,7 @@ public async Task Cannot_filter_on_ManyToOne_relationship() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -79,7 +81,7 @@ public async Task Cannot_filter_on_OneToMany_relationship() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -100,7 +102,7 @@ public async Task Cannot_filter_on_ManyToMany_relationship() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs index 60e10e7..e14889a 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs @@ -33,12 +33,26 @@ public sealed class FilterOperatorTests : IClassFixture _testContext; public FilterOperatorTests(IntegrationTestContext testContext) { _testContext = testContext; + testContext.UseResourceTypesInNamespace(typeof(FilterableResource).Namespace); + testContext.UseController(); } @@ -66,8 +80,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someString").With(value => value.Should().Be(resource.SomeString)); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someString").WhoseValue.Should().Be(resource.SomeString); + + responseDocument.Links.Should().NotBeNull(); + responseDocument.Links.Self.Should().Be("http://localhost/filterableResources?filter=equals(someString,'This%2c+that+%26+more+%2b+some')"); + responseDocument.Links.First.Should().Be("http://localhost/filterableResources?filter=equals(someString,%27This,%20that%20%26%20more%20%2B%20some%27)"); } [Fact] @@ -82,12 +100,13 @@ public async Task Cannot_filter_equality_on_two_attributes() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Comparing attributes against each other is not supported when using MongoDB."); error.Detail.Should().BeNull(); + error.Source.Should().BeNull(); } [Theory] @@ -128,8 +147,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someInt32").With(value => value.Should().Be(resource.SomeInt32)); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someInt32").WhoseValue.Should().Be(resource.SomeInt32); } [Theory] @@ -170,8 +189,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someDouble").With(value => value.Should().Be(resource.SomeDouble)); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someDouble").WhoseValue.Should().Be(resource.SomeDouble); } [Theory] @@ -220,10 +239,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someDateTimeInUtcZone") - .With(value => value.Should().Be(resource.SomeDateTimeInUtcZone)); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someDateTimeInUtcZone").WhoseValue.Should().Be(resource.SomeDateTimeInUtcZone); } [Theory] @@ -272,9 +290,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someDateTimeOffset").With(value => value.Should().Be(resource.SomeDateTimeOffset)); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someDateTimeOffset").WhoseValue.Should().Be(resource.SomeDateTimeOffset); } [Theory] @@ -314,8 +332,98 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someTimeSpan").With(value => value.Should().Be(resource.SomeTimeSpan)); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someTimeSpan").WhoseValue.Should().Be(resource.SomeTimeSpan); + } + + [Theory] + [InlineData(IsoDateOnlyLowerBound, IsoDateOnlyUpperBound, ComparisonOperator.LessThan, IsoDateOnlyInTheRange)] + [InlineData(IsoDateOnlyLowerBound, IsoDateOnlyUpperBound, ComparisonOperator.LessThan, IsoDateOnlyUpperBound)] + [InlineData(IsoDateOnlyLowerBound, IsoDateOnlyUpperBound, ComparisonOperator.LessOrEqual, IsoDateOnlyInTheRange)] + [InlineData(IsoDateOnlyLowerBound, IsoDateOnlyUpperBound, ComparisonOperator.LessOrEqual, IsoDateOnlyLowerBound)] + [InlineData(IsoDateOnlyUpperBound, IsoDateOnlyLowerBound, ComparisonOperator.GreaterThan, IsoDateOnlyInTheRange)] + [InlineData(IsoDateOnlyUpperBound, IsoDateOnlyLowerBound, ComparisonOperator.GreaterThan, IsoDateOnlyLowerBound)] + [InlineData(IsoDateOnlyUpperBound, IsoDateOnlyLowerBound, ComparisonOperator.GreaterOrEqual, IsoDateOnlyInTheRange)] + [InlineData(IsoDateOnlyUpperBound, IsoDateOnlyLowerBound, ComparisonOperator.GreaterOrEqual, IsoDateOnlyUpperBound)] + [InlineData(InvariantDateOnlyLowerBound, InvariantDateOnlyUpperBound, ComparisonOperator.LessThan, InvariantDateOnlyInTheRange)] + [InlineData(InvariantDateOnlyLowerBound, InvariantDateOnlyUpperBound, ComparisonOperator.LessThan, InvariantDateOnlyUpperBound)] + [InlineData(InvariantDateOnlyLowerBound, InvariantDateOnlyUpperBound, ComparisonOperator.LessOrEqual, InvariantDateOnlyInTheRange)] + [InlineData(InvariantDateOnlyLowerBound, InvariantDateOnlyUpperBound, ComparisonOperator.LessOrEqual, InvariantDateOnlyLowerBound)] + [InlineData(InvariantDateOnlyUpperBound, InvariantDateOnlyLowerBound, ComparisonOperator.GreaterThan, InvariantDateOnlyInTheRange)] + [InlineData(InvariantDateOnlyUpperBound, InvariantDateOnlyLowerBound, ComparisonOperator.GreaterThan, InvariantDateOnlyLowerBound)] + [InlineData(InvariantDateOnlyUpperBound, InvariantDateOnlyLowerBound, ComparisonOperator.GreaterOrEqual, InvariantDateOnlyInTheRange)] + [InlineData(InvariantDateOnlyUpperBound, InvariantDateOnlyLowerBound, ComparisonOperator.GreaterOrEqual, InvariantDateOnlyUpperBound)] + public async Task Can_filter_comparison_on_DateOnly(string matchingValue, string nonMatchingValue, ComparisonOperator filterOperator, string filterValue) + { + // Arrange + var resource = new FilterableResource + { + SomeDateOnly = DateOnly.Parse(matchingValue, CultureInfo.InvariantCulture) + }; + + var otherResource = new FilterableResource + { + SomeDateOnly = DateOnly.Parse(nonMatchingValue, CultureInfo.InvariantCulture) + }; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.FilterableResources.AddRange(resource, otherResource); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someDateOnly,'{filterValue}')"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someDateOnly").WhoseValue.Should().Be(resource.SomeDateOnly); + } + + [Theory] + [InlineData(TimeOnlyLowerBound, TimeOnlyUpperBound, ComparisonOperator.LessThan, TimeOnlyInTheRange)] + [InlineData(TimeOnlyLowerBound, TimeOnlyUpperBound, ComparisonOperator.LessThan, TimeOnlyUpperBound)] + [InlineData(TimeOnlyLowerBound, TimeOnlyUpperBound, ComparisonOperator.LessOrEqual, TimeOnlyInTheRange)] + [InlineData(TimeOnlyLowerBound, TimeOnlyUpperBound, ComparisonOperator.LessOrEqual, TimeOnlyLowerBound)] + [InlineData(TimeOnlyUpperBound, TimeOnlyLowerBound, ComparisonOperator.GreaterThan, TimeOnlyInTheRange)] + [InlineData(TimeOnlyUpperBound, TimeOnlyLowerBound, ComparisonOperator.GreaterThan, TimeOnlyLowerBound)] + [InlineData(TimeOnlyUpperBound, TimeOnlyLowerBound, ComparisonOperator.GreaterOrEqual, TimeOnlyInTheRange)] + [InlineData(TimeOnlyUpperBound, TimeOnlyLowerBound, ComparisonOperator.GreaterOrEqual, TimeOnlyUpperBound)] + public async Task Can_filter_comparison_on_TimeOnly(string matchingValue, string nonMatchingValue, ComparisonOperator filterOperator, string filterValue) + { + // Arrange + var resource = new FilterableResource + { + SomeTimeOnly = TimeOnly.Parse(matchingValue, CultureInfo.InvariantCulture) + }; + + var otherResource = new FilterableResource + { + SomeTimeOnly = TimeOnly.Parse(nonMatchingValue, CultureInfo.InvariantCulture) + }; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.FilterableResources.AddRange(resource, otherResource); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/filterableResources?filter={filterOperator.ToString().Camelize()}(someTimeOnly,'{filterValue}')"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someTimeOnly").WhoseValue.Should().Be(resource.SomeTimeOnly); } [Theory] @@ -352,8 +460,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someString").With(value => value.Should().Be(resource.SomeString)); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someString").WhoseValue.Should().Be(resource.SomeString); } [Theory] @@ -388,8 +496,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("someString").With(value => value.Should().Be(resource.SomeString)); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("someString").WhoseValue.Should().Be(resource.SomeString); } [Fact] @@ -404,7 +512,7 @@ public async Task Cannot_filter_on_has() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -425,7 +533,7 @@ public async Task Cannot_filter_on_has_with_nested_condition() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -446,7 +554,7 @@ public async Task Cannot_filter_on_count() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -492,7 +600,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(resource1.StringId); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs index 9be84ed..d572e4d 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs @@ -15,6 +15,8 @@ public FilterTests(IntegrationTestContext { _testContext = testContext; + testContext.UseResourceTypesInNamespace(typeof(Blog).Namespace); + testContext.UseController(); } @@ -22,7 +24,7 @@ public FilterTests(IntegrationTestContext public async Task Can_filter_on_ID() { // Arrange - List accounts = _fakers.WebAccount.Generate(2); + List accounts = _fakers.WebAccount.GenerateList(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -39,8 +41,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[0].StringId); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("userName").With(value => value.Should().Be(accounts[0].UserName)); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("userName").WhoseValue.Should().Be(accounts[0].UserName); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterableResource.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterableResource.cs index 17ec845..6554ce6 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterableResource.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Filtering/FilterableResource.cs @@ -79,12 +79,28 @@ public sealed class FilterableResource : HexStringMongoIdentifiable [Attr] public TimeSpan? SomeNullableTimeSpan { get; set; } + [Attr] + public DateOnly SomeDateOnly { get; set; } + + [Attr] + public DateOnly? SomeNullableDateOnly { get; set; } + + [Attr] + public TimeOnly SomeTimeOnly { get; set; } + + [Attr] + public TimeOnly? SomeNullableTimeOnly { get; set; } + [Attr] public DayOfWeek SomeEnum { get; set; } [Attr] public DayOfWeek? SomeNullableEnum { get; set; } + [HasOne] + [BsonIgnore] + public FilterableResource? Parent { get; set; } + [HasMany] [BsonIgnore] public ICollection Children { get; set; } = new List(); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs index 63c5c1f..e72fbfc 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs @@ -14,6 +14,8 @@ public IncludeTests(IntegrationTestContext(); } @@ -29,7 +31,7 @@ public async Task Cannot_include_in_primary_resources() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Label.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Label.cs index 5c267fb..1cc8ce3 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Label.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Label.cs @@ -6,6 +6,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings; [UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")] public sealed class Label : HexStringMongoIdentifiable { [Attr] diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/LoginAttempt.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/LoginAttempt.cs index 5e06cea..412b964 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/LoginAttempt.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/LoginAttempt.cs @@ -5,6 +5,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings; [UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings")] public sealed class LoginAttempt : HexStringMongoIdentifiable { [Attr] diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs index cb54c88..03567be 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs @@ -19,6 +19,8 @@ public PaginationWithTotalCountTests(IntegrationTestContext(); var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); @@ -29,7 +31,7 @@ public PaginationWithTotalCountTests(IntegrationTestContext posts = _fakers.BlogPost.Generate(2); + List posts = _fakers.BlogPost.GenerateList(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -46,10 +48,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); - responseDocument.Links.ShouldNotBeNull(); + responseDocument.Links.Should().NotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts?page%5Bsize%5D=1"); responseDocument.Links.Last.Should().Be($"{HostPrefix}/blogPosts?page%5Bnumber%5D=2&page%5Bsize%5D=1"); @@ -64,7 +66,7 @@ public async Task Uses_default_page_number_and_size() var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); options.DefaultPageSize = new PageSize(2); - List posts = _fakers.BlogPost.Generate(3); + List posts = _fakers.BlogPost.GenerateList(3); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -81,11 +83,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Data.ManyValue[0].Id.Should().Be(posts[0].StringId); responseDocument.Data.ManyValue[1].Id.Should().Be(posts[1].StringId); - responseDocument.Links.ShouldNotBeNull(); + responseDocument.Links.Should().NotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.First.Should().Be(responseDocument.Links.Self); responseDocument.Links.Last.Should().Be($"{HostPrefix}{route}?page%5Bnumber%5D=2"); @@ -94,13 +96,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Returns_all_resources_when_paging_is_disabled() + public async Task Returns_all_resources_when_pagination_is_disabled() { // Arrange var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); options.DefaultPageSize = null; - List posts = _fakers.BlogPost.Generate(25); + List posts = _fakers.BlogPost.GenerateList(25); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -117,9 +119,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(25); + responseDocument.Data.ManyValue.Should().HaveCount(25); - responseDocument.Links.ShouldNotBeNull(); + responseDocument.Links.Should().NotBeNull(); responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs index 63ec7c6..2790fb8 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs @@ -15,6 +15,8 @@ public RangeValidationTests(IntegrationTestContext(); } @@ -22,7 +24,7 @@ public RangeValidationTests(IntegrationTestContext blogs = _fakers.Blog.Generate(3); + List blogs = _fakers.Blog.GenerateList(3); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs index 1a3489e..a80dda2 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/QueryStringDbContext.cs @@ -5,14 +5,10 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -public sealed class QueryStringDbContext : MongoDbContextShim +public sealed class QueryStringDbContext(IMongoDatabase database) + : MongoDbContextShim(database) { public MongoDbSetShim Blogs => Set(); public MongoDbSetShim Posts => Set(); public MongoDbSetShim Accounts => Set(); - - public QueryStringDbContext(IMongoDatabase database) - : base(database) - { - } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/QueryStringFakers.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/QueryStringFakers.cs index e0b6004..84e1eee 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/QueryStringFakers.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/QueryStringFakers.cs @@ -1,33 +1,30 @@ using Bogus; using TestBuildingBlocks; -// @formatter:wrap_chained_method_calls chop_always -// @formatter:keep_existing_linebreaks true +// @formatter:wrap_chained_method_calls chop_if_long +// @formatter:wrap_before_first_method_call true namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings; -internal sealed class QueryStringFakers : FakerContainer +internal sealed class QueryStringFakers { - private readonly Lazy> _lazyBlogFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(blog => blog.Title, faker => faker.Lorem.Word()) - .RuleFor(blog => blog.PlatformName, faker => faker.Company.CompanyName())); + private readonly Lazy> _lazyBlogFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(blog => blog.Title, faker => faker.Lorem.Word()) + .RuleFor(blog => blog.PlatformName, faker => faker.Company.CompanyName())); - private readonly Lazy> _lazyBlogPostFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(blogPost => blogPost.Caption, faker => faker.Lorem.Sentence()) - .RuleFor(blogPost => blogPost.Url, faker => faker.Internet.Url())); + private readonly Lazy> _lazyBlogPostFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(blogPost => blogPost.Caption, faker => faker.Lorem.Sentence()) + .RuleFor(blogPost => blogPost.Url, faker => faker.Internet.Url())); - private readonly Lazy> _lazyWebAccountFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(webAccount => webAccount.UserName, faker => faker.Person.UserName) - .RuleFor(webAccount => webAccount.Password, faker => faker.Internet.Password()) - .RuleFor(webAccount => webAccount.DisplayName, faker => faker.Person.FullName) - .RuleFor(webAccount => webAccount.DateOfBirth, faker => faker.Person.DateOfBirth.TruncateToWholeMilliseconds()) - .RuleFor(webAccount => webAccount.EmailAddress, faker => faker.Internet.Email())); + private readonly Lazy> _lazyWebAccountFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(webAccount => webAccount.UserName, faker => faker.Person.UserName) + .RuleFor(webAccount => webAccount.Password, faker => faker.Internet.Password()) + .RuleFor(webAccount => webAccount.DisplayName, faker => faker.Person.FullName) + .RuleFor(webAccount => webAccount.DateOfBirth, faker => faker.Person.DateOfBirth.TruncateToWholeMilliseconds()) + .RuleFor(webAccount => webAccount.EmailAddress, faker => faker.Internet.Email())); public Faker Blog => _lazyBlogFaker.Value; public Faker BlogPost => _lazyBlogPostFaker.Value; diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs index f0846b7..6c6fc94 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs @@ -15,6 +15,8 @@ public SortTests(IntegrationTestContext t { _testContext = testContext; + testContext.UseResourceTypesInNamespace(typeof(Blog).Namespace); + testContext.UseController(); testContext.UseController(); testContext.UseController(); @@ -24,7 +26,7 @@ public SortTests(IntegrationTestContext t public async Task Can_sort_in_primary_resources() { // Arrange - List posts = _fakers.BlogPost.Generate(3); + List posts = _fakers.BlogPost.GenerateList(3); posts[0].Caption = "B"; posts[1].Caption = "A"; posts[2].Caption = "C"; @@ -44,7 +46,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(3); + responseDocument.Data.ManyValue.Should().HaveCount(3); responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); responseDocument.Data.ManyValue[1].Id.Should().Be(posts[0].StringId); responseDocument.Data.ManyValue[2].Id.Should().Be(posts[2].StringId); @@ -62,7 +64,7 @@ public async Task Cannot_sort_on_OneToMany_relationship() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -83,7 +85,7 @@ public async Task Cannot_sort_on_ManyToMany_relationship() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -104,7 +106,7 @@ public async Task Cannot_sort_on_ManyToOne_relationship() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -117,7 +119,7 @@ public async Task Cannot_sort_on_ManyToOne_relationship() public async Task Can_sort_descending_by_ID() { // Arrange - List accounts = _fakers.WebAccount.Generate(3); + List accounts = _fakers.WebAccount.GenerateList(3); accounts[0].Id = "5ff752c4f7c9a9a8373991b2"; accounts[1].Id = "5ff752c3f7c9a9a8373991b1"; accounts[2].Id = "5ff752c2f7c9a9a8373991b0"; @@ -141,7 +143,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(3); + responseDocument.Data.ManyValue.Should().HaveCount(3); responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[1].StringId); responseDocument.Data.ManyValue[1].Id.Should().Be(accounts[2].StringId); responseDocument.Data.ManyValue[2].Id.Should().Be(accounts[0].StringId); @@ -151,7 +153,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Sorts_by_ID_if_none_specified() { // Arrange - List accounts = _fakers.WebAccount.Generate(4); + List accounts = _fakers.WebAccount.GenerateList(4); accounts[0].Id = "5ff8a7bcb2a9b83724282718"; accounts[1].Id = "5ff8a7bcb2a9b83724282717"; accounts[2].Id = "5ff8a7bbb2a9b83724282716"; @@ -172,7 +174,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(4); + responseDocument.Data.ManyValue.Should().HaveCount(4); responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[2].StringId); responseDocument.Data.ManyValue[1].Id.Should().Be(accounts[1].StringId); responseDocument.Data.ManyValue[2].Id.Should().Be(accounts[0].StringId); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/ResourceCaptureStore.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/ResourceCaptureStore.cs index 4eeea37..56e7ab9 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/ResourceCaptureStore.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/ResourceCaptureStore.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings.SparseFiel public sealed class ResourceCaptureStore { - internal List Resources { get; } = new(); + internal List Resources { get; } = []; internal void Add(IEnumerable resources) { diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs index 400ee49..80fc442 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/ResultCapturingRepository.cs @@ -2,6 +2,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Repositories; using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.QueryableBuilding; using JsonApiDotNetCore.Resources; namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings.SparseFieldSets; @@ -10,18 +11,15 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.QueryStrings.SparseFiel /// Enables sparse fieldset tests to verify which fields were (not) retrieved from the database. /// [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class ResultCapturingRepository : MongoRepository +public sealed class ResultCapturingRepository( + IMongoDataAccess mongoDataAccess, ITargetedFields targetedFields, IResourceGraph resourceGraph, IResourceFactory resourceFactory, + IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, IQueryableBuilder queryableBuilder, + ResourceCaptureStore captureStore) + : MongoRepository(mongoDataAccess, targetedFields, resourceGraph, resourceFactory, constraintProviders, resourceDefinitionAccessor, + queryableBuilder) where TResource : class, IIdentifiable { - private readonly ResourceCaptureStore _captureStore; - - public ResultCapturingRepository(IMongoDataAccess mongoDataAccess, ITargetedFields targetedFields, IResourceGraph resourceGraph, - IResourceFactory resourceFactory, IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, - ResourceCaptureStore captureStore) - : base(mongoDataAccess, targetedFields, resourceGraph, resourceFactory, constraintProviders, resourceDefinitionAccessor) - { - _captureStore = captureStore; - } + private readonly ResourceCaptureStore _captureStore = captureStore; public override async Task> GetAsync(QueryLayer queryLayer, CancellationToken cancellationToken) { diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs index b5b2344..8879c97 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs @@ -17,17 +17,19 @@ public SparseFieldSetTests(IntegrationTestContext(); testContext.UseController(); testContext.UseController(); - testContext.ConfigureServicesAfterStartup(services => + testContext.ConfigureServices(services => { - services.AddSingleton(); - services.AddResourceRepository>(); services.AddResourceRepository>(); services.AddResourceRepository>(); + + services.AddSingleton(); }); } @@ -43,7 +45,7 @@ public async Task Cannot_select_fields_with_relationship_in_primary_resources() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -59,7 +61,7 @@ public async Task Can_select_attribute_in_primary_resources() var store = _testContext.Factory.Services.GetRequiredService(); store.Clear(); - BlogPost post = _fakers.BlogPost.Generate(); + BlogPost post = _fakers.BlogPost.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -76,13 +78,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); - responseDocument.Data.ManyValue[0].Attributes.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("caption").With(value => value.Should().Be(post.Caption)); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("caption").WhoseValue.Should().Be(post.Caption); responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; postCaptured.Caption.Should().Be(post.Caption); postCaptured.Url.Should().BeNull(); } @@ -99,7 +101,7 @@ public async Task Cannot_select_relationship_in_primary_resources() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -115,7 +117,7 @@ public async Task Can_select_attribute_in_primary_resource_by_ID() var store = _testContext.Factory.Services.GetRequiredService(); store.Clear(); - BlogPost post = _fakers.BlogPost.Generate(); + BlogPost post = _fakers.BlogPost.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -131,13 +133,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldHaveCount(1); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("url").With(value => value.Should().Be(post.Url)); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("url").WhoseValue.Should().Be(post.Url); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; postCaptured.Url.Should().Be(post.Url); postCaptured.Caption.Should().BeNull(); } @@ -146,7 +148,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_select_fields_of_ManyToOne_relationship() { // Arrange - BlogPost post = _fakers.BlogPost.Generate(); + BlogPost post = _fakers.BlogPost.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -162,7 +164,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -175,7 +177,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_select_fields_of_OneToMany_relationship() { // Arrange - WebAccount account = _fakers.WebAccount.Generate(); + WebAccount account = _fakers.WebAccount.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -191,7 +193,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -204,7 +206,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_select_fields_of_ManyToMany_relationship() { // Arrange - BlogPost post = _fakers.BlogPost.Generate(); + BlogPost post = _fakers.BlogPost.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -220,7 +222,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -236,7 +238,7 @@ public async Task Can_select_ID() var store = _testContext.Factory.Services.GetRequiredService(); store.Clear(); - BlogPost post = _fakers.BlogPost.Generate(); + BlogPost post = _fakers.BlogPost.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -253,13 +255,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); - responseDocument.Data.ManyValue[0].Attributes.ShouldHaveCount(1); - responseDocument.Data.ManyValue[0].Attributes.ShouldContainKey("caption").With(value => value.Should().Be(post.Caption)); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes.Should().ContainKey("caption").WhoseValue.Should().Be(post.Caption); responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; postCaptured.Id.Should().Be(post.Id); postCaptured.Caption.Should().Be(post.Caption); postCaptured.Url.Should().BeNull(); @@ -272,7 +274,7 @@ public async Task Can_select_empty_fieldset() var store = _testContext.Factory.Services.GetRequiredService(); store.Clear(); - BlogPost post = _fakers.BlogPost.Generate(); + BlogPost post = _fakers.BlogPost.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -289,24 +291,24 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); responseDocument.Data.ManyValue[0].Attributes.Should().BeNull(); responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); - var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); + var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).Which; postCaptured.Id.Should().Be(post.Id); postCaptured.Url.Should().BeNull(); } [Fact] - public async Task Retrieves_all_properties_when_fieldset_contains_readonly_attribute() + public async Task Fetches_all_scalar_properties_when_fieldset_contains_readonly_attribute() { // Arrange var store = _testContext.Factory.Services.GetRequiredService(); store.Clear(); - Blog blog = _fakers.Blog.Generate(); + Blog blog = _fakers.Blog.GenerateOne(); blog.IsPublished = true; await _testContext.RunOnDatabaseAsync(async dbContext => @@ -323,13 +325,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldHaveCount(1); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("showAdvertisements").With(value => value.Should().Be(blog.ShowAdvertisements)); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("showAdvertisements").WhoseValue.Should().Be(blog.ShowAdvertisements); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); - var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).And.Subject.Single(); + var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).Which; blogCaptured.ShowAdvertisements.Should().Be(blog.ShowAdvertisements); blogCaptured.IsPublished.Should().Be(blog.IsPublished); blogCaptured.Title.Should().Be(blog.Title); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/WebAccount.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/WebAccount.cs index de3c673..e3c2e1c 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/WebAccount.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/QueryStrings/WebAccount.cs @@ -12,7 +12,7 @@ public sealed class WebAccount : HexStringMongoIdentifiable [Attr] public string UserName { get; set; } = null!; - [Attr(Capabilities = ~AttrCapabilities.AllowView)] + [Attr(Capabilities = AttrCapabilities.All & ~AttrCapabilities.AllowView)] public string Password { get; set; } = null!; [Attr] diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 571b318..36006e2 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -1,8 +1,6 @@ using System.Net; using FluentAssertions; -using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.Extensions.DependencyInjection; using TestBuildingBlocks; using Xunit; @@ -17,19 +15,18 @@ public CreateResourceTests(IntegrationTestContext(); testContext.UseController(); testContext.UseController(); - - var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); - options.AllowClientGeneratedIds = false; } [Fact] public async Task Can_create_resource_with_string_ID() { // Arrange - WorkItem newWorkItem = _fakers.WorkItem.Generate(); + WorkItem newWorkItem = _fakers.WorkItem.GenerateOne(); newWorkItem.DueAt = null; var requestBody = new @@ -52,13 +49,13 @@ public async Task Can_create_resource_with_string_ID() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("workItems"); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("description").With(value => value.Should().Be(newWorkItem.Description)); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("dueAt").With(value => value.Should().Be(newWorkItem.DueAt)); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("description").WhoseValue.Should().Be(newWorkItem.Description); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("dueAt").WhoseValue.Should().Be(newWorkItem.DueAt); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); - string newWorkItemId = responseDocument.Data.SingleValue.Id.ShouldNotBeNull(); + string newWorkItemId = responseDocument.Data.SingleValue.Id.Should().NotBeNull().And.Subject; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -93,7 +90,7 @@ public async Task Cannot_create_resource_with_int_ID() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.InternalServerError); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); @@ -127,13 +124,13 @@ public async Task Can_create_resource_without_attributes_or_relationships() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("workItems"); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("description").With(value => value.Should().BeNull()); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("dueAt").With(value => value.Should().BeNull()); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("description").WhoseValue.Should().BeNull(); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("dueAt").WhoseValue.Should().BeNull(); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); - string newWorkItemId = responseDocument.Data.SingleValue.Id.ShouldNotBeNull(); + string newWorkItemId = responseDocument.Data.SingleValue.Id.Should().NotBeNull().And.Subject; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -169,14 +166,14 @@ public async Task Cannot_create_resource_with_client_generated_ID() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.Forbidden); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Failed to deserialize request body: The use of client-generated IDs is disabled."); error.Detail.Should().BeNull(); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Pointer.Should().Be("/data/id"); - error.Meta.ShouldContainKey("requestBody").With(value => value.ShouldNotBeNull().ToString().ShouldNotBeEmpty()); + error.Meta.Should().HaveRequestBody(); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs index 06f0ef8..1f40db8 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs @@ -17,23 +17,22 @@ public CreateResourceWithClientGeneratedIdTests(IntegrationTestContext(); testContext.UseController(); - testContext.ConfigureServicesAfterStartup(services => - { - services.AddResourceDefinition(); - }); + testContext.ConfigureServices(services => services.AddResourceDefinition()); var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); - options.AllowClientGeneratedIds = true; + options.ClientIdGeneration = ClientIdGenerationMode.Required; } [Fact] public async Task Can_create_resource_with_client_generated_string_ID_having_side_effects() { // Arrange - WorkItemGroup newGroup = _fakers.WorkItemGroup.Generate(); + WorkItemGroup newGroup = _fakers.WorkItemGroup.GenerateOne(); newGroup.Id = "free-format-client-generated-id-1"; var requestBody = new @@ -57,12 +56,12 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_sid // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); - string groupName = $"{newGroup.Name}{ContainerTypeToHideFromAutoDiscovery.ImplicitlyChangingWorkItemGroupDefinition.Suffix}"; + string groupName = $"{newGroup.Name}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"; - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("workItemGroups"); responseDocument.Data.SingleValue.Id.Should().Be(newGroup.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("name").With(value => value.Should().Be(groupName)); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("name").WhoseValue.Should().Be(groupName); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -77,7 +76,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_resource_with_client_generated_string_ID_having_side_effects_with_fieldset() { // Arrange - WorkItemGroup newGroup = _fakers.WorkItemGroup.Generate(); + WorkItemGroup newGroup = _fakers.WorkItemGroup.GenerateOne(); newGroup.Id = "free-format-client-generated-id-2"; var requestBody = new @@ -101,13 +100,13 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_sid // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); - string groupName = $"{newGroup.Name}{ContainerTypeToHideFromAutoDiscovery.ImplicitlyChangingWorkItemGroupDefinition.Suffix}"; + string groupName = $"{newGroup.Name}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"; - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("workItemGroups"); responseDocument.Data.SingleValue.Id.Should().Be(newGroup.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldHaveCount(1); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("name").With(value => value.Should().Be(groupName)); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("name").WhoseValue.Should().Be(groupName); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -122,7 +121,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_resource_with_client_generated_string_ID_having_no_side_effects() { // Arrange - RgbColor newColor = _fakers.RgbColor.Generate(); + RgbColor newColor = _fakers.RgbColor.GenerateOne(); newColor.Id = "free-format-client-generated-id-3"; var requestBody = new @@ -160,7 +159,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_resource_with_client_generated_string_ID_having_no_side_effects_with_fieldset() { // Arrange - RgbColor newColor = _fakers.RgbColor.Generate(); + RgbColor newColor = _fakers.RgbColor.GenerateOne(); newColor.Id = "free-format-client-generated-id-4"; var requestBody = new @@ -198,10 +197,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_resource_for_existing_client_generated_ID() { // Arrange - RgbColor existingColor = _fakers.RgbColor.Generate(); + RgbColor existingColor = _fakers.RgbColor.GenerateOne(); existingColor.Id = "free-format-client-generated-id-5"; - string newDisplayName = _fakers.RgbColor.Generate().DisplayName; + string newDisplayName = _fakers.RgbColor.GenerateOne().DisplayName; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -230,7 +229,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.Conflict); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index 626547c..c560e56 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -15,6 +15,8 @@ public CreateResourceWithToManyRelationshipTests(IntegrationTestContext(); } @@ -22,7 +24,7 @@ public CreateResourceWithToManyRelationshipTests(IntegrationTestContext { @@ -60,7 +62,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index 337d48a..12d5711 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -15,6 +15,8 @@ public CreateResourceWithToOneRelationshipTests(IntegrationTestContext(); } @@ -22,10 +24,10 @@ public CreateResourceWithToOneRelationshipTests(IntegrationTestContext { @@ -65,7 +67,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs index 99cac72..70e3307 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -15,6 +15,8 @@ public DeleteResourceTests(IntegrationTestContext(); } @@ -22,7 +24,7 @@ public DeleteResourceTests(IntegrationTestContext { @@ -42,9 +44,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - WorkItem? workItemsInDatabase = await dbContext.WorkItems.FirstWithIdOrDefaultAsync(existingWorkItem.Id); + WorkItem? workItemInDatabase = await dbContext.WorkItems.FirstWithIdOrDefaultAsync(existingWorkItem.Id); - workItemsInDatabase.Should().BeNull(); + workItemInDatabase.Should().BeNull(); }); } @@ -62,7 +64,7 @@ public async Task Cannot_delete_unknown_resource() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs index 884349d..af5dee5 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs @@ -15,6 +15,8 @@ public FetchRelationshipTests(IntegrationTestContext(); testContext.UseController(); } @@ -22,7 +24,7 @@ public FetchRelationshipTests(IntegrationTestContext { @@ -38,7 +40,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -51,7 +53,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_OneToMany_relationship() { // Arrange - UserAccount userAccount = _fakers.UserAccount.Generate(); + UserAccount userAccount = _fakers.UserAccount.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -67,7 +69,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -80,7 +82,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_ManyToMany_relationship() { // Arrange - WorkItem workItem = _fakers.WorkItem.Generate(); + WorkItem workItem = _fakers.WorkItem.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -96,7 +98,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index e254772..dc2aa26 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -15,6 +15,8 @@ public FetchResourceTests(IntegrationTestContext(); testContext.UseController(); } @@ -23,7 +25,7 @@ public FetchResourceTests(IntegrationTestContext workItems = _fakers.WorkItem.Generate(2); + List workItems = _fakers.WorkItem.GenerateList(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -40,20 +42,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); ResourceObject item1 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItems[0].StringId); item1.Type.Should().Be("workItems"); - item1.Attributes.ShouldContainKey("description").With(value => value.Should().Be(workItems[0].Description)); - item1.Attributes.ShouldContainKey("dueAt").With(value => value.Should().Be(workItems[0].DueAt)); - item1.Attributes.ShouldContainKey("priority").With(value => value.Should().Be(workItems[0].Priority)); + item1.Attributes.Should().ContainKey("description").WhoseValue.Should().Be(workItems[0].Description); + item1.Attributes.Should().ContainKey("dueAt").WhoseValue.Should().Be(workItems[0].DueAt); + item1.Attributes.Should().ContainKey("priority").WhoseValue.Should().Be(workItems[0].Priority); item1.Relationships.Should().BeNull(); ResourceObject item2 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItems[1].StringId); item2.Type.Should().Be("workItems"); - item2.Attributes.ShouldContainKey("description").With(value => value.Should().Be(workItems[1].Description)); - item2.Attributes.ShouldContainKey("dueAt").With(value => value.Should().Be(workItems[1].DueAt)); - item2.Attributes.ShouldContainKey("priority").With(value => value.Should().Be(workItems[1].Priority)); + item2.Attributes.Should().ContainKey("description").WhoseValue.Should().Be(workItems[1].Description); + item2.Attributes.Should().ContainKey("dueAt").WhoseValue.Should().Be(workItems[1].DueAt); + item2.Attributes.Should().ContainKey("priority").WhoseValue.Should().Be(workItems[1].Priority); item2.Relationships.Should().BeNull(); } @@ -61,7 +63,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_get_primary_resource_by_ID() { // Arrange - WorkItem workItem = _fakers.WorkItem.Generate(); + WorkItem workItem = _fakers.WorkItem.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -77,12 +79,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("workItems"); responseDocument.Data.SingleValue.Id.Should().Be(workItem.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("description").With(value => value.Should().Be(workItem.Description)); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("dueAt").With(value => value.Should().Be(workItem.DueAt)); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("priority").With(value => value.Should().Be(workItem.Priority)); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("description").WhoseValue.Should().Be(workItem.Description); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("dueAt").WhoseValue.Should().Be(workItem.DueAt); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("priority").WhoseValue.Should().Be(workItem.Priority); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } @@ -100,7 +102,7 @@ public async Task Cannot_get_primary_resource_for_unknown_ID() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); @@ -112,7 +114,7 @@ public async Task Cannot_get_primary_resource_for_unknown_ID() public async Task Cannot_get_secondary_ManyToOne_resource() { // Arrange - WorkItem workItem = _fakers.WorkItem.Generate(); + WorkItem workItem = _fakers.WorkItem.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -128,7 +130,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -141,7 +143,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_secondary_OneToMany_resources() { // Arrange - UserAccount userAccount = _fakers.UserAccount.Generate(); + UserAccount userAccount = _fakers.UserAccount.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -157,7 +159,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -170,7 +172,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_secondary_ManyToMany_resources() { // Arrange - WorkItem workItem = _fakers.WorkItem.Generate(); + WorkItem workItem = _fakers.WorkItem.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -186,7 +188,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs index 4e9f358..4c610bf 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs @@ -6,37 +6,28 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.ReadWrite; -[UsedImplicitly] -internal sealed partial class ContainerTypeToHideFromAutoDiscovery +/// +/// Used to simulate side effects that occur in the database while saving, typically caused by database triggers. +/// +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public sealed class ImplicitlyChangingWorkItemDefinition(IResourceGraph resourceGraph, ReadWriteDbContext dbContext) + : JsonApiResourceDefinition(resourceGraph) { - /// - /// Used to simulate side effects that occur in the database while saving, typically caused by database triggers. - /// - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class ImplicitlyChangingWorkItemDefinition : JsonApiResourceDefinition - { - internal const string Suffix = " (changed)"; + internal const string Suffix = " (changed)"; - private readonly ReadWriteDbContext _dbContext; + private readonly ReadWriteDbContext _dbContext = dbContext; - public ImplicitlyChangingWorkItemDefinition(IResourceGraph resourceGraph, ReadWriteDbContext dbContext) - : base(resourceGraph) - { - _dbContext = dbContext; - } - - public override async Task OnWriteSucceededAsync(WorkItem resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + public override async Task OnWriteSucceededAsync(WorkItem resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + { + if (writeOperation is not WriteOperationKind.DeleteResource) { - if (writeOperation is not WriteOperationKind.DeleteResource) + await _dbContext.WorkItems.ExecuteAsync(async collection => { - await _dbContext.WorkItems.ExecuteAsync(async collection => - { - resource.Description += Suffix; + resource.Description += Suffix; - FilterDefinition filter = Builders.Filter.Eq(item => item.Id, resource.Id); - await collection.ReplaceOneAsync(filter, resource, cancellationToken: cancellationToken); - }); - } + FilterDefinition filter = Builders.Filter.Eq(item => item.Id, resource.Id); + await collection.ReplaceOneAsync(filter, resource, cancellationToken: cancellationToken); + }); } } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs index e9abd46..570d745 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs @@ -6,36 +6,28 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.ReadWrite; -internal sealed partial class ContainerTypeToHideFromAutoDiscovery +/// +/// Used to simulate side effects that occur in the database while saving, typically caused by database triggers. +/// +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public sealed class ImplicitlyChangingWorkItemGroupDefinition(IResourceGraph resourceGraph, ReadWriteDbContext dbContext) + : JsonApiResourceDefinition(resourceGraph) { - /// - /// Used to simulate side effects that occur in the database while saving, typically caused by database triggers. - /// - [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class ImplicitlyChangingWorkItemGroupDefinition : JsonApiResourceDefinition - { - internal const string Suffix = " (changed)"; + internal const string Suffix = " (changed)"; - private readonly ReadWriteDbContext _dbContext; + private readonly ReadWriteDbContext _dbContext = dbContext; - public ImplicitlyChangingWorkItemGroupDefinition(IResourceGraph resourceGraph, ReadWriteDbContext dbContext) - : base(resourceGraph) - { - _dbContext = dbContext; - } - - public override async Task OnWriteSucceededAsync(WorkItemGroup resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + public override async Task OnWriteSucceededAsync(WorkItemGroup resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + { + if (writeOperation is not WriteOperationKind.DeleteResource) { - if (writeOperation is not WriteOperationKind.DeleteResource) + await _dbContext.Groups.ExecuteAsync(async collection => { - await _dbContext.Groups.ExecuteAsync(async collection => - { - resource.Name += Suffix; + resource.Name += Suffix; - FilterDefinition filter = Builders.Filter.Eq(group => group.Id, resource.Id); - await collection.ReplaceOneAsync(filter, resource, cancellationToken: cancellationToken); - }); - } + FilterDefinition filter = Builders.Filter.Eq(group => group.Id, resource.Id); + await collection.ReplaceOneAsync(filter, resource, cancellationToken: cancellationToken); + }); } } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ReadWriteDbContext.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ReadWriteDbContext.cs index f3cfd72..6cd2ad0 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ReadWriteDbContext.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ReadWriteDbContext.cs @@ -5,16 +5,12 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.ReadWrite; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -public sealed class ReadWriteDbContext : MongoDbContextShim +public sealed class ReadWriteDbContext(IMongoDatabase database) + : MongoDbContextShim(database) { public MongoDbSetShim WorkItems => Set(); public MongoDbSetShim WorkTags => Set(); public MongoDbSetShim Groups => Set(); public MongoDbSetShim RgbColors => Set(); public MongoDbSetShim UserAccounts => Set(); - - public ReadWriteDbContext(IMongoDatabase database) - : base(database) - { - } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ReadWriteFakers.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ReadWriteFakers.cs index 6742754..0c84e48 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ReadWriteFakers.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/ReadWriteFakers.cs @@ -2,45 +2,38 @@ using MongoDB.Bson; using TestBuildingBlocks; -// @formatter:wrap_chained_method_calls chop_always -// @formatter:keep_existing_linebreaks true +// @formatter:wrap_chained_method_calls chop_if_long +// @formatter:wrap_before_first_method_call true namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.ReadWrite; -internal sealed class ReadWriteFakers : FakerContainer +internal sealed class ReadWriteFakers { - private readonly Lazy> _lazyWorkItemFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(workItem => workItem.Description, faker => faker.Lorem.Sentence()) - .RuleFor(workItem => workItem.DueAt, faker => faker.Date.Future() - .TruncateToWholeMilliseconds()) - .RuleFor(workItem => workItem.Priority, faker => faker.PickRandom())); - - private readonly Lazy> _lazyWorkTagFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(workTag => workTag.Text, faker => faker.Lorem.Word()) - .RuleFor(workTag => workTag.IsBuiltIn, faker => faker.Random.Bool())); - - private readonly Lazy> _lazyUserAccountFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(userAccount => userAccount.FirstName, faker => faker.Name.FirstName()) - .RuleFor(userAccount => userAccount.LastName, faker => faker.Name.LastName())); - - private readonly Lazy> _lazyWorkItemGroupFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(group => group.Name, faker => faker.Lorem.Word()) - .RuleFor(group => group.IsPublic, faker => faker.Random.Bool())); - - private readonly Lazy> _lazyRgbColorFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(color => color.Id, faker => ObjectId.GenerateNewId(faker.Date.Past()) - .ToString()) - .RuleFor(color => color.DisplayName, faker => faker.Commerce.Color())); + private readonly Lazy> _lazyWorkItemFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(workItem => workItem.Description, faker => faker.Lorem.Sentence()) + .RuleFor(workItem => workItem.DueAt, faker => faker.Date.Future().TruncateToWholeMilliseconds()) + .RuleFor(workItem => workItem.Priority, faker => faker.PickRandom())); + + private readonly Lazy> _lazyWorkTagFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(workTag => workTag.Text, faker => faker.Lorem.Word()) + .RuleFor(workTag => workTag.IsBuiltIn, faker => faker.Random.Bool())); + + private readonly Lazy> _lazyUserAccountFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(userAccount => userAccount.FirstName, faker => faker.Name.FirstName()) + .RuleFor(userAccount => userAccount.LastName, faker => faker.Name.LastName())); + + private readonly Lazy> _lazyWorkItemGroupFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(group => group.Name, faker => faker.Lorem.Word()) + .RuleFor(group => group.IsPublic, faker => faker.Random.Bool())); + + private readonly Lazy> _lazyRgbColorFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(color => color.Id, faker => ObjectId.GenerateNewId(faker.Date.Past()).ToString()) + .RuleFor(color => color.DisplayName, faker => faker.Commerce.Color())); public Faker WorkItem => _lazyWorkItemFaker.Value; public Faker WorkTag => _lazyWorkTagFaker.Value; diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index 7487ca4..0e60f81 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -15,6 +15,8 @@ public AddToToManyRelationshipTests(IntegrationTestContext(); } @@ -22,8 +24,8 @@ public AddToToManyRelationshipTests(IntegrationTestContext { @@ -52,7 +54,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -65,8 +67,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_add_to_ManyToMany_relationship() { // Arrange - WorkItem existingWorkItem = _fakers.WorkItem.Generate(); - WorkTag existingTag = _fakers.WorkTag.Generate(); + WorkItem existingWorkItem = _fakers.WorkItem.GenerateOne(); + WorkTag existingTag = _fakers.WorkTag.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -95,7 +97,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index 0de1b7b..aeb987d 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -15,6 +15,8 @@ public RemoveFromToManyRelationshipTests(IntegrationTestContext(); } @@ -22,8 +24,8 @@ public RemoveFromToManyRelationshipTests(IntegrationTestContext { @@ -52,7 +54,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -65,8 +67,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_remove_from_ManyToMany_relationship() { // Arrange - WorkItem existingWorkItem = _fakers.WorkItem.Generate(); - existingWorkItem.Tags = _fakers.WorkTag.Generate(1).ToHashSet(); + WorkItem existingWorkItem = _fakers.WorkItem.GenerateOne(); + existingWorkItem.Tags = _fakers.WorkTag.GenerateSet(1); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -95,7 +97,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index 2c78ccd..7d22d4d 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -15,6 +15,8 @@ public ReplaceToManyRelationshipTests(IntegrationTestContext(); } @@ -22,8 +24,8 @@ public ReplaceToManyRelationshipTests(IntegrationTestContext { @@ -52,7 +54,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -65,8 +67,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_replace_ManyToMany_relationship() { // Arrange - WorkItem existingWorkItem = _fakers.WorkItem.Generate(); - WorkTag existingTag = _fakers.WorkTag.Generate(); + WorkItem existingWorkItem = _fakers.WorkItem.GenerateOne(); + WorkTag existingTag = _fakers.WorkTag.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -95,7 +97,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index 0e76c43..545ff89 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -15,6 +15,8 @@ public UpdateToOneRelationshipTests(IntegrationTestContext(); } @@ -22,8 +24,8 @@ public UpdateToOneRelationshipTests(IntegrationTestContext { @@ -49,7 +51,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index ed39b18..005c180 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -15,6 +15,8 @@ public ReplaceToManyRelationshipTests(IntegrationTestContext(); } @@ -22,8 +24,8 @@ public ReplaceToManyRelationshipTests(IntegrationTestContext { @@ -63,7 +65,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); @@ -76,8 +78,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_replace_ManyToMany_relationship() { // Arrange - WorkItem existingWorkItem = _fakers.WorkItem.Generate(); - WorkTag existingTag = _fakers.WorkTag.Generate(); + WorkItem existingWorkItem = _fakers.WorkItem.GenerateOne(); + WorkTag existingTag = _fakers.WorkTag.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -117,7 +119,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index 4faf659..123fa48 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -16,15 +16,17 @@ public UpdateResourceTests(IntegrationTestContext(); testContext.UseController(); testContext.UseController(); testContext.UseController(); - testContext.ConfigureServicesAfterStartup(services => + testContext.ConfigureServices(services => { - services.AddResourceDefinition(); - services.AddResourceDefinition(); + services.AddResourceDefinition(); + services.AddResourceDefinition(); }); } @@ -32,7 +34,7 @@ public UpdateResourceTests(IntegrationTestContext { @@ -78,8 +80,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_partially_update_resource_with_string_ID() { // Arrange - WorkItemGroup existingGroup = _fakers.WorkItemGroup.Generate(); - string newName = _fakers.WorkItemGroup.Generate().Name; + WorkItemGroup existingGroup = _fakers.WorkItemGroup.GenerateOne(); + string newName = _fakers.WorkItemGroup.GenerateOne().Name; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -108,13 +110,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - string groupName = $"{newName}{ContainerTypeToHideFromAutoDiscovery.ImplicitlyChangingWorkItemGroupDefinition.Suffix}"; + string groupName = $"{newName}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"; - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("workItemGroups"); responseDocument.Data.SingleValue.Id.Should().Be(existingGroup.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("name").With(value => value.Should().Be(groupName)); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("isPublic").With(value => value.Should().Be(existingGroup.IsPublic)); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("name").WhoseValue.Should().Be(groupName); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("isPublic").WhoseValue.Should().Be(existingGroup.IsPublic); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -130,8 +132,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_completely_update_resource_with_string_ID() { // Arrange - RgbColor existingColor = _fakers.RgbColor.Generate(); - string newDisplayName = _fakers.RgbColor.Generate().DisplayName; + RgbColor existingColor = _fakers.RgbColor.GenerateOne(); + string newDisplayName = _fakers.RgbColor.GenerateOne().DisplayName; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -174,8 +176,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_without_side_effects() { // Arrange - UserAccount existingUserAccount = _fakers.UserAccount.Generate(); - UserAccount newUserAccount = _fakers.UserAccount.Generate(); + UserAccount existingUserAccount = _fakers.UserAccount.GenerateOne(); + UserAccount newUserAccount = _fakers.UserAccount.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -220,8 +222,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_side_effects() { // Arrange - WorkItem existingWorkItem = _fakers.WorkItem.Generate(); - string newDescription = _fakers.WorkItem.Generate().Description!; + WorkItem existingWorkItem = _fakers.WorkItem.GenerateOne(); + string newDescription = _fakers.WorkItem.GenerateOne().Description!; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -251,15 +253,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - string itemDescription = $"{newDescription}{ContainerTypeToHideFromAutoDiscovery.ImplicitlyChangingWorkItemDefinition.Suffix}"; + string itemDescription = $"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"; - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("workItems"); responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("description").With(value => value.Should().Be(itemDescription)); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("dueAt").With(value => value.Should().BeNull()); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("priority").With(value => value.Should().Be(existingWorkItem.Priority)); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("isImportant").With(value => value.Should().Be(existingWorkItem.IsImportant)); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("description").WhoseValue.Should().Be(itemDescription); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("dueAt").WhoseValue.Should().BeNull(); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("priority").WhoseValue.Should().Be(existingWorkItem.Priority); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("isImportant").WhoseValue.Should().Be(existingWorkItem.IsImportant); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -276,8 +278,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_side_effects_with_primary_fieldset() { // Arrange - WorkItem existingWorkItem = _fakers.WorkItem.Generate(); - string newDescription = _fakers.WorkItem.Generate().Description!; + WorkItem existingWorkItem = _fakers.WorkItem.GenerateOne(); + string newDescription = _fakers.WorkItem.GenerateOne().Description!; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -307,14 +309,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - string itemDescription = $"{newDescription}{ContainerTypeToHideFromAutoDiscovery.ImplicitlyChangingWorkItemDefinition.Suffix}"; + string itemDescription = $"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"; - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Type.Should().Be("workItems"); responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldHaveCount(2); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("description").With(value => value.Should().Be(itemDescription)); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("priority").With(value => value.Should().Be(existingWorkItem.Priority)); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(2); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("description").WhoseValue.Should().Be(itemDescription); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("priority").WhoseValue.Should().Be(existingWorkItem.Priority); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -350,7 +352,7 @@ public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index 2a0faf2..1f14cc3 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -15,6 +15,8 @@ public UpdateToOneRelationshipTests(IntegrationTestContext(); } @@ -22,8 +24,8 @@ public UpdateToOneRelationshipTests(IntegrationTestContext { @@ -60,7 +62,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/WorkItem.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/WorkItem.cs index 4eecb06..bd13296 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/WorkItem.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/WorkItem.cs @@ -18,7 +18,7 @@ public sealed class WorkItem : HexStringMongoIdentifiable [Attr] public WorkItemPriority Priority { get; set; } - [Attr(Capabilities = ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))] + [Attr(Capabilities = AttrCapabilities.All & ~(AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange))] [BsonIgnore] public bool IsImportant { diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/WorkTag.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/WorkTag.cs index 48de6cf..bbd12e5 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/WorkTag.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ReadWrite/WorkTag.cs @@ -6,6 +6,7 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.ReadWrite; [UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "JsonApiDotNetCoreMongoDbTests.IntegrationTests.ReadWrite")] public sealed class WorkTag : HexStringMongoIdentifiable { [Attr] diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs index b2b8fa6..206db10 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs @@ -6,17 +6,11 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.ResourceDefinitions.Reading; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class MoonDefinition : HitCountingResourceDefinition +public sealed class MoonDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) + : HitCountingResourceDefinition(resourceGraph, hitCounter) { protected override ResourceDefinitionExtensibilityPoints ExtensibilityPointsToTrack => ResourceDefinitionExtensibilityPoints.Reading; - public MoonDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) - : base(resourceGraph, hitCounter) - { - // This constructor will be resolved from the container, which means - // you can take on any dependency that is also defined in the container. - } - public override QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters() { base.OnRegisterQueryableHandlersForQueryStringParameters(); @@ -29,7 +23,7 @@ public override QueryStringParameterHandlers OnRegisterQueryableHandlersFo private static IQueryable FilterByRadius(IQueryable source, StringValues parameterValue) { - bool isFilterOnLargerThan = bool.Parse(parameterValue); + bool isFilterOnLargerThan = bool.Parse(parameterValue.ToString()); return isFilterOnLargerThan ? source.Where(moon => moon.SolarRadius > 1m) : source.Where(moon => moon.SolarRadius <= 1m); } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs index 567fa04..c2122d6 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs @@ -6,21 +6,13 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.ResourceDefinitions.Reading; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class PlanetDefinition : HitCountingResourceDefinition +public sealed class PlanetDefinition(IResourceGraph resourceGraph, IClientSettingsProvider clientSettingsProvider, ResourceDefinitionHitCounter hitCounter) + : HitCountingResourceDefinition(resourceGraph, hitCounter) { - private readonly IClientSettingsProvider _clientSettingsProvider; + private readonly IClientSettingsProvider _clientSettingsProvider = clientSettingsProvider; protected override ResourceDefinitionExtensibilityPoints ExtensibilityPointsToTrack => ResourceDefinitionExtensibilityPoints.Reading; - public PlanetDefinition(IResourceGraph resourceGraph, IClientSettingsProvider clientSettingsProvider, ResourceDefinitionHitCounter hitCounter) - : base(resourceGraph, hitCounter) - { - // This constructor will be resolved from the container, which means - // you can take on any dependency that is also defined in the container. - - _clientSettingsProvider = clientSettingsProvider; - } - public override FilterExpression? OnApplyFilter(FilterExpression? existingFilter) { base.OnApplyFilter(existingFilter); diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs index 521da09..e05f114 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Text.Json; using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; @@ -18,12 +17,18 @@ public ResourceDefinitionReadTests(IntegrationTestContext(); testContext.UseController(); testContext.UseController(); - testContext.ConfigureServicesAfterStartup(services => + testContext.ConfigureServices(services => { + services.AddResourceDefinition(); + services.AddResourceDefinition(); + services.AddResourceDefinition(); + services.AddSingleton(); services.AddSingleton(); }); @@ -47,7 +52,7 @@ public async Task Filter_from_resource_definition_is_applied() var settingsProvider = (TestClientSettingsProvider)_testContext.Factory.Services.GetRequiredService(); settingsProvider.HidePlanetsWithPrivateName(); - List planets = _fakers.Planet.Generate(4); + List planets = _fakers.Planet.GenerateList(4); planets[0].PrivateName = "A"; planets[2].PrivateName = "B"; @@ -66,15 +71,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Data.ManyValue[0].Id.Should().Be(planets[1].StringId); responseDocument.Data.ManyValue[1].Id.Should().Be(planets[3].StringId); - responseDocument.Meta.ShouldContainKey("total").With(value => - { - JsonElement element = value.Should().BeOfType().Subject; - element.GetInt32().Should().Be(2); - }); + responseDocument.Meta.Should().ContainTotal(2); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -99,7 +100,7 @@ public async Task Filter_from_resource_definition_and_query_string_are_applied() var settingsProvider = (TestClientSettingsProvider)_testContext.Factory.Services.GetRequiredService(); settingsProvider.HidePlanetsWithPrivateName(); - List planets = _fakers.Planet.Generate(4); + List planets = _fakers.Planet.GenerateList(4); planets[0].HasRingSystem = true; planets[0].PrivateName = "A"; @@ -126,14 +127,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(planets[3].StringId); - responseDocument.Meta.ShouldContainKey("total").With(value => - { - JsonElement element = value.Should().BeOfType().Subject; - element.GetInt32().Should().Be(1); - }); + responseDocument.Meta.Should().ContainTotal(1); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -154,7 +151,7 @@ public async Task Sort_from_resource_definition_is_applied() // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - List stars = _fakers.Star.Generate(3); + List stars = _fakers.Star.GenerateList(3); stars[0].SolarMass = 500m; stars[0].SolarRadius = 1m; @@ -180,7 +177,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(3); + responseDocument.Data.ManyValue.Should().HaveCount(3); responseDocument.Data.ManyValue[0].Id.Should().Be(stars[1].StringId); responseDocument.Data.ManyValue[1].Id.Should().Be(stars[0].StringId); responseDocument.Data.ManyValue[2].Id.Should().Be(stars[2].StringId); @@ -206,7 +203,7 @@ public async Task Sort_from_query_string_is_applied() // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - List stars = _fakers.Star.Generate(3); + List stars = _fakers.Star.GenerateList(3); stars[0].Name = "B"; stars[0].SolarRadius = 10m; @@ -232,7 +229,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(3); + responseDocument.Data.ManyValue.Should().HaveCount(3); responseDocument.Data.ManyValue[0].Id.Should().Be(stars[2].StringId); responseDocument.Data.ManyValue[1].Id.Should().Be(stars[0].StringId); responseDocument.Data.ManyValue[2].Id.Should().Be(stars[1].StringId); @@ -258,7 +255,7 @@ public async Task Page_size_from_resource_definition_is_applied() // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - List stars = _fakers.Star.Generate(10); + List stars = _fakers.Star.GenerateList(10); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -275,7 +272,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(5); + responseDocument.Data.ManyValue.Should().HaveCount(5); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -300,7 +297,7 @@ public async Task Attribute_inclusion_from_resource_definition_is_applied_for_om // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - Star star = _fakers.Star.Generate(); + Star star = _fakers.Star.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -316,10 +313,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("name").With(value => value.Should().Be(star.Name)); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("kind").With(value => value.Should().Be(star.Kind)); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("name").WhoseValue.Should().Be(star.Name); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("kind").WhoseValue.Should().Be(star.Kind); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -340,7 +337,7 @@ public async Task Attribute_inclusion_from_resource_definition_is_applied_for_fi // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - Star star = _fakers.Star.Generate(); + Star star = _fakers.Star.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -356,11 +353,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldHaveCount(2); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("name").With(value => value.Should().Be(star.Name)); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("solarRadius").With(value => value.Should().Be(star.SolarRadius)); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(2); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("name").WhoseValue.Should().Be(star.Name); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("solarRadius").WhoseValue.Should().Be(star.SolarRadius); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -381,7 +378,7 @@ public async Task Attribute_exclusion_from_resource_definition_is_applied_for_om // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - Star star = _fakers.Star.Generate(); + Star star = _fakers.Star.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -397,9 +394,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("name").With(value => value.Should().Be(star.Name)); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("name").WhoseValue.Should().Be(star.Name); responseDocument.Data.SingleValue.Attributes.Should().NotContainKey("isVisibleFromEarth"); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); @@ -421,7 +418,7 @@ public async Task Attribute_exclusion_from_resource_definition_is_applied_for_fi // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - Star star = _fakers.Star.Generate(); + Star star = _fakers.Star.GenerateOne(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -437,10 +434,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); - responseDocument.Data.SingleValue.Attributes.ShouldHaveCount(1); - responseDocument.Data.SingleValue.Attributes.ShouldContainKey("name").With(value => value.Should().Be(star.Name)); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("name").WhoseValue.Should().Be(star.Name); responseDocument.Data.SingleValue.Relationships.Should().BeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -461,7 +458,7 @@ public async Task Queryable_parameter_handler_from_resource_definition_is_applie // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - List moons = _fakers.Moon.Generate(2); + List moons = _fakers.Moon.GenerateList(2); moons[0].SolarRadius = .5m; moons[1].SolarRadius = 50m; @@ -480,7 +477,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(moons[1].StringId); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -504,7 +501,7 @@ public async Task Queryable_parameter_handler_from_resource_definition_and_query // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - List moons = _fakers.Moon.Generate(4); + List moons = _fakers.Moon.GenerateList(4); moons[0].Name = "Alpha1"; moons[0].SolarRadius = 1m; @@ -533,7 +530,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.ManyValue.ShouldHaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Data.ManyValue[0].Id.Should().Be(moons[2].StringId); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -557,8 +554,8 @@ public async Task Queryable_parameter_handler_from_resource_definition_is_not_ap // Arrange var hitCounter = _testContext.Factory.Services.GetRequiredService(); - Planet planet = _fakers.Planet.Generate(); - planet.Moons = _fakers.Moon.Generate(1).ToHashSet(); + Planet planet = _fakers.Planet.GenerateOne(); + planet.Moons = _fakers.Moon.GenerateSet(1); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -574,13 +571,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.ShouldHaveStatusCode(HttpStatusCode.BadRequest); - responseDocument.Errors.ShouldHaveCount(1); + responseDocument.Errors.Should().HaveCount(1); ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Custom query string parameters cannot be used on nested resource endpoints."); error.Detail.Should().Be("Query string parameter 'isLargerThanTheSun' cannot be used on a nested resource endpoint."); - error.Source.ShouldNotBeNull(); + error.Source.Should().NotBeNull(); error.Source.Parameter.Should().Be("isLargerThanTheSun"); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/StarDefinition.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/StarDefinition.cs index 455a089..5a55410 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/StarDefinition.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/StarDefinition.cs @@ -6,17 +6,11 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.ResourceDefinitions.Reading; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public sealed class StarDefinition : HitCountingResourceDefinition +public sealed class StarDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) + : HitCountingResourceDefinition(resourceGraph, hitCounter) { protected override ResourceDefinitionExtensibilityPoints ExtensibilityPointsToTrack => ResourceDefinitionExtensibilityPoints.Reading; - public StarDefinition(IResourceGraph resourceGraph, ResourceDefinitionHitCounter hitCounter) - : base(resourceGraph, hitCounter) - { - // This constructor will be resolved from the container, which means - // you can take on any dependency that is also defined in the container. - } - public override SortExpression OnApplySort(SortExpression? existingSort) { base.OnApplySort(existingSort); @@ -26,11 +20,10 @@ public override SortExpression OnApplySort(SortExpression? existingSort) private SortExpression GetDefaultSortOrder() { - return CreateSortExpressionFromLambda(new PropertySortOrder - { + return CreateSortExpressionFromLambda([ (star => star.SolarMass, ListSortDirection.Descending), (star => star.SolarRadius, ListSortDirection.Descending) - }); + ]); } public override PaginationExpression OnApplyPagination(PaginationExpression? existingPagination) @@ -52,12 +45,14 @@ public override PaginationExpression OnApplyPagination(PaginationExpression? exi { base.OnApplySparseFieldSet(existingSparseFieldSet); - // @formatter:keep_existing_linebreaks true + // @formatter:wrap_chained_method_calls chop_always + // @formatter:wrap_before_first_method_call true return existingSparseFieldSet .Including(star => star.Kind, ResourceGraph) .Excluding(star => star.IsVisibleFromEarth, ResourceGraph); - // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_before_first_method_call restore + // @formatter:wrap_chained_method_calls restore } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/UniverseDbContext.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/UniverseDbContext.cs index f8bd720..417d995 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/UniverseDbContext.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/UniverseDbContext.cs @@ -5,14 +5,10 @@ namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.ResourceDefinitions.Reading; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -public sealed class UniverseDbContext : MongoDbContextShim +public sealed class UniverseDbContext(IMongoDatabase database) + : MongoDbContextShim(database) { public MongoDbSetShim Stars => Set(); public MongoDbSetShim Planets => Set(); public MongoDbSetShim Moons => Set(); - - public UniverseDbContext(IMongoDatabase database) - : base(database) - { - } } diff --git a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/UniverseFakers.cs b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/UniverseFakers.cs index 1fbd3c2..7684481 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/UniverseFakers.cs +++ b/test/JsonApiDotNetCoreMongoDbTests/IntegrationTests/ResourceDefinitions/Reading/UniverseFakers.cs @@ -1,34 +1,31 @@ using Bogus; using TestBuildingBlocks; -// @formatter:wrap_chained_method_calls chop_always -// @formatter:keep_existing_linebreaks true +// @formatter:wrap_chained_method_calls chop_if_long +// @formatter:wrap_before_first_method_call true namespace JsonApiDotNetCoreMongoDbTests.IntegrationTests.ResourceDefinitions.Reading; -internal sealed class UniverseFakers : FakerContainer +internal sealed class UniverseFakers { - private readonly Lazy> _lazyStarFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(star => star.Name, faker => faker.Random.Word()) - .RuleFor(star => star.Kind, faker => faker.PickRandom()) - .RuleFor(star => star.SolarRadius, faker => faker.Random.Decimal(.01M, 1000M)) - .RuleFor(star => star.SolarMass, faker => faker.Random.Decimal(.001M, 100M)) - .RuleFor(star => star.IsVisibleFromEarth, faker => faker.Random.Bool())); + private readonly Lazy> _lazyStarFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(star => star.Name, faker => faker.Random.Word()) + .RuleFor(star => star.Kind, faker => faker.PickRandom()) + .RuleFor(star => star.SolarRadius, faker => faker.Random.Decimal(.01M, 1000M)) + .RuleFor(star => star.SolarMass, faker => faker.Random.Decimal(.001M, 100M)) + .RuleFor(star => star.IsVisibleFromEarth, faker => faker.Random.Bool())); - private readonly Lazy> _lazyPlanetFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(planet => planet.PublicName, faker => faker.Random.Word()) - .RuleFor(planet => planet.HasRingSystem, faker => faker.Random.Bool()) - .RuleFor(planet => planet.SolarMass, faker => faker.Random.Decimal(.001M, 100M))); + private readonly Lazy> _lazyPlanetFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(planet => planet.PublicName, faker => faker.Random.Word()) + .RuleFor(planet => planet.HasRingSystem, faker => faker.Random.Bool()) + .RuleFor(planet => planet.SolarMass, faker => faker.Random.Decimal(.001M, 100M))); - private readonly Lazy> _lazyMoonFaker = new(() => - new Faker() - .UseSeed(GetFakerSeed()) - .RuleFor(moon => moon.Name, faker => faker.Random.Word()) - .RuleFor(moon => moon.SolarRadius, faker => faker.Random.Decimal(.01M, 1000M))); + private readonly Lazy> _lazyMoonFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(moon => moon.Name, faker => faker.Random.Word()) + .RuleFor(moon => moon.SolarRadius, faker => faker.Random.Decimal(.01M, 1000M))); public Faker Star => _lazyStarFaker.Value; public Faker Planet => _lazyPlanetFaker.Value; diff --git a/test/JsonApiDotNetCoreMongoDbTests/JsonApiDotNetCoreMongoDbTests.csproj b/test/JsonApiDotNetCoreMongoDbTests/JsonApiDotNetCoreMongoDbTests.csproj index c202a91..22338d5 100644 --- a/test/JsonApiDotNetCoreMongoDbTests/JsonApiDotNetCoreMongoDbTests.csproj +++ b/test/JsonApiDotNetCoreMongoDbTests/JsonApiDotNetCoreMongoDbTests.csproj @@ -1,13 +1,9 @@ - $(TargetFrameworkName) + net9.0;net8.0 - - - PreserveNewest - - + @@ -16,7 +12,8 @@ - + + diff --git a/test/JsonApiDotNetCoreMongoDbTests/xunit.runner.json b/test/JsonApiDotNetCoreMongoDbTests/xunit.runner.json deleted file mode 100644 index 9db029b..0000000 --- a/test/JsonApiDotNetCoreMongoDbTests/xunit.runner.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "parallelizeAssembly": false, - "parallelizeTestCollections": false -} diff --git a/test/TestBuildingBlocks/AssemblyInfo.cs b/test/TestBuildingBlocks/AssemblyInfo.cs deleted file mode 100644 index 82d1291..0000000 --- a/test/TestBuildingBlocks/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -// https://github.com/coverlet-coverage/coverlet/blob/master/Documentation/MSBuildIntegration.md#excluding-from-coverage -[assembly: ExcludeFromCodeCoverage] diff --git a/test/TestBuildingBlocks/FakerContainer.cs b/test/TestBuildingBlocks/FakerExtensions.cs similarity index 63% rename from test/TestBuildingBlocks/FakerContainer.cs rename to test/TestBuildingBlocks/FakerExtensions.cs index 99cce6e..75714e6 100644 --- a/test/TestBuildingBlocks/FakerContainer.cs +++ b/test/TestBuildingBlocks/FakerExtensions.cs @@ -1,21 +1,26 @@ using System.Diagnostics; using System.Reflection; -using Bogus.DataSets; -using FluentAssertions.Extensions; +using Bogus; using Xunit; namespace TestBuildingBlocks; -public abstract class FakerContainer +public static class FakerExtensions { - static FakerContainer() + public static Faker MakeDeterministic(this Faker faker, DateTime? systemTimeUtc = null) + where T : class { + int seed = GetFakerSeed(); + faker.UseSeed(seed); + // Setting the system DateTime to kind Utc, so that faker calls like PastOffset() don't depend on the system time zone. // See https://docs.microsoft.com/en-us/dotnet/api/system.datetimeoffset.op_implicit?view=net-6.0#remarks - Date.SystemClock = () => 1.January(2020).AsUtc(); + faker.UseDateTimeReference(systemTimeUtc ?? IntegrationTest.DefaultDateTimeUtc.UtcDateTime); + + return faker; } - protected static int GetFakerSeed() + private static int GetFakerSeed() { // The goal here is to have stable data over multiple test runs, but at the same time different data per test case. @@ -34,7 +39,7 @@ private static MethodBase GetTestMethod() if (testMethod == null) { // If called after the first await statement, the test method is no longer on the stack, - // but has been replaced with the compiler-generated async/wait state machine. + // but has been replaced with the compiler-generated async/await state machine. throw new InvalidOperationException("Fakers can only be used from within (the start of) a test method."); } @@ -74,4 +79,27 @@ private static int GetDeterministicHashCode(string source) return hash1 + hash2 * 1566083941; } } + + // The methods below exist so that a non-nullable return type is inferred. + // The Bogus NuGet package is not annotated for nullable reference types. + + public static T GenerateOne(this Faker faker) + where T : class + { + return faker.Generate(); + } + +#pragma warning disable AV1130 // Return type in method signature should be an interface to an unchangeable collection + public static List GenerateList(this Faker faker, int count) + where T : class + { + return faker.Generate(count); + } + + public static HashSet GenerateSet(this Faker faker, int count) + where T : class + { + return faker.Generate(count).ToHashSet(); + } +#pragma warning restore AV1130 // Return type in method signature should be an interface to an unchangeable collection } diff --git a/test/TestBuildingBlocks/FluentExtensions.cs b/test/TestBuildingBlocks/FluentExtensions.cs new file mode 100644 index 0000000..1ceebc0 --- /dev/null +++ b/test/TestBuildingBlocks/FluentExtensions.cs @@ -0,0 +1,45 @@ +using FluentAssertions; +using FluentAssertions.Numeric; +using FluentAssertions.Primitives; +using JetBrains.Annotations; +using SysNotNull = System.Diagnostics.CodeAnalysis.NotNullAttribute; + +// ReSharper disable UnusedMethodReturnValue.Global + +namespace TestBuildingBlocks; + +public static class FluentExtensions +{ + private const decimal NumericPrecision = 0.00000000001M; + + /// + /// Same as , but with + /// default precision. + /// + [CustomAssertion] + public static AndConstraint> BeApproximately(this NullableNumericAssertions parent, decimal? expectedValue, + string because = "", params object[] becauseArgs) + { + return parent.BeApproximately(expectedValue, NumericPrecision, because, becauseArgs); + } + + // Workaround for source.Should().NotBeNull().And.Subject having declared type 'object'. + [System.Diagnostics.Contracts.Pure] + public static StrongReferenceTypeAssertions RefShould([SysNotNull] this T? actualValue) + where T : class + { + actualValue.Should().NotBeNull(); + return new StrongReferenceTypeAssertions(actualValue); + } + + public static void With(this T subject, [InstantHandle] Action continuation) + { + continuation(subject); + } + + public sealed class StrongReferenceTypeAssertions(TReference subject) + : ReferenceTypeAssertions>(subject) + { + protected override string Identifier => "subject"; + } +} diff --git a/test/TestBuildingBlocks/FluentMetaExtensions.cs b/test/TestBuildingBlocks/FluentMetaExtensions.cs new file mode 100644 index 0000000..7ed040c --- /dev/null +++ b/test/TestBuildingBlocks/FluentMetaExtensions.cs @@ -0,0 +1,37 @@ +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Collections; + +namespace TestBuildingBlocks; + +public static class FluentMetaExtensions +{ + /// + /// Asserts that a "meta" dictionary contains a single element named "total" with the specified value. + /// + [CustomAssertion] +#pragma warning disable AV1553 // Do not use optional parameters with default value null for strings, collections or tasks + public static void ContainTotal(this GenericDictionaryAssertions, string, object?> source, int expected, + string? keyName = null) +#pragma warning restore AV1553 // Do not use optional parameters with default value null for strings, collections or tasks + { + JsonElement element = GetMetaJsonElement(source, keyName ?? "total"); + element.GetInt32().Should().Be(expected); + } + + /// + /// Asserts that a "meta" dictionary contains a single element named "requestBody" that isn't empty. + /// + [CustomAssertion] + public static void HaveRequestBody(this GenericDictionaryAssertions, string, object?> source) + { + JsonElement element = GetMetaJsonElement(source, "requestBody"); + element.ToString().Should().NotBeEmpty(); + } + + private static JsonElement GetMetaJsonElement(GenericDictionaryAssertions, string, object?> source, string metaKey) + { + object? value = source.ContainKey(metaKey).WhoseValue; + return value.Should().BeOfType().Subject; + } +} diff --git a/test/TestBuildingBlocks/IntegrationTest.cs b/test/TestBuildingBlocks/IntegrationTest.cs index c66e6c5..4fc245a 100644 --- a/test/TestBuildingBlocks/IntegrationTest.cs +++ b/test/TestBuildingBlocks/IntegrationTest.cs @@ -1,49 +1,73 @@ using System.Net.Http.Headers; using System.Text; using System.Text.Json; +using FluentAssertions.Extensions; using JsonApiDotNetCore.Middleware; +using Xunit; namespace TestBuildingBlocks; /// -/// A base class for tests that conveniently enables to execute HTTP requests against JSON:API endpoints. +/// A base class for tests that conveniently enables to execute HTTP requests against JSON:API endpoints. It throttles tests that are running in parallel +/// to avoid exceeding the maximum active database connections. /// -public abstract class IntegrationTest +public abstract class IntegrationTest : IAsyncLifetime { + private static readonly MediaTypeHeaderValue DefaultMediaType = MediaTypeHeaderValue.Parse(JsonApiMediaType.Default.ToString()); + + private static readonly MediaTypeWithQualityHeaderValue OperationsMediaType = + MediaTypeWithQualityHeaderValue.Parse(JsonApiMediaType.AtomicOperations.ToString()); + + private static readonly SemaphoreSlim ThrottleSemaphore = GetDefaultThrottleSemaphore(); + + internal static DateTimeOffset DefaultDateTimeUtc { get; } = 1.January(2020).At(1, 2, 3).AsUtc(); + protected abstract JsonSerializerOptions SerializerOptions { get; } + private static SemaphoreSlim GetDefaultThrottleSemaphore() + { + int maxConcurrentTestRuns = OperatingSystem.IsWindows() && Environment.GetEnvironmentVariable("CI") != null ? 32 : 64; + return new SemaphoreSlim(maxConcurrentTestRuns); + } + public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteGetAsync(string requestUrl, Action? setRequestHeaders = null) { return await ExecuteRequestAsync(HttpMethod.Get, requestUrl, null, null, setRequestHeaders); } +#pragma warning disable AV1553 // Do not use optional parameters with default value null for strings, collections or tasks public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecutePostAsync(string requestUrl, - object requestBody, string contentType = HeaderConstants.MediaType, Action? setRequestHeaders = null) + object requestBody, string? contentType = null, Action? setRequestHeaders = null) +#pragma warning restore AV1553 // Do not use optional parameters with default value null for strings, collections or tasks { - return await ExecuteRequestAsync(HttpMethod.Post, requestUrl, requestBody, contentType, setRequestHeaders); + MediaTypeHeaderValue mediaType = contentType == null ? DefaultMediaType : MediaTypeHeaderValue.Parse(contentType); + + return await ExecuteRequestAsync(HttpMethod.Post, requestUrl, requestBody, mediaType, setRequestHeaders); } public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecutePostAtomicAsync(string requestUrl, - object requestBody, string contentType = HeaderConstants.AtomicOperationsMediaType, Action? setRequestHeaders = null) + object requestBody) { - return await ExecuteRequestAsync(HttpMethod.Post, requestUrl, requestBody, contentType, setRequestHeaders); + Action setRequestHeaders = headers => headers.Accept.Add(OperationsMediaType); + + return await ExecuteRequestAsync(HttpMethod.Post, requestUrl, requestBody, OperationsMediaType, setRequestHeaders); } public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecutePatchAsync(string requestUrl, - object requestBody, string contentType = HeaderConstants.MediaType, Action? setRequestHeaders = null) + object requestBody, Action? setRequestHeaders = null) { - return await ExecuteRequestAsync(HttpMethod.Patch, requestUrl, requestBody, contentType, setRequestHeaders); + return await ExecuteRequestAsync(HttpMethod.Patch, requestUrl, requestBody, DefaultMediaType, setRequestHeaders); } public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteDeleteAsync(string requestUrl, - object? requestBody = null, string contentType = HeaderConstants.MediaType, Action? setRequestHeaders = null) + object? requestBody = null, Action? setRequestHeaders = null) { - return await ExecuteRequestAsync(HttpMethod.Delete, requestUrl, requestBody, contentType, setRequestHeaders); + return await ExecuteRequestAsync(HttpMethod.Delete, requestUrl, requestBody, DefaultMediaType, setRequestHeaders); } private async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteRequestAsync(HttpMethod method, - string requestUrl, object? requestBody, string? contentType, Action? setRequestHeaders) + string requestUrl, object? requestBody, MediaTypeHeaderValue? contentType, Action? setRequestHeaders) { using var request = new HttpRequestMessage(method, requestUrl); string? requestText = SerializeRequest(requestBody); @@ -56,7 +80,7 @@ public abstract class IntegrationTest if (contentType != null) { - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); + request.Content.Headers.ContentType = contentType; } } @@ -99,4 +123,15 @@ public abstract class IntegrationTest throw new FormatException($"Failed to deserialize response body to JSON:\n{responseText}", exception); } } + + public async Task InitializeAsync() + { + await ThrottleSemaphore.WaitAsync(); + } + + public virtual Task DisposeAsync() + { + _ = ThrottleSemaphore.Release(); + return Task.CompletedTask; + } } diff --git a/test/TestBuildingBlocks/IntegrationTestContext.cs b/test/TestBuildingBlocks/IntegrationTestContext.cs index 2e3cc72..b16cd2f 100644 --- a/test/TestBuildingBlocks/IntegrationTestContext.cs +++ b/test/TestBuildingBlocks/IntegrationTestContext.cs @@ -1,5 +1,6 @@ -using System.Runtime.InteropServices; +using System.Reflection; using System.Text.Json; +using EphemeralMongo; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.MongoDb.Configuration; @@ -10,8 +11,8 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; -using Mongo2Go; using MongoDB.Driver; namespace TestBuildingBlocks; @@ -28,15 +29,15 @@ namespace TestBuildingBlocks; /// . /// [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] -public class IntegrationTestContext : IntegrationTest, IDisposable +public class IntegrationTestContext : IntegrationTest where TStartup : class where TMongoDbContextShim : MongoDbContextShim { - private readonly Lazy _runner; + private readonly Lazy _runner; private readonly Lazy> _lazyFactory; + private readonly HashSet _resourceClrTypes = []; private readonly TestControllerProvider _testControllerProvider = new(); - - private Action? _afterServicesConfiguration; + private Action? _configureServices; protected override JsonSerializerOptions SerializerOptions { @@ -49,31 +50,25 @@ protected override JsonSerializerOptions SerializerOptions public WebApplicationFactory Factory => _lazyFactory.Value; - /// - /// Set this to true to enable transactions support in MongoDB. - /// - public bool StartMongoDbInSingleNodeReplicaSetMode { get; set; } - public IntegrationTestContext() { - _runner = new Lazy(StartMongoDb); + _runner = new Lazy(StartMongoDb); _lazyFactory = new Lazy>(CreateFactory); } - private MongoDbRunner StartMongoDb() + private IMongoRunner StartMongoDb() + { + return MongoRunnerProvider.Instance.Get(); + } + + public void UseResourceTypesInNamespace(string? codeNamespace) { - // Increasing maxTransactionLockRequestTimeoutMillis (default=5) as workaround for occasional - // "Unable to acquire lock" error when running tests locally. - string arguments = "--quiet --setParameter maxTransactionLockRequestTimeoutMillis=40"; + Assembly assembly = typeof(TStartup).Assembly; - if (!StartMongoDbInSingleNodeReplicaSetMode) + foreach (Type resourceClrType in ResourceTypeFinder.GetResourceClrTypesInNamespace(assembly, codeNamespace)) { - // MongoDbRunner watches console output to detect when the replica set has stabilized. So we can only fully - // suppress console output if not running in this mode. - arguments += RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? " --logappend --logpath NUL" : " --logpath /dev/null"; + _resourceClrTypes.Add(resourceClrType); } - - return MongoDbRunner.Start(singleNodeReplSet: StartMongoDbInSingleNodeReplicaSetMode, additionalMongodArguments: arguments); } public void UseController() @@ -91,31 +86,36 @@ private WebApplicationFactory CreateFactory() { var factory = new IntegrationTestWebApplicationFactory(); - factory.ConfigureServicesBeforeStartup(services => + factory.ConfigureServices(services => { + _configureServices?.Invoke(services); + services.ReplaceControllers(_testControllerProvider); - services.AddSingleton(_ => + services.TryAddSingleton(_ => { var client = new MongoClient(_runner.Value.ConnectionString); - return client.GetDatabase($"JsonApiDotNetCore_MongoDb_{new Random().Next()}_Test"); + return client.GetDatabase($"JsonApiDotNetCore_MongoDb_{Random.Shared.Next()}_Test"); }); - services.AddJsonApi(ConfigureJsonApiOptions, facade => facade.AddAssembly(typeof(TStartup).Assembly)); - services.AddJsonApiMongoDb(); + services.TryAddScoped(); - services.AddScoped(typeof(IResourceReadRepository<,>), typeof(MongoRepository<,>)); - services.AddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoRepository<,>)); - services.AddScoped(typeof(IResourceRepository<,>), typeof(MongoRepository<,>)); + services.TryAddScoped(typeof(IResourceReadRepository<,>), typeof(MongoRepository<,>)); + services.TryAddScoped(typeof(IResourceWriteRepository<,>), typeof(MongoRepository<,>)); + services.TryAddScoped(typeof(IResourceRepository<,>), typeof(MongoRepository<,>)); - services.AddScoped(); - }); + services.AddJsonApi(ConfigureJsonApiOptions, resources: builder => + { + foreach (Type resourceClrType in _resourceClrTypes) + { + builder.Add(resourceClrType); + } + }); - factory.ConfigureServicesAfterStartup(_afterServicesConfiguration); + services.AddJsonApiMongoDb(); + }); - // We have placed an appsettings.json in the TestBuildingBlock project folder and set the content root to there. Note that controllers - // are not discovered in the content root but are registered manually using IntegrationTestContext.UseController. - return factory.WithWebHostBuilder(builder => builder.UseSolutionRelativeContentRoot($"test/{nameof(TestBuildingBlocks)}")); + return factory; } private void ConfigureJsonApiOptions(JsonApiOptions options) @@ -125,22 +125,14 @@ private void ConfigureJsonApiOptions(JsonApiOptions options) options.SerializerOptions.WriteIndented = true; } - public void Dispose() + public void ConfigureServices(Action configureServices) { - if (_lazyFactory.IsValueCreated) + if (_configureServices != null && _configureServices != configureServices) { - _lazyFactory.Value.Dispose(); + throw new InvalidOperationException($"Do not call {nameof(ConfigureServices)} multiple times."); } - if (_runner.IsValueCreated) - { - _runner.Value.Dispose(); - } - } - - public void ConfigureServicesAfterStartup(Action servicesConfiguration) - { - _afterServicesConfiguration = servicesConfiguration; + _configureServices = configureServices; } public async Task RunOnDatabaseAsync(Func asyncAction) @@ -151,43 +143,56 @@ public async Task RunOnDatabaseAsync(Func asyncAction await asyncAction(mongoDbContextShim); } + public override async Task DisposeAsync() + { + try + { + if (_lazyFactory.IsValueCreated) + { + await _lazyFactory.Value.DisposeAsync(); + } + + if (_runner.IsValueCreated) + { + _runner.Value.Dispose(); + } + } + finally + { + await base.DisposeAsync(); + } + } + private sealed class IntegrationTestWebApplicationFactory : WebApplicationFactory { - private Action? _beforeServicesConfiguration; - private Action? _afterServicesConfiguration; + private Action? _configureServices; - public void ConfigureServicesBeforeStartup(Action? servicesConfiguration) + public void ConfigureServices(Action? configureServices) { - _beforeServicesConfiguration = servicesConfiguration; + _configureServices = configureServices; } - public void ConfigureServicesAfterStartup(Action? servicesConfiguration) + protected override void ConfigureWebHost(IWebHostBuilder builder) { - _afterServicesConfiguration = servicesConfiguration; + // We have placed an appsettings.json in the TestBuildingBlocks project directory and set the content root to there. Note that + // controllers are not discovered in the content root, but are registered manually using IntegrationTestContext.UseController. + builder.UseSolutionRelativeContentRoot($"test/{nameof(TestBuildingBlocks)}"); } protected override IHostBuilder CreateHostBuilder() { // @formatter:wrap_chained_method_calls chop_always - // @formatter:keep_existing_linebreaks true + // @formatter:wrap_before_first_method_call true - return Host.CreateDefaultBuilder(null) + return Host + .CreateDefaultBuilder(null) .ConfigureWebHostDefaults(webBuilder => { - webBuilder.ConfigureServices(services => - { - _beforeServicesConfiguration?.Invoke(services); - }); - + webBuilder.ConfigureServices(services => _configureServices?.Invoke(services)); webBuilder.UseStartup(); - - webBuilder.ConfigureServices(services => - { - _afterServicesConfiguration?.Invoke(services); - }); }); - // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_before_first_method_call restore // @formatter:wrap_chained_method_calls restore } } diff --git a/test/TestBuildingBlocks/MarkedText.cs b/test/TestBuildingBlocks/MarkedText.cs new file mode 100644 index 0000000..003fd0a --- /dev/null +++ b/test/TestBuildingBlocks/MarkedText.cs @@ -0,0 +1,44 @@ +using System.Diagnostics; +using JetBrains.Annotations; + +namespace TestBuildingBlocks; + +[PublicAPI] +[DebuggerDisplay($"{{{nameof(Source)}}}")] +public sealed class MarkedText +{ + public string Source { get; } + public int Position { get; } + public string Text { get; } + + public MarkedText(string source, char marker) + { + ArgumentNullException.ThrowIfNull(source); + + Source = source; + Position = GetPositionFromMarker(marker); + Text = source.Replace(marker.ToString(), string.Empty); + } + + private int GetPositionFromMarker(char marker) + { + int position = Source.IndexOf(marker); + + if (position == -1) + { + throw new InvalidOperationException("Marker not found."); + } + + if (Source.IndexOf(marker, position + 1) != -1) + { + throw new InvalidOperationException("Multiple markers found."); + } + + return position; + } + + public override string ToString() + { + return $"Failed at position {Position + 1}: {Source}"; + } +} diff --git a/test/TestBuildingBlocks/MongoDbContextShim.cs b/test/TestBuildingBlocks/MongoDbContextShim.cs index 323af40..82356d8 100644 --- a/test/TestBuildingBlocks/MongoDbContextShim.cs +++ b/test/TestBuildingBlocks/MongoDbContextShim.cs @@ -7,15 +7,10 @@ namespace TestBuildingBlocks; /// Provides an Entity Framework Core DbContext-like abstraction that translates to MongoDB calls. This makes it easier to keep tests in sync with the /// main repository. /// -public abstract class MongoDbContextShim +public abstract class MongoDbContextShim(IMongoDatabase database) { - private readonly IMongoDatabase _database; - private readonly List _dbSetShims = new(); - - protected MongoDbContextShim(IMongoDatabase database) - { - _database = database; - } + private readonly IMongoDatabase _database = database; + private readonly List _dbSetShims = []; protected MongoDbSetShim Set() where TEntity : IMongoIdentifiable diff --git a/test/TestBuildingBlocks/MongoDbSetShim.cs b/test/TestBuildingBlocks/MongoDbSetShim.cs index 7e58223..8a2801f 100644 --- a/test/TestBuildingBlocks/MongoDbSetShim.cs +++ b/test/TestBuildingBlocks/MongoDbSetShim.cs @@ -18,7 +18,7 @@ public sealed class MongoDbSetShim : MongoDbSetShim where TEntity : IMongoIdentifiable { private readonly IMongoCollection _collection; - private readonly List _entitiesToInsert = new(); + private readonly List _entitiesToInsert = []; internal MongoDbSetShim(IMongoCollection collection) { @@ -42,7 +42,7 @@ public void AddRange(IEnumerable entities) internal override async Task PersistAsync(CancellationToken cancellationToken) { - if (_entitiesToInsert.Any()) + if (_entitiesToInsert.Count > 0) { if (_entitiesToInsert.Count == 1) { diff --git a/test/TestBuildingBlocks/MongoRunnerProvider.cs b/test/TestBuildingBlocks/MongoRunnerProvider.cs new file mode 100644 index 0000000..6339deb --- /dev/null +++ b/test/TestBuildingBlocks/MongoRunnerProvider.cs @@ -0,0 +1,122 @@ +using EphemeralMongo; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace TestBuildingBlocks; + +// Based on https://gist.github.com/asimmon/612b2d54f1a0d2b4e1115590d456e0be. +internal sealed class MongoRunnerProvider +{ + public static readonly MongoRunnerProvider Instance = new(); + private static readonly GuidSerializer StandardGuidSerializer = new(GuidRepresentation.Standard); + +#if NET8_0 + private readonly object _lockObject = new(); +#else + private readonly Lock _lockObject = new(); +#endif + + private IMongoRunner? _runner; + private int _useCounter; + + private MongoRunnerProvider() + { + } + + public IMongoRunner Get() + { + lock (_lockObject) + { + if (_runner == null) + { + BsonSerializer.TryRegisterSerializer(StandardGuidSerializer); + + var runnerOptions = new MongoRunnerOptions + { + // Single-node replica set mode is required for transaction support in MongoDB. + UseSingleNodeReplicaSet = true, + AdditionalArguments = ["--quiet"] + }; + + _runner = MongoRunner.Run(runnerOptions); + } + + _useCounter++; + return new MongoRunnerWrapper(this, _runner); + } + } + + private void Detach() + { + lock (_lockObject) + { + if (_runner != null) + { + _useCounter--; + + if (_useCounter == 0) + { + _runner.Dispose(); + _runner = null; + } + } + } + } + + private sealed class MongoRunnerWrapper(MongoRunnerProvider owner, IMongoRunner underlyingMongoRunner) : IMongoRunner + { + private readonly MongoRunnerProvider _owner = owner; + private IMongoRunner? _underlyingMongoRunner = underlyingMongoRunner; + + public string ConnectionString + { + get + { + ObjectDisposedException.ThrowIf(_underlyingMongoRunner == null, typeof(IMongoRunner)); + return _underlyingMongoRunner.ConnectionString; + } + } + + public void Import(string database, string collection, string inputFilePath, string[]? additionalArguments = null, bool drop = false, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_underlyingMongoRunner == null, typeof(IMongoRunner)); + + _underlyingMongoRunner.Import(database, collection, inputFilePath, additionalArguments, drop, cancellationToken); + } + + public async Task ImportAsync(string database, string collection, string inputFilePath, string[]? additionalArguments = null, bool drop = false, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_underlyingMongoRunner == null, typeof(IMongoRunner)); + + await _underlyingMongoRunner.ImportAsync(database, collection, inputFilePath, additionalArguments, drop, cancellationToken); + } + + public void Export(string database, string collection, string outputFilePath, string[]? additionalArguments = null, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_underlyingMongoRunner == null, typeof(IMongoRunner)); + + _underlyingMongoRunner.Export(database, collection, outputFilePath, additionalArguments, cancellationToken); + } + + public async Task ExportAsync(string database, string collection, string outputFilePath, string[]? additionalArguments = null, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_underlyingMongoRunner == null, typeof(IMongoRunner)); + + await _underlyingMongoRunner.ExportAsync(database, collection, outputFilePath, additionalArguments, cancellationToken); + } + + public void Dispose() + { + if (_underlyingMongoRunner != null) + { + _underlyingMongoRunner = null; + _owner.Detach(); + } + } + } +} diff --git a/test/TestBuildingBlocks/NullabilityAssertionExtensions.cs b/test/TestBuildingBlocks/NullabilityAssertionExtensions.cs deleted file mode 100644 index a4242b4..0000000 --- a/test/TestBuildingBlocks/NullabilityAssertionExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -using FluentAssertions; -using JetBrains.Annotations; -using SysNotNull = System.Diagnostics.CodeAnalysis.NotNullAttribute; - -// ReSharper disable PossibleMultipleEnumeration -#pragma warning disable CS8777 // Parameter must have a non-null value when exiting. - -namespace TestBuildingBlocks; - -public static class NullabilityAssertionExtensions -{ - [CustomAssertion] - public static T ShouldNotBeNull([SysNotNull] this T? subject) - { - subject.Should().NotBeNull(); - return subject!; - } - - [CustomAssertion] - public static void ShouldNotBeEmpty([SysNotNull] this string? subject) - { - subject.Should().NotBeEmpty(); - } - - [CustomAssertion] - public static void ShouldHaveCount([SysNotNull] this IEnumerable? subject, int expected) - { - subject.Should().HaveCount(expected); - } - - [CustomAssertion] - public static TValue? ShouldContainKey([SysNotNull] this IDictionary? subject, TKey expected) - { - subject.Should().ContainKey(expected); - - return subject![expected]; - } - - public static void With(this T subject, [InstantHandle] Action continuation) - { - continuation(subject); - } -} diff --git a/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs b/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs deleted file mode 100644 index c78a005..0000000 --- a/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using FluentAssertions; -using FluentAssertions.Numeric; -using JetBrains.Annotations; - -namespace TestBuildingBlocks; - -[PublicAPI] -public static class ObjectAssertionsExtensions -{ - private const decimal NumericPrecision = 0.00000000001M; - - /// - /// Same as , but with - /// default precision. - /// - [CustomAssertion] - public static AndConstraint> BeApproximately(this NullableNumericAssertions parent, decimal? expectedValue, - string because = "", params object[] becauseArgs) - { - return parent.BeApproximately(expectedValue, NumericPrecision, because, becauseArgs); - } -} diff --git a/test/TestBuildingBlocks/Properties/AssemblyInfo.cs b/test/TestBuildingBlocks/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..2af69d7 --- /dev/null +++ b/test/TestBuildingBlocks/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Diagnostics.CodeAnalysis; + +// Justification: This assembly contains building blocks for writing tests. It does not contain code that ships. +[assembly: ExcludeFromCodeCoverage] diff --git a/test/TestBuildingBlocks/ResourceTypeFinder.cs b/test/TestBuildingBlocks/ResourceTypeFinder.cs new file mode 100644 index 0000000..a1db4f1 --- /dev/null +++ b/test/TestBuildingBlocks/ResourceTypeFinder.cs @@ -0,0 +1,32 @@ +using System.Collections.Concurrent; +using System.Reflection; +using JsonApiDotNetCore.MongoDb; +using JsonApiDotNetCore.Resources; + +#pragma warning disable AV1008 // Class should not be static + +namespace TestBuildingBlocks; + +internal static class ResourceTypeFinder +{ + private static readonly ConcurrentDictionary> ResourceTypesPerAssembly = new(); + private static readonly ConcurrentDictionary> ResourceTypesPerNamespace = new(); + + public static IReadOnlySet GetResourceClrTypesInNamespace(Assembly assembly, string? codeNamespace) + { + IReadOnlySet resourceClrTypesInAssembly = ResourceTypesPerAssembly.GetOrAdd(assembly, GetResourceClrTypesInAssembly); + + string namespaceKey = codeNamespace ?? string.Empty; + return ResourceTypesPerNamespace.GetOrAdd(namespaceKey, _ => FilterTypesInNamespace(resourceClrTypesInAssembly, codeNamespace)); + } + + private static IReadOnlySet GetResourceClrTypesInAssembly(Assembly assembly) + { + return assembly.GetTypes().Where(type => type.IsAssignableTo(typeof(IIdentifiable))).ToHashSet().AsReadOnly(); + } + + private static IReadOnlySet FilterTypesInNamespace(IEnumerable resourceClrTypesInAssembly, string? codeNamespace) + { + return resourceClrTypesInAssembly.Where(resourceClrType => resourceClrType.Namespace == codeNamespace).ToHashSet().AsReadOnly(); + } +} diff --git a/test/TestBuildingBlocks/ServiceCollectionExtensions.cs b/test/TestBuildingBlocks/ServiceCollectionExtensions.cs index 7ee211a..886a5f0 100644 --- a/test/TestBuildingBlocks/ServiceCollectionExtensions.cs +++ b/test/TestBuildingBlocks/ServiceCollectionExtensions.cs @@ -9,6 +9,9 @@ internal static class ServiceCollectionExtensions { public static void ReplaceControllers(this IServiceCollection services, TestControllerProvider provider) { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(provider); + services.AddMvcCore().ConfigureApplicationPartManager(manager => { RemoveExistingControllerFeatureProviders(manager); diff --git a/test/TestBuildingBlocks/TestBuildingBlocks.csproj b/test/TestBuildingBlocks/TestBuildingBlocks.csproj index db9e8da..79c9947 100644 --- a/test/TestBuildingBlocks/TestBuildingBlocks.csproj +++ b/test/TestBuildingBlocks/TestBuildingBlocks.csproj @@ -1,20 +1,24 @@ - + - $(TargetFrameworkName) + net9.0;net8.0 + + - + - - + + + + - - - + + + diff --git a/test/TestBuildingBlocks/TestControllerProvider.cs b/test/TestBuildingBlocks/TestControllerProvider.cs index 664fe34..4f183e5 100644 --- a/test/TestBuildingBlocks/TestControllerProvider.cs +++ b/test/TestBuildingBlocks/TestControllerProvider.cs @@ -5,9 +5,9 @@ namespace TestBuildingBlocks; internal sealed class TestControllerProvider : ControllerFeatureProvider { - private readonly ISet _allowedControllerTypes = new HashSet(); + private readonly HashSet _allowedControllerTypes = []; - internal ISet ControllerAssemblies { get; } = new HashSet(); + internal HashSet ControllerAssemblies { get; } = []; public void AddController(Type controller) { diff --git a/test/TestBuildingBlocks/appsettings.json b/test/TestBuildingBlocks/appsettings.json index edbd7e4..5c69fe7 100644 --- a/test/TestBuildingBlocks/appsettings.json +++ b/test/TestBuildingBlocks/appsettings.json @@ -2,8 +2,11 @@ "Logging": { "LogLevel": { "Default": "Warning", + // Disable logging to keep the output from C/I build clean. Errors are expected to occur while testing failure handling. + "Microsoft.AspNetCore.Hosting.Diagnostics": "None", "Microsoft.Hosting.Lifetime": "Warning", - "Microsoft.EntityFrameworkCore": "Critical" + "Microsoft.EntityFrameworkCore": "Critical", + "JsonApiDotNetCore": "Critical" } } } diff --git a/tests.runsettings b/tests.runsettings new file mode 100644 index 0000000..1f4ca55 --- /dev/null +++ b/tests.runsettings @@ -0,0 +1,20 @@ + + + + true + + + true + + + + + + **/test/**/*.* + ObsoleteAttribute,GeneratedCodeAttribute,TestSDKAutoGeneratedCode + true + + + + + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy