diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000000..fb4dc5b46b --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "jetbrains.resharper.globaltools": { + "version": "2020.3.3", + "commands": [ + "jb" + ] + }, + "regitlint": { + "version": "2.1.3", + "commands": [ + "regitlint" + ] + } + } +} diff --git a/.editorconfig b/.editorconfig index 3499a1f7a6..b6d9a8990c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,25 +1,123 @@ -# EditorConfig is awesome: http://EditorConfig.org - -# top-most EditorConfig file +# Remove the line below if you want to inherit .editorconfig settings from higher directories root = true [*] -end_of_line = lf -insert_final_newline = true indent_style = space indent_size = 4 charset = utf-8 +trim_trailing_whitespace = true +end_of_line = lf +insert_final_newline = true -[*.{csproj,props}] +[*.{csproj,json}] indent_size = 2 -[*.{cs,vb}] -dotnet_naming_rule.private_members_with_underscore.symbols = private_fields -dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore -dotnet_naming_rule.private_members_with_underscore.severity = suggestion +[*.{cs}] +#### .NET Coding Conventions #### + +# Organize usings +dotnet_sort_system_directives_first = true + +# 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 + +# 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 + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion +csharp_style_pattern_local_over_anonymous_function = false: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 + +# Parameter preferences +dotnet_code_quality_unused_parameters = non_public:suggestion + +# 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 + +# Code-block preferences +csharp_prefer_braces = true:suggestion + +# Expression-level preferences +csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:suggestion + + +#### C# Formatting Rules #### + +# Indentation preferences +csharp_indent_case_contents_when_block = false + +# Wrapping preferences +csharp_preserve_single_line_statements = false + + +#### Naming styles #### + +dotnet_diagnostic.IDE1006.severity = warning + +# Naming rules +dotnet_naming_rule.private_const_fields_should_be_pascal_case.symbols = private_const_fields +dotnet_naming_rule.private_const_fields_should_be_pascal_case.style = pascal_case +dotnet_naming_rule.private_const_fields_should_be_pascal_case.severity = warning + +dotnet_naming_rule.private_static_readonly_fields_should_be_pascal_case.symbols = private_static_readonly_fields +dotnet_naming_rule.private_static_readonly_fields_should_be_pascal_case.style = pascal_case +dotnet_naming_rule.private_static_readonly_fields_should_be_pascal_case.severity = warning + +dotnet_naming_rule.private_static_or_readonly_fields_should_start_with_underscore.symbols = private_static_or_readonly_fields +dotnet_naming_rule.private_static_or_readonly_fields_should_start_with_underscore.style = camel_case_prefix_with_underscore +dotnet_naming_rule.private_static_or_readonly_fields_should_start_with_underscore.severity = warning + +dotnet_naming_rule.locals_and_parameters_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_and_parameters_should_be_camel_case.style = camel_case +dotnet_naming_rule.locals_and_parameters_should_be_camel_case.severity = warning + +dotnet_naming_rule.types_and_members_should_be_pascal_case.symbols = types_and_members +dotnet_naming_rule.types_and_members_should_be_pascal_case.style = pascal_case +dotnet_naming_rule.types_and_members_should_be_pascal_case.severity = warning + +# Symbol specifications +dotnet_naming_symbols.private_const_fields.applicable_kinds = field +dotnet_naming_symbols.private_const_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_const_fields.required_modifiers = const + +dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = static,readonly + +dotnet_naming_symbols.private_static_or_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_or_readonly_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_static_or_readonly_fields.required_modifiers = static readonly + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = local,parameter +dotnet_naming_symbols.locals_and_parameters.applicable_accessibilities = * + +dotnet_naming_symbols.types_and_members.applicable_kinds = * +dotnet_naming_symbols.types_and_members.applicable_accessibilities = * + +# Naming styles +dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_symbols.private_fields.applicable_kinds = field -dotnet_naming_symbols.private_fields.applicable_accessibilities = private +dotnet_naming_style.camel_case_prefix_with_underscore.required_prefix = _ +dotnet_naming_style.camel_case_prefix_with_underscore.capitalization = camel_case -dotnet_naming_style.prefix_underscore.capitalization = camel_case -dotnet_naming_style.prefix_underscore.required_prefix = _ \ No newline at end of file +dotnet_naming_style.camel_case.capitalization = camel_case diff --git a/.github/CONTRIBUTING.MD b/.github/CONTRIBUTING.MD deleted file mode 100644 index 22e7f4d363..0000000000 --- a/.github/CONTRIBUTING.MD +++ /dev/null @@ -1,41 +0,0 @@ -# Contributing - -## Workflow - -1. Search through the issues to see if your particular issue has already been discovered and possibly addressed -2. Open an issue if you can't find anything helpful -3. Open a PR for proposed changes - -## Commit Guidelines - -I have chosen to loosely follow the [Angular Commit Guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit) - -# Documentation - -If you'd like to help us improve our documentation, please checkout our [GitHub pages repository](https://github.com/json-api-dotnet/json-api-dotnet.github.io) where we host our documentation. - -# Backporting Features To Prior Releases - -To backport a feature that has been approved and merged into `master`: - -## Requester - -Open an issue requesting the feature you would like backported. The change will be reviewed based on: - -- compatibility -- difficulty in porting the change -- the added value the feature provides for users on the older versions -- how difficult it is for users to migrate from the older version to the newer version - -## Maintainer - -- Checkout the version you want to apply the feature on top of and create a new branch to release the new version: - ``` - git checkout tags/v2.5.1 -b release/2.5.2 - ``` -- Cherrypick the merge commit: `git cherry-pick {git commit SHA}` -- Bump the package version in the csproj -- Make any other compatibility, documentation or tooling related changes -- Push the branch to origin and verify the build -- Once the build is verified, create a GitHub release, tagging the release branch -- Open a PR back to master with any other additions diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000000..f4fa0f699b --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,102 @@ +# I don't want to read this whole thing I just have a question!!! + +> Note: Please don't file an issue to ask a question. + +You'll get faster results by using our official [Gitter channel](https://gitter.im/json-api-dotnet-core/Lobby) or [StackOverflow](https://stackoverflow.com/search?q=jsonapidotnetcore) where the community chimes in with helpful advice if you have questions. + +# How can I contribute? + +## Reporting bugs + +This section guides you through submitting a bug report. +Following these guidelines helps maintainers and the community understand your report, reproduce the behavior and find related reports. + +Before creating bug reports: +- Perform a search to see if the problem has already been reported. If it has and the issue is still open, add a comment to the existing issue instead of opening a new one. If you find a Closed issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. +- Clone the source and run the project locally. You might be able to find the cause of the problem and fix things yourself. Most importantly, check if you can reproduce the problem in the latest version of the master branch. + +When you are creating a bug report, please include as many details as possible. +Fill out the issue template, the information it asks for helps us resolve issues faster. + +### How do I submit a (good) bug report? + +Bugs are tracked as [GitHub issues](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues). Create an issue and provide the following information by filling in the template. +Explain the problem and include additional details to help maintainers reproduce the problem: + +- **Use a clear and descriptive title** for the issue to identify the problem. +- **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, don't just say what you did, but explain how you did it. +- **Provide specific examples to demonstrate the steps.** Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://docs.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks). +- **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. Explain which behavior you expected to see instead and why. +- **If you're reporting a crash**, include the full exception stack trace. + +## Suggesting enhancements + +This section guides you through submitting an enhancement suggestion, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions. + +Before creating enhancement suggestions: +- Check the [documentation](https://www.jsonapi.net/usage/resources/index.html) and [integration tests](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreExampleTests/IntegrationTests) for existing features. You might discover the enhancement is already available. +- Perform a search to see if the feature has already been reported. If it has and the issue is still open, add a comment to the existing issue instead of opening a new one. + +When you are creating an enhancement suggestion, please include as many details as possible. Fill in the template, including the steps that you imagine you would take if the feature you're requesting existed. + +- **Use a clear and descriptive title** for the issue to identify the suggestion. +- **Provide a step-by-step description of the suggested enhancement** in as many details as possible. +- **Provide specific examples to demonstrate the usage.** Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://docs.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks). +- **Describe the current behavior and explain which behavior you expected to see instead** and why. +- **Explain why this enhancement would be useful** to most users and isn't something that can or should be implemented in your API project directly. +- **Verify that your enhancement does not conflict** with the [JSON:API specification](https://jsonapi.org/). + +## Your first code contribution + +Unsure where to begin contributing? You can start by looking through these [beginner](https://github.com/json-api-dotnet/JsonApiDotNetCore/labels/good%20first%20issue) and [help-wanted](https://github.com/json-api-dotnet/JsonApiDotNetCore/labels/help%20wanted) issues. + +## Pull requests + +Please follow these steps to have your contribution considered by the maintainers: + +- **The worst thing in the world is opening a PR that gets rejected** after you've put a lot of effort in it. So for any non-trivial changes, open an issue first to discuss your approach and ensure it fits the product vision. +- Follow all instructions in the template. Don't forget to add tests and update documentation. +- After you submit your pull request, verify that all status checks are passing. In release builds, all compiler warnings are treated as errors, so you should address them before push. + +We use [CSharpGuidelines](https://csharpcodingguidelines.com/) as our coding standard (with a few minor exceptions). Coding style is validated during PR build, where we inject an extra settings layer that promotes various suggestions to warning level. This ensures a high-quality codebase without interfering too much when editing code. +You can run the following [PowerShell scripts](https://github.com/PowerShell/PowerShell/releases) locally: +- `inspectcode.ps1`: Scans the code for style violations and opens the result in your web browser. +- `cleanupcode.ps1` Reformats the entire codebase to match with our configured style. + +Code inspection violations can be addressed in several ways, depending on the situation: +- Types that are reported to be never instantiated (because the IoC container creates them dynamically) should be decorated with `[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]`. +- Exposed models that contain members never being read/written/assigned to should be decorated with `[UsedImplicitly(ImplicitUseTargetFlags.Members)]`. +- Types that are part of our public API surface can be decorated with `[PublicAPI]`. This suppresses messages such as "type can be marked sealed/internal", "virtual member is never overridden", "member is never used" etc. +- Incorrect violations can be [suppressed](https://www.jetbrains.com/help/resharper/Code_Analysis__Code_Inspections.html#ids-of-code-inspections) using a code comment. + +In few cases, the automatic reformat decreases the readability of code. For example, when calling a Fluent API using chained method calls. This can be prevented using [formatter directives](https://www.jetbrains.com/help/resharper/Enforcing_Code_Formatting_Rules.html#configure): + +```c# +public sealed class AppDbContext : DbContext +{ + protected override void OnModelCreating(ModelBuilder builder) + { + // @formatter:wrap_chained_method_calls chop_always + + builder.Entity() + .HasOne(musicTrack => musicTrack.Lyric) + .WithOne(lyric => lyric.Track) + .HasForeignKey(); + + // @formatter:wrap_chained_method_calls restore + } +} +``` + +## Backporting and hotfixes (for maintainers) + +- Checkout the version you want to apply the feature on top of and create a new branch to release the new version: + ``` + git checkout tags/v2.5.1 -b release/2.5.2 + ``` +- Cherrypick the merge commit: `git cherry-pick {git commit SHA}` +- Bump the package version in the csproj +- Make any other compatibility, documentation or tooling related changes +- Push the branch to origin and verify the build +- Once the build is verified, create a GitHub release, tagging the release branch +- Open a PR back to master with any other additions diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 23ead1d25c..ed1eea959b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,6 +4,7 @@ Closes #{ISSUE_NUMBER} #### QUALITY CHECKLIST - [ ] Changes implemented in code +- [ ] Complies with our [contributing guidelines](./.github/CONTRIBUTING.md) - [ ] Adapted tests - [ ] Documentation updated - [ ] Created issue to update [Templates](https://github.com/json-api-dotnet/Templates/issues/new): {ISSUE_NUMBER} diff --git a/Build.ps1 b/Build.ps1 index 25a0bac1f7..e614f84c29 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -17,15 +17,64 @@ function CheckLastExitCode { } } +function RunInspectCode { + $outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml') + dotnet jb inspectcode JsonApiDotNetCore.sln --output="$outputPath" --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -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) { + Write-Output "Running code cleanup in cibuild for pull request" + + $sourceCommitHash = $env:APPVEYOR_PULL_REQUEST_HEAD_COMMIT + $targetCommitHash = git rev-parse "$env:APPVEYOR_REPO_BRANCH" + + Write-Output "Source commit hash = $sourceCommitHash" + Write-Output "Target commit hash = $targetCommitHash" + + dotnet regitlint -s JsonApiDotNetCore.sln --print-command --jb --profile --jb --profile='\"JADNC Full Cleanup\"' --jb --properties:Configuration=Release --jb --verbosity=WARN -f commits -a $sourceCommitHash -b $targetCommitHash --fail-on-diff --print-diff + CheckLastExitCode + } +} + $revision = @{ $true = $env:APPVEYOR_BUILD_NUMBER; $false = 1 }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; $revision = "{0:D4}" -f [convert]::ToInt32($revision, 10) -dotnet restore +dotnet tool restore CheckLastExitCode dotnet build -c Release CheckLastExitCode +RunInspectCode +RunCleanupCode + dotnet test -c Release --no-build CheckLastExitCode diff --git a/JetBrainsInspectCodeTransform.xslt b/JetBrainsInspectCodeTransform.xslt new file mode 100644 index 0000000000..098821f29f --- /dev/null +++ b/JetBrainsInspectCodeTransform.xslt @@ -0,0 +1,51 @@ + + + + + + + + + JetBrains Inspect Code Report + + + + +

JetBrains InspectCode Report

+ + +

+ : +

+ + + + + + + + + + + + + +
FileLine NumberMessage
+ + + + + +
+
+
+
+
+ + +
+
diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings new file mode 100644 index 0000000000..0404af6c5b --- /dev/null +++ b/JsonApiDotNetCore.sln.DotSettings @@ -0,0 +1,629 @@ + + // 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.ArgumentGuard.NotNull($EXPR$, $NAME$); + 199 + 5000 + 99 + 100 + 200 + 1000 + 500 + 3000 + 50 + False + SOLUTION + True + True + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + WARNING + SUGGESTION + SUGGESTION + WARNING + WARNING + HINT + WARNING + WARNING + SUGGESTION + WARNING + SUGGESTION + SUGGESTION + HINT + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + WARNING + DO_NOT_SHOW + HINT + SUGGESTION + SUGGESTION + WARNING + SUGGESTION + SUGGESTION + DO_NOT_SHOW + SUGGESTION + SUGGESTION + SUGGESTION + SUGGESTION + WARNING + SUGGESTION + SUGGESTION + SUGGESTION + WARNING + WARNING + WARNING + SUGGESTION + WARNING + HINT + WARNING + 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" /><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><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><CSReorderTypeMembers>True</CSReorderTypeMembers><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags></Profile> + JADNC Full Cleanup + Required + Required + Required + Required + Conditional + False + 1 + 1 + 1 + 1 + True + True + True + True + True + True + 1 + 1 + False + False + False + False + False + False + False + False + True + NEVER + NEVER + False + NEVER + False + False + NEVER + False + True + False + True + False + False + CHOP_ALWAYS + True + True + True + WRAP_IF_LONG + 160 + WRAP_IF_LONG + CHOP_ALWAYS + CHOP_ALWAYS + True + True + 2 + True + 2 + False + False + 2 + RemoveIndent + RemoveIndent + False + 8 + OneStep + OnSingleLine + 2 + OnSingleLine + False + 150 + False + 2 + False + True + 1 + OneStep + OnSingleLine + True + 2 + OneStep + OnSingleLine + False + 160 + <?xml version="1.0" encoding="utf-16"?> +<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> + <TypePattern DisplayName="Non-reorderable types" RemoveRegions="All"> + <TypePattern.Match> + <Or> + <And> + <Kind Is="Interface" /> + <Or> + <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /> + <HasAttribute Name="System.Runtime.InteropServices.ComImport" /> + </Or> + </And> + <Kind Is="Struct" /> + <HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" /> + <HasAttribute Name="JetBrains.Annotations.NoReorder" /> + </Or> + </TypePattern.Match> + </TypePattern> + <TypePattern DisplayName="xUnit.net Test Classes" RemoveRegions="All"> + <TypePattern.Match> + <And> + <Kind Is="Class" /> + <HasMember> + <And> + <Kind Is="Method" /> + <HasAttribute Name="Xunit.FactAttribute" Inherited="True" /> + </And> + </HasMember> + </And> + </TypePattern.Match> + <Entry DisplayName="Constants"> + <Entry.Match> + <And> + <Kind Is="Constant" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Group DisplayName="Fields"> + <Entry DisplayName="Static Readonly"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Static /> + <Readonly /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Static"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Static /> + <Not> + <Readonly /> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Instance Readonly"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Not> + <Static /> + </Not> + <Readonly /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Instance"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Not> + <And> + <Static /> + <Readonly /> + </And> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Properties"> + <Entry DisplayName="Static"> + <Entry.Match> + <And> + <Kind Is="Property" /> + <Static /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Instance"> + <Entry.Match> + <And> + <Kind Is="Property" /> + <Not> + <Static /> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + </Group> + <Entry DisplayName="Indexers"> + <Entry.Match> + <And> + <Kind Is="Indexer" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Group DisplayName="Events"> + <Entry DisplayName="Static"> + <Entry.Match> + <And> + <Kind Is="Event" /> + <Static /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Instance"> + <Entry.Match> + <And> + <Kind Is="Event" /> + <Not> + <Static /> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + </Group> + <Entry DisplayName="Setup/Teardown methods"> + <Entry.Match> + <Or> + <Kind Is="Constructor" /> + <And> + <Kind Is="Method" /> + <ImplementsInterface Name="System.IDisposable" /> + </And> + </Or> + </Entry.Match> + <Entry.SortBy> + <Kind Order="Constructor" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Test methods" Priority="100"> + <Entry.Match> + <And> + <Kind Is="Method" /> + <Or> + <HasAttribute Name="Xunit.FactAttribute" /> + <HasAttribute Name="Xunit.TheoryAttribute" /> + </Or> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Other methods" Priority="100"> + <Entry.Match> + <Kind Is="Method" /> + </Entry.Match> + </Entry> + <Entry DisplayName="Operators"> + <Entry.Match> + <Kind Is="Operator" /> + </Entry.Match> + </Entry> + <Entry DisplayName="Destructor"> + <Entry.Match> + <And> + <Kind Is="Destructor" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Nested types"> + <Entry.Match> + <Kind Is="Type" /> + </Entry.Match> + </Entry> + </TypePattern> + <TypePattern DisplayName="Default Pattern" RemoveRegions="All"> + <Entry DisplayName="Constants"> + <Entry.Match> + <And> + <Kind Is="Constant" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Group DisplayName="Fields"> + <Entry DisplayName="Static Readonly"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Static /> + <Readonly /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Static"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Static /> + <Not> + <Readonly /> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Instance Readonly"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Not> + <Static /> + </Not> + <Readonly /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Instance"> + <Entry.Match> + <And> + <Kind Is="Field" /> + <Not> + <And> + <Static /> + <Readonly /> + </And> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Properties"> + <Entry DisplayName="Static"> + <Entry.Match> + <And> + <Kind Is="Property" /> + <Static /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Instance"> + <Entry.Match> + <And> + <Kind Is="Property" /> + <Not> + <Static /> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + </Group> + <Entry DisplayName="Indexers"> + <Entry.Match> + <And> + <Kind Is="Indexer" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Group DisplayName="Events"> + <Entry DisplayName="Static"> + <Entry.Match> + <And> + <Kind Is="Event" /> + <Static /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Instance"> + <Entry.Match> + <And> + <Kind Is="Event" /> + <Not> + <Static /> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + </Group> + <Group DisplayName="Constructors"> + <Entry DisplayName="Static"> + <Entry.Match> + <And> + <Kind Is="Constructor" /> + <Static /> + </And> + </Entry.Match> + </Entry> + <Entry DisplayName="Instance"> + <Entry.Match> + <And> + <Kind Is="Constructor" /> + <Not> + <Static /> + </Not> + </And> + </Entry.Match> + <Entry.SortBy> + <Access /> + </Entry.SortBy> + </Entry> + </Group> + <Entry DisplayName="Methods"> + <Entry.Match> + <Kind Is="Method" /> + </Entry.Match> + </Entry> + <Entry DisplayName="Operators"> + <Entry.Match> + <Kind Is="Operator" /> + </Entry.Match> + </Entry> + <Entry DisplayName="Destructor"> + <Entry.Match> + <And> + <Kind Is="Destructor" /> + </And> + </Entry.Match> + <Entry.SortBy> + <Access Order="Private Internal Protected ProtectedInternal Public" /> + </Entry.SortBy> + </Entry> + <Entry DisplayName="Nested types"> + <Entry.Match> + <Kind Is="Type" /> + </Entry.Match> + </Entry> + </TypePattern> +</Patterns> + UseExplicitType + UseVarWhenEvident + UseVarWhenEvident + True + False + False + False + False + <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + True + True + True + True + True + True + Replace argument null check using throw expression with Guard clause + True + True + False + + IdentifierPlaceholder + True + True + False + + IdentifierPlaceholder + True + True + False + + IdentifierPlaceholder + True + CSHARP + False + Replace argument null check with Guard clause + JsonApiDotNetCore.ArgumentGuard.NotNull($argument$, nameof($argument$)); +$left$ = $right$; + $left$ = $right$ ?? throw new ArgumentNullException(nameof($argument$)); + SUGGESTION + True + Replace classic argument null check with Guard clause + True + True + False + + IdentifierPlaceholder + True + CSHARP + False + Replace argument null check with Guard clause + JsonApiDotNetCore.ArgumentGuard.NotNull($argument$, nameof($argument$)); + if ($argument$ == null) throw new ArgumentNullException(nameof($argument$)); + SUGGESTION + True + Replace collection null/empty check with extension method + True + False + + ExpressionPlaceholder + True + CSHARP + False + Replace collection null/empty check with extension method + $collection$.IsNullOrEmpty() + $collection$ == null || !$collection$.Any() + SUGGESTION + True + True + True + True + True + True + True + True + diff --git a/README.md b/README.md index 4e0f0d1c27..690f8ae866 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ See [our documentation](https://www.jsonapi.net/) for detailed usage. ### Models -```csharp +```c# public class Article : Identifiable { [Attr] @@ -52,7 +52,7 @@ public class Article : Identifiable ### Controllers -```csharp +```c# public class ArticlesController : JsonApiController
{ public ArticlesController(IJsonApiOptions options, IResourceService
resourceService, @@ -65,7 +65,7 @@ public class ArticlesController : JsonApiController
### Middleware -```csharp +```c# public class Startup { public IServiceProvider ConfigureServices(IServiceCollection services) @@ -104,9 +104,9 @@ And then to run the tests: dotnet test ``` -### Compiler warnings +## Contributing -The `Release` build configuration is set to fail on warnings. That means when submitting a PR there shouldn't be any compiler warnings because the CI build it set to `Release`. +Have a question, found a bug or want to submit code changes? See our [contributing guidelines](./.github/CONTRIBUTING.md). ## Compatibility diff --git a/benchmarks/BenchmarkResource.cs b/benchmarks/BenchmarkResource.cs index 50c132c8a7..acc1511844 100644 --- a/benchmarks/BenchmarkResource.cs +++ b/benchmarks/BenchmarkResource.cs @@ -7,7 +7,7 @@ namespace Benchmarks [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class BenchmarkResource : Identifiable { - [Attr(PublicName = BenchmarkResourcePublicNames.NameAttr)] + [Attr(PublicName = BenchmarkResourcePublicNames.NameAttr)] public string Name { get; set; } [HasOne] diff --git a/benchmarks/BenchmarkResourcePublicNames.cs b/benchmarks/BenchmarkResourcePublicNames.cs index b97db1ea64..b8d6fdae12 100644 --- a/benchmarks/BenchmarkResourcePublicNames.cs +++ b/benchmarks/BenchmarkResourcePublicNames.cs @@ -5,4 +5,4 @@ internal static class BenchmarkResourcePublicNames public const string NameAttr = "full-name"; public const string Type = "simple-types"; } -} \ No newline at end of file +} diff --git a/benchmarks/DependencyFactory.cs b/benchmarks/DependencyFactory.cs index 62aee6f301..d5ca4af6b6 100644 --- a/benchmarks/DependencyFactory.cs +++ b/benchmarks/DependencyFactory.cs @@ -7,7 +7,7 @@ internal static class DependencyFactory { public static IResourceGraph CreateResourceGraph(IJsonApiOptions options) { - ResourceGraphBuilder builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); + var builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); builder.Add(BenchmarkResourcePublicNames.Type); return builder.Build(); } diff --git a/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs b/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs index 3486ce30d9..b3dbef6232 100644 --- a/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs +++ b/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs @@ -5,7 +5,9 @@ namespace Benchmarks.LinkBuilder { // ReSharper disable once ClassCanBeSealed.Global - [MarkdownExporter, SimpleJob(launchCount: 3, warmupCount: 10, targetCount: 20), MemoryDiagnoser] + [MarkdownExporter] + [SimpleJob(3, 10, 20)] + [MemoryDiagnoser] public class LinkBuilderGetNamespaceFromPathBenchmarks { private const string RequestPath = "/api/some-really-long-namespace-path/resources/current/articles/?some"; @@ -13,14 +15,20 @@ public class LinkBuilderGetNamespaceFromPathBenchmarks private const char PathDelimiter = '/'; [Benchmark] - public void UsingStringSplit() => GetNamespaceFromPathUsingStringSplit(RequestPath, ResourceName); + public void UsingStringSplit() + { + GetNamespaceFromPathUsingStringSplit(RequestPath, ResourceName); + } [Benchmark] - public void UsingReadOnlySpan() => GetNamespaceFromPathUsingReadOnlySpan(RequestPath, ResourceName); + public void UsingReadOnlySpan() + { + GetNamespaceFromPathUsingReadOnlySpan(RequestPath, ResourceName); + } private static void GetNamespaceFromPathUsingStringSplit(string path, string resourceName) { - StringBuilder namespaceBuilder = new StringBuilder(path.Length); + var namespaceBuilder = new StringBuilder(path.Length); string[] segments = path.Split('/'); for (int index = 1; index < segments.Length; index++) @@ -56,6 +64,7 @@ private static void GetNamespaceFromPathUsingReadOnlySpan(string path, string re bool isAtEnd = lastCharacterIndex == pathSpan.Length; bool hasDelimiterAfterSegment = pathSpan.Length >= lastCharacterIndex + 1 && pathSpan[lastCharacterIndex].Equals(PathDelimiter); + if (isAtEnd || hasDelimiterAfterSegment) { _ = pathSpan.Slice(0, index).ToString(); diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs index 963b0322e8..0d745a795d 100644 --- a/benchmarks/Program.cs +++ b/benchmarks/Program.cs @@ -16,6 +16,7 @@ private static void Main(string[] args) typeof(QueryParserBenchmarks), typeof(LinkBuilderGetNamespaceFromPathBenchmarks) }); + switcher.Run(args); } } diff --git a/benchmarks/Query/QueryParserBenchmarks.cs b/benchmarks/Query/QueryParserBenchmarks.cs index f34f37f919..8ba23d56f0 100644 --- a/benchmarks/Query/QueryParserBenchmarks.cs +++ b/benchmarks/Query/QueryParserBenchmarks.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.ComponentModel.Design; using BenchmarkDotNet.Attributes; using JsonApiDotNetCore; @@ -14,7 +15,9 @@ namespace Benchmarks.Query { // ReSharper disable once ClassCanBeSealed.Global - [MarkdownExporter, SimpleJob(launchCount: 3, warmupCount: 10, targetCount: 20), MemoryDiagnoser] + [MarkdownExporter] + [SimpleJob(3, 10, 20)] + [MemoryDiagnoser] public class QueryParserBenchmarks { private readonly FakeRequestQueryStringAccessor _queryStringAccessor = new FakeRequestQueryStringAccessor(); @@ -40,18 +43,18 @@ public QueryParserBenchmarks() _queryStringReaderForAll = CreateQueryParameterDiscoveryForAll(resourceGraph, request, options, _queryStringAccessor); } - private static QueryStringReader CreateQueryParameterDiscoveryForSort(IResourceGraph resourceGraph, - JsonApiRequest request, IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor) + private static QueryStringReader CreateQueryParameterDiscoveryForSort(IResourceGraph resourceGraph, JsonApiRequest request, IJsonApiOptions options, + FakeRequestQueryStringAccessor queryStringAccessor) { var sortReader = new SortQueryStringParameterReader(request, resourceGraph); - var readers = sortReader.AsEnumerable(); + IEnumerable readers = sortReader.AsEnumerable(); return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance); } - private static QueryStringReader CreateQueryParameterDiscoveryForAll(IResourceGraph resourceGraph, - JsonApiRequest request, IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor) + private static QueryStringReader CreateQueryParameterDiscoveryForAll(IResourceGraph resourceGraph, JsonApiRequest request, IJsonApiOptions options, + FakeRequestQueryStringAccessor queryStringAccessor) { var resourceFactory = new ResourceFactory(new ServiceContainer()); @@ -63,7 +66,8 @@ private static QueryStringReader CreateQueryParameterDiscoveryForAll(IResourceGr var defaultsReader = new DefaultsQueryStringParameterReader(options); var nullsReader = new NullsQueryStringParameterReader(options); - var readers = ArrayFactory.Create(includeReader, filterReader, sortReader, sparseFieldSetReader, paginationReader, defaultsReader, nullsReader); + IQueryStringParameterReader[] readers = ArrayFactory.Create(includeReader, filterReader, sortReader, + sparseFieldSetReader, paginationReader, defaultsReader, nullsReader); return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance); } @@ -71,7 +75,7 @@ private static QueryStringReader CreateQueryParameterDiscoveryForAll(IResourceGr [Benchmark] public void AscendingSort() { - var queryString = $"?sort={BenchmarkResourcePublicNames.NameAttr}"; + string queryString = $"?sort={BenchmarkResourcePublicNames.NameAttr}"; _queryStringAccessor.SetQueryString(queryString); _queryStringReaderForSort.ReadAll(null); @@ -80,23 +84,26 @@ public void AscendingSort() [Benchmark] public void DescendingSort() { - var queryString = $"?sort=-{BenchmarkResourcePublicNames.NameAttr}"; + string queryString = $"?sort=-{BenchmarkResourcePublicNames.NameAttr}"; _queryStringAccessor.SetQueryString(queryString); _queryStringReaderForSort.ReadAll(null); } [Benchmark] - public void ComplexQuery() => Run(100, () => + public void ComplexQuery() { - const string resourceName = BenchmarkResourcePublicNames.Type; - const string attrName = BenchmarkResourcePublicNames.NameAttr; + Run(100, () => + { + const string resourceName = BenchmarkResourcePublicNames.Type; + const string attrName = BenchmarkResourcePublicNames.NameAttr; - var queryString = $"?filter[{attrName}]=abc,eq:abc&sort=-{attrName}&include=child&page[size]=1&fields[{resourceName}]={attrName}"; + string queryString = $"?filter[{attrName}]=abc,eq:abc&sort=-{attrName}&include=child&page[size]=1&fields[{resourceName}]={attrName}"; - _queryStringAccessor.SetQueryString(queryString); - _queryStringReaderForAll.ReadAll(null); - }); + _queryStringAccessor.SetQueryString(queryString); + _queryStringReaderForAll.ReadAll(null); + }); + } private void Run(int iterations, Action action) { diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs index 3d43a1fe87..9ecc308e7b 100644 --- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs @@ -44,6 +44,9 @@ public JsonApiDeserializerBenchmarks() } [Benchmark] - public object DeserializeSimpleObject() => _jsonApiDeserializer.Deserialize(Content); + public object DeserializeSimpleObject() + { + return _jsonApiDeserializer.Deserialize(Content); + } } } diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs index c3f613ab56..088be638c4 100644 --- a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs @@ -29,9 +29,9 @@ public JsonApiSerializerBenchmarks() IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options); IFieldsToSerialize fieldsToSerialize = CreateFieldsToSerialize(resourceGraph); - var metaBuilder = new Mock().Object; - var linkBuilder = new Mock().Object; - var includeBuilder = new Mock().Object; + IMetaBuilder metaBuilder = new Mock().Object; + ILinkBuilder linkBuilder = new Mock().Object; + IIncludedResourceObjectBuilder includeBuilder = new Mock().Object; var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings()); @@ -48,12 +48,15 @@ private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resource new SparseFieldSetQueryStringParameterReader(request, resourceGraph) }; - var accessor = new Mock().Object; + IResourceDefinitionAccessor accessor = new Mock().Object; return new FieldsToSerialize(resourceGraph, constraintProviders, accessor, request); } [Benchmark] - public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(Content); + public object SerializeSimpleObject() + { + return _jsonApiSerializer.Serialize(Content); + } } } diff --git a/cleanupcode.ps1 b/cleanupcode.ps1 new file mode 100644 index 0000000000..605ebff705 --- /dev/null +++ b/cleanupcode.ps1 @@ -0,0 +1,17 @@ +#Requires -Version 7.0 + +# This script reformats the entire codebase to make it compliant with our coding guidelines. + +dotnet tool restore + +if ($LASTEXITCODE -ne 0) { + throw "Tool restore failed with exit code $LASTEXITCODE" +} + +dotnet build -c Release + +if ($LASTEXITCODE -ne 0) { + throw "Build failed with exit code $LASTEXITCODE" +} + +dotnet regitlint -s JsonApiDotNetCore.sln --print-command --jb --profile --jb --profile='\"JADNC Full Cleanup\"' --jb --properties:Configuration=Release --jb --verbosity=WARN diff --git a/docs/README.md b/docs/README.md index c716490bb7..bd33197f00 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,15 +4,18 @@ In addition, the example request/response pairs are generated by executing `curl # Installation Run the following commands once to setup your system: + ``` choco install docfx -y ``` + ``` npm install -g httpserver ``` # Running The next command regenerates the documentation website and opens it in your default browser: + ``` pwsh ./build-dev.ps1 ``` diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 971bf93466..11a5e53471 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -3,16 +3,19 @@ Click [here](https://www.nuget.org/packages/JsonApiDotnetCore/) for the latest NuGet version. ### CLI + ``` dotnet add package JsonApiDotnetCore ``` ### Visual Studio + ```powershell Install-Package JsonApiDotnetCore ``` ### *.csproj + ```xml diff --git a/docs/getting-started/step-by-step.md b/docs/getting-started/step-by-step.md index 3924601dcf..d6d530d750 100644 --- a/docs/getting-started/step-by-step.md +++ b/docs/getting-started/step-by-step.md @@ -53,7 +53,9 @@ Nothing special here, just an ordinary `DbContext` public class AppDbContext : DbContext { public AppDbContext(DbContextOptions options) - : base(options) { } + : base(options) + { + } public DbSet People { get; set; } } @@ -67,12 +69,11 @@ where `TResource` is the model that inherits from `Identifiable` ```c# public class PeopleController : JsonApiController { - public PeopleController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, + public PeopleController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) - : base(options, loggerFactory, resourceService) - { } + : base(options, loggerFactory, resourceService) + { + } } ``` @@ -123,6 +124,7 @@ public void Configure(IApplicationBuilder app, AppDbContext context) { Name = "John Doe" }); + context.SaveChanges(); } diff --git a/docs/usage/errors.md b/docs/usage/errors.md index 1ed623802f..96722739b4 100644 --- a/docs/usage/errors.md +++ b/docs/usage/errors.md @@ -5,6 +5,7 @@ You can create a custom error by throwing a `JsonApiException` (which accepts an Please keep in mind that JSON:API requires Title to be a generic message, while Detail should contain information about the specific problem occurence. From a controller method: + ```c# return Conflict(new Error(HttpStatusCode.Conflict) { @@ -14,6 +15,7 @@ return Conflict(new Error(HttpStatusCode.Conflict) ``` From other code: + ```c# throw new JsonApiException(new Error(HttpStatusCode.Conflict) { diff --git a/docs/usage/extensibility/controllers.md b/docs/usage/extensibility/controllers.md index 7ff26077e5..f46c9c108e 100644 --- a/docs/usage/extensibility/controllers.md +++ b/docs/usage/extensibility/controllers.md @@ -5,12 +5,11 @@ You need to create controllers that inherit from `JsonApiController` ```c# public class ArticlesController : JsonApiController
{ - public ArticlesController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, + public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) : base(options, loggerFactory, resourceService) - { } + { + } } ``` @@ -22,13 +21,12 @@ If your model is using a type other than `int` for the primary key, you must exp public class ArticlesController : JsonApiController //---------------------------------------------------------- ^^^^ { - public ArticlesController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, + public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) //----------------------- ^^^^ : base(options, loggerFactory, resourceService) - { } + { + } } ``` @@ -43,12 +41,11 @@ This approach is ok, but introduces some boilerplate that can easily be avoided. ```c# public class ArticlesController : BaseJsonApiController
{ - public ArticlesController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, + public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) : base(options, loggerFactory, resourceService) - { } + { + } [HttpGet] public override async Task GetAsync(CancellationToken cancellationToken) @@ -80,12 +77,11 @@ An attempt to use one of the blacklisted methods will result in a HTTP 405 Metho [HttpReadOnly] public class ArticlesController : BaseJsonApiController
{ - public ArticlesController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, + public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) : base(options, loggerFactory, resourceService) - { } + { + } } ``` @@ -100,12 +96,11 @@ For more information about resource injection, see the next section titled Resou ```c# public class ReportsController : BaseJsonApiController { - public ReportsController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, + public ReportsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) - { } + { + } [HttpGet] public override async Task GetAsync(CancellationToken cancellationToken) diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md index 9a95296754..7cbadd1442 100644 --- a/docs/usage/extensibility/middleware.md +++ b/docs/usage/extensibility/middleware.md @@ -5,14 +5,16 @@ It is possible to replace JsonApiDotNetCore middleware components by configuring ## Configuring the IoC container The following example replaces the internal exception filter with a custom implementation. + ```c# /// In Startup.ConfigureServices -services.AddService() +services.AddService(); ``` ## Configuring `MvcOptions` The following example replaces all internal filters with a custom filter. + ```c# public class Startup { @@ -22,7 +24,7 @@ public class Startup { services.AddSingleton(); - var builder = services.AddMvcCore(); + IMvcCoreBuilder builder = services.AddMvcCore(); services.AddJsonApi(mvcBuilder: builder); // Ensure this call is placed after the AddJsonApi call. @@ -35,8 +37,8 @@ public class Startup public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // Ensure this call is placed before the UseEndpoints call. - _postConfigureMvcOptions = mvcOptions => - { + _postConfigureMvcOptions = mvcOptions => + { mvcOptions.Filters.Clear(); mvcOptions.Filters.Insert(0, app.ApplicationServices.GetService()); diff --git a/docs/usage/extensibility/repositories.md b/docs/usage/extensibility/repositories.md index ac9a447389..05943b5aa1 100644 --- a/docs/usage/extensibility/repositories.md +++ b/docs/usage/extensibility/repositories.md @@ -36,12 +36,9 @@ public class ArticleRepository : EntityFrameworkCoreRepository
{ private readonly IAuthenticationService _authenticationService; - public ArticleRepository( - IAuthenticationService authenticationService, - ITargetedFields targetedFields, - IDbContextResolver contextResolver, - IResourceGraph resourceGraph, - IGenericServiceFactory genericServiceFactory, + public ArticleRepository(IAuthenticationService authenticationService, + ITargetedFields targetedFields, IDbContextResolver contextResolver, + IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) @@ -68,13 +65,15 @@ This example shows a single `DbContextARepository` for all entities that are mem public class DbContextARepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { - public DbContextARepository(ITargetedFields targetedFields, DbContextResolver contextResolver, - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + public DbContextARepository(ITargetedFields targetedFields, + DbContextResolver contextResolver, + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ IResourceGraph resourceGraph, IGenericServiceFactory genericServiceFactory, - IResourceFactory resourceFactory, IEnumerable constraintProviders, + IResourceFactory resourceFactory, + IEnumerable constraintProviders, ILoggerFactory loggerFactory) - : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, - constraintProviders, loggerFactory) + : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, + resourceFactory, constraintProviders, loggerFactory) { } } @@ -91,5 +90,5 @@ services.AddDbContext(options => options.UseSqlite("Data Source=B.db services.AddScoped, DbContextARepository>(); services.AddScoped, DbContextBRepository>(); -services.AddJsonApi(dbContextTypes: new[] {typeof(DbContextA), typeof(DbContextB)}); +services.AddJsonApi(dbContextTypes: new[] { typeof(DbContextA), typeof(DbContextB) }); ``` diff --git a/docs/usage/extensibility/services.md b/docs/usage/extensibility/services.md index d033648cc0..7880189eee 100644 --- a/docs/usage/extensibility/services.md +++ b/docs/usage/extensibility/services.md @@ -15,22 +15,19 @@ public class TodoItemService : JsonApiResourceService { private readonly INotificationService _notificationService; - public TodoItemService( - IResourceRepositoryAccessor repositoryAccessor, - IQueryLayerComposer queryLayerComposer, - IPaginationContext paginationContext, - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IJsonApiRequest request, + public TodoItemService(IResourceRepositoryAccessor repositoryAccessor, + IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext, + IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, - request, resourceChangeTracker, hookExecutor) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, + loggerFactory, request, resourceChangeTracker, hookExecutor) { _notificationService = notificationService; } - public override async Task CreateAsync(TodoItem resource, CancellationToken cancellationToken) + public override async Task CreateAsync(TodoItem resource, + CancellationToken cancellationToken) { // Call the base implementation var newResource = await base.CreateAsync(resource, cancellationToken); @@ -69,7 +66,8 @@ public class ProductService : IResourceService _dao = dao; } - public async Task> GetAsync(CancellationToken cancellationToken) + public async Task> GetAsync( + CancellationToken cancellationToken) { return await _dao.GetProductsAsync(cancellationToken); } @@ -152,22 +150,22 @@ Then in the controller, you should inherit from the base controller and pass the ```c# public class ArticlesController : BaseJsonApiController
{ - public ArticlesController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - ICreateService create, - IDeleteService delete) + public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory, + ICreateService create, IDeleteService delete) : base(options, loggerFactory, create: create, delete: delete) - { } + { + } [HttpPost] - public override async Task PostAsync([FromBody] Article resource, CancellationToken cancellationToken) + public override async Task PostAsync([FromBody] Article resource, + CancellationToken cancellationToken) { return await base.PostAsync(resource, cancellationToken); } [HttpDelete("{id}")] - public override async TaskDeleteAsync(int id, CancellationToken cancellationToken) + public override async TaskDeleteAsync(int id, + CancellationToken cancellationToken) { return await base.DeleteAsync(id, cancellationToken); } diff --git a/docs/usage/meta.md b/docs/usage/meta.md index 0e91f8ea4f..6f052103e4 100644 --- a/docs/usage/meta.md +++ b/docs/usage/meta.md @@ -18,7 +18,7 @@ public sealed class CopyrightResponseMeta : IResponseMeta return new Dictionary { ["copyright"] = "Copyright (C) 2002 Umbrella Corporation.", - ["authors"] = new[] {"Alice", "Red Queen"} + ["authors"] = new[] { "Alice", "Red Queen" } }; } } @@ -44,7 +44,8 @@ Resource-specific metadata can be added by implementing `IResourceDefinition { - public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public PersonDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { } @@ -54,7 +55,8 @@ public class PersonDefinition : JsonApiResourceDefinition { return new Dictionary { - ["notice"] = "Check our intranet at http://www.example.com/employees/" + person.StringId + " for personal details." + ["notice"] = "Check our intranet at http://www.example.com/employees/" + + person.StringId + " for personal details." }; } diff --git a/docs/usage/options.md b/docs/usage/options.md index beeea414db..287c1c52f1 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -90,6 +90,7 @@ options.SerializerSettings.Formatting = Formatting.Indented; ``` The default naming convention (as used in the routes and resource/attribute/relationship names) is also determined here, and can be changed (default is camel-case): + ```c# options.SerializerSettings.ContractResolver = new DefaultContractResolver { diff --git a/docs/usage/reading/filtering.md b/docs/usage/reading/filtering.md index dbf6d81c7f..90b03462d8 100644 --- a/docs/usage/reading/filtering.md +++ b/docs/usage/reading/filtering.md @@ -33,9 +33,11 @@ Comparison operators compare an attribute against a constant value (between quot ```http GET /users?filter=equals(displayName,'Brian O''Connor') HTTP/1.1 ``` + ```http GET /users?filter=equals(displayName,null) HTTP/1.1 ``` + ```http GET /users?filter=equals(displayName,lastName) HTTP/1.1 ``` @@ -45,6 +47,7 @@ Comparison operators can be combined with the `count` function, which acts on Ha ```http GET /blogs?filter=lessThan(count(owner.articles),'10') HTTP/1.1 ``` + ```http GET /customers?filter=greaterThan(count(orders),count(invoices)) HTTP/1.1 ``` diff --git a/docs/usage/reading/sparse-fieldset-selection.md b/docs/usage/reading/sparse-fieldset-selection.md index 8a84001b88..b5510be945 100644 --- a/docs/usage/reading/sparse-fieldset-selection.md +++ b/docs/usage/reading/sparse-fieldset-selection.md @@ -5,11 +5,13 @@ Put the resource type to apply the fieldset on between the brackets. This can be used on the resource being requested, as well as on nested endpoints and/or included resources. Top-level example: + ```http GET /articles?fields[articles]=title,body,comments HTTP/1.1 ``` Nested endpoint example: + ```http GET /api/blogs/1/articles?fields[articles]=title,body,comments HTTP/1.1 ``` @@ -17,16 +19,19 @@ GET /api/blogs/1/articles?fields[articles]=title,body,comments HTTP/1.1 When combined with the `include` query string parameter, a subset of related fields can be specified too. Example for an included HasOne relationship: + ```http GET /articles?include=author&fields[authors]=name HTTP/1.1 ``` Example for an included HasMany relationship: + ```http GET /articles?include=revisions&fields[revisions]=publishTime HTTP/1.1 ``` Example for both top-level and relationship: + ```http GET /articles?include=author&fields[articles]=title,body,author&fields[authors]=name HTTP/1.1 ``` diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index 59fbe7bfc2..e2cd8d8ad4 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -86,13 +86,17 @@ services.AddJsonApi(resources: builder => 2. The model is decorated with a `ResourceAttribute` ```c# [Resource("myResources")] -public class MyModel : Identifiable { } +public class MyModel : Identifiable +{ +} ``` 3. The configured naming convention (by default this is camel-case). ```c# // this will be registered as "myModels" -public class MyModel : Identifiable { } +public class MyModel : Identifiable +{ +} ``` The default naming convention can be changed in [options](~/usage/options.md#custom-serializer-settings). diff --git a/docs/usage/resources/hooks.md b/docs/usage/resources/hooks.md index cd08f38fce..68295fa44a 100644 --- a/docs/usage/resources/hooks.md +++ b/docs/usage/resources/hooks.md @@ -1,9 +1,11 @@ # Resource Hooks -This section covers the usage of **Resource Hooks**, which is a feature of`ResourceDefinition`. See the [ResourceDefinition usage guide](resource-definitions.md) for a general explanation on how to set up a `ResourceDefinition`. For a quick start, jump right to the [Getting started: most minimal example](#getting-started-most-minimal-example) section. +This section covers the usage of **Resource Hooks**, which is a feature of`ResourceHooksDefinition`. See the [ResourceDefinition usage guide](resource-definitions.md) for a general explanation on how to set up a `JsonApiResourceDefinition`. For a quick start, jump right to the [Getting started: most minimal example](#getting-started-most-minimal-example) section. -By implementing resource hooks on a `ResourceDefintion`, it is possible to intercept the execution of the **Resource Service Layer** (RSL) in various ways. This enables the developer to conveniently define business logic without having to override the RSL. It can be used to implement e.g. +> Note: Resource Hooks are an experimental feature and are turned off by default. They are subject to change or be replaced in a future version. + +By implementing resource hooks on a `ResourceHooksDefintion`, it is possible to intercept the execution of the **Resource Service Layer** (RSL) in various ways. This enables the developer to conveniently define business logic without having to override the RSL. It can be used to implement e.g. * Authorization * [Event-based synchronisation between microservices](https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/integration-event-based-microservice-communications) * Logging @@ -11,7 +13,7 @@ By implementing resource hooks on a `ResourceDefintion`, it is possible to in This usage guide covers the following sections 1. [**Semantics: pipelines, actions and hooks**](#1-semantics-pipelines-actions-and-hooks) -Understanding the semantics will be helpful in identifying which hooks on `ResourceDefinition` you need to implement for your use-case. +Understanding the semantics will be helpful in identifying which hooks on `ResourceHooksDefinition` you need to implement for your use-case. 2. [**Basic usage**](#2-basic-usage) Some examples to get you started. * [**Getting started: most minimal example**](#getting-started-most-minimal-example) @@ -37,14 +39,14 @@ The different execution flows within the RSL that may be intercepted can be iden * **Get**: reading a resource (triggered by the endpoint `GET /my-resource`). * **GetSingle**: reading a single resource (triggered by the endpoint `GET /my-resource/1`). -See the [ResourcePipeline](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/feat/%23477/src/JsonApiDotNetCore/Hooks/Execution/ResourcePipelineEnum.cs) enum for a full list of available pipelines. +See the [ResourcePipeline](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/745d2fb6b6c9dd21ff794284a193977fdc699fe6/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs) enum for a full list of available pipelines. ## Actions -Each pipeline is associated with a set of **actions** that work on resources and their relationships. These actions reflect the associated database operations that is performed by JsonApiDotNetCore (in the Repository Layer). Typically, the RSL will execute some service-layer-related code, then invoke the Repository Layer which will perform these actions, after which the execution returns to the RSL. +Each pipeline is associated with a set of **actions** that work on resources and their relationships. These actions reflect the associated database operations that are performed by JsonApiDotNetCore (in the Repository Layer). Typically, the RSL will execute some service-layer-related code, then invoke the Repository Layer which will perform these actions, after which the execution returns to the RSL. -Note that some actions are shared across different pipelines, and note that most pipelines perform multiple actions. There are two types of actions: **main resource actions** and **nested resource actions**. +Note that some actions are shared across different pipelines, and note that most pipelines perform multiple actions. There are two types of actions: **primary resource actions** and **nested resource actions**. -### Main resource actions +### Primary resource actions Most actions are trivial in the context of the pipeline where they're executed from. They may be recognised as the familiar *CRUD* operations of an API. These actions are: * The `create` action: the **Post** pipeline will `create` a resource @@ -52,7 +54,7 @@ Most actions are trivial in the context of the pipeline where they're executed f * The `update` action: the **Patch** pipeline will `update` a resource. * The `delete` action: the **Delete** pipeline will `delete` a resource. -These actions are called the **main resource actions** of a particular pipeline because **they act on the request resource**. For example, when an `Article` is created through the **Post** pipeline, its main action, `create`, will work on that `Article`. +These actions are called the **primary resource actions** of a particular pipeline because **they act on the request resource**. For example, when an `Article` is created through the **Post** pipeline, its main action, `create`, will work on that `Article`. ### Nested Resource Actions Some other actions might be overlooked, namely the nested resource actions. These actions are @@ -61,13 +63,13 @@ Some other actions might be overlooked, namely the nested resource actions. Thes * `implicit update relationship` for implicitly affected relationships * `read` for included relationships -These actions are called **nested resource actions** of a particular pipeline because **they act on involved (nested) resources** instead of the main request resource. For example, when loading articles and their respective authors (`GET /articles?include=author`), the `read` action on `Article` is the main action, and the `read` action on `Person` is the nested action. +These actions are called **nested resource actions** of a particular pipeline because **they act on involved (nested) resources** instead of the primary request resource. For example, when loading articles and their respective authors (`GET /articles?include=author`), the `read` action on `Article` is the primary action, and the `read` action on `Person` is the nested action. -**The `update relationship` action** +#### The `update relationship` action [As per the Json:Api specification](https://jsonapi.org/format/#crud-creating](https://jsonapi.org/format/#crud-creating), the **Post** pipeline also allows for an `update relationship` action on an already existing resource. For example, when creating an `Article` it is possible to simultaneously relate it to an existing `Person` by setting its author. In this case, the `update relationship` action is a nested action that will work on that `Person`. -**The `implicit update relationship` action** -the **Delete** pipeline also allows for an `implicit update relationship` action on an already existing resource. For example, for an `Article` that its author property assigned to a particular `Person`, the relationship between them is destroyed when this article is deleted. This update is "implicit" in the sense that no explicit information about that `Person` was provided in the request that triggered this pipeline. An `implicit update relationship` action is therefore performed on that `Person`. See [this section](#advanced-authorization-implicitly-affected-resources) for a more detailed. +#### The `implicit update relationship` action +the **Delete** pipeline also allows for an `implicit update relationship` action on an already existing resource. For example, for an `Article` that its author property assigned to a particular `Person`, the relationship between them is destroyed when this article is deleted. This update is "implicit" in the sense that no explicit information about that `Person` was provided in the request that triggered this pipeline. An `implicit update relationship` action is therefore performed on that `Person`. See [this section](#advanced-authorization-implicitly-affected-resources) for a more detailed explanation. ### Shared actions Note that **some actions are shared across pipelines**. For example, both the **Post** and **Patch** pipeline can perform the `update relationship` action on an (already existing) involved resource. Similarly, the **Get** and **GetSingle** pipelines perform the same `read` action. @@ -75,21 +77,21 @@ Note that **some actions are shared across pipelines**. For example, both the ** For a complete list of actions associated with each pipeline, see the [overview table](#4-hook-execution-overview). ## Hooks -For all actions it is possible to implement **at least one hook** to intercept its execution. These hooks can be implemented by overriding the corresponding virtual implementation on `ResourceDefintion`. (Note that the base implementation is a dummy implementation, which is ignored when firing hooks.) +For all actions it is possible to implement **at least one hook** to intercept its execution. These hooks can be implemented by overriding the corresponding virtual implementation on `ResourceHooksDefintion`. (Note that the base implementation is a dummy implementation, which is ignored when firing hooks.) ### Action related hooks As an example, consider the `create` action for the `Article` Resource. This action can be intercepted by overriding the -* `ResourceDefintion
.BeforeCreate` hook for custom logic **just before** execution of the main `create` action -* `ResourceDefintion
.AfterCreate` hook for custom logic **just after** execution of the main `create` action +* `ResourceHooksDefinition
.BeforeCreate` hook for custom logic **just before** execution of the main `create` action +* `ResourceHooksDefinition
.AfterCreate` hook for custom logic **just after** execution of the main `create` action If with the creation of an `Article` a relationship to `Person` is updated simultaneously, this can be intercepted by overriding the -* `ResourceDefintion.BeforeUpdateRelationship` hook for custom logic **just before** the execution of the nested `update relationship` action. -* `ResourceDefintion.AfterUpdateRelationship` hook for custom logic **just after** the execution of the nested `update relationship` action. +* `ResourceHooksDefinition.BeforeUpdateRelationship` hook for custom logic **just before** the execution of the nested `update relationship` action. +* `ResourceHooksDefinition.AfterUpdateRelationship` hook for custom logic **just after** the execution of the nested `update relationship` action. ### OnReturn hook As mentioned in the previous section, some actions are shared across hooks. One of these actions is the `return` action. Although not strictly compatible with the *CRUD* vocabulary, and although not executed by the Repository Layer, pipelines are also said to perform a `return` action when any content is to be returned from the API. For example, the **Delete** pipeline does not return any content, but a *HTTP 204 No Content* instead, and will therefore not perform a `return` action. On the contrary, the **Get** pipeline does return content, and will therefore perform a `return action` -Any return content can be intercepted and transformed as desired by implementing the `ResourceDefintion.OnReturn` hook which intercepts the `return` action. For this action, there is no distinction between a `Before` and `After` hook, because no code after a `return` statement can be evaluated. Note that the `return` action can work on *main resources as well as nested resources*, see [this example below](#transforming-data-with-onreturn). +Any return content can be intercepted and transformed as desired by implementing the `ResourceHooksDefinition.OnReturn` hook which intercepts the `return` action. For this action, there is no distinction between a `Before` and `After` hook, because no code after a `return` statement can be evaluated. Note that the `return` action can work on *primary resources as well as nested resources*, see [this example below](#transforming-data-with-onreturn).

For an overview of all pipelines, hooks and actions, see the table below, and for more detailed information about the available hooks, see the [IResourceHookContainer](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/ab1f96d8255532461da47d290c5440b9e7e6a4a5/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs) interface. @@ -101,98 +103,117 @@ To use resource hooks, you are required to turn them on in your `startup.cs` con ```c# public void ConfigureServices(IServiceCollection services) { - ... - services.AddJsonApi( - options => - { - options.EnableResourceHooks = true; // default is false - options.LoadDatabaseValues = false; // default is false - } - ); - ... + // ... + + services.AddJsonApi(options => + { + options.EnableResourceHooks = true; // default is false + options.LoadDatabaseValues = false; // default is false + }); + + // ... } ``` + For this example, we may set `LoadDatabaseValues` to `false`. See the [Loading database values](#loading-database-values) example for more information about this option. -The simplest case of resource hooks we can then implement should not require a lot of explanation. This hook would triggered by any default JsonApiDotNetCore API route for `Article`. +The simplest case of resource hooks we can then implement should not require a lot of explanation. This hook would be triggered by any default JsonApiDotNetCore API route for `Article`. + ```c# -public class ArticleResource : ResourceDefinition
+public class ArticleResource : ResourceHooksDefinition
{ - public override IEnumerable
OnReturn(HashSet
entities, ResourcePipeline pipeline) + public override IEnumerable
OnReturn(HashSet
entities, + ResourcePipeline pipeline) { Console.WriteLine("This hook does not do much apart from writing this message" + - " to the console just before serving the content"); + " to the console just before serving the content."); return entities; } } ``` + ## Logging This example shows how some actions can be logged on the level of API users. First consider the following scoped service which creates a logger bound to a particular user and request. + ```c# -/// This is a scoped service, which means wil log will have a request-based +/// This is a scoped service, which means log will have a request-based /// unique id associated to it. public class UserActionsLogger : IUserActionsLogger { public ILogger Instance { get; private set; } - public UserActionsLogger(ILoggerFactory loggerFactory, - IUserService userService) + + public UserActionsLogger(ILoggerFactory loggerFactory, IUserService userService) { var userId = userService.GetUser().Id; - Instance = loggerFactory.CreateLogger($"[request: {Guid.NewGuid()}" - + "user: {userId}]"); + Instance = + loggerFactory.CreateLogger($"[request: {Guid.NewGuid()}" + "user: {userId}]"); } } ``` -Now, let's assume our application has two resources: `Article` and `Person`, and that there exist a one-to-one and one-to-many relationship between them (`Article has one Author` and `Article has many Reviewers`). Let's assume we are required to log the following events + +Now, let's assume our application has two resources: `Article` and `Person`, and that there exist a one-to-one and one-to-many relationship between them (`Article` has one `Author` and `Article` has many `Reviewers`). Let's assume we are required to log the following events: * An API user deletes an article * An API user removes the `Author` relationship of a person * An API user removes the `Reviewer` relationship of a person -This could be achieved in the following way +This could be achieved in the following way: + ```c# /// Note that resource definitions are also registered as scoped services. -public class ArticleResource : ResourceDefinition
+public class ArticleResource : ResourceHooksDefinition
{ private readonly ILogger _userLogger; + public ArticleResource(IUserActionsLogger logService) { _userLogger = logService.Instance; } - public override void AfterDelete(HashSet
entities, ResourcePipeline pipeline, bool succeeded) + public override void AfterDelete(HashSet
entities, ResourcePipeline pipeline, + bool succeeded) { - if (!succeeded) return - foreach (Article a in entities) + if (!succeeded) + { + return; + } + + foreach (Article article in entities) { - _userLogger.Log(LogLevel.Information, $"Deleted article '{a.Name}' with id {a.Id}"); + _userLogger.Log(LogLevel.Information, + $"Deleted article '{article.Name}' with id {article.Id}"); } } } -public class PersonResource : ResourceDefinition +public class PersonResource : ResourceHooksDefinition { private readonly ILogger _userLogger; + public PersonResource(IUserActionsLogger logService) { _userLogger = logService.Instance; } - public override void AfterUpdateRelationship(IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) + public override void AfterUpdateRelationship( + IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) { - var updatedRelationshipsToArticle = relationshipHelper.EntitiesRelatedTo
(); + var updatedRelationshipsToArticle = relationshipHelper.EntitiesRelatedTo
(); + foreach (var updated in updatedRelationshipsToArticle) { RelationshipAttribute relationship = updated.Key; HashSet affectedEntities = updated.Value; - foreach (Person p in affectedEntities) + foreach (Person person in affectedEntities) { if (pipeline == ResourcePipeline.Delete) { - _userLogger.Log(LogLevel.Information, $"Deleted the {relationship.PublicRelationshipName}" + - "relationship to Article for person '{p.FirstName + p.LastName}' with {p.Id}"); + _userLogger.Log(LogLevel.Information, + $"Deleted the {relationship.PublicRelationshipName} relationship " + + $"to Article for person '{person.FirstName} {person.LastName}' " + + $"with id {person.Id}"); } } } @@ -200,43 +221,47 @@ public class PersonResource : ResourceDefinition } ``` -If eg. a API user deletes an article with title *JSON:API paints my bikeshed!* that had related as author *John* and as reviewer *Frank*, the logs generated logs would look something like +If eg. an API user deletes an article with title *JSON:API paints my bikeshed!* that had related as author *John Doe* and as reviewer *Frank Miller*, the logs generated logs would look something like ``` [request: 186190e3-1900-4329-9181-42082258e7b4, user: dd1cd99d-60e9-45ca-8d03-a0330b07bdec] Deleted article 'JSON:API paints my bikeshed!' with id fac0436b-7aa5-488e-9de7-dbe00ff8f04d -[request: 186190e3-1900-4329-9181-42082258e7b4, user: dd1cd99d-60e9-45ca-8d03-a0330b07bdec] Deleted the author relationship to Article for person 'John' with id 2ec3990d-c816-4d6d-8531-7da4a030d4d0 -[request: 186190e3-1900-4329-9181-42082258e7b4, user: dd1cd99d-60e9-45ca-8d03-a0330b07bdec] Deleted the reviewer relationship to Article for person 'Frank' with id 42ad6eb2-b813-4261-8fc1-0db1233e665f +[request: 186190e3-1900-4329-9181-42082258e7b4, user: dd1cd99d-60e9-45ca-8d03-a0330b07bdec] Deleted the author relationship to Article for person 'John Doe' with id 2ec3990d-c816-4d6d-8531-7da4a030d4d0 +[request: 186190e3-1900-4329-9181-42082258e7b4, user: dd1cd99d-60e9-45ca-8d03-a0330b07bdec] Deleted the reviewer relationship to Article for person 'Frank Miller' with id 42ad6eb2-b813-4261-8fc1-0db1233e665f ``` + ## Transforming data with OnReturn -Using the `OnReturn` hook, any set of resources can be manipulated as desired before serving it from the API. One of the use-cases for this is being able to perform a [filtered included](https://github.com/aspnet/EntityFrameworkCore/issues/1833), which is currently not supported by Entity Framework Core. +Using the `OnReturn` hook, any set of resources can be manipulated as desired before serving it from the API. One of the use-cases for this is being able to perform a [filtered include](https://github.com/aspnet/EntityFrameworkCore/issues/1833), which is currently not supported by Entity Framework Core. -As an example, consider again an application with the `Article` and `Person` resource, and let's assume the following business rules -* when reading `Article`s, we never want to show articles for which the `SoftDeleted` property is set to true. -* when reading `Person`s, we never want to show people who wish to remain anonymous (`Anonymous` is set to true). +As an example, consider again an application with the `Article` and `Person` resource, and let's assume the following business rules: +* when reading `Article`s, we never want to show articles for which the `IsSoftDeleted` property is set to true. +* when reading `Person`s, we never want to show people who wish to remain anonymous (`IsAnonymous` is set to true). -This can be achieved as follows. +This can be achieved as follows: ```c# -public class ArticleResource : ResourceDefinition
+public class ArticleResource : ResourceHooksDefinition
{ - public override IEnumerable
OnReturn(HashSet
entities, ResourcePipeline pipeline) + public override IEnumerable
OnReturn(HashSet
entities, + ResourcePipeline pipeline) { - return entities.Where(a => a.SoftDeleted == false); + return entities.Where(article => !article.IsSoftDeleted); } } -public class PersonResource : ResourceDefinition +public class PersonResource : ResourceHooksDefinition { - public override IEnumerable OnReturn(HashSet entities, ResourcePipeline pipeline) + public override IEnumerable OnReturn(HashSet entities, + ResourcePipeline pipeline) { if (pipeline == ResourcePipeline.Get) { - return entities.Where(p => p.Anonymous == false); + return entities.Where(person => !person.IsAnonymous); } return entities; } } ``` + Note that not only anonymous people will be excluded when directly performing a `GET /people`, but also when included through relationships, like `GET /articles?include=author,reviewers`. Simultaneously, `if` condition that checks for `ResourcePipeline.Get` in the `PersonResource` ensures we still get expected responses from the API when eg. creating a person with `WantsPrivacy` set to true. ## Loading database values @@ -244,36 +269,46 @@ When a hook is executed for a particular resource, JsonApiDotNetCore can load th * having a diff between a previous and new state of a resource (for example when updating a resource) * performing authorization rules based on the property of a resource. -For example, consider a scenario in with the following two requirements: +For example, consider a scenario with the following two requirements: * We need to log all updates on resources revealing their old and new value. * We need to check if the property `IsLocked` is set is `true`, and if so, cancel the operation. - Consider an `Article` with title *Hello there* and API user trying to update the the title of this article to *Bye bye*. The above requirements could be implemented as follows +Consider an `Article` with title *Hello there* and API user trying to update the the title of this article to *Bye bye*. The above requirements could be implemented as follows: + ```c# -public class ArticleResource : ResourceDefinition
+public class ArticleResource : ResourceHooksDefinition
{ private readonly ILogger _logger; - private readonly IJsonApiContext _context; - public constructor ArticleResource(ILogger logger, IJsonApiContext context) + private readonly ITargetedFields _targetedFields; + + public constructor ArticleResource(ILogger logger, ITargetedFields targetedFields) { _logger = logger; - _context = context; + _targetedFields = targetedFields; } - public override IEnumerable
BeforeUpdate(IResourceDiff
entityDiff, ResourcePipeline pipeline) + public override IEnumerable
BeforeUpdate(IResourceDiff
entityDiff, + ResourcePipeline pipeline) { - // PropertyGetter is a helper class that takes care of accessing the values on an instance of Article using reflection. + // PropertyGetter is a helper class that takes care of accessing the values + // on an instance of Article using reflection. var getter = new PropertyGetter
(); - // ResourceDiff is a class that is like a list that contains ResourceDiffPair elements + // ResourceDiff is like a list that contains ResourceDiffPair elements foreach (ResourceDiffPair
affected in entityDiff) { - var currentDatabaseState = affected.DatabaseValue; // the current state in the database - var proposedValueFromRequest = affected.Entity; // the value from the request + // the current state in the database + var currentDatabaseState = affected.DatabaseValue; - if (currentDatabaseState.IsLocked) throw new JsonApiException(403, "Forbidden: this article is locked!") + // the value from the request + var proposedValueFromRequest = affected.Entity; - foreach (var attr in _context.AttributesToUpdate) + if (currentDatabaseState.IsLocked) + { + throw new JsonApiException(403, "Forbidden: this article is locked!") + } + + foreach (var attr in _targetedFields.Attributes) { var oldValue = getter(currentDatabaseState, attr); var newValue = getter(proposedValueFromRequest, attr); @@ -281,6 +316,7 @@ public class ArticleResource : ResourceDefinition
_logger.LogAttributeUpdate(oldValue, newValue) } } + // You must return IEnumerable
from this hook. // This means that you could reduce the set of entities that is // affected by this request, eg. by entityDiff.Entities.Where( ... ); @@ -288,85 +324,95 @@ public class ArticleResource : ResourceDefinition
} } ``` -In this case the `ResourceDiffPair.DatabaseValue` is `null`. If you try to access all database values at once (`ResourceDiff.DatabaseValues`) when it they are turned off, an exception will be thrown. -Note that database values are turned on by default. They can be turned of globally by configuring the startup as follows: +In this case the `ResourceDiffPair.DatabaseValue` is `null`. If you try to access all database values at once (`ResourceDiff.DatabaseValues`) when it is turned off, an exception will be thrown. + +Note that database values are turned off by default. They can be turned on globally by configuring the startup as follows: + ```c# public void ConfigureServices(IServiceCollection services) { - ... - services.AddJsonApi( - options => - { - options.LoadDatabaseValues = false; // default is false - } - ); - ... + // ... + + services.AddJsonApi(options => + { + options.LoadDatabaseValues = true; + }); + + // ... } ``` The global setting can be used together with per-hook configuration hooks using the `LoadDatabaseValues` attribute: + ```c# -public class ArticleResource : ResourceDefinition
+public class ArticleResource : ResourceHooksDefinition
{ - [LoadDatabaseValues(true)] - public override IEnumerable
BeforeUpdate(IResourceDiff
entityDiff, ResourcePipeline pipeline) + [LoadDatabaseValues(true)] + public override IEnumerable
BeforeUpdate(IResourceDiff
entityDiff, + ResourcePipeline pipeline) { - .... + // ... } - [LoadDatabaseValues(false)] - public override IEnumerable BeforeUpdateRelationships(HashSet ids, IAffectedRelationships
resourcesByRelationship, ResourcePipeline pipeline) + [LoadDatabaseValues(false)] + public override IEnumerable BeforeUpdateRelationships(HashSet ids, + IAffectedRelationships
resourcesByRelationship, ResourcePipeline pipeline) { - // the entities stored in the IAffectedRelationships
instance - // are plain resource identifier objects when LoadDatabaseValues is turned off, - // or objects loaded from the database when LoadDatabaseValues is turned on. - .... + // the entities stored in the IAffectedRelationships
instance + // are plain resource identifier objects when LoadDatabaseValues is turned off, + // or objects loaded from the database when LoadDatabaseValues is turned on. } } } ``` -Note that there are some hooks that the `LoadDatabaseValues` option and attribute does not affect. The only hooks that are affected are: +Note that there are some hooks that the `LoadDatabaseValues` option and attribute does not affect. The only hooks that are affected are: * `BeforeUpdate` * `BeforeUpdateRelationship` -* `BeforeDelete` - +* `BeforeDelete` # 3. Advanced usage ## Simple authorization: explicitly affected resources -Resource hooks can be used to easily implement authorization in your application. As an example, consider the case in which an API user is not allowed to see anonymous people, which is reflected by the `Anonymous` property on `Person` being set to true`true`. The API should handle this as follows: +Resource hooks can be used to easily implement authorization in your application. As an example, consider the case in which an API user is not allowed to see anonymous people, which is reflected by the `Anonymous` property on `Person` being set to `true`. The API should handle this as follows: * When reading people (`GET /people`), it should hide all people that are set to anonymous. * When reading a single person (`GET /people/{id}`), it should throw an authorization error if the particular requested person is set to anonymous. This can be achieved as follows: + ```c# -public class PersonResource : ResourceDefinition +public class PersonResource : ResourceHooksDefinition { - private readonly _IAuthorizationHelper _auth; + private readonly _IAuthorizationHelper _auth; + public constructor PersonResource(IAuthorizationHelper auth) { // IAuthorizationHelper is a helper service that handles all authorization related logic. _auth = auth; } - public override IEnumerable OnReturn(HashSet entities, ResourcePipeline pipeline) + public override IEnumerable OnReturn(HashSet entities, + ResourcePipeline pipeline) { - if (!_auth.CanSeeSecretPeople()) - { - if (pipeline == ResourcePipeline.GetSingle) - { - throw new JsonApiException(403, "Forbidden to view this person", new UnauthorizedAccessException()); - } - entities = entities.Where( p => !p.IsSecret) - } - return entities; + if (!_auth.CanSeeSecretPeople()) + { + if (pipeline == ResourcePipeline.GetSingle) + { + throw new JsonApiException(403, "Forbidden to view this person", + new UnauthorizedAccessException()); + } + + entities = entities.Where(person => !person.IsSecret) + } + + return entities; } } ``` -This example of authorization is considered simple because it only involves one resource. The next example shows a more complex case + +This example of authorization is considered simple because it only involves one resource. The next example shows a more complex case. ## Advanced authorization: implicitly affected resources Let's consider an authorization scenario for which we are required to implement multiple hooks across multiple resource definitions. We will assume the following: @@ -374,82 +420,109 @@ Let's consider an authorization scenario for which we are required to implement * The author of article `Old Article` is person `Alice`. * The author of article `New Article` is person `Bob`. -Now let's consider an API user that tries to update `New Article` by setting its author to `Alice`. The request would look something like `PATCH /articles/{NewArticleId}` with a body containing a reference to `Alice`. +Now let's consider an API user that tries to update `New Article` by setting its author to `Alice`. The request would look something like `PATCH /articles/{ArticleId}` with a body containing a reference to `Alice`. First to all, we wish to authorize this operation by the verifying permissions related to the resources that are **explicity affected** by it: -1. Is the API user allowed to update `New Article`? +1. Is the API user allowed to update `Article`? 2. Is the API user allowed to update `Alice`? -Apart from this, we also wish to verify permissions for the resources that are **implicitly affected** by this operation: `Bob` and `Old Article`. Setting `Alice` as the new author of `New Article` will result in removing the following two relationships: `Bob` being an author of `New Article`, and `Alice` being an author of `Old Article`. Therefore, we wish wish to verify the related permissions: +Apart from this, we also wish to verify permissions for the resources that are **implicitly affected** by this operation: `Bob` and `Old Article`. Setting `Alice` as the new author of `Article` will result in removing the following two relationships: `Bob` being an author of `Article`, and `Alice` being an author of `Old Article`. Therefore, we wish wish to verify the related permissions: 3. Is the API user allowed to update `Bob`? 4. Is the API user allowed to update `Old Article`? This authorization requirement can be fulfilled as follows. -For checking the permissions for the explicitly affected resources, `New Article` and `Alice`, we may implement the `BeforeUpdate` hook for `Article`: +For checking the permissions for the explicitly affected resources, `Article` and `Alice`, we may implement the `BeforeUpdate` hook for `Article`: + ```c# -public override IEnumerable
BeforeUpdate(IResourceDiff
entityDiff, ResourcePipeline pipeline) +public override IEnumerable
BeforeUpdate(IResourceDiff
entityDiff, + ResourcePipeline pipeline) { if (pipeline == ResourcePipeline.Patch) { - Article a = entityDiff.RequestEntities.Single(); - if (!_auth.CanEditResource(a)) + Article article = entityDiff.RequestEntities.Single(); + + if (!_auth.CanEditResource(article)) { - throw new JsonApiException(403, "Forbidden to update properties of this article", new UnauthorizedAccessException()); + throw new JsonApiException(403, "Forbidden to update properties of this article", + new UnauthorizedAccessException()); } - if (entityDiff.GetByRelationship().Any() && _auth.CanEditRelationship(a)) + + if (entityDiff.GetByRelationship().Any() && + _auth.CanEditRelationship(article)) { - throw new JsonApiException(403, "Forbidden to update relationship of this article", new UnauthorizedAccessException()); + throw new JsonApiException(403, "Forbidden to update relationship of this article", + new UnauthorizedAccessException()); } } + return entityDiff.RequestEntities; } ``` - and the `BeforeUpdateRelationship` hook for `Person`: +and the `BeforeUpdateRelationship` hook for `Person`: + ```c# -public override IEnumerable BeforeUpdateRelationship(HashSet ids, IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) +public override IEnumerable BeforeUpdateRelationship(HashSet ids, + IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) { var updatedOwnerships = resourcesByRelationship.GetByRelationship
(); + if (updatedOwnerships.Any()) { - Person p = resourcesByRelationship.GetByRelationship
().Single().Value.First(); - if (_auth.CanEditRelationship
(p)) + Person person = + resourcesByRelationship.GetByRelationship
().Single().Value.First(); + + if (_auth.CanEditRelationship
(person)) { - throw new JsonApiException(403, "Forbidden to update relationship of this person", new UnauthorizedAccessException()); + throw new JsonApiException(403, "Forbidden to update relationship of this person", + new UnauthorizedAccessException()); } } + return ids; } ``` To verify the permissions for the implicitly affected resources, `Old Article` and `Bob`, we need to implement the `BeforeImplicitUpdateRelationship` hook for `Article`: + ```c# -public override void BeforeImplicitUpdateRelationship(IAffectedRelationships
resourcesByRelationship, ResourcePipeline pipeline) +public override void BeforeImplicitUpdateRelationship( + IAffectedRelationships
resourcesByRelationship, ResourcePipeline pipeline) { var updatedOwnerships = resourcesByRelationship.GetByRelationship(); + if (updatedOwnerships.Any()) { - Article a = resourcesByRelationship.GetByRelationship().Single().Value.First(); - if (_auth.CanEditRelationship(a)) + Article article = + resourcesByRelationship.GetByRelationship().Single().Value.First(); + + if (_auth.CanEditRelationship(article)) { - throw new JsonApiException(403, "Forbidden to update relationship of this article", new UnauthorizedAccessException()); + throw new JsonApiException(403, "Forbidden to update relationship of this article", + new UnauthorizedAccessException()); } } } ``` + and similarly for `Person`: + ```c# -public override void BeforeImplicitUpdateRelationship(IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) +public override void BeforeImplicitUpdateRelationship( + IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) { var updatedOwnerships = resourcesByRelationship.GetByRelationship
(); if (updatedOwnerships.Any()) { - Person p = resourcesByRelationship.GetByRelationship
().Single().Value.First(); - if (_auth.CanEditRelationship
(p)) + Person person = + resourcesByRelationship.GetByRelationship
().Single().Value.First(); + + if (_auth.CanEditRelationship
(person)) { - throw new JsonApiException(403, "Forbidden to update relationship of this article", new UnauthorizedAccessException()); + throw new JsonApiException(403, "Forbidden to update relationship of this article", + new UnauthorizedAccessException()); } } } @@ -464,57 +537,58 @@ If you want to use Resource Hooks without Entity Framework Core, there are sever If you are required to use the `BeforeImplicitUpdateRelationship` hook (see previous example), there is an additional requirement. For this hook, given a particular relationship, JsonApiDotNetCore needs to be able to resolve the inverse relationship. For example: if `Article` has one author (a `Person`), then it needs to be able to resolve the `RelationshipAttribute` that corresponds to the inverse relationship for the `author` property. There are two approaches : 1. **Tell JsonApiDotNetCore how to do this only for the relevant models**. If you're using the `BeforeImplicitUpdateRelationship` hook only for a small set of models, eg only for the relationship of the example, then it is easiest to provide the `inverseNavigationProperty` as follows: + ```c# public class Article : Identifiable { - ... - [HasOne("author", inverseNavigationProperty: "OwnerOfArticle")] + [HasOne("author", InverseNavigationProperty: "OwnerOfArticle")] public Person Author { get; set; } - ... } + public class Person : Identifiable { - ... [HasOne("article")] public Article OwnerOfArticle { get; set; } - ... } ``` -2. **Tell JsonApiDotNetCore how to do this in general**. For full support, you can provide JsonApiDotNetCore with a custom service implementation of the `IInverseRelationships` interface. relationship of the example, then it is easiest to provide the `inverseNavigationProperty` as follows: + +2. **Tell JsonApiDotNetCore how to do this in general**. For full support, you can provide JsonApiDotNetCore with a custom service implementation of the `IInverseNavigationResolver` interface. relationship of the example, then it is easiest to provide the `InverseNavigationProperty` as follows: + ```c# -public class CustomInverseRelationshipsResolver : IInverseRelationships +public class CustomInverseNavigationResolver : IInverseNavigationResolver { public void Resolve() { - // the implementation of this method depends completely + // the implementation of this method depends completely on // the data access layer you're using. - // It should set the RelationshipAttribute.InverseRelationship property + // It should set the RelationshipAttribute.InverseNavigationProperty property // for all (relevant) relationships. - // To have an idea of how to implement this method, see the InverseRelationships class - // in the source code of JsonApiDotNetCore: - // https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/59a93590ac4f05c9c246eca9459b49e331250805/src/JsonApiDotNetCore/Internal/InverseRelationships.cs + // To have an idea of how to implement this method, see the InverseNavigationResolver class + // in the source code of JsonApiDotNetCore. } } ``` -This service will then be called run once at startup and take care of the metadata that is required for `BeforeImplicitUpdateRelationship` to be supported. -*Note: don't forget to register this singleton service with the service provider.* +This service will then be run once at startup and take care of the metadata that is required for `BeforeImplicitUpdateRelationship` to be supported. +*Note: don't forget to register this singleton service with the service provider.* ## Synchronizing data across microservices If your application is built using a microservices infrastructure, it may be relevant to propagate data changes between microservices, [see this article for more information](https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/integration-event-based-microservice-communications). In this example, we will assume the implementation of an event bus and we will publish data consistency integration events using resource hooks. ```c# -public class ArticleResource : ResourceDefinition
+public class ArticleResource : ResourceHooksDefinition
{ private readonly IEventBus _bus; private readonly IJsonApiContext _context; + public ArticleResource(IEventBus bus, IJsonApiContext context) { _bus = bus; _context = context; } + public override void AfterCreate(HashSet
entities, ResourcePipeline pipeline) { foreach (var article in entities ) @@ -524,7 +598,8 @@ public class ArticleResource : ResourceDefinition
} } - public override void AfterDelete(HashSet
entities, ResourcePipeline pipeline, bool succeeded) + public override void AfterDelete(HashSet
entities, ResourcePipeline pipeline, + bool succeeded) { foreach (var article in entities) { @@ -537,8 +612,12 @@ public class ArticleResource : ResourceDefinition
{ foreach (var article in entities) { - // You could inject IJsonApiContext and use it to pass along only the attributes that were updated - var @event = new ResourceUpdatedEvent(article, properties: _context.AttributesToUpdate); + // You could inject ITargetedFields and use it to pass along + // only the attributes that were updated + + var @event = new ResourceUpdatedEvent(article, + properties: _targetedFields.Attributes); + _bus.Publish(@event); } } @@ -546,18 +625,21 @@ public class ArticleResource : ResourceDefinition
``` ## Hooks for many-to-many join tables -In this example we consider an application with a many-to-many relationships: `Article` and `Tag`, with an internally used `ArticleTag` throughtype. +In this example we consider an application with a many-to-many relationship: `Article` and `Tag`, with an internally used `ArticleTag` join-type. + +Usually, join table records will not contain any extra information other than that which is used internally for the many-to-many relationship. For this example, the join-type should then look like: -Usually, join table records will not contain any extra information other than that which is used internally for the many-to-many relationship. For this example, the throughtype should then look like: ```c# public class ArticleTag { public int ArticleId { get; set; } public Article Article { get; set; } + public int TagId { get; set; } public Tag Tag { get; set; } } ``` + If we then eg. implement the `AfterRead` and `OnReturn` hook for `Article` and `Tag`, and perform a `GET /articles?include=tags` request, we may expect the following order of execution: 1. Article AfterRead @@ -570,28 +652,37 @@ Note that under the hood, the *join table records* (instances of `ArticleTag`) a Sometimes, however, relevant data may be stored in the join table of a many-to-many relationship. Let's imagine we wish to add a property `LinkDate` to the join table that reflects when a tag was added to an article. In this case, we may want to execute business logic related to these records: we may for example want to hide any tags that were added to an article longer than 2 weeks ago. In order to achieve this, we need to change `ArticleTag` to `ArticleTagWithLinkDate` as follows: + ```c# public class ArticleTagWithLinkDate : Identifiable { public int ArticleId { get; set; } + [HasOne("Article")] public Article Article { get; set; } + public int TagId { get; set; } + [HasOne("Tag")] public Tag Tag { get; set; } + public DateTime LinkDate { get; set; } } ``` + Then, we may implement a hook for `ArticleTagWithLinkDate` as usual: + ```c# -public class ArticleTagWithLinkDateResource : ResourceDefinition +public class ArticleTagWithLinkDateResource : ResourceHooksDefinition { - public override IEnumerable OnReturn(HashSet entities, ResourcePipeline pipeline) + public override IEnumerable OnReturn( + HashSet entities, ResourcePipeline pipeline) { - return entities.Where(e => (DateTime.Now - e.LinkDate) < 14); + return entities.Where(article => (DateTime.Now - article.LinkDate) < 14); } } ``` + Then, for the same request `GET /articles?include=tags`, the order of execution of the hooks will look like: 1. Article AfterRead 2. Tag AfterRead diff --git a/docs/usage/resources/index.md b/docs/usage/resources/index.md index f6d056034a..ad164d82d4 100644 --- a/docs/usage/resources/index.md +++ b/docs/usage/resources/index.md @@ -4,22 +4,25 @@ At a minimum, resources must implement `IIdentifiable` where `TId` is the t ```c# public class Person : Identifiable -{ } +{ +} ``` You can use the non-generic `Identifiable` if your primary key is an integer. ```c# public class Person : Identifiable -{ } +{ +} -// is the same as +// is the same as: public class Person : Identifiable -{ } +{ +} ``` -If you need to hang annotations or attributes on the `Id` property, +If you need to attach annotations or attributes on the `Id` property, you can override the virtual property. ```c# diff --git a/docs/usage/resources/relationships.md b/docs/usage/resources/relationships.md index 5185d297d1..ac31adf14a 100644 --- a/docs/usage/resources/relationships.md +++ b/docs/usage/resources/relationships.md @@ -37,8 +37,11 @@ However, under the covers it will use the join type and Entity Framework Core's ```c# public class Article : Identifiable { - [NotMapped] // tells Entity Framework Core to ignore this property - [HasManyThrough(nameof(ArticleTags))] // tells JsonApiDotNetCore to use the join table below + // tells Entity Framework Core to ignore this property + [NotMapped] + + // tells JsonApiDotNetCore to use the join table below + [HasManyThrough(nameof(ArticleTags))] public ICollection Tags { get; set; } // this is the Entity Framework Core navigation to the join table @@ -92,7 +95,8 @@ public class ShippingAddress : Identifiable get { return Country.DisplayName; } } - [EagerLoad] // not exposed as resource, but adds .Include("Country") to the query + // not exposed as resource, but adds .Include("Country") to the query + [EagerLoad] public Country Country { get; set; } } diff --git a/docs/usage/resources/resource-definitions.md b/docs/usage/resources/resource-definitions.md index 77eaa11062..e355a30d9a 100644 --- a/docs/usage/resources/resource-definitions.md +++ b/docs/usage/resources/resource-definitions.md @@ -26,7 +26,8 @@ Note: to exclude attributes unconditionally, use `[Attr(Capabilities = ~AttrCapa ```c# public class UserDefinition : JsonApiResourceDefinition { - public UserDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public UserDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { } @@ -84,7 +85,8 @@ You can define the default sort order if no `sort` query string parameter is pro ```c# public class AccountDefinition : JsonApiResourceDefinition { - public AccountDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public AccountDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { } @@ -111,7 +113,8 @@ You may want to enforce pagination on large database tables. ```c# public class AccessLogDefinition : JsonApiResourceDefinition { - public AccessLogDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public AccessLogDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { } @@ -141,7 +144,8 @@ Soft-deletion sets `IsSoftDeleted` to `true` instead of actually deleting the re ```c# public class AccountDefinition : JsonApiResourceDefinition { - public AccountDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public AccountDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { } @@ -150,8 +154,8 @@ public class AccountDefinition : JsonApiResourceDefinition var resourceContext = ResourceGraph.GetResourceContext(); var isSoftDeletedAttribute = - resourceContext.Attributes.Single(a => - a.Property.Name == nameof(Account.IsSoftDeleted)); + resourceContext.Attributes.Single(account => + account.Property.Name == nameof(Account.IsSoftDeleted)); var isNotSoftDeleted = new ComparisonExpression(ComparisonOperator.Equals, new ResourceFieldChainExpression(isSoftDeletedAttribute), @@ -160,7 +164,7 @@ public class AccountDefinition : JsonApiResourceDefinition return existingFilter == null ? (FilterExpression) isNotSoftDeleted : new LogicalExpression(LogicalOperator.And, - new[] {isNotSoftDeleted, existingFilter}); + new[] { isNotSoftDeleted, existingFilter }); } } ``` @@ -170,7 +174,8 @@ public class AccountDefinition : JsonApiResourceDefinition ```c# public class EmployeeDefinition : JsonApiResourceDefinition { - public EmployeeDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public EmployeeDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { } @@ -204,7 +209,8 @@ But it only works on primary resource endpoints (for example: /articles, but not ```c# public class ItemDefinition : JsonApiResourceDefinition { - public ItemDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public ItemDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { } diff --git a/docs/usage/routing.md b/docs/usage/routing.md index b33168e044..10a556173b 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -6,10 +6,10 @@ You can add a namespace to all URLs by specifying it in ConfigureServices. ```c# public void ConfigureServices(IServiceCollection services) { - services.AddJsonApi( - options => options.Namespace = "api/v1"); + services.AddJsonApi(options => options.Namespace = "api/v1"); } ``` + Which results in URLs like: https://yourdomain.com/api/v1/people ## Default Routing Convention @@ -17,7 +17,9 @@ Which results in URLs like: https://yourdomain.com/api/v1/people The library will configure routes for all controllers in your project. By default, routes are camel-cased. This is based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the JSON:API spec. ```c# -public class OrderLine : Identifiable { } +public class OrderLine : Identifiable +{ +} public class OrderLineController : JsonApiController { @@ -38,9 +40,13 @@ The exposed name of the resource ([which can be customized](~/usage/resource-gra ### Non-JSON:API controllers If a controller does not inherit from `JsonApiController`, the [configured naming convention](~/usage/options.md#custom-serializer-settings) is applied to the name of the controller. + ```c# -public class OrderLineController : ControllerBase { } +public class OrderLineController : ControllerBase +{ +} ``` + ```http GET /orderLines HTTP/1.1 ``` @@ -48,6 +54,7 @@ GET /orderLines HTTP/1.1 ## Disabling the Default Routing Convention It is possible to bypass the default routing convention for a controller. + ```c# [Route("v1/custom/route/orderLines"), DisableRoutingConvention] public class OrderLineController : JsonApiController @@ -59,11 +66,13 @@ public class OrderLineController : JsonApiController } } ``` + It is required to match your custom url with the exposed name of the associated resource. ## Advanced Usage: Custom Routing Convention It is possible to replace the built-in routing convention with a [custom routing convention](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/application-model?view=aspnetcore-3.1#sample-custom-routing-convention) by registering an implementation of `IJsonApiRoutingConvention`. + ```c# public void ConfigureServices(IServiceCollection services) { diff --git a/docs/usage/writing/bulk-batch-operations.md b/docs/usage/writing/bulk-batch-operations.md index 41e8ed2f52..bbf2db2ec2 100644 --- a/docs/usage/writing/bulk-batch-operations.md +++ b/docs/usage/writing/bulk-batch-operations.md @@ -13,6 +13,7 @@ On failure, the zero-based index of the failing operation is returned in the `er ## Usage To enable operations, add a controller to your project that inherits from `JsonApiOperationsController` or `BaseJsonApiOperationsController`: + ```c# public sealed class OperationsController : JsonApiOperationsController { @@ -26,6 +27,7 @@ public sealed class OperationsController : JsonApiOperationsController ``` You'll need to send the next Content-Type in a POST request for operations: + ``` application/vnd.api+json; ext="https://jsonapi.org/ext/atomic" ``` @@ -84,14 +86,17 @@ For example requests, see our suite of tests in JsonApiDotNetCoreExampleTests.In ## Configuration The maximum number of operations per request defaults to 10, which you can change from Startup.cs: + ```c# services.AddJsonApi(options => options.MaximumOperationsPerRequest = 250); ``` + Or, if you want to allow unconstrained, set it to `null` instead. ### Multiple controllers You can register multiple operations controllers using custom routes, for example: + ```c# [DisableRoutingConvention, Route("/operations/musicTracks/create")] public sealed class CreateMusicTrackOperationsController : JsonApiOperationsController diff --git a/inspectcode.ps1 b/inspectcode.ps1 new file mode 100644 index 0000000000..83987d5e69 --- /dev/null +++ b/inspectcode.ps1 @@ -0,0 +1,33 @@ +#Requires -Version 7.0 + +# This script runs code inspection and opens the results in a web browser. + +dotnet tool restore + +if ($LASTEXITCODE -ne 0) { + throw "Tool restore failed with exit code $LASTEXITCODE" +} + +dotnet build -c Release + +if ($LASTEXITCODE -ne 0) { + throw "Build 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.sln --output="$outputPath" --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=SolutionPersonal -dsl=ProjectPersonal + +if ($LASTEXITCODE -ne 0) { + throw "Code inspection failed with exit code $LASTEXITCODE" +} + +[xml]$xml = Get-Content "$outputPath" +if ($xml.report.Issues -and $xml.report.Issues.Project) { + $xslt = new-object System.Xml.Xsl.XslCompiledTransform; + $xslt.Load("$pwd/JetBrainsInspectCodeTransform.xslt"); + $xslt.Transform($outputPath, $resultPath); + + Write-Output "Opening results in browser" + Invoke-Item "$resultPath" +} diff --git a/src/Examples/GettingStarted/Controllers/PeopleController.cs b/src/Examples/GettingStarted/Controllers/PeopleController.cs index 0337a159d3..c7600be15a 100644 --- a/src/Examples/GettingStarted/Controllers/PeopleController.cs +++ b/src/Examples/GettingStarted/Controllers/PeopleController.cs @@ -8,11 +8,9 @@ namespace GettingStarted.Controllers { public sealed class PeopleController : JsonApiController { - public PeopleController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) + public PeopleController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) - { } + { + } } } diff --git a/src/Examples/GettingStarted/Program.cs b/src/Examples/GettingStarted/Program.cs index fad81e1bba..68bca0ae86 100644 --- a/src/Examples/GettingStarted/Program.cs +++ b/src/Examples/GettingStarted/Program.cs @@ -10,11 +10,12 @@ public static void Main(string[] args) CreateHostBuilder(args).Build().Run(); } - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + private static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } } } diff --git a/src/Examples/GettingStarted/Properties/launchSettings.json b/src/Examples/GettingStarted/Properties/launchSettings.json index 9ea0e9d79b..b68c2481ed 100644 --- a/src/Examples/GettingStarted/Properties/launchSettings.json +++ b/src/Examples/GettingStarted/Properties/launchSettings.json @@ -1,29 +1,29 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:14141" - } + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14141" + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "api/people", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": false, - "launchUrl": "api/people", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Kestrel": { - "commandName": "Project", - "launchBrowser": false, - "launchUrl": "api/people", - "applicationUrl": "http://localhost:14141", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } + "Kestrel": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "api/people", + "applicationUrl": "http://localhost:14141", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } + } } diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs index 8d7eacdeea..b942ecc98d 100644 --- a/src/Examples/GettingStarted/Startup.cs +++ b/src/Examples/GettingStarted/Startup.cs @@ -14,21 +14,19 @@ public sealed class Startup // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddDbContext( - options => options.UseSqlite("Data Source=sample.db")); + services.AddDbContext(options => options.UseSqlite("Data Source=sample.db")); - services.AddJsonApi( - options => - { - options.Namespace = "api"; - options.UseRelativeLinks = true; - options.IncludeTotalResourceCount = true; - options.SerializerSettings.Formatting = Formatting.Indented; - }); + services.AddJsonApi(options => + { + options.Namespace = "api"; + options.UseRelativeLinks = true; + options.IncludeTotalResourceCount = true; + options.SerializerSettings.Formatting = Formatting.Indented; + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - [UsedImplicitly] + [UsedImplicitly] public void Configure(IApplicationBuilder app, SampleDbContext context) { context.Database.EnsureDeleted(); @@ -41,8 +39,8 @@ public void Configure(IApplicationBuilder app, SampleDbContext context) } private static void CreateSampleData(SampleDbContext context) - { - // Note: The generate-examples.ps1 script (to create example requests in documentation) depends on these. + { + // Note: The generate-examples.ps1 script (to create example requests in documentation) depends on these. context.Books.AddRange(new Book { diff --git a/src/Examples/GettingStarted/appsettings.json b/src/Examples/GettingStarted/appsettings.json index eb57d97280..c2ba4deaa9 100644 --- a/src/Examples/GettingStarted/appsettings.json +++ b/src/Examples/GettingStarted/appsettings.json @@ -1,10 +1,10 @@ { - "Logging": { - "LogLevel": { - "Default": "Warning", - "Microsoft": "Warning", - "Microsoft.EntityFrameworkCore.Database.Command": "Information" - } - }, - "AllowedHosts": "*" + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" + } + }, + "AllowedHosts": "*" } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs index a553851ccd..0735952114 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs @@ -8,11 +8,9 @@ namespace JsonApiDotNetCoreExample.Controllers { public sealed class ArticlesController : JsonApiController
{ - public ArticlesController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService
resourceService) + public ArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService
resourceService) : base(options, loggerFactory, resourceService) - { } + { + } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs index 789c31cb95..963e85e996 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs @@ -8,11 +8,9 @@ namespace JsonApiDotNetCoreExample.Controllers { public sealed class AuthorsController : JsonApiController { - public AuthorsController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) + public AuthorsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) - { } + { + } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs index 90a864f07c..49708c5465 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs @@ -10,7 +10,11 @@ public sealed class NonJsonApiController : ControllerBase [HttpGet] public IActionResult Get() { - var result = new[] {"Welcome!"}; + string[] result = + { + "Welcome!" + }; + return Ok(result); } @@ -24,21 +28,21 @@ public async Task PostAsync() return BadRequest("Please send your name."); } - var result = "Hello, " + name; + string result = "Hello, " + name; return Ok(result); } [HttpPut] public IActionResult Put([FromBody] string name) { - var result = "Hi, " + name; + string result = "Hi, " + name; return Ok(result); } [HttpPatch] public IActionResult Patch(string name) { - var result = "Good day, " + name; + string result = "Good day, " + name; return Ok(result); } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs index 247bd52d78..4851336a9a 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/OperationsController.cs @@ -9,8 +9,8 @@ namespace JsonApiDotNetCoreExample.Controllers { public sealed class OperationsController : JsonApiOperationsController { - public OperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IOperationsProcessor processor, IJsonApiRequest request, ITargetedFields targetedFields) + public OperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request, + ITargetedFields targetedFields) : base(options, loggerFactory, processor, request, targetedFields) { } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs index 4b0116ece6..430790bc6e 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PeopleController.cs @@ -8,11 +8,9 @@ namespace JsonApiDotNetCoreExample.Controllers { public sealed class PeopleController : JsonApiController { - public PeopleController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) + public PeopleController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) - { } + { + } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs index dbd9057a12..a28a7033d6 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsController.cs @@ -8,11 +8,9 @@ namespace JsonApiDotNetCoreExample.Controllers { public sealed class TodoItemsController : JsonApiController { - public TodoItemsController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) + public TodoItemsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) - { } + { + } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs index 312c0f50bc..11a742ced4 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/UsersController.cs @@ -8,11 +8,9 @@ namespace JsonApiDotNetCoreExample.Controllers { public sealed class UsersController : JsonApiController { - public UsersController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) + public UsersController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) - { } + { + } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs index a438f44831..9d13b17493 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs @@ -15,7 +15,8 @@ public sealed class AppDbContext : DbContext public DbSet AuthorDifferentDbContextName { get; set; } public DbSet Users { get; set; } - public AppDbContext(DbContextOptions options) : base(options) + public AppDbContext(DbContextOptions options) + : base(options) { } @@ -30,10 +31,18 @@ protected override void OnModelCreating(ModelBuilder builder) .WithMany(p => p.TodoItems); builder.Entity() - .HasKey(bc => new {bc.ArticleId, bc.TagId}); + .HasKey(bc => new + { + bc.ArticleId, + bc.TagId + }); builder.Entity() - .HasKey(bc => new {bc.ArticleId, bc.TagId}); + .HasKey(bc => new + { + bc.ArticleId, + bc.TagId + }); builder.Entity() .HasOne(t => t.StakeHolderTodoItem) diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs index 6d4bc02cdb..5ea4353ecd 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleHooksDefinition.cs @@ -14,7 +14,10 @@ namespace JsonApiDotNetCoreExample.Definitions [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class ArticleHooksDefinition : ResourceHooksDefinition
{ - public ArticleHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public ArticleHooksDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) + { + } public override IEnumerable
OnReturn(HashSet
resources, ResourcePipeline pipeline) { @@ -26,8 +29,7 @@ public override IEnumerable
OnReturn(HashSet
resources, Resour }); } - return resources.Where(t => t.Caption != "This should not be included"); + return resources.Where(article => article.Caption != "This should not be included"); } } } - diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs index 845fc61eea..54e20746c7 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableHooksDefinition.cs @@ -9,17 +9,20 @@ namespace JsonApiDotNetCoreExample.Definitions { - public abstract class LockableHooksDefinition : ResourceHooksDefinition where T : class, IIsLockable, IIdentifiable + public abstract class LockableHooksDefinition : ResourceHooksDefinition + where T : class, IIsLockable, IIdentifiable { private readonly IResourceGraph _resourceGraph; - protected LockableHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + + protected LockableHooksDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { _resourceGraph = resourceGraph; } protected void DisallowLocked(IEnumerable resources) { - foreach (var resource in resources ?? Enumerable.Empty()) + foreach (T resource in resources ?? Enumerable.Empty()) { if (resource.IsLocked) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs index 8d73f9eaec..a7a025bede 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportHooksDefinition.cs @@ -13,7 +13,8 @@ namespace JsonApiDotNetCoreExample.Definitions [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class PassportHooksDefinition : LockableHooksDefinition { - public PassportHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public PassportHooksDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { } @@ -35,7 +36,7 @@ public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary

OnReturn(HashSet resources, ResourcePipeline pipeline) { - return resources.Where(p => !p.IsLocked); + return resources.Where(passport => !passport.IsLocked); } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs index 28e1d51955..1d70f5892b 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonHooksDefinition.cs @@ -10,9 +10,13 @@ namespace JsonApiDotNetCoreExample.Definitions [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class PersonHooksDefinition : LockableHooksDefinition { - public PersonHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public PersonHooksDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) + { + } - public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) + public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, + ResourcePipeline pipeline) { BeforeImplicitUpdateRelationship(resourcesByRelationship, pipeline); return ids; diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs index 2ae1a543c6..b170715e95 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagHooksDefinition.cs @@ -11,11 +11,14 @@ namespace JsonApiDotNetCoreExample.Definitions [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class TagHooksDefinition : ResourceHooksDefinition { - public TagHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public TagHooksDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) + { + } public override IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) { - return resources.Where(t => t.Name != "This should not be included"); + return resources.Where(tag => tag.Name != "This should not be included"); } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemHooksDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemHooksDefinition.cs index 892b12c7a8..9e59aa6569 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemHooksDefinition.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TodoItemHooksDefinition.cs @@ -13,7 +13,10 @@ namespace JsonApiDotNetCoreExample.Definitions [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class TodoItemHooksDefinition : LockableHooksDefinition { - public TodoItemHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public TodoItemHooksDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) + { + } public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) { @@ -34,7 +37,7 @@ public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary OnReturn(HashSet resources, ResourcePipeline pipeline) { - return resources.Where(t => t.Description != "This should not be included"); + return resources.Where(todoItem => todoItem.Description != "This should not be included"); } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs index 84de22fcdf..df1adfdbcd 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs @@ -18,11 +18,13 @@ public sealed class Article : Identifiable [NotMapped] [HasManyThrough(nameof(ArticleTags))] public ISet Tags { get; set; } + public ISet ArticleTags { get; set; } [NotMapped] [HasManyThrough(nameof(IdentifiableArticleTags))] public ICollection IdentifiableTags { get; set; } + public ICollection IdentifiableArticleTags { get; set; } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/IdentifiableArticleTag.cs b/src/Examples/JsonApiDotNetCoreExample/Models/IdentifiableArticleTag.cs index ba44bf56ad..d8cbab1366 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/IdentifiableArticleTag.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/IdentifiableArticleTag.cs @@ -8,10 +8,12 @@ namespace JsonApiDotNetCoreExample.Models public sealed class IdentifiableArticleTag : Identifiable { public int ArticleId { get; set; } + [HasOne] public Article Article { get; set; } public int TagId { get; set; } + [HasOne] public Tag Tag { get; set; } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Program.cs b/src/Examples/JsonApiDotNetCoreExample/Program.cs index e2866c25ec..4c97d8a7f4 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Program.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Program.cs @@ -11,11 +11,12 @@ public static void Main(string[] args) CreateHostBuilder(args).Build().Run(); } - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + private static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json b/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json index 1e3998e4f0..6a5108a8ad 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json +++ b/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json @@ -1,30 +1,30 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:14140", - "sslPort": 44340 - } + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14140", + "sslPort": 44340 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "api/v1/todoItems", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": false, - "launchUrl": "api/v1/todoItems", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Kestrel": { - "commandName": "Project", - "launchBrowser": false, - "launchUrl": "api/v1/todoItems", - "applicationUrl": "https://localhost:44340;http://localhost:14140", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } + "Kestrel": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "api/v1/todoItems", + "applicationUrl": "https://localhost:44340;http://localhost:14140", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } + } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs index 0ee7454da6..19879aef27 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs @@ -5,8 +5,8 @@ namespace JsonApiDotNetCoreExample.Startups { ///

- /// Empty startup class, required for integration tests. - /// Changes in ASP.NET Core 3 no longer allow Startup class to be defined in test projects. See https://github.com/aspnet/AspNetCore/issues/15373. + /// Empty startup class, required for integration tests. Changes in ASP.NET Core 3 no longer allow Startup class to be defined in test projects. See + /// https://github.com/aspnet/AspNetCore/issues/15373. /// public abstract class EmptyStartup { diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index fa7580865d..2db12c7434 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -49,7 +49,7 @@ private void ConfigureJsonApiOptions(JsonApiOptions options) public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment) { - using (var scope = app.ApplicationServices.CreateScope()) + using (IServiceScope scope = app.ApplicationServices.CreateScope()) { var appDbContext = scope.ServiceProvider.GetRequiredService(); appDbContext.Database.EnsureCreated(); diff --git a/src/Examples/JsonApiDotNetCoreExample/appsettings.json b/src/Examples/JsonApiDotNetCoreExample/appsettings.json index 2b56699951..b1f0227dcd 100644 --- a/src/Examples/JsonApiDotNetCoreExample/appsettings.json +++ b/src/Examples/JsonApiDotNetCoreExample/appsettings.json @@ -1,13 +1,13 @@ { - "Data": { - "DefaultConnection": "Host=localhost;Port=5432;Database=JsonApiDotNetCoreExample;User ID=postgres;Password=###" - }, - "Logging": { - "LogLevel": { - "Default": "Warning", - "Microsoft": "Warning", - "Microsoft.EntityFrameworkCore.Database.Command": "Warning" - } - }, - "AllowedHosts": "*" + "Data": { + "DefaultConnection": "Host=localhost;Port=5432;Database=JsonApiDotNetCoreExample;User ID=postgres;Password=###" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Warning" + } + }, + "AllowedHosts": "*" } diff --git a/src/Examples/MultiDbContextExample/Controllers/ResourceAsController.cs b/src/Examples/MultiDbContextExample/Controllers/ResourceAsController.cs index b406411c41..4e976acdc0 100644 --- a/src/Examples/MultiDbContextExample/Controllers/ResourceAsController.cs +++ b/src/Examples/MultiDbContextExample/Controllers/ResourceAsController.cs @@ -8,8 +8,7 @@ namespace MultiDbContextExample.Controllers { public sealed class ResourceAsController : JsonApiController { - public ResourceAsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public ResourceAsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/src/Examples/MultiDbContextExample/Controllers/ResourceBsController.cs b/src/Examples/MultiDbContextExample/Controllers/ResourceBsController.cs index 7268bb7969..bd61b7aa2e 100644 --- a/src/Examples/MultiDbContextExample/Controllers/ResourceBsController.cs +++ b/src/Examples/MultiDbContextExample/Controllers/ResourceBsController.cs @@ -8,8 +8,7 @@ namespace MultiDbContextExample.Controllers { public sealed class ResourceBsController : JsonApiController { - public ResourceBsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public ResourceBsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/src/Examples/MultiDbContextExample/Program.cs b/src/Examples/MultiDbContextExample/Program.cs index 5d138239c0..d5800d95e8 100644 --- a/src/Examples/MultiDbContextExample/Program.cs +++ b/src/Examples/MultiDbContextExample/Program.cs @@ -12,8 +12,7 @@ public static void Main(string[] args) private static IHostBuilder CreateHostBuilder(string[] args) { - return Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); + return Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); } } } diff --git a/src/Examples/MultiDbContextExample/Properties/launchSettings.json b/src/Examples/MultiDbContextExample/Properties/launchSettings.json index 1f33cc0f31..6d7e1b5cbd 100644 --- a/src/Examples/MultiDbContextExample/Properties/launchSettings.json +++ b/src/Examples/MultiDbContextExample/Properties/launchSettings.json @@ -1,30 +1,30 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:14150", - "sslPort": 44350 - } + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14150", + "sslPort": 44350 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "/resourceBs", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": false, - "launchUrl": "/resourceBs", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Kestrel": { - "commandName": "Project", - "launchBrowser": false, - "launchUrl": "/resourceBs", - "applicationUrl": "https://localhost:44350;http://localhost:14150", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } + "Kestrel": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "/resourceBs", + "applicationUrl": "https://localhost:44350;http://localhost:14150", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } + } } diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs index 44174777f8..13fb0bbe48 100644 --- a/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextARepository.cs @@ -13,9 +13,8 @@ namespace MultiDbContextExample.Repositories public sealed class DbContextARepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { - public DbContextARepository(ITargetedFields targetedFields, DbContextResolver contextResolver, - IResourceGraph resourceGraph, IResourceFactory resourceFactory, - IEnumerable constraintProviders, ILoggerFactory loggerFactory) + public DbContextARepository(ITargetedFields targetedFields, DbContextResolver contextResolver, IResourceGraph resourceGraph, + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) { } diff --git a/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs index eb2cc5fc2b..0b65caa945 100644 --- a/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs +++ b/src/Examples/MultiDbContextExample/Repositories/DbContextBRepository.cs @@ -13,9 +13,8 @@ namespace MultiDbContextExample.Repositories public sealed class DbContextBRepository : EntityFrameworkCoreRepository where TResource : class, IIdentifiable { - public DbContextBRepository(ITargetedFields targetedFields, DbContextResolver contextResolver, - IResourceGraph resourceGraph, IResourceFactory resourceFactory, - IEnumerable constraintProviders, ILoggerFactory loggerFactory) + public DbContextBRepository(ITargetedFields targetedFields, DbContextResolver contextResolver, IResourceGraph resourceGraph, + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) { } diff --git a/src/Examples/MultiDbContextExample/Startup.cs b/src/Examples/MultiDbContextExample/Startup.cs index b1b7b6c57e..bc76c3cd9a 100644 --- a/src/Examples/MultiDbContextExample/Startup.cs +++ b/src/Examples/MultiDbContextExample/Startup.cs @@ -25,13 +25,16 @@ public void ConfigureServices(IServiceCollection services) services.AddJsonApi(options => { options.IncludeExceptionStackTraceInErrors = true; - }, dbContextTypes: new[] {typeof(DbContextA), typeof(DbContextB)}); + }, dbContextTypes: new[] + { + typeof(DbContextA), + typeof(DbContextB) + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. [UsedImplicitly] - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DbContextA dbContextA, - DbContextB dbContextB) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DbContextA dbContextA, DbContextB dbContextB) { if (env.IsDevelopment()) { diff --git a/src/Examples/MultiDbContextExample/appsettings.json b/src/Examples/MultiDbContextExample/appsettings.json index 4442d5117f..d9d9a9bff6 100644 --- a/src/Examples/MultiDbContextExample/appsettings.json +++ b/src/Examples/MultiDbContextExample/appsettings.json @@ -1,10 +1,10 @@ { - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" } diff --git a/src/Examples/NoEntityFrameworkExample/Controllers/WorkItemsController.cs b/src/Examples/NoEntityFrameworkExample/Controllers/WorkItemsController.cs index 79b994d479..63ab620b93 100644 --- a/src/Examples/NoEntityFrameworkExample/Controllers/WorkItemsController.cs +++ b/src/Examples/NoEntityFrameworkExample/Controllers/WorkItemsController.cs @@ -8,11 +8,9 @@ namespace NoEntityFrameworkExample.Controllers { public sealed class WorkItemsController : JsonApiController { - public WorkItemsController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) + public WorkItemsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) - { } + { + } } } diff --git a/src/Examples/NoEntityFrameworkExample/Program.cs b/src/Examples/NoEntityFrameworkExample/Program.cs index c5b2eaa194..6653408dc7 100755 --- a/src/Examples/NoEntityFrameworkExample/Program.cs +++ b/src/Examples/NoEntityFrameworkExample/Program.cs @@ -10,11 +10,12 @@ public static void Main(string[] args) CreateHostBuilder(args).Build().Run(); } - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + private static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } } } diff --git a/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json b/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json index e7b6f86e76..32bc82dfc2 100644 --- a/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json +++ b/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json @@ -1,30 +1,30 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:14149", - "sslPort": 44349 - } + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14149", + "sslPort": 44349 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "/api/reports", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": false, - "launchUrl": "/api/reports", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Kestrel": { - "commandName": "Project", - "launchBrowser": false, - "launchUrl": "/api/reports", - "applicationUrl": "https://localhost:44349;http://localhost:14149", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } + "Kestrel": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "/api/reports", + "applicationUrl": "https://localhost:44349;http://localhost:14149", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } + } } diff --git a/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs b/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs index c0fdc425cf..f934e7dc9b 100644 --- a/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs +++ b/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs @@ -36,9 +36,13 @@ public async Task> GetAsync(CancellationToken canc public async Task GetAsync(int id, CancellationToken cancellationToken) { const string commandText = @"select * from ""WorkItems"" where ""Id""=@id"; - var commandDefinition = new CommandDefinition(commandText, new {id}, cancellationToken: cancellationToken); - var workItems = await QueryAsync(async connection => await connection.QueryAsync(commandDefinition)); + var commandDefinition = new CommandDefinition(commandText, new + { + id + }, cancellationToken: cancellationToken); + + IReadOnlyCollection workItems = await QueryAsync(async connection => await connection.QueryAsync(commandDefinition)); return workItems.Single(); } @@ -59,14 +63,18 @@ public async Task CreateAsync(WorkItem resource, CancellationToken can var commandDefinition = new CommandDefinition(commandText, new { - title = resource.Title, isBlocked = resource.IsBlocked, durationInHours = resource.DurationInHours, projectId = resource.ProjectId + title = resource.Title, + isBlocked = resource.IsBlocked, + durationInHours = resource.DurationInHours, + projectId = resource.ProjectId }, cancellationToken: cancellationToken); - var workItems = await QueryAsync(async connection => await connection.QueryAsync(commandDefinition)); + IReadOnlyCollection workItems = await QueryAsync(async connection => await connection.QueryAsync(commandDefinition)); return workItems.Single(); } - public Task AddToToManyRelationshipAsync(int primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken) + public Task AddToToManyRelationshipAsync(int primaryId, string relationshipName, ISet secondaryResourceIds, + CancellationToken cancellationToken) { throw new NotImplementedException(); } @@ -85,11 +93,14 @@ public async Task DeleteAsync(int id, CancellationToken cancellationToken) { const string commandText = @"delete from ""WorkItems"" where ""Id""=@id"; - await QueryAsync(async connection => - await connection.QueryAsync(new CommandDefinition(commandText, new {id}, cancellationToken: cancellationToken))); + await QueryAsync(async connection => await connection.QueryAsync(new CommandDefinition(commandText, new + { + id + }, cancellationToken: cancellationToken))); } - public Task RemoveFromToManyRelationshipAsync(int primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken) + public Task RemoveFromToManyRelationshipAsync(int primaryId, string relationshipName, ISet secondaryResourceIds, + CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/src/Examples/NoEntityFrameworkExample/Startup.cs b/src/Examples/NoEntityFrameworkExample/Startup.cs index 6699be3376..8e0d20afa5 100644 --- a/src/Examples/NoEntityFrameworkExample/Startup.cs +++ b/src/Examples/NoEntityFrameworkExample/Startup.cs @@ -25,17 +25,13 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddJsonApi( - options => options.Namespace = "api/v1", - resources: builder => builder.Add("workItems") - ); + services.AddJsonApi(options => options.Namespace = "api/v1", resources: builder => builder.Add("workItems")); services.AddScoped, WorkItemService>(); services.AddDbContext(options => { - options.UseNpgsql(_connectionString, - postgresOptions => postgresOptions.SetPostgresVersion(new Version(9, 6))); + options.UseNpgsql(_connectionString, postgresOptions => postgresOptions.SetPostgresVersion(new Version(9, 6))); }); } diff --git a/src/Examples/NoEntityFrameworkExample/appsettings.json b/src/Examples/NoEntityFrameworkExample/appsettings.json index ef303ea8e2..7036e98f9d 100644 --- a/src/Examples/NoEntityFrameworkExample/appsettings.json +++ b/src/Examples/NoEntityFrameworkExample/appsettings.json @@ -1,13 +1,13 @@ { - "Data": { - "DefaultConnection": "Host=localhost;Port=5432;Database=NoEntityFrameworkExample;User ID=postgres;Password=###" - }, - "Logging": { - "LogLevel": { - "Default": "Warning", - "Microsoft": "Warning", - "Microsoft.EntityFrameworkCore.Database.Command": "Information" - } - }, - "AllowedHosts": "*" + "Data": { + "DefaultConnection": "Host=localhost;Port=5432;Database=NoEntityFrameworkExample;User ID=postgres;Password=###" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" + } + }, + "AllowedHosts": "*" } diff --git a/src/Examples/ReportsExample/Controllers/ReportsController.cs b/src/Examples/ReportsExample/Controllers/ReportsController.cs index c9820f8a22..c154dc5562 100644 --- a/src/Examples/ReportsExample/Controllers/ReportsController.cs +++ b/src/Examples/ReportsExample/Controllers/ReportsController.cs @@ -10,14 +10,12 @@ namespace ReportsExample.Controllers { [Route("api/[controller]")] - public class ReportsController : BaseJsonApiController + public class ReportsController : BaseJsonApiController { - public ReportsController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IGetAllService getAll) + public ReportsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IGetAllService getAll) : base(options, loggerFactory, getAll) - { } + { + } [HttpGet] public override async Task GetAsync(CancellationToken cancellationToken) diff --git a/src/Examples/ReportsExample/Program.cs b/src/Examples/ReportsExample/Program.cs index ee8edaa620..6356b0f52a 100644 --- a/src/Examples/ReportsExample/Program.cs +++ b/src/Examples/ReportsExample/Program.cs @@ -10,11 +10,12 @@ public static void Main(string[] args) CreateHostBuilder(args).Build().Run(); } - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + private static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } } } diff --git a/src/Examples/ReportsExample/Properties/launchSettings.json b/src/Examples/ReportsExample/Properties/launchSettings.json index 0fcb421625..ee2eba1f80 100644 --- a/src/Examples/ReportsExample/Properties/launchSettings.json +++ b/src/Examples/ReportsExample/Properties/launchSettings.json @@ -1,30 +1,30 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:14148", - "sslPort": 44348 - } + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14148", + "sslPort": 44348 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "/api/reports", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": false, - "launchUrl": "/api/reports", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Kestrel": { - "commandName": "Project", - "launchBrowser": false, - "launchUrl": "/api/reports", - "applicationUrl": "https://localhost:44348;http://localhost:14148", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } + "Kestrel": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "/api/reports", + "applicationUrl": "https://localhost:44348;http://localhost:14148", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } + } } diff --git a/src/Examples/ReportsExample/Services/ReportService.cs b/src/Examples/ReportsExample/Services/ReportService.cs index 2fcb4367f0..cd9447bd3f 100644 --- a/src/Examples/ReportsExample/Services/ReportService.cs +++ b/src/Examples/ReportsExample/Services/ReportService.cs @@ -22,7 +22,7 @@ public Task> GetAsync(CancellationToken cancellation { _logger.LogInformation("GetAsync"); - var reports = GetReports(); + IReadOnlyCollection reports = GetReports(); return Task.FromResult(reports); } diff --git a/src/Examples/ReportsExample/Startup.cs b/src/Examples/ReportsExample/Startup.cs index fcb5274e8f..a030441258 100644 --- a/src/Examples/ReportsExample/Startup.cs +++ b/src/Examples/ReportsExample/Startup.cs @@ -6,12 +6,10 @@ namespace ReportsExample { public sealed class Startup { - // This method gets called by the runtime. Use this method to add services to the container. + // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddJsonApi( - options => options.Namespace = "api", - discovery => discovery.AddCurrentAssembly()); + services.AddJsonApi(options => options.Namespace = "api", discovery => discovery.AddCurrentAssembly()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/src/Examples/ReportsExample/appsettings.json b/src/Examples/ReportsExample/appsettings.json index 69a873bd79..357fc4f63c 100644 --- a/src/Examples/ReportsExample/appsettings.json +++ b/src/Examples/ReportsExample/appsettings.json @@ -1,9 +1,9 @@ { - "Logging": { - "LogLevel": { - "Default": "Warning", - "Microsoft": "Warning" - } - }, - "AllowedHosts": "*" -} + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransactionFactory.cs b/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransactionFactory.cs index e115c133be..3d30ebe089 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransactionFactory.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/EntityFrameworkCoreTransactionFactory.cs @@ -3,6 +3,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Repositories; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; namespace JsonApiDotNetCore.AtomicOperations { @@ -26,11 +27,10 @@ public EntityFrameworkCoreTransactionFactory(IDbContextResolver dbContextResolve /// public async Task BeginTransactionAsync(CancellationToken cancellationToken) { - var dbContext = _dbContextResolver.GetContext(); + DbContext dbContext = _dbContextResolver.GetContext(); - var transaction = _options.TransactionIsolationLevel != null - ? await dbContext.Database.BeginTransactionAsync(_options.TransactionIsolationLevel.Value, - cancellationToken) + IDbContextTransaction transaction = _options.TransactionIsolationLevel != null + ? await dbContext.Database.BeginTransactionAsync(_options.TransactionIsolationLevel.Value, cancellationToken) : await dbContext.Database.BeginTransactionAsync(cancellationToken); return new EntityFrameworkCoreTransaction(transaction, dbContext); diff --git a/src/JsonApiDotNetCore/AtomicOperations/IOperationProcessorAccessor.cs b/src/JsonApiDotNetCore/AtomicOperations/IOperationProcessorAccessor.cs index 3045e33f7d..693bd6098b 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/IOperationProcessorAccessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/IOperationProcessorAccessor.cs @@ -6,12 +6,12 @@ namespace JsonApiDotNetCore.AtomicOperations { /// - /// Retrieves an instance from the D/I container and invokes a method on it. + /// Retrieves an instance from the D/I container and invokes a method on it. /// public interface IOperationProcessorAccessor { /// - /// Invokes on a processor compatible with the operation kind. + /// Invokes on a processor compatible with the operation kind. /// Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken); } diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs index 2c8b7ab53c..56faba2b66 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs @@ -49,7 +49,7 @@ public void Assign(string localId, string resourceType, string stringId) AssertIsDeclared(localId); - var item = _idsTracked[localId]; + LocalIdState item = _idsTracked[localId]; AssertSameResourceType(resourceType, item.ResourceType, localId); @@ -69,7 +69,7 @@ public string GetValue(string localId, string resourceType) AssertIsDeclared(localId); - var item = _idsTracked[localId]; + LocalIdState item = _idsTracked[localId]; AssertSameResourceType(resourceType, item.ResourceType, localId); diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs index 95ce18da6d..47ab571cb3 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs @@ -4,6 +4,7 @@ using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.AtomicOperations { @@ -35,7 +36,7 @@ public void Validate(IEnumerable operations) try { - foreach (var operation in operations) + foreach (OperationContainer operation in operations) { ValidateOperation(operation); @@ -44,7 +45,7 @@ public void Validate(IEnumerable operations) } catch (JsonApiException exception) { - foreach (var error in exception.Errors) + foreach (Error error in exception.Errors) { error.Source.Pointer = $"/atomic:operations[{operationIndex}]" + error.Source.Pointer; } @@ -64,7 +65,7 @@ private void ValidateOperation(OperationContainer operation) AssertLocalIdIsAssigned(operation.Resource); } - foreach (var secondaryResource in operation.GetSecondaryResources()) + foreach (IIdentifiable secondaryResource in operation.GetSecondaryResources()) { AssertLocalIdIsAssigned(secondaryResource); } @@ -79,7 +80,7 @@ private void DeclareLocalId(IIdentifiable resource) { if (resource.LocalId != null) { - var resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); _localIdTracker.Declare(resource.LocalId, resourceContext.PublicName); } } @@ -88,8 +89,7 @@ private void AssignLocalId(OperationContainer operation) { if (operation.Resource.LocalId != null) { - var resourceContext = - _resourceContextProvider.GetResourceContext(operation.Resource.GetType()); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(operation.Resource.GetType()); _localIdTracker.Assign(operation.Resource.LocalId, resourceContext.PublicName, string.Empty); } @@ -99,7 +99,7 @@ private void AssertLocalIdIsAssigned(IIdentifiable resource) { if (resource.LocalId != null) { - var resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); _localIdTracker.GetValue(resource.LocalId, resourceContext.PublicName); } } diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs index 551e564358..f387316720 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs @@ -17,8 +17,7 @@ public class OperationProcessorAccessor : IOperationProcessorAccessor private readonly IResourceContextProvider _resourceContextProvider; private readonly IServiceProvider _serviceProvider; - public OperationProcessorAccessor(IResourceContextProvider resourceContextProvider, - IServiceProvider serviceProvider) + public OperationProcessorAccessor(IResourceContextProvider resourceContextProvider, IServiceProvider serviceProvider) { ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); @@ -32,17 +31,17 @@ public Task ProcessAsync(OperationContainer operation, Cance { ArgumentGuard.NotNull(operation, nameof(operation)); - var processor = ResolveProcessor(operation); + IOperationProcessor processor = ResolveProcessor(operation); return processor.ProcessAsync(operation, cancellationToken); } protected virtual IOperationProcessor ResolveProcessor(OperationContainer operation) { - var processorInterface = GetProcessorInterface(operation.Kind); - var resourceContext = _resourceContextProvider.GetResourceContext(operation.Resource.GetType()); + Type processorInterface = GetProcessorInterface(operation.Kind); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(operation.Resource.GetType()); - var processorType = processorInterface.MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); - return (IOperationProcessor) _serviceProvider.GetRequiredService(processorType); + Type processorType = processorInterface.MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); + return (IOperationProcessor)_serviceProvider.GetRequiredService(processorType); } private static Type GetProcessorInterface(OperationKind kind) diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs index 4cbd1f938b..e522dabe51 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs @@ -24,9 +24,8 @@ public class OperationsProcessor : IOperationsProcessor private readonly ITargetedFields _targetedFields; private readonly LocalIdValidator _localIdValidator; - public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccessor, - IOperationsTransactionFactory operationsTransactionFactory, ILocalIdTracker localIdTracker, - IResourceContextProvider resourceContextProvider, IJsonApiRequest request, ITargetedFields targetedFields) + public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccessor, IOperationsTransactionFactory operationsTransactionFactory, + ILocalIdTracker localIdTracker, IResourceContextProvider resourceContextProvider, IJsonApiRequest request, ITargetedFields targetedFields) { ArgumentGuard.NotNull(operationProcessorAccessor, nameof(operationProcessorAccessor)); ArgumentGuard.NotNull(operationsTransactionFactory, nameof(operationsTransactionFactory)); @@ -45,8 +44,7 @@ public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccesso } /// - public virtual async Task> ProcessAsync(IList operations, - CancellationToken cancellationToken) + public virtual async Task> ProcessAsync(IList operations, CancellationToken cancellationToken) { ArgumentGuard.NotNull(operations, nameof(operations)); @@ -55,16 +53,17 @@ public virtual async Task> ProcessAsync(IList(); - await using var transaction = await _operationsTransactionFactory.BeginTransactionAsync(cancellationToken); + await using IOperationsTransaction transaction = await _operationsTransactionFactory.BeginTransactionAsync(cancellationToken); + try { - foreach (var operation in operations) + foreach (OperationContainer operation in operations) { operation.SetTransactionId(transaction.TransactionId); await transaction.BeforeProcessOperationAsync(cancellationToken); - var result = await ProcessOperationAsync(operation, cancellationToken); + OperationContainer result = await ProcessOperationAsync(operation, cancellationToken); results.Add(result); await transaction.AfterProcessOperationAsync(cancellationToken); @@ -78,7 +77,7 @@ public virtual async Task> ProcessAsync(IList> ProcessAsync(IList ProcessOperationAsync(OperationContainer operation, - CancellationToken cancellationToken) + protected virtual async Task ProcessOperationAsync(OperationContainer operation, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -128,7 +125,7 @@ protected void TrackLocalIdsForOperation(OperationContainer operation) AssignStringId(operation.Resource); } - foreach (var secondaryResource in operation.GetSecondaryResources()) + foreach (IIdentifiable secondaryResource in operation.GetSecondaryResources()) { AssignStringId(secondaryResource); } @@ -138,7 +135,7 @@ private void DeclareLocalId(IIdentifiable resource) { if (resource.LocalId != null) { - var resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); _localIdTracker.Declare(resource.LocalId, resourceContext.PublicName); } } @@ -147,7 +144,7 @@ private void AssignStringId(IIdentifiable resource) { if (resource.LocalId != null) { - var resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); resource.StringId = _localIdTracker.GetValue(resource.LocalId, resourceContext.PublicName); } } diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs index c988d8da00..127316556d 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/AddToRelationshipProcessor.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -21,16 +22,14 @@ public AddToRelationshipProcessor(IAddToRelationshipService serv } /// - public virtual async Task ProcessAsync(OperationContainer operation, - CancellationToken cancellationToken) + public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { ArgumentGuard.NotNull(operation, nameof(operation)); - var primaryId = (TId) operation.Resource.GetTypedId(); - var secondaryResourceIds = operation.GetSecondaryResources(); + var primaryId = (TId)operation.Resource.GetTypedId(); + ISet secondaryResourceIds = operation.GetSecondaryResources(); - await _service.AddToToManyRelationshipAsync(primaryId, operation.Request.Relationship.PublicName, - secondaryResourceIds, cancellationToken); + await _service.AddToToManyRelationshipAsync(primaryId, operation.Request.Relationship.PublicName, secondaryResourceIds, cancellationToken); return null; } diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs index 9c8547a676..8a8bdef8ad 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/CreateProcessor.cs @@ -16,8 +16,7 @@ public class CreateProcessor : ICreateProcessor private readonly ILocalIdTracker _localIdTracker; private readonly IResourceContextProvider _resourceContextProvider; - public CreateProcessor(ICreateService service, ILocalIdTracker localIdTracker, - IResourceContextProvider resourceContextProvider) + public CreateProcessor(ICreateService service, ILocalIdTracker localIdTracker, IResourceContextProvider resourceContextProvider) { ArgumentGuard.NotNull(service, nameof(service)); ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker)); @@ -29,17 +28,16 @@ public CreateProcessor(ICreateService service, ILocalIdTracker l } /// - public virtual async Task ProcessAsync(OperationContainer operation, - CancellationToken cancellationToken) + public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { ArgumentGuard.NotNull(operation, nameof(operation)); - var newResource = await _service.CreateAsync((TResource) operation.Resource, cancellationToken); + TResource newResource = await _service.CreateAsync((TResource)operation.Resource, cancellationToken); if (operation.Resource.LocalId != null) { - var serverId = newResource != null ? newResource.StringId : operation.Resource.StringId; - var resourceContext = _resourceContextProvider.GetResourceContext(); + string serverId = newResource != null ? newResource.StringId : operation.Resource.StringId; + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(); _localIdTracker.Assign(operation.Resource.LocalId, resourceContext.PublicName, serverId); } diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/DeleteProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/DeleteProcessor.cs index dd91f23384..929ffe73a9 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/DeleteProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/DeleteProcessor.cs @@ -21,12 +21,11 @@ public DeleteProcessor(IDeleteService service) } /// - public virtual async Task ProcessAsync(OperationContainer operation, - CancellationToken cancellationToken) + public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { ArgumentGuard.NotNull(operation, nameof(operation)); - var id = (TId) operation.Resource.GetTypedId(); + var id = (TId)operation.Resource.GetTypedId(); await _service.DeleteAsync(id, cancellationToken); return null; diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/IAddToRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/IAddToRelationshipProcessor.cs index 113771ab4d..2f7c10a3f7 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/IAddToRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/IAddToRelationshipProcessor.cs @@ -8,8 +8,12 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors /// /// Processes a single operation to add resources to a to-many relationship. /// - /// The resource type. - /// The resource identifier type. + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// [PublicAPI] public interface IAddToRelationshipProcessor : IOperationProcessor where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/ICreateProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/ICreateProcessor.cs index 79ffaf4090..0f747a9dd0 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/ICreateProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/ICreateProcessor.cs @@ -8,8 +8,12 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors /// /// Processes a single operation to create a new resource with attributes, relationships or both. /// - /// The resource type. - /// The resource identifier type. + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// [PublicAPI] public interface ICreateProcessor : IOperationProcessor where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/IDeleteProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/IDeleteProcessor.cs index 3c5fbff948..4e5206054d 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/IDeleteProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/IDeleteProcessor.cs @@ -8,8 +8,12 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors /// /// Processes a single operation to delete an existing resource. /// - /// The resource type. - /// The resource identifier type. + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// [PublicAPI] public interface IDeleteProcessor : IOperationProcessor where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/ISetRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/ISetRelationshipProcessor.cs index 4943cdf97f..8dafc839a4 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/ISetRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/ISetRelationshipProcessor.cs @@ -8,8 +8,12 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors /// /// Processes a single operation to perform a complete replacement of a relationship on an existing resource. /// - /// The resource type. - /// The resource identifier type. + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// [PublicAPI] public interface ISetRelationshipProcessor : IOperationProcessor where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/IUpdateProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/IUpdateProcessor.cs index 7c8967e8e7..48847f6ddb 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/IUpdateProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/IUpdateProcessor.cs @@ -6,11 +6,15 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors { /// - /// Processes a single operation to update the attributes and/or relationships of an existing resource. - /// Only the values of sent attributes are replaced. And only the values of sent relationships are replaced. + /// Processes a single operation to update the attributes and/or relationships of an existing resource. Only the values of sent attributes are replaced. + /// And only the values of sent relationships are replaced. /// - /// The resource type. - /// The resource identifier type. + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// [PublicAPI] public interface IUpdateProcessor : IOperationProcessor where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs index 5a3c339417..b41169510d 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/RemoveFromRelationshipProcessor.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -21,16 +22,14 @@ public RemoveFromRelationshipProcessor(IRemoveFromRelationshipService - public virtual async Task ProcessAsync(OperationContainer operation, - CancellationToken cancellationToken) + public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { ArgumentGuard.NotNull(operation, nameof(operation)); - var primaryId = (TId) operation.Resource.GetTypedId(); - var secondaryResourceIds = operation.GetSecondaryResources(); + var primaryId = (TId)operation.Resource.GetTypedId(); + ISet secondaryResourceIds = operation.GetSecondaryResources(); - await _service.RemoveFromToManyRelationshipAsync(primaryId, operation.Request.Relationship.PublicName, - secondaryResourceIds, cancellationToken); + await _service.RemoveFromToManyRelationshipAsync(primaryId, operation.Request.Relationship.PublicName, secondaryResourceIds, cancellationToken); return null; } diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs index 1f9e825537..b2f9ca3678 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -23,28 +24,26 @@ public SetRelationshipProcessor(ISetRelationshipService service) } /// - public virtual async Task ProcessAsync(OperationContainer operation, - CancellationToken cancellationToken) + public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { ArgumentGuard.NotNull(operation, nameof(operation)); - var primaryId = (TId) operation.Resource.GetTypedId(); + var primaryId = (TId)operation.Resource.GetTypedId(); object rightValue = GetRelationshipRightValue(operation); - await _service.SetRelationshipAsync(primaryId, operation.Request.Relationship.PublicName, rightValue, - cancellationToken); + await _service.SetRelationshipAsync(primaryId, operation.Request.Relationship.PublicName, rightValue, cancellationToken); return null; } private static object GetRelationshipRightValue(OperationContainer operation) { - var relationship = operation.Request.Relationship; - var rightValue = relationship.GetValue(operation.Resource); + RelationshipAttribute relationship = operation.Request.Relationship; + object rightValue = relationship.GetValue(operation.Resource); if (relationship is HasManyAttribute) { - var rightResources = TypeHelper.ExtractResources(rightValue); + ICollection rightResources = TypeHelper.ExtractResources(rightValue); return rightResources.ToHashSet(IdentifiableComparer.Instance); } diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/UpdateProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/UpdateProcessor.cs index f1828caaaf..151d91adfe 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/UpdateProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/UpdateProcessor.cs @@ -21,13 +21,12 @@ public UpdateProcessor(IUpdateService service) } /// - public virtual async Task ProcessAsync(OperationContainer operation, - CancellationToken cancellationToken) + public virtual async Task ProcessAsync(OperationContainer operation, CancellationToken cancellationToken) { ArgumentGuard.NotNull(operation, nameof(operation)); - var resource = (TResource) operation.Resource; - var updated = await _service.UpdateAsync(resource.Id, resource, cancellationToken); + var resource = (TResource)operation.Resource; + TResource updated = await _service.UpdateAsync(resource.Id, resource, cancellationToken); return updated == null ? null : operation.WithResource(updated); } diff --git a/src/JsonApiDotNetCore/CollectionExtensions.cs b/src/JsonApiDotNetCore/CollectionExtensions.cs new file mode 100644 index 0000000000..be9962dc2d --- /dev/null +++ b/src/JsonApiDotNetCore/CollectionExtensions.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore +{ + internal static class CollectionExtensions + { + [Pure] + [ContractAnnotation("source: null => true")] + public static bool IsNullOrEmpty(this IEnumerable source) + { + if (source == null) + { + return true; + } + + return !source.Any(); + } + } +} diff --git a/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs index f0a4e6715e..740cbdac0f 100644 --- a/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ApplicationBuilderExtensions.cs @@ -9,10 +9,11 @@ public static class ApplicationBuilderExtensions /// /// Registers the JsonApiDotNetCore middleware. /// - /// The to add the middleware to. + /// + /// The to add the middleware to. + /// /// - /// The code below is the minimal that is required for proper activation, - /// which should be added to your Startup.Configure method. + /// The code below is the minimal that is required for proper activation, which should be added to your Startup.Configure method. /// ().CreateScope(); + using IServiceScope scope = builder.ApplicationServices.GetRequiredService().CreateScope(); var inverseNavigationResolver = scope.ServiceProvider.GetRequiredService(); inverseNavigationResolver.Resolve(); - var jsonApiApplicationBuilder = builder.ApplicationServices.GetRequiredService(); + var jsonApiApplicationBuilder = builder.ApplicationServices.GetRequiredService(); + jsonApiApplicationBuilder.ConfigureMvcOptions = options => { var inputFormatter = builder.ApplicationServices.GetRequiredService(); diff --git a/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs b/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs index ca71ae0470..5806790fb5 100644 --- a/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs +++ b/src/JsonApiDotNetCore/Configuration/GenericServiceFactory.cs @@ -35,7 +35,7 @@ public TInterface Get(Type openGenericType, Type resourceType, Type private TInterface GetInternal(Type openGenericType, params Type[] types) { - var concreteType = openGenericType.MakeGenericType(types); + Type concreteType = openGenericType.MakeGenericType(types); return (TInterface)_serviceProvider.GetService(concreteType); } diff --git a/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs b/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs index 63177fdfd2..a730642e71 100644 --- a/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs +++ b/src/JsonApiDotNetCore/Configuration/IGenericServiceFactory.cs @@ -4,14 +4,14 @@ namespace JsonApiDotNetCore.Configuration { /// - /// Represents the Service Locator design pattern. Used to obtain object instances for types are not known until runtime. - /// This is only used by resource hooks and subject to be removed in a future version. + /// Represents the Service Locator design pattern. Used to obtain object instances for types are not known until runtime. This is only used by resource + /// hooks and subject to be removed in a future version. /// [PublicAPI] public interface IGenericServiceFactory { /// - /// Constructs the generic type and locates the service, then casts to . + /// Constructs the generic type and locates the service, then casts to . /// /// /// (Type openGenericType, Type resourceType); /// - /// Constructs the generic type and locates the service, then casts to . + /// Constructs the generic type and locates the service, then casts to . /// /// /// - /// Responsible for populating . - /// - /// This service is instantiated in the configure phase of the application. - /// - /// When using a data access layer different from EF Core, and when using ResourceHooks - /// that depend on the inverse navigation property (BeforeImplicitUpdateRelationship), - /// you will need to override this service, or set explicitly. + /// Responsible for populating . This service is instantiated in the configure phase of the + /// application. When using a data access layer different from EF Core, and when using ResourceHooks that depend on the inverse navigation property + /// (BeforeImplicitUpdateRelationship), you will need to override this service, or set + /// explicitly. /// [PublicAPI] public interface IInverseNavigationResolver { /// - /// This method is called upon startup by JsonApiDotNetCore. It resolves inverse navigations. + /// This method is called upon startup by JsonApiDotNetCore. It resolves inverse navigations. /// void Resolve(); } diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index ab46a78af2..707d1d0944 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -12,6 +12,15 @@ namespace JsonApiDotNetCore.Configuration /// public interface IJsonApiOptions { + internal NamingStrategy SerializerNamingStrategy + { + get + { + var contractResolver = SerializerSettings.ContractResolver as DefaultContractResolver; + return contractResolver?.NamingStrategy ?? JsonApiOptions.DefaultNamingStrategy; + } + } + /// /// The URL prefix to use for exposed endpoints. /// @@ -21,14 +30,12 @@ public interface IJsonApiOptions string Namespace { get; } /// - /// Specifies the default query string capabilities that can be used on exposed JSON:API attributes. - /// Defaults to . + /// Specifies the default query string capabilities that can be used on exposed JSON:API attributes. Defaults to . /// AttrCapabilities DefaultAttrCapabilities { get; } /// - /// Whether or not stack traces should be serialized in objects. - /// False by default. + /// Whether or not stack traces should be serialized in objects. False by default. /// bool IncludeExceptionStackTraceInErrors { get; } @@ -57,33 +64,26 @@ public interface IJsonApiOptions bool UseRelativeLinks { get; } /// - /// Configures which links to show in the - /// object. Defaults to . - /// This setting can be overruled per resource type by - /// adding on the class definition of a resource. + /// Configures which links to show in the object. Defaults to . This + /// setting can be overruled per resource type by adding on the class definition of a resource. /// LinkTypes TopLevelLinks { get; } /// - /// Configures which links to show in the - /// object. Defaults to . - /// This setting can be overruled per resource type by - /// adding on the class definition of a resource. + /// Configures which links to show in the object. Defaults to . This + /// setting can be overruled per resource type by adding on the class definition of a resource. /// LinkTypes ResourceLinks { get; } /// - /// Configures which links to show in the - /// object. Defaults to . - /// This setting can be overruled for all relationships per resource type by - /// adding on the class definition of a resource. - /// This can be further overruled per relationship by setting . + /// Configures which links to show in the object. Defaults to . This + /// setting can be overruled for all relationships per resource type by adding on the class definition of a + /// resource. This can be further overruled per relationship by setting . /// LinkTypes RelationshipLinks { get; } /// - /// Whether or not the total resource count should be included in all document-level meta objects. - /// False by default. + /// Whether or not the total resource count should be included in all document-level meta objects. False by default. /// bool IncludeTotalResourceCount { get; } @@ -103,78 +103,68 @@ public interface IJsonApiOptions PageNumber MaximumPageNumber { get; } /// - /// Whether or not to enable ASP.NET Core model state validation. - /// False by default. + /// Whether or not to enable ASP.NET Core model state validation. False by default. /// bool ValidateModelState { get; } /// - /// Whether or not clients can provide IDs when creating resources. When not allowed, a 403 Forbidden response is returned - /// if a client attempts to create a resource with a defined ID. - /// False by default. + /// Whether or not clients can provide IDs when creating resources. When not allowed, a 403 Forbidden response is returned if a client attempts to create + /// a resource with a defined ID. False by default. /// bool AllowClientGeneratedIds { get; } /// - /// Whether or not resource hooks are enabled. - /// This is currently an experimental feature and subject to change in future versions. - /// Defaults to False. + /// Whether or not resource hooks are enabled. This is currently an experimental feature and subject to change in future versions. Defaults to False. /// public bool EnableResourceHooks { get; } /// - /// Whether or not database values should be included by default for resource hooks. - /// Ignored if EnableResourceHooks is set to false. - /// False by default. + /// Whether or not database values should be included by default for resource hooks. Ignored if EnableResourceHooks is set to false. False by default. /// bool LoadDatabaseValues { get; } /// - /// Whether or not to produce an error on unknown query string parameters. - /// False by default. + /// Whether or not to produce an error on unknown query string parameters. False by default. /// bool AllowUnknownQueryStringParameters { get; } /// - /// Determines whether legacy filter notation in query strings, such as =eq:, =like:, and =in: is enabled. - /// False by default. + /// Determines whether legacy filter notation in query strings, such as =eq:, =like:, and =in: is enabled. False by default. /// bool EnableLegacyFilterNotation { get; } /// - /// Determines whether the serialization setting can be controlled using a query string parameter. - /// False by default. + /// Determines whether the serialization setting can be controlled using a query string + /// parameter. False by default. /// bool AllowQueryStringOverrideForSerializerNullValueHandling { get; } /// - /// Determines whether the serialization setting can be controlled using a query string parameter. - /// False by default. + /// Determines whether the serialization setting can be controlled using a query string + /// parameter. False by default. /// bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; } /// - /// Controls how many levels deep includes are allowed to be nested. - /// For example, MaximumIncludeDepth=1 would allow ?include=articles but not ?include=articles.revisions. - /// null by default, which means unconstrained. + /// Controls how many levels deep includes are allowed to be nested. For example, MaximumIncludeDepth=1 would allow ?include=articles but not + /// ?include=articles.revisions. null by default, which means unconstrained. /// int? MaximumIncludeDepth { get; } /// - /// Limits the maximum number of operations allowed per atomic:operations request. Defaults to 10. - /// Set to null for unlimited. + /// Limits the maximum number of operations allowed per atomic:operations request. Defaults to 10. Set to null for unlimited. /// int? MaximumOperationsPerRequest { get; } /// - /// Enables to override the default isolation level for database transactions, enabling to balance between consistency and performance. - /// Defaults to null, which leaves this up to Entity Framework Core to choose (and then it varies per database provider). + /// Enables to override the default isolation level for database transactions, enabling to balance between consistency and performance. Defaults to + /// null, which leaves this up to Entity Framework Core to choose (and then it varies per database provider). /// IsolationLevel? TransactionIsolationLevel { get; } /// - /// Specifies the settings that are used by the . - /// Note that at some places a few settings are ignored, to ensure JSON:API spec compliance. + /// Specifies the settings that are used by the . Note that at some places a few settings are ignored, to ensure JSON:API + /// spec compliance. /// /// The next example changes the naming convention to kebab casing. /// /// JsonSerializerSettings SerializerSettings { get; } - - internal NamingStrategy SerializerNamingStrategy - { - get - { - var contractResolver = SerializerSettings.ContractResolver as DefaultContractResolver; - return contractResolver?.NamingStrategy ?? JsonApiOptions.DefaultNamingStrategy; - } - } } } diff --git a/src/JsonApiDotNetCore/Configuration/IRequestScopedServiceProvider.cs b/src/JsonApiDotNetCore/Configuration/IRequestScopedServiceProvider.cs index a3e72e1fb8..327eeca353 100644 --- a/src/JsonApiDotNetCore/Configuration/IRequestScopedServiceProvider.cs +++ b/src/JsonApiDotNetCore/Configuration/IRequestScopedServiceProvider.cs @@ -3,9 +3,10 @@ namespace JsonApiDotNetCore.Configuration { /// - /// An interface used to separate the registration of the global - /// from a request-scoped service provider. This is useful in cases when we need to - /// manually resolve services from the request scope (e.g. operation processors). + /// An interface used to separate the registration of the global from a request-scoped service provider. This is useful + /// in cases when we need to manually resolve services from the request scope (e.g. operation processors). /// - public interface IRequestScopedServiceProvider : IServiceProvider { } + public interface IRequestScopedServiceProvider : IServiceProvider + { + } } diff --git a/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs b/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs index a03b40870d..a452bf30c9 100644 --- a/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs +++ b/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Configuration { /// - /// Responsible for getting s from the . + /// Responsible for getting s from the . /// public interface IResourceContextProvider { @@ -27,6 +27,7 @@ public interface IResourceContextProvider /// /// Gets the resource metadata for the specified resource type. /// - ResourceContext GetResourceContext() where TResource : class, IIdentifiable; + ResourceContext GetResourceContext() + where TResource : class, IIdentifiable; } } diff --git a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs index a02045257c..acbfc5961e 100644 --- a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs @@ -14,53 +14,70 @@ namespace JsonApiDotNetCore.Configuration public interface IResourceGraph : IResourceContextProvider { /// - /// Gets all fields (attributes and relationships) for - /// that are targeted by the selector. If no selector is provided, all - /// exposed fields are returned. + /// Gets all fields (attributes and relationships) for that are targeted by the selector. If no selector is provided, + /// all exposed fields are returned. /// - /// The resource for which to retrieve fields. - /// Should be of the form: (TResource e) => new { e.Field1, e.Field2 } - IReadOnlyCollection GetFields(Expression> selector = null) where TResource : class, IIdentifiable; - + /// + /// The resource for which to retrieve fields. + /// + /// + /// Should be of the form: (TResource e) => new { e.Field1, e.Field2 } + /// + IReadOnlyCollection GetFields(Expression> selector = null) + where TResource : class, IIdentifiable; + /// - /// Gets all attributes for - /// that are targeted by the selector. If no selector is provided, all - /// exposed fields are returned. + /// Gets all attributes for that are targeted by the selector. If no selector is provided, all exposed fields are + /// returned. /// - /// The resource for which to retrieve attributes. - /// Should be of the form: (TResource e) => new { e.Attribute1, e.Attribute2 } - IReadOnlyCollection GetAttributes(Expression> selector = null) where TResource : class, IIdentifiable; - + /// + /// The resource for which to retrieve attributes. + /// + /// + /// Should be of the form: (TResource e) => new { e.Attribute1, e.Attribute2 } + /// + IReadOnlyCollection GetAttributes(Expression> selector = null) + where TResource : class, IIdentifiable; + /// - /// Gets all relationships for - /// that are targeted by the selector. If no selector is provided, all - /// exposed fields are returned. + /// Gets all relationships for that are targeted by the selector. If no selector is provided, all exposed fields are + /// returned. /// - /// The resource for which to retrieve relationships. - /// Should be of the form: (TResource e) => new { e.Relationship1, e.Relationship2 } - IReadOnlyCollection GetRelationships(Expression> selector = null) where TResource : class, IIdentifiable; - + /// + /// The resource for which to retrieve relationships. + /// + /// + /// Should be of the form: (TResource e) => new { e.Relationship1, e.Relationship2 } + /// + IReadOnlyCollection GetRelationships(Expression> selector = null) + where TResource : class, IIdentifiable; + /// /// Gets all exposed fields (attributes and relationships) for the specified type. /// - /// The resource type. Must implement . + /// + /// The resource type. Must implement . + /// IReadOnlyCollection GetFields(Type type); - + /// /// Gets all exposed attributes for the specified type. /// - /// The resource type. Must implement . + /// + /// The resource type. Must implement . + /// IReadOnlyCollection GetAttributes(Type type); - + /// /// Gets all exposed relationships for the specified type. /// - /// The resource type. Must implement . + /// + /// The resource type. Must implement . + /// IReadOnlyCollection GetRelationships(Type type); - + /// - /// Traverses the resource graph, looking for the inverse relationship of the specified - /// . + /// Traverses the resource graph, looking for the inverse relationship of the specified . /// RelationshipAttribute GetInverseRelationship(RelationshipAttribute relationship); } diff --git a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs index 4c895a9251..5675a1b023 100644 --- a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs +++ b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs @@ -14,8 +14,7 @@ public class InverseNavigationResolver : IInverseNavigationResolver private readonly IResourceContextProvider _resourceContextProvider; private readonly IEnumerable _dbContextResolvers; - public InverseNavigationResolver(IResourceContextProvider resourceContextProvider, - IEnumerable dbContextResolvers) + public InverseNavigationResolver(IResourceContextProvider resourceContextProvider, IEnumerable dbContextResolvers) { ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); ArgumentGuard.NotNull(dbContextResolvers, nameof(dbContextResolvers)); @@ -27,7 +26,7 @@ public InverseNavigationResolver(IResourceContextProvider resourceContextProvide /// public void Resolve() { - foreach (var dbContextResolver in _dbContextResolvers) + foreach (IDbContextResolver dbContextResolver in _dbContextResolvers) { DbContext dbContext = dbContextResolver.GetContext(); Resolve(dbContext); @@ -39,6 +38,7 @@ private void Resolve(DbContext dbContext) foreach (ResourceContext resourceContext in _resourceContextProvider.GetResourceContexts()) { IEntityType entityType = dbContext.Model.FindEntityType(resourceContext.ResourceType); + if (entityType != null) { ResolveRelationships(resourceContext.Relationships, entityType); @@ -48,7 +48,7 @@ private void Resolve(DbContext dbContext) private void ResolveRelationships(IReadOnlyCollection relationships, IEntityType entityType) { - foreach (var relationship in relationships) + foreach (RelationshipAttribute relationship in relationships) { if (!(relationship is HasManyThroughAttribute)) { diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 43c0413001..9f04418849 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -22,14 +22,15 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace JsonApiDotNetCore.Configuration { /// - /// A utility class that builds a JsonApi application. It registers all required services - /// and allows the user to override parts of the startup configuration. + /// A utility class that builds a JsonApi application. It registers all required services and allows the user to override parts of the startup + /// configuration. /// internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder, IDisposable { @@ -39,7 +40,7 @@ internal sealed class JsonApiApplicationBuilder : IJsonApiApplicationBuilder, ID private readonly ResourceGraphBuilder _resourceGraphBuilder; private readonly ServiceDiscoveryFacade _serviceDiscoveryFacade; private readonly ServiceProvider _intermediateProvider; - + public Action ConfigureMvcOptions { get; set; } public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mvcBuilder) @@ -58,15 +59,15 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv } /// - /// Executes the action provided by the user to configure . + /// Executes the action provided by the user to configure . /// public void ConfigureJsonApiOptions(Action configureOptions) { configureOptions?.Invoke(_options); } - + /// - /// Executes the action provided by the user to configure . + /// Executes the action provided by the user to configure . /// public void ConfigureAutoDiscovery(Action configureAutoDiscovery) { @@ -74,13 +75,13 @@ public void ConfigureAutoDiscovery(Action configureAutoD } /// - /// Configures and builds the resource graph with resources from the provided sources and adds it to the DI container. + /// Configures and builds the resource graph with resources from the provided sources and adds it to the DI container. /// public void AddResourceGraph(ICollection dbContextTypes, Action configureResourceGraph) { _serviceDiscoveryFacade.DiscoverResources(); - foreach (var dbContextType in dbContextTypes) + foreach (Type dbContextType in dbContextTypes) { var dbContext = (DbContext)_intermediateProvider.GetRequiredService(dbContextType); AddResourcesFromDbContext(dbContext, _resourceGraphBuilder); @@ -88,7 +89,7 @@ public void AddResourceGraph(ICollection dbContextTypes, Action dbContextTypes) { _services.AddScoped(typeof(DbContextResolver<>)); - foreach (var dbContextType in dbContextTypes) + foreach (Type dbContextType in dbContextTypes) { - var contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); + Type contextResolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); _services.AddScoped(typeof(IDbContextResolver), contextResolverType); } @@ -183,8 +184,8 @@ private void AddMiddlewareLayer() private void AddResourceLayer() { - RegisterImplementationForOpenInterfaces(ServiceDiscoveryFacade.ResourceDefinitionInterfaces, - typeof(JsonApiResourceDefinition<>), typeof(JsonApiResourceDefinition<,>)); + RegisterImplementationForOpenInterfaces(ServiceDiscoveryFacade.ResourceDefinitionInterfaces, typeof(JsonApiResourceDefinition<>), + typeof(JsonApiResourceDefinition<,>)); _services.AddScoped(); _services.AddScoped(); @@ -193,25 +194,23 @@ private void AddResourceLayer() private void AddRepositoryLayer() { - RegisterImplementationForOpenInterfaces(ServiceDiscoveryFacade.RepositoryInterfaces, - typeof(EntityFrameworkCoreRepository<>), typeof(EntityFrameworkCoreRepository<,>)); + RegisterImplementationForOpenInterfaces(ServiceDiscoveryFacade.RepositoryInterfaces, typeof(EntityFrameworkCoreRepository<>), + typeof(EntityFrameworkCoreRepository<,>)); _services.AddScoped(); } private void AddServiceLayer() { - RegisterImplementationForOpenInterfaces(ServiceDiscoveryFacade.ServiceInterfaces, - typeof(JsonApiResourceService<>), typeof(JsonApiResourceService<,>)); + RegisterImplementationForOpenInterfaces(ServiceDiscoveryFacade.ServiceInterfaces, typeof(JsonApiResourceService<>), + typeof(JsonApiResourceService<,>)); } private void RegisterImplementationForOpenInterfaces(HashSet openGenericInterfaces, Type intImplementation, Type implementation) { - foreach (var openGenericInterface in openGenericInterfaces) + foreach (Type openGenericInterface in openGenericInterfaces) { - var implementationType = openGenericInterface.GetGenericArguments().Length == 1 - ? intImplementation - : implementation; + Type implementationType = openGenericInterface.GetGenericArguments().Length == 1 ? intImplementation : implementation; _services.AddScoped(openGenericInterface, implementationType); } @@ -256,7 +255,7 @@ private void RegisterDependentService() } private void AddResourceHooks() - { + { if (_options.EnableResourceHooks) { _services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>)); @@ -303,12 +302,12 @@ private void AddOperationsLayer() private void AddResourcesFromDbContext(DbContext dbContext, ResourceGraphBuilder builder) { - foreach (var entityType in dbContext.Model.GetEntityTypes()) + foreach (IEntityType entityType in dbContext.Model.GetEntityTypes()) { builder.Add(entityType.ClrType); } } - + public void Dispose() { _intermediateProvider.Dispose(); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs b/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs index e40935696d..19f9edc531 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiModelMetadataProvider.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Configuration { /// - /// Custom implementation of to support JSON:API partial patching. + /// Custom implementation of to support JSON:API partial patching. /// internal sealed class JsonApiModelMetadataProvider : DefaultModelMetadataProvider { @@ -20,7 +20,8 @@ public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsPro } /// - public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IOptions optionsAccessor, IRequestScopedServiceProvider serviceProvider) + public JsonApiModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IOptions optionsAccessor, + IRequestScopedServiceProvider serviceProvider) : base(detailsProvider, optionsAccessor) { _jsonApiValidationFilter = new JsonApiValidationFilter(serviceProvider); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 85811578fd..c1079fad58 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -12,6 +12,10 @@ public sealed class JsonApiOptions : IJsonApiOptions { internal static readonly NamingStrategy DefaultNamingStrategy = new CamelCaseNamingStrategy(); + // Workaround for https://github.com/dotnet/efcore/issues/21026 + internal bool DisableTopPagination { get; set; } + internal bool DisableChildrenPagination { get; set; } + /// public string Namespace { get; set; } @@ -86,9 +90,5 @@ public sealed class JsonApiOptions : IJsonApiOptions NamingStrategy = DefaultNamingStrategy } }; - - // Workaround for https://github.com/dotnet/efcore/issues/21026 - internal bool DisableTopPagination { get; set; } - internal bool DisableChildrenPagination { get; set; } } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs index 9baa1e3194..f4f9d5e1a7 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs @@ -32,13 +32,15 @@ public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEnt return true; } - var isTopResourceInPrimaryRequest = string.IsNullOrEmpty(parentEntry.Key) && IsAtPrimaryEndpoint(request); + bool isTopResourceInPrimaryRequest = string.IsNullOrEmpty(parentEntry.Key) && IsAtPrimaryEndpoint(request); + if (!isTopResourceInPrimaryRequest) { return false; } var httpContextAccessor = _serviceProvider.GetRequiredService(); + if (httpContextAccessor.HttpContext.Request.Method == HttpMethods.Patch || request.OperationKind == OperationKind.UpdateResource) { var targetedFields = _serviceProvider.GetRequiredService(); diff --git a/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs b/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs index 0bd2b71477..649a219c0b 100644 --- a/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs +++ b/src/JsonApiDotNetCore/Configuration/RequestScopedServiceProvider.cs @@ -22,8 +22,7 @@ public object GetService(Type serviceType) if (_httpContextAccessor.HttpContext == null) { - throw new InvalidOperationException( - $"Cannot resolve scoped service '{serviceType.FullName}' outside the context of an HTTP request. " + + throw new InvalidOperationException($"Cannot resolve scoped service '{serviceType.FullName}' outside the context of an HTTP request. " + "If you are hitting this error in automated tests, you should instead inject your own " + "IRequestScopedServiceProvider implementation. See the GitHub repository for how we do this internally. " + "https://github.com/json-api-dotnet/JsonApiDotNetCore/search?q=TestScopedServiceProvider&unscoped_q=TestScopedServiceProvider"); diff --git a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs index 355c11e3ac..4ba2975a10 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs @@ -12,6 +12,8 @@ namespace JsonApiDotNetCore.Configuration [PublicAPI] public class ResourceContext { + private IReadOnlyCollection _fields; + /// /// The publicly exposed resource name. /// @@ -28,14 +30,12 @@ public class ResourceContext public Type IdentityType { get; set; } /// - /// Exposed resource attributes. - /// See https://jsonapi.org/format/#document-resource-object-attributes. + /// Exposed resource attributes. See https://jsonapi.org/format/#document-resource-object-attributes. /// public IReadOnlyCollection Attributes { get; set; } /// - /// Exposed resource relationships. - /// See https://jsonapi.org/format/#document-resource-object-relationships. + /// Exposed resource relationships. See https://jsonapi.org/format/#document-resource-object-relationships. /// public IReadOnlyCollection Relationships { get; set; } @@ -44,42 +44,36 @@ public class ResourceContext /// public IReadOnlyCollection EagerLoads { get; set; } - private IReadOnlyCollection _fields; - /// - /// Exposed resource attributes and relationships. - /// See https://jsonapi.org/format/#document-resource-object-fields. + /// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. /// public IReadOnlyCollection Fields => _fields ??= Attributes.Cast().Concat(Relationships).ToArray(); /// - /// Configures which links to show in the - /// object for this resource type. - /// Defaults to , which falls back to . + /// Configures which links to show in the object for this resource type. Defaults to + /// , which falls back to . /// /// - /// In the process of building the resource graph, this value is set based on usage. + /// In the process of building the resource graph, this value is set based on usage. /// public LinkTypes TopLevelLinks { get; internal set; } = LinkTypes.NotConfigured; /// - /// Configures which links to show in the - /// object for this resource type. - /// Defaults to , which falls back to . + /// Configures which links to show in the object for this resource type. Defaults to + /// , which falls back to . /// /// - /// In the process of building the resource graph, this value is set based on usage. + /// In the process of building the resource graph, this value is set based on usage. /// public LinkTypes ResourceLinks { get; internal set; } = LinkTypes.NotConfigured; /// - /// Configures which links to show in the - /// object for all relationships of this resource type. - /// Defaults to , which falls back to . - /// This can be overruled per relationship by setting . + /// Configures which links to show in the object for all relationships of this resource type. + /// Defaults to , which falls back to . This can be overruled per + /// relationship by setting . /// /// - /// In the process of building the resource graph, this value is set based on usage. + /// In the process of building the resource graph, this value is set based on usage. /// public LinkTypes RelationshipLinks { get; internal set; } = LinkTypes.NotConfigured; diff --git a/src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs b/src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs index 56f7450214..667d6680fa 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceDescriptorAssemblyCache.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -9,7 +10,8 @@ namespace JsonApiDotNetCore.Configuration /// internal sealed class ResourceDescriptorAssemblyCache { - private readonly Dictionary> _resourceDescriptorsPerAssembly = new Dictionary>(); + private readonly Dictionary> _resourceDescriptorsPerAssembly = + new Dictionary>(); public void RegisterAssembly(Assembly assembly) { @@ -28,8 +30,7 @@ public void RegisterAssembly(Assembly assembly) private void EnsureAssembliesScanned() { - foreach (var assemblyToScan in _resourceDescriptorsPerAssembly.Where(pair => pair.Value == null) - .Select(pair => pair.Key).ToArray()) + foreach (Assembly assemblyToScan in _resourceDescriptorsPerAssembly.Where(pair => pair.Value == null).Select(pair => pair.Key).ToArray()) { _resourceDescriptorsPerAssembly[assemblyToScan] = ScanForResourceDescriptors(assemblyToScan).ToArray(); } @@ -37,9 +38,10 @@ private void EnsureAssembliesScanned() private static IEnumerable ScanForResourceDescriptors(Assembly assembly) { - foreach (var type in assembly.GetTypes()) + foreach (Type type in assembly.GetTypes()) { - var resourceDescriptor = TypeLocator.TryGetResourceDescriptor(type); + ResourceDescriptor resourceDescriptor = TypeLocator.TryGetResourceDescriptor(type); + if (resourceDescriptor != null) { yield return resourceDescriptor; diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index a52af699c1..3f56e3fffe 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -12,8 +13,8 @@ namespace JsonApiDotNetCore.Configuration [PublicAPI] public class ResourceGraph : IResourceGraph { - private readonly IReadOnlyCollection _resources; private static readonly Type ProxyTargetAccessorType = Type.GetType("Castle.DynamicProxy.IProxyTargetAccessor, Castle.Core"); + private readonly IReadOnlyCollection _resources; public ResourceGraph(IReadOnlyCollection resources) { @@ -23,7 +24,10 @@ public ResourceGraph(IReadOnlyCollection resources) } /// - public IReadOnlyCollection GetResourceContexts() => _resources; + public IReadOnlyCollection GetResourceContexts() + { + return _resources; + } /// public ResourceContext GetResourceContext(string resourceName) @@ -44,23 +48,29 @@ public ResourceContext GetResourceContext(Type resourceType) } /// - public ResourceContext GetResourceContext() where TResource : class, IIdentifiable - => GetResourceContext(typeof(TResource)); + public ResourceContext GetResourceContext() + where TResource : class, IIdentifiable + { + return GetResourceContext(typeof(TResource)); + } /// - public IReadOnlyCollection GetFields(Expression> selector = null) where TResource : class, IIdentifiable + public IReadOnlyCollection GetFields(Expression> selector = null) + where TResource : class, IIdentifiable { return Getter(selector); } /// - public IReadOnlyCollection GetAttributes(Expression> selector = null) where TResource : class, IIdentifiable + public IReadOnlyCollection GetAttributes(Expression> selector = null) + where TResource : class, IIdentifiable { return Getter(selector, FieldFilterType.Attribute).Cast().ToArray(); } /// - public IReadOnlyCollection GetRelationships(Expression> selector = null) where TResource : class, IIdentifiable + public IReadOnlyCollection GetRelationships(Expression> selector = null) + where TResource : class, IIdentifiable { return Getter(selector, FieldFilterType.Relationship).Cast().ToArray(); } @@ -99,14 +109,15 @@ public RelationshipAttribute GetInverseRelationship(RelationshipAttribute relati return null; } - return GetResourceContext(relationship.RightType) - .Relationships - .SingleOrDefault(r => r.Property == relationship.InverseNavigationProperty); + return GetResourceContext(relationship.RightType).Relationships.SingleOrDefault(r => r.Property == relationship.InverseNavigationProperty); } - private IReadOnlyCollection Getter(Expression> selector = null, FieldFilterType type = FieldFilterType.None) where TResource : class, IIdentifiable + private IReadOnlyCollection Getter(Expression> selector = null, + FieldFilterType type = FieldFilterType.None) + where TResource : class, IIdentifiable { IReadOnlyCollection available; + if (type == FieldFilterType.Attribute) { available = GetResourceContext(typeof(TResource)).Attributes; @@ -127,10 +138,10 @@ private IReadOnlyCollection Getter(Expression var targeted = new List(); - var selectorBody = RemoveConvert(selector.Body); + Expression selectorBody = RemoveConvert(selector.Body); if (selectorBody is MemberExpression memberExpression) - { + { // model => model.Field1 try { @@ -144,9 +155,10 @@ private IReadOnlyCollection Getter(Expression } if (selectorBody is NewExpression newExpression) - { + { // model => new { model.Field1, model.Field2 } string memberName = null; + try { if (newExpression.Members == null) @@ -154,11 +166,12 @@ private IReadOnlyCollection Getter(Expression return targeted; } - foreach (var member in newExpression.Members) + foreach (MemberInfo member in newExpression.Members) { memberName = member.Name; targeted.Add(available.Single(f => f.Property.Name == memberName)); } + return targeted; } catch (InvalidOperationException) @@ -167,17 +180,18 @@ private IReadOnlyCollection Getter(Expression } } - throw new ArgumentException( - $"The expression '{selector}' should select a single property or select multiple properties into an anonymous type. " + + throw new ArgumentException($"The expression '{selector}' should select a single property or select multiple properties into an anonymous type. " + "For example: 'article => article.Title' or 'article => new { article.Title, article.PageCount }'."); } - private bool IsLazyLoadingProxyForResourceType(Type resourceType) => - ProxyTargetAccessorType?.IsAssignableFrom(resourceType) ?? false; + private bool IsLazyLoadingProxyForResourceType(Type resourceType) + { + return ProxyTargetAccessorType?.IsAssignableFrom(resourceType) ?? false; + } private static Expression RemoveConvert(Expression expression) { - var innerExpression = expression; + Expression innerExpression = expression; while (true) { diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 7b07847682..a6bb501ffd 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Configuration { /// - /// Builds and configures the . + /// Builds and configures the . /// [PublicAPI] public class ResourceGraphBuilder @@ -30,7 +30,7 @@ public ResourceGraphBuilder(IJsonApiOptions options, ILoggerFactory loggerFactor } /// - /// Constructs the . + /// Constructs the . /// public IResourceGraph Build() { @@ -41,6 +41,7 @@ public IResourceGraph Build() private void SetResourceLinksOptions(ResourceContext resourceContext) { var attribute = (ResourceLinksAttribute)resourceContext.ResourceType.GetCustomAttribute(typeof(ResourceLinksAttribute)); + if (attribute != null) { resourceContext.RelationshipLinks = attribute.RelationshipLinks; @@ -48,38 +49,54 @@ private void SetResourceLinksOptions(ResourceContext resourceContext) resourceContext.TopLevelLinks = attribute.TopLevelLinks; } } - + /// /// Adds a JSON:API resource with int as the identifier type. /// - /// The resource model type. + /// + /// The resource model type. + /// /// - /// The name under which the resource is publicly exposed by the API. - /// If nothing is specified, the configured naming convention formatter will be applied. + /// The name under which the resource is publicly exposed by the API. If nothing is specified, the configured naming convention formatter will be + /// applied. /// - public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable - => Add(publicName); - + public ResourceGraphBuilder Add(string publicName = null) + where TResource : class, IIdentifiable + { + return Add(publicName); + } + /// /// Adds a JSON:API resource. /// - /// The resource model type. - /// The resource model identifier type. + /// + /// The resource model type. + /// + /// + /// The resource model identifier type. + /// /// - /// The name under which the resource is publicly exposed by the API. - /// If nothing is specified, the configured naming convention formatter will be applied. + /// The name under which the resource is publicly exposed by the API. If nothing is specified, the configured naming convention formatter will be + /// applied. /// - public ResourceGraphBuilder Add(string publicName = null) where TResource : class, IIdentifiable - => Add(typeof(TResource), typeof(TId), publicName); - + public ResourceGraphBuilder Add(string publicName = null) + where TResource : class, IIdentifiable + { + return Add(typeof(TResource), typeof(TId), publicName); + } + /// /// Adds a JSON:API resource. /// - /// The resource model type. - /// The resource model identifier type. + /// + /// The resource model type. + /// + /// + /// The resource model identifier type. + /// /// - /// The name under which the resource is publicly exposed by the API. - /// If nothing is specified, the configured naming convention formatter will be applied. + /// The name under which the resource is publicly exposed by the API. If nothing is specified, the configured naming convention formatter will be + /// applied. /// public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string publicName = null) { @@ -92,10 +109,10 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu if (TypeHelper.IsOrImplementsInterface(resourceType, typeof(IIdentifiable))) { - var effectivePublicName = publicName ?? FormatResourceName(resourceType); - var effectiveIdType = idType ?? TypeLocator.TryGetIdType(resourceType); - - var resourceContext = CreateResourceContext(effectivePublicName, resourceType, effectiveIdType); + string effectivePublicName = publicName ?? FormatResourceName(resourceType); + Type effectiveIdType = idType ?? TypeLocator.TryGetIdType(resourceType); + + ResourceContext resourceContext = CreateResourceContext(effectivePublicName, resourceType, effectiveIdType); _resources.Add(resourceContext); } else @@ -106,15 +123,18 @@ public ResourceGraphBuilder Add(Type resourceType, Type idType = null, string pu return this; } - private ResourceContext CreateResourceContext(string publicName, Type resourceType, Type idType) => new ResourceContext + private ResourceContext CreateResourceContext(string publicName, Type resourceType, Type idType) { - PublicName = publicName, - ResourceType = resourceType, - IdentityType = idType, - Attributes = GetAttributes(resourceType), - Relationships = GetRelationships(resourceType), - EagerLoads = GetEagerLoads(resourceType) - }; + return new ResourceContext + { + PublicName = publicName, + ResourceType = resourceType, + IdentityType = idType, + Attributes = GetAttributes(resourceType), + Relationships = GetRelationships(resourceType), + EagerLoads = GetEagerLoads(resourceType) + }; + } private IReadOnlyCollection GetAttributes(Type resourceType) { @@ -122,7 +142,7 @@ private IReadOnlyCollection GetAttributes(Type resourceType) var attributes = new List(); - foreach (var property in resourceType.GetProperties()) + foreach (PropertyInfo property in resourceType.GetProperties()) { var attribute = (AttrAttribute)property.GetCustomAttribute(typeof(AttrAttribute)); @@ -137,6 +157,7 @@ private IReadOnlyCollection GetAttributes(Type resourceType) Property = property, Capabilities = _options.DefaultAttrCapabilities }; + attributes.Add(idAttr); continue; } @@ -156,6 +177,7 @@ private IReadOnlyCollection GetAttributes(Type resourceType) attributes.Add(attribute); } + return attributes; } @@ -164,10 +186,12 @@ private IReadOnlyCollection GetRelationships(Type resourc ArgumentGuard.NotNull(resourceType, nameof(resourceType)); var attributes = new List(); - var properties = resourceType.GetProperties(); - foreach (var prop in properties) + PropertyInfo[] properties = resourceType.GetProperties(); + + foreach (PropertyInfo prop in properties) { var attribute = (RelationshipAttribute)prop.GetCustomAttribute(typeof(RelationshipAttribute)); + if (attribute == null) { continue; @@ -181,14 +205,16 @@ private IReadOnlyCollection GetRelationships(Type resourc if (attribute is HasManyThroughAttribute hasManyThroughAttribute) { - var throughProperty = properties.SingleOrDefault(p => p.Name == hasManyThroughAttribute.ThroughPropertyName); + PropertyInfo throughProperty = properties.SingleOrDefault(p => p.Name == hasManyThroughAttribute.ThroughPropertyName); + if (throughProperty == null) { throw new InvalidConfigurationException($"Invalid {nameof(HasManyThroughAttribute)} on '{resourceType}.{attribute.Property.Name}': " + $"Resource does not contain a property named '{hasManyThroughAttribute.ThroughPropertyName}'."); } - var throughType = TryGetThroughType(throughProperty); + Type throughType = TryGetThroughType(throughProperty); + if (throughType == null) { throw new InvalidConfigurationException($"Invalid {nameof(HasManyThroughAttribute)} on '{resourceType}.{attribute.Property.Name}': " + @@ -201,45 +227,52 @@ private IReadOnlyCollection GetRelationships(Type resourc // ArticleTag hasManyThroughAttribute.ThroughType = throughType; - var throughProperties = throughType.GetProperties(); + PropertyInfo[] throughProperties = throughType.GetProperties(); // ArticleTag.Article if (hasManyThroughAttribute.LeftPropertyName != null) { // In case of a self-referencing many-to-many relationship, the left property name must be specified. - hasManyThroughAttribute.LeftProperty = hasManyThroughAttribute.ThroughType.GetProperty(hasManyThroughAttribute.LeftPropertyName) - ?? throw new InvalidConfigurationException($"'{throughType}' does not contain a navigation property named '{hasManyThroughAttribute.LeftPropertyName}'."); + hasManyThroughAttribute.LeftProperty = hasManyThroughAttribute.ThroughType.GetProperty(hasManyThroughAttribute.LeftPropertyName) ?? + throw new InvalidConfigurationException( + $"'{throughType}' does not contain a navigation property named '{hasManyThroughAttribute.LeftPropertyName}'."); } else { // In case of a non-self-referencing many-to-many relationship, we just pick the single compatible type. - hasManyThroughAttribute.LeftProperty = throughProperties.SingleOrDefault(x => x.PropertyType.IsAssignableFrom(resourceType)) - ?? throw new InvalidConfigurationException($"'{throughType}' does not contain a navigation property to type '{resourceType}'."); + hasManyThroughAttribute.LeftProperty = throughProperties.SingleOrDefault(x => x.PropertyType.IsAssignableFrom(resourceType)) ?? + throw new InvalidConfigurationException($"'{throughType}' does not contain a navigation property to type '{resourceType}'."); } // ArticleTag.ArticleId - var leftIdPropertyName = hasManyThroughAttribute.LeftIdPropertyName ?? hasManyThroughAttribute.LeftProperty.Name + "Id"; - hasManyThroughAttribute.LeftIdProperty = throughProperties.SingleOrDefault(x => x.Name == leftIdPropertyName) - ?? throw new InvalidConfigurationException($"'{throughType}' does not contain a relationship ID property to type '{resourceType}' with name '{leftIdPropertyName}'."); + string leftIdPropertyName = hasManyThroughAttribute.LeftIdPropertyName ?? hasManyThroughAttribute.LeftProperty.Name + "Id"; + + hasManyThroughAttribute.LeftIdProperty = throughProperties.SingleOrDefault(x => x.Name == leftIdPropertyName) ?? + throw new InvalidConfigurationException( + $"'{throughType}' does not contain a relationship ID property to type '{resourceType}' with name '{leftIdPropertyName}'."); // ArticleTag.Tag if (hasManyThroughAttribute.RightPropertyName != null) { // In case of a self-referencing many-to-many relationship, the right property name must be specified. - hasManyThroughAttribute.RightProperty = hasManyThroughAttribute.ThroughType.GetProperty(hasManyThroughAttribute.RightPropertyName) - ?? throw new InvalidConfigurationException($"'{throughType}' does not contain a navigation property named '{hasManyThroughAttribute.RightPropertyName}'."); + hasManyThroughAttribute.RightProperty = hasManyThroughAttribute.ThroughType.GetProperty(hasManyThroughAttribute.RightPropertyName) ?? + throw new InvalidConfigurationException( + $"'{throughType}' does not contain a navigation property named '{hasManyThroughAttribute.RightPropertyName}'."); } else { // In case of a non-self-referencing many-to-many relationship, we just pick the single compatible type. - hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.RightType) - ?? throw new InvalidConfigurationException($"'{throughType}' does not contain a navigation property to type '{hasManyThroughAttribute.RightType}'."); + hasManyThroughAttribute.RightProperty = throughProperties.SingleOrDefault(x => x.PropertyType == hasManyThroughAttribute.RightType) ?? + throw new InvalidConfigurationException( + $"'{throughType}' does not contain a navigation property to type '{hasManyThroughAttribute.RightType}'."); } // ArticleTag.TagId - var rightIdPropertyName = hasManyThroughAttribute.RightIdPropertyName ?? hasManyThroughAttribute.RightProperty.Name + "Id"; - hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName) - ?? throw new InvalidConfigurationException($"'{throughType}' does not contain a relationship ID property to type '{hasManyThroughAttribute.RightType}' with name '{rightIdPropertyName}'."); + string rightIdPropertyName = hasManyThroughAttribute.RightIdPropertyName ?? hasManyThroughAttribute.RightProperty.Name + "Id"; + + hasManyThroughAttribute.RightIdProperty = throughProperties.SingleOrDefault(x => x.Name == rightIdPropertyName) ?? + throw new InvalidConfigurationException( + $"'{throughType}' does not contain a relationship ID property to type '{hasManyThroughAttribute.RightType}' with name '{rightIdPropertyName}'."); } } @@ -250,10 +283,12 @@ private Type TryGetThroughType(PropertyInfo throughProperty) { if (throughProperty.PropertyType.IsGenericType) { - var typeArguments = throughProperty.PropertyType.GetGenericArguments(); + Type[] typeArguments = throughProperty.PropertyType.GetGenericArguments(); + if (typeArguments.Length == 1) { - var constructedThroughType = typeof(ICollection<>).MakeGenericType(typeArguments[0]); + Type constructedThroughType = typeof(ICollection<>).MakeGenericType(typeArguments[0]); + if (TypeHelper.IsOrImplementsInterface(throughProperty.PropertyType, constructedThroughType)) { return typeArguments[0]; @@ -281,11 +316,12 @@ private IReadOnlyCollection GetEagerLoads(Type resourceType, } var attributes = new List(); - var properties = resourceType.GetProperties(); + PropertyInfo[] properties = resourceType.GetProperties(); - foreach (var property in properties) + foreach (PropertyInfo property in properties) { - var attribute = (EagerLoadAttribute) property.GetCustomAttribute(typeof(EagerLoadAttribute)); + var attribute = (EagerLoadAttribute)property.GetCustomAttribute(typeof(EagerLoadAttribute)); + if (attribute == null) { continue; @@ -303,8 +339,7 @@ private IReadOnlyCollection GetEagerLoads(Type resourceType, private Type TypeOrElementType(Type type) { - var interfaces = type.GetInterfaces() - .Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)).ToArray(); + Type[] interfaces = type.GetInterfaces().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)).ToArray(); return interfaces.Length == 1 ? interfaces.Single().GenericTypeArguments[0] : type; } diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index ac2e28b22a..a63d734fe4 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -19,17 +19,13 @@ public static class ServiceCollectionExtensions /// /// Configures JsonApiDotNetCore by registering resources manually. /// - public static IServiceCollection AddJsonApi(this IServiceCollection services, - Action options = null, - Action discovery = null, - Action resources = null, - IMvcCoreBuilder mvcBuilder = null, + public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options = null, + Action discovery = null, Action resources = null, IMvcCoreBuilder mvcBuilder = null, ICollection dbContextTypes = null) { ArgumentGuard.NotNull(services, nameof(services)); - SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, - dbContextTypes ?? Array.Empty()); + SetupApplicationBuilder(services, options, discovery, resources, mvcBuilder, dbContextTypes ?? Array.Empty()); return services; } @@ -37,19 +33,16 @@ public static IServiceCollection AddJsonApi(this IServiceCollection services, /// /// Configures JsonApiDotNetCore by registering resources from an Entity Framework Core model. /// - public static IServiceCollection AddJsonApi(this IServiceCollection services, - Action options = null, - Action discovery = null, - Action resources = null, - IMvcCoreBuilder mvcBuilder = null) + public static IServiceCollection AddJsonApi(this IServiceCollection services, Action options = null, + Action discovery = null, Action resources = null, IMvcCoreBuilder mvcBuilder = null) where TDbContext : DbContext { return AddJsonApi(services, options, discovery, resources, mvcBuilder, typeof(TDbContext).AsArray()); } private static void SetupApplicationBuilder(IServiceCollection services, Action configureOptions, - Action configureAutoDiscovery, - Action configureResourceGraph, IMvcCoreBuilder mvcBuilder, ICollection dbContextTypes) + Action configureAutoDiscovery, Action configureResourceGraph, IMvcCoreBuilder mvcBuilder, + ICollection dbContextTypes) { using var applicationBuilder = new JsonApiApplicationBuilder(services, mvcBuilder ?? services.AddMvcCore()); @@ -60,28 +53,29 @@ private static void SetupApplicationBuilder(IServiceCollection services, Action< applicationBuilder.DiscoverInjectables(); applicationBuilder.ConfigureServiceContainer(dbContextTypes); } - + /// - /// Enables client serializers for sending requests and receiving responses - /// in JSON:API format. Internally only used for testing. - /// Will be extended in the future to be part of a JsonApiClientDotNetCore package. + /// Enables client serializers for sending requests and receiving responses in JSON:API format. Internally only used for testing. Will be extended in the + /// future to be part of a JsonApiClientDotNetCore package. /// public static IServiceCollection AddClientSerialization(this IServiceCollection services) { ArgumentGuard.NotNull(services, nameof(services)); services.AddScoped(); + services.AddScoped(sp => { var graph = sp.GetRequiredService(); return new RequestSerializer(graph, new ResourceObjectBuilder(graph, new ResourceObjectBuilderSettings())); }); + return services; } /// - /// Adds IoC container registrations for the various JsonApiDotNetCore resource service interfaces, - /// such as , and the various others. + /// Adds IoC container registrations for the various JsonApiDotNetCore resource service interfaces, such as , + /// and the various others. /// public static IServiceCollection AddResourceService(this IServiceCollection services) { @@ -93,8 +87,8 @@ public static IServiceCollection AddResourceService(this IServiceColle } /// - /// Adds IoC container registrations for the various JsonApiDotNetCore resource repository interfaces, - /// such as and . + /// Adds IoC container registrations for the various JsonApiDotNetCore resource repository interfaces, such as + /// and . /// public static IServiceCollection AddResourceRepository(this IServiceCollection services) { @@ -108,22 +102,23 @@ public static IServiceCollection AddResourceRepository(this IServic private static void RegisterForConstructedType(IServiceCollection services, Type implementationType, IEnumerable openGenericInterfaces) { bool seenCompatibleInterface = false; - var resourceDescriptor = TryGetResourceTypeFromServiceImplementation(implementationType); + ResourceDescriptor resourceDescriptor = TryGetResourceTypeFromServiceImplementation(implementationType); if (resourceDescriptor != null) { - foreach (var openGenericInterface in openGenericInterfaces) + foreach (Type openGenericInterface in openGenericInterfaces) { // A shorthand interface is one where the ID type is omitted. // e.g. IResourceService is the shorthand for IResourceService - var isShorthandInterface = openGenericInterface.GetTypeInfo().GenericTypeParameters.Length == 1; + bool isShorthandInterface = openGenericInterface.GetTypeInfo().GenericTypeParameters.Length == 1; + if (isShorthandInterface && resourceDescriptor.IdType != typeof(int)) { // We can't create a shorthand for ID types other than int. continue; } - var constructedType = isShorthandInterface + Type constructedType = isShorthandInterface ? openGenericInterface.MakeGenericType(resourceDescriptor.ResourceType) : openGenericInterface.MakeGenericType(resourceDescriptor.ResourceType, resourceDescriptor.IdType); @@ -137,21 +132,20 @@ private static void RegisterForConstructedType(IServiceCollection services, Type if (!seenCompatibleInterface) { - throw new InvalidConfigurationException( - $"{implementationType} does not implement any of the expected JsonApiDotNetCore interfaces.");} + throw new InvalidConfigurationException($"{implementationType} does not implement any of the expected JsonApiDotNetCore interfaces."); + } } private static ResourceDescriptor TryGetResourceTypeFromServiceImplementation(Type serviceType) { - foreach (var @interface in serviceType.GetInterfaces()) + foreach (Type @interface in serviceType.GetInterfaces()) { - var firstGenericArgument = @interface.IsGenericType - ? @interface.GenericTypeArguments.First() - : null; + Type firstGenericArgument = @interface.IsGenericType ? @interface.GenericTypeArguments.First() : null; if (firstGenericArgument != null) { - var resourceDescriptor = TypeLocator.TryGetResourceDescriptor(firstGenericArgument); + ResourceDescriptor resourceDescriptor = TypeLocator.TryGetResourceDescriptor(firstGenericArgument); + if (resourceDescriptor != null) { return resourceDescriptor; diff --git a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs index bdcd027a84..41e3a390bc 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceDiscoveryFacade.cs @@ -19,7 +19,8 @@ namespace JsonApiDotNetCore.Configuration [PublicAPI] public class ServiceDiscoveryFacade { - internal static readonly HashSet ServiceInterfaces = new HashSet { + internal static readonly HashSet ServiceInterfaces = new HashSet + { typeof(IResourceService<>), typeof(IResourceService<,>), typeof(IResourceCommandService<>), @@ -48,7 +49,8 @@ public class ServiceDiscoveryFacade typeof(IRemoveFromRelationshipService<,>) }; - internal static readonly HashSet RepositoryInterfaces = new HashSet { + internal static readonly HashSet RepositoryInterfaces = new HashSet + { typeof(IResourceRepository<>), typeof(IResourceRepository<,>), typeof(IResourceWriteRepository<>), @@ -57,7 +59,8 @@ public class ServiceDiscoveryFacade typeof(IResourceReadRepository<,>) }; - internal static readonly HashSet ResourceDefinitionInterfaces = new HashSet { + internal static readonly HashSet ResourceDefinitionInterfaces = new HashSet + { typeof(IResourceDefinition<>), typeof(IResourceDefinition<,>) }; @@ -68,7 +71,8 @@ public class ServiceDiscoveryFacade private readonly IJsonApiOptions _options; private readonly ResourceDescriptorAssemblyCache _assemblyCache = new ResourceDescriptorAssemblyCache(); - public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder resourceGraphBuilder, IJsonApiOptions options, ILoggerFactory loggerFactory) + public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder resourceGraphBuilder, IJsonApiOptions options, + ILoggerFactory loggerFactory) { ArgumentGuard.NotNull(services, nameof(services)); ArgumentGuard.NotNull(resourceGraphBuilder, nameof(resourceGraphBuilder)); @@ -84,7 +88,10 @@ public ServiceDiscoveryFacade(IServiceCollection services, ResourceGraphBuilder /// /// Mark the calling assembly for scanning of resources and injectables. /// - public ServiceDiscoveryFacade AddCurrentAssembly() => AddAssembly(Assembly.GetCallingAssembly()); + public ServiceDiscoveryFacade AddCurrentAssembly() + { + return AddAssembly(Assembly.GetCallingAssembly()); + } /// /// Mark the specified assembly for scanning of resources and injectables. @@ -101,7 +108,7 @@ public ServiceDiscoveryFacade AddAssembly(Assembly assembly) internal void DiscoverResources() { - foreach (var resourceDescriptor in _assemblyCache.GetResourceDescriptorsPerAssembly().SelectMany(tuple => tuple.resourceDescriptors)) + foreach (ResourceDescriptor resourceDescriptor in _assemblyCache.GetResourceDescriptorsPerAssembly().SelectMany(tuple => tuple.resourceDescriptors)) { AddResource(resourceDescriptor); } @@ -109,7 +116,7 @@ internal void DiscoverResources() internal void DiscoverInjectables() { - foreach (var (assembly, resourceDescriptors) in _assemblyCache.GetResourceDescriptorsPerAssembly()) + foreach ((Assembly assembly, IReadOnlyCollection resourceDescriptors) in _assemblyCache.GetResourceDescriptorsPerAssembly()) { AddDbContextResolvers(assembly); AddInjectables(resourceDescriptors, assembly); @@ -118,7 +125,7 @@ internal void DiscoverInjectables() private void AddInjectables(IReadOnlyCollection resourceDescriptors, Assembly assembly) { - foreach (var resourceDescriptor in resourceDescriptors) + foreach (ResourceDescriptor resourceDescriptor in resourceDescriptors) { AddServices(assembly, resourceDescriptor); AddRepositories(assembly, resourceDescriptor); @@ -133,14 +140,15 @@ private void AddInjectables(IReadOnlyCollection resourceDesc private void AddDbContextResolvers(Assembly assembly) { - var dbContextTypes = TypeLocator.GetDerivedTypes(assembly, typeof(DbContext)); - foreach (var dbContextType in dbContextTypes) + IEnumerable dbContextTypes = TypeLocator.GetDerivedTypes(assembly, typeof(DbContext)); + + foreach (Type dbContextType in dbContextTypes) { - var resolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); + Type resolverType = typeof(DbContextResolver<>).MakeGenericType(dbContextType); _services.AddScoped(typeof(IDbContextResolver), resolverType); } } - + private void AddResource(ResourceDescriptor resourceDescriptor) { _resourceGraphBuilder.Add(resourceDescriptor.ResourceType, resourceDescriptor.IdType); @@ -150,7 +158,7 @@ private void AddResourceHookDefinitions(Assembly assembly, ResourceDescriptor id { try { - var resourceDefinition = TypeLocator.GetDerivedGenericTypes(assembly, typeof(ResourceHooksDefinition<>), identifiable.ResourceType) + Type resourceDefinition = TypeLocator.GetDerivedGenericTypes(assembly, typeof(ResourceHooksDefinition<>), identifiable.ResourceType) .SingleOrDefault(); if (resourceDefinition != null) @@ -160,13 +168,14 @@ private void AddResourceHookDefinitions(Assembly assembly, ResourceDescriptor id } catch (InvalidOperationException e) { - throw new InvalidConfigurationException($"Cannot define multiple ResourceHooksDefinition<> implementations for '{identifiable.ResourceType}'", e); + throw new InvalidConfigurationException($"Cannot define multiple ResourceHooksDefinition<> implementations for '{identifiable.ResourceType}'", + e); } } private void AddServices(Assembly assembly, ResourceDescriptor resourceDescriptor) { - foreach (var serviceInterface in ServiceInterfaces) + foreach (Type serviceInterface in ServiceInterfaces) { RegisterImplementations(assembly, serviceInterface, resourceDescriptor); } @@ -174,15 +183,15 @@ private void AddServices(Assembly assembly, ResourceDescriptor resourceDescripto private void AddRepositories(Assembly assembly, ResourceDescriptor resourceDescriptor) { - foreach (var repositoryInterface in RepositoryInterfaces) + foreach (Type repositoryInterface in RepositoryInterfaces) { RegisterImplementations(assembly, repositoryInterface, resourceDescriptor); } } - + private void AddResourceDefinitions(Assembly assembly, ResourceDescriptor resourceDescriptor) { - foreach (var resourceDefinitionInterface in ResourceDefinitionInterfaces) + foreach (Type resourceDefinitionInterface in ResourceDefinitionInterfaces) { RegisterImplementations(assembly, resourceDefinitionInterface, resourceDescriptor); } @@ -190,14 +199,16 @@ private void AddResourceDefinitions(Assembly assembly, ResourceDescriptor resour private void RegisterImplementations(Assembly assembly, Type interfaceType, ResourceDescriptor resourceDescriptor) { - var genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 + Type[] genericArguments = interfaceType.GetTypeInfo().GenericTypeParameters.Length == 2 ? ArrayFactory.Create(resourceDescriptor.ResourceType, resourceDescriptor.IdType) : ArrayFactory.Create(resourceDescriptor.ResourceType); - var result = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); + (Type implementation, Type registrationInterface)? + result = TypeLocator.GetGenericInterfaceImplementation(assembly, interfaceType, genericArguments); + if (result != null) { - var (implementation, registrationInterface) = result.Value; + (Type implementation, Type registrationInterface) = result.Value; _services.AddScoped(registrationInterface, implementation); } } diff --git a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs index a3684b77ca..303cb32c43 100644 --- a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs @@ -12,11 +12,13 @@ namespace JsonApiDotNetCore.Configuration internal static class TypeLocator { /// - /// Attempts to lookup the ID type of the specified resource type. Returns null if it does not implement . + /// Attempts to lookup the ID type of the specified resource type. Returns null if it does not implement . /// public static Type TryGetIdType(Type resourceType) { - var identifiableInterface = resourceType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IIdentifiable<>)); + Type identifiableInterface = resourceType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IIdentifiable<>)); + return identifiableInterface?.GetGenericArguments()[0]; } @@ -27,7 +29,8 @@ public static ResourceDescriptor TryGetResourceDescriptor(Type type) { if (TypeHelper.IsOrImplementsInterface(type, typeof(IIdentifiable))) { - var idType = TryGetIdType(type); + Type idType = TryGetIdType(type); + if (idType != null) { return new ResourceDescriptor(type, idType); @@ -40,15 +43,22 @@ public static ResourceDescriptor TryGetResourceDescriptor(Type type) /// /// Gets all implementations of a generic interface. /// - /// The assembly to search in. - /// The open generic interface. - /// Generic type parameters to construct the generic interface. + /// + /// The assembly to search in. + /// + /// + /// The open generic interface. + /// + /// + /// Generic type parameters to construct the generic interface. + /// /// /// ), typeof(Article), typeof(Guid)); /// ]]> /// - public static (Type implementation, Type registrationInterface)? GetGenericInterfaceImplementation(Assembly assembly, Type openGenericInterface, params Type[] interfaceGenericTypeArguments) + public static (Type implementation, Type registrationInterface)? GetGenericInterfaceImplementation(Assembly assembly, Type openGenericInterface, + params Type[] interfaceGenericTypeArguments) { ArgumentGuard.NotNull(assembly, nameof(assembly)); ArgumentGuard.NotNull(openGenericInterface, nameof(openGenericInterface)); @@ -57,16 +67,14 @@ public static (Type implementation, Type registrationInterface)? GetGenericInter if (!openGenericInterface.IsInterface || !openGenericInterface.IsGenericType || openGenericInterface != openGenericInterface.GetGenericTypeDefinition()) { - throw new ArgumentException( - $"Specified type '{openGenericInterface.FullName}' " + "is not an open generic interface.", + throw new ArgumentException($"Specified type '{openGenericInterface.FullName}' " + "is not an open generic interface.", nameof(openGenericInterface)); } if (interfaceGenericTypeArguments.Length != openGenericInterface.GetGenericArguments().Length) { throw new ArgumentException( - $"Interface '{openGenericInterface.FullName}' " + - $"requires {openGenericInterface.GetGenericArguments().Length} type parameters " + + $"Interface '{openGenericInterface.FullName}' " + $"requires {openGenericInterface.GetGenericArguments().Length} type parameters " + $"instead of {interfaceGenericTypeArguments.Length}.", nameof(interfaceGenericTypeArguments)); } @@ -74,15 +82,19 @@ public static (Type implementation, Type registrationInterface)? GetGenericInter .FirstOrDefault(result => result != null); } - private static (Type implementation, Type registrationInterface)? FindGenericInterfaceImplementationForType(Type nextType, Type openGenericInterface, Type[] interfaceGenericTypeArguments) + private static (Type implementation, Type registrationInterface)? FindGenericInterfaceImplementationForType(Type nextType, Type openGenericInterface, + Type[] interfaceGenericTypeArguments) { - foreach (var nextGenericInterface in nextType.GetInterfaces().Where(type => type.IsGenericType)) + foreach (Type nextGenericInterface in nextType.GetInterfaces().Where(type => type.IsGenericType)) { - var nextOpenGenericInterface = nextGenericInterface.GetGenericTypeDefinition(); + Type nextOpenGenericInterface = nextGenericInterface.GetGenericTypeDefinition(); + if (nextOpenGenericInterface == openGenericInterface) { - var nextGenericArguments = nextGenericInterface.GetGenericArguments(); - if (nextGenericArguments.Length == interfaceGenericTypeArguments.Length && nextGenericArguments.SequenceEqual(interfaceGenericTypeArguments)) + Type[] nextGenericArguments = nextGenericInterface.GetGenericArguments(); + + if (nextGenericArguments.Length == interfaceGenericTypeArguments.Length && + nextGenericArguments.SequenceEqual(interfaceGenericTypeArguments)) { return (nextType, nextOpenGenericInterface.MakeGenericType(interfaceGenericTypeArguments)); } @@ -95,9 +107,15 @@ private static (Type implementation, Type registrationInterface)? FindGenericInt /// /// Gets all derivatives of the concrete, generic type. /// - /// The assembly to search. - /// The open generic type, e.g. `typeof(ResourceDefinition<>)`. - /// Parameters to the generic type. + /// + /// The assembly to search. + /// + /// + /// The open generic type, e.g. `typeof(ResourceDefinition<>)`. + /// + /// + /// Parameters to the generic type. + /// /// /// ), typeof(Article)) @@ -105,15 +123,19 @@ private static (Type implementation, Type registrationInterface)? FindGenericInt /// public static IReadOnlyCollection GetDerivedGenericTypes(Assembly assembly, Type openGenericType, params Type[] genericArguments) { - var genericType = openGenericType.MakeGenericType(genericArguments); + Type genericType = openGenericType.MakeGenericType(genericArguments); return GetDerivedTypes(assembly, genericType).ToArray(); } /// /// Gets all derivatives of the specified type. /// - /// The assembly to search. - /// The inherited type. + /// + /// The assembly to search. + /// + /// + /// The inherited type. + /// /// /// /// GetDerivedGenericTypes(assembly, typeof(DbContext)) @@ -121,7 +143,7 @@ public static IReadOnlyCollection GetDerivedGenericTypes(Assembly assembly /// public static IEnumerable GetDerivedTypes(Assembly assembly, Type inheritedType) { - foreach (var type in assembly.GetTypes()) + foreach (Type type in assembly.GetTypes()) { if (inheritedType.IsAssignableFrom(type)) { diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs index c325d96d47..e363b1aabb 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/DisableQueryStringAttribute.cs @@ -21,9 +21,8 @@ namespace JsonApiDotNetCore.Controllers.Annotations [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] public sealed class DisableQueryStringAttribute : Attribute { - public IReadOnlyCollection ParameterNames { get; } - public static readonly DisableQueryStringAttribute Empty = new DisableQueryStringAttribute(StandardQueryStringParameters.None); + public IReadOnlyCollection ParameterNames { get; } /// /// Disables one or more of the builtin query parameters for a controller. @@ -34,8 +33,7 @@ public DisableQueryStringAttribute(StandardQueryStringParameters parameters) foreach (StandardQueryStringParameters value in Enum.GetValues(typeof(StandardQueryStringParameters))) { - if (value != StandardQueryStringParameters.None && value != StandardQueryStringParameters.All && - parameters.HasFlag(value)) + if (value != StandardQueryStringParameters.None && value != StandardQueryStringParameters.All && parameters.HasFlag(value)) { parameterNames.Add(value.ToString()); } @@ -45,9 +43,8 @@ public DisableQueryStringAttribute(StandardQueryStringParameters parameters) } /// - /// It is allowed to use a comma-separated list of strings to indicate which query parameters - /// should be disabled, because the user may have defined custom query parameters that are - /// not included in the enum. + /// It is allowed to use a comma-separated list of strings to indicate which query parameters should be disabled, because the user may have defined + /// custom query parameters that are not included in the enum. /// public DisableQueryStringAttribute(string parameterNames) { @@ -58,7 +55,7 @@ public DisableQueryStringAttribute(string parameterNames) public bool ContainsParameter(StandardQueryStringParameters parameter) { - var name = parameter.ToString(); + string name = parameter.ToString(); return ParameterNames.Contains(name); } } diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs index 4450aca1ce..5cd68b2e6f 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/DisableRoutingConventionAttribute.cs @@ -13,5 +13,6 @@ namespace JsonApiDotNetCore.Controllers.Annotations [PublicAPI] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface)] public sealed class DisableRoutingConventionAttribute : Attribute - { } + { + } } diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs index 269086a586..b4c0fb7675 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/HttpReadOnlyAttribute.cs @@ -14,6 +14,11 @@ namespace JsonApiDotNetCore.Controllers.Annotations [PublicAPI] public sealed class HttpReadOnlyAttribute : HttpRestrictAttribute { - protected override string[] Methods { get; } = { "POST", "PATCH", "DELETE" }; + protected override string[] Methods { get; } = + { + "POST", + "PATCH", + "DELETE" + }; } } diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs index 00f4dbc195..c2534471f9 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/HttpRestrictAttribute.cs @@ -10,14 +10,12 @@ public abstract class HttpRestrictAttribute : ActionFilterAttribute { protected abstract string[] Methods { get; } - public override async Task OnActionExecutionAsync( - ActionExecutingContext context, - ActionExecutionDelegate next) + public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { ArgumentGuard.NotNull(context, nameof(context)); ArgumentGuard.NotNull(next, nameof(next)); - var method = context.HttpContext.Request.Method; + string method = context.HttpContext.Request.Method; if (!CanExecuteAction(method)) { diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs index e898dc0118..93733d6885 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpDeleteAttribute.cs @@ -14,6 +14,9 @@ namespace JsonApiDotNetCore.Controllers.Annotations [PublicAPI] public sealed class NoHttpDeleteAttribute : HttpRestrictAttribute { - protected override string[] Methods { get; } = { "DELETE" }; + protected override string[] Methods { get; } = + { + "DELETE" + }; } } diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs index 01dc0e31ba..29a84b386a 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPatchAttribute.cs @@ -14,6 +14,9 @@ namespace JsonApiDotNetCore.Controllers.Annotations [PublicAPI] public sealed class NoHttpPatchAttribute : HttpRestrictAttribute { - protected override string[] Methods { get; } = { "PATCH" }; + protected override string[] Methods { get; } = + { + "PATCH" + }; } } diff --git a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs index 34a0c8a83c..1d47890739 100644 --- a/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/Annotations/NoHttpPostAttribute.cs @@ -14,6 +14,9 @@ namespace JsonApiDotNetCore.Controllers.Annotations [PublicAPI] public sealed class NoHttpPostAttribute : HttpRestrictAttribute { - protected override string[] Methods { get; } = { "POST" }; + protected override string[] Methods { get; } = + { + "POST" + }; } } diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index 1d5640db00..d625abbdec 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -15,9 +15,14 @@ namespace JsonApiDotNetCore.Controllers /// /// Implements the foundational ASP.NET Core controller layer in the JsonApiDotNetCore architecture that delegates to a Resource Service. /// - /// The resource type. - /// The resource identifier type. - public abstract class BaseJsonApiController : CoreJsonApiController where TResource : class, IIdentifiable + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// + public abstract class BaseJsonApiController : CoreJsonApiController + where TResource : class, IIdentifiable { private readonly IJsonApiOptions _options; private readonly IGetAllService _getAll; @@ -35,40 +40,29 @@ public abstract class BaseJsonApiController : CoreJsonApiControl /// /// Creates an instance from a read/write service. /// - protected BaseJsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) + protected BaseJsonApiController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : this(options, loggerFactory, resourceService, resourceService) - { } + { + } /// /// Creates an instance from separate services for reading and writing. /// - protected BaseJsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceQueryService queryService = null, + protected BaseJsonApiController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceQueryService queryService = null, IResourceCommandService commandService = null) - : this(options, loggerFactory, queryService, queryService, queryService, queryService, commandService, - commandService, commandService, commandService, commandService, commandService) - { } + : this(options, loggerFactory, queryService, queryService, queryService, queryService, commandService, commandService, commandService, + commandService, commandService, commandService) + { + } /// /// Creates an instance from separate services for the various individual read and write methods. /// - protected BaseJsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetSecondaryService getSecondary = null, - IGetRelationshipService getRelationship = null, - ICreateService create = null, - IAddToRelationshipService addToRelationship = null, - IUpdateService update = null, - ISetRelationshipService setRelationship = null, - IDeleteService delete = null, + protected BaseJsonApiController(IJsonApiOptions options, ILoggerFactory loggerFactory, IGetAllService getAll = null, + IGetByIdService getById = null, IGetSecondaryService getSecondary = null, + IGetRelationshipService getRelationship = null, ICreateService create = null, + IAddToRelationshipService addToRelationship = null, IUpdateService update = null, + ISetRelationshipService setRelationship = null, IDeleteService delete = null, IRemoveFromRelationshipService removeFromRelationship = null) { ArgumentGuard.NotNull(options, nameof(options)); @@ -89,8 +83,7 @@ protected BaseJsonApiController( } /// - /// Gets a collection of top-level (non-nested) resources. - /// Example: GET /articles HTTP/1.1 + /// Gets a collection of top-level (non-nested) resources. Example: GET /articles HTTP/1.1 /// public virtual async Task GetAsync(CancellationToken cancellationToken) { @@ -101,38 +94,41 @@ public virtual async Task GetAsync(CancellationToken cancellation throw new RequestMethodNotAllowedException(HttpMethod.Get); } - var resources = await _getAll.GetAsync(cancellationToken); + IReadOnlyCollection resources = await _getAll.GetAsync(cancellationToken); return Ok(resources); } /// - /// Gets a single top-level (non-nested) resource by ID. - /// Example: /articles/1 + /// Gets a single top-level (non-nested) resource by ID. Example: /articles/1 /// public virtual async Task GetAsync(TId id, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id}); + _traceWriter.LogMethodStart(new + { + id + }); if (_getById == null) { throw new RequestMethodNotAllowedException(HttpMethod.Get); } - var resource = await _getById.GetAsync(id, cancellationToken); + TResource resource = await _getById.GetAsync(id, cancellationToken); return Ok(resource); } /// - /// Gets a single resource or multiple resources at a nested endpoint. - /// Examples: - /// GET /articles/1/author HTTP/1.1 - /// GET /articles/1/revisions HTTP/1.1 + /// Gets a single resource or multiple resources at a nested endpoint. Examples: GET /articles/1/author HTTP/1.1 GET /articles/1/revisions HTTP/1.1 /// public virtual async Task GetSecondaryAsync(TId id, string relationshipName, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id, relationshipName}); + _traceWriter.LogMethodStart(new + { + id, + relationshipName + }); ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); @@ -141,19 +137,21 @@ public virtual async Task GetSecondaryAsync(TId id, string relati throw new RequestMethodNotAllowedException(HttpMethod.Get); } - var relationship = await _getSecondary.GetSecondaryAsync(id, relationshipName, cancellationToken); + object relationship = await _getSecondary.GetSecondaryAsync(id, relationshipName, cancellationToken); return Ok(relationship); } /// - /// Gets a single resource relationship. - /// Example: GET /articles/1/relationships/author HTTP/1.1 - /// Example: GET /articles/1/relationships/revisions HTTP/1.1 + /// Gets a single resource relationship. Example: GET /articles/1/relationships/author HTTP/1.1 Example: GET /articles/1/relationships/revisions HTTP/1.1 /// public virtual async Task GetRelationshipAsync(TId id, string relationshipName, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id, relationshipName}); + _traceWriter.LogMethodStart(new + { + id, + relationshipName + }); ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); @@ -162,18 +160,20 @@ public virtual async Task GetRelationshipAsync(TId id, string rel throw new RequestMethodNotAllowedException(HttpMethod.Get); } - var rightResources = await _getRelationship.GetRelationshipAsync(id, relationshipName, cancellationToken); + object rightResources = await _getRelationship.GetRelationshipAsync(id, relationshipName, cancellationToken); return Ok(rightResources); } /// - /// Creates a new resource with attributes, relationships or both. - /// Example: POST /articles HTTP/1.1 + /// Creates a new resource with attributes, relationships or both. Example: POST /articles HTTP/1.1 /// public virtual async Task PostAsync([FromBody] TResource resource, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {resource}); + _traceWriter.LogMethodStart(new + { + resource + }); ArgumentGuard.NotNull(resource, nameof(resource)); @@ -189,12 +189,13 @@ public virtual async Task PostAsync([FromBody] TResource resource if (_options.ValidateModelState && !ModelState.IsValid) { - throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, _options.SerializerNamingStrategy); + throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, + _options.SerializerNamingStrategy); } - var newResource = await _create.CreateAsync(resource, cancellationToken); + TResource newResource = await _create.CreateAsync(resource, cancellationToken); - var resourceId = (newResource ?? resource).StringId; + string resourceId = (newResource ?? resource).StringId; string locationUrl = $"{HttpContext.Request.Path}/{resourceId}"; if (newResource == null) @@ -207,16 +208,29 @@ public virtual async Task PostAsync([FromBody] TResource resource } /// - /// Adds resources to a to-many relationship. - /// Example: POST /articles/1/revisions HTTP/1.1 + /// Adds resources to a to-many relationship. Example: POST /articles/1/revisions HTTP/1.1 /// - /// The identifier of the primary resource. - /// The relationship to add resources to. - /// The set of resources to add to the relationship. - /// Propagates notification that request handling should be canceled. - public virtual async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken) + /// + /// The identifier of the primary resource. + /// + /// + /// The relationship to add resources to. + /// + /// + /// The set of resources to add to the relationship. + /// + /// + /// Propagates notification that request handling should be canceled. + /// + public virtual async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id, relationshipName, secondaryResourceIds}); + _traceWriter.LogMethodStart(new + { + id, + relationshipName, + secondaryResourceIds + }); ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); @@ -232,13 +246,16 @@ public virtual async Task PostRelationshipAsync(TId id, string re } /// - /// Updates the attributes and/or relationships of an existing resource. - /// Only the values of sent attributes are replaced. And only the values of sent relationships are replaced. - /// Example: PATCH /articles/1 HTTP/1.1 + /// Updates the attributes and/or relationships of an existing resource. Only the values of sent attributes are replaced. And only the values of sent + /// relationships are replaced. Example: PATCH /articles/1 HTTP/1.1 /// public virtual async Task PatchAsync(TId id, [FromBody] TResource resource, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id, resource}); + _traceWriter.LogMethodStart(new + { + id, + resource + }); ArgumentGuard.NotNull(resource, nameof(resource)); @@ -249,25 +266,39 @@ public virtual async Task PatchAsync(TId id, [FromBody] TResource if (_options.ValidateModelState && !ModelState.IsValid) { - throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, _options.SerializerNamingStrategy); + throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, + _options.SerializerNamingStrategy); } - var updated = await _update.UpdateAsync(id, resource, cancellationToken); - return updated == null ? (IActionResult) NoContent() : Ok(updated); + TResource updated = await _update.UpdateAsync(id, resource, cancellationToken); + return updated == null ? (IActionResult)NoContent() : Ok(updated); } /// - /// Performs a complete replacement of a relationship on an existing resource. - /// Example: PATCH /articles/1/relationships/author HTTP/1.1 - /// Example: PATCH /articles/1/relationships/revisions HTTP/1.1 + /// Performs a complete replacement of a relationship on an existing resource. Example: PATCH /articles/1/relationships/author HTTP/1.1 Example: PATCH + /// /articles/1/relationships/revisions HTTP/1.1 /// - /// The identifier of the primary resource. - /// The relationship for which to perform a complete replacement. - /// The resource or set of resources to assign to the relationship. - /// Propagates notification that request handling should be canceled. - public virtual async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object secondaryResourceIds, CancellationToken cancellationToken) + /// + /// The identifier of the primary resource. + /// + /// + /// The relationship for which to perform a complete replacement. + /// + /// + /// The resource or set of resources to assign to the relationship. + /// + /// + /// Propagates notification that request handling should be canceled. + /// + public virtual async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object secondaryResourceIds, + CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id, relationshipName, secondaryResourceIds}); + _traceWriter.LogMethodStart(new + { + id, + relationshipName, + secondaryResourceIds + }); ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); @@ -282,12 +313,14 @@ public virtual async Task PatchRelationshipAsync(TId id, string r } /// - /// Deletes an existing resource. - /// Example: DELETE /articles/1 HTTP/1.1 + /// Deletes an existing resource. Example: DELETE /articles/1 HTTP/1.1 /// public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id}); + _traceWriter.LogMethodStart(new + { + id + }); if (_delete == null) { @@ -300,16 +333,29 @@ public virtual async Task DeleteAsync(TId id, CancellationToken c } /// - /// Removes resources from a to-many relationship. - /// Example: DELETE /articles/1/relationships/revisions HTTP/1.1 + /// Removes resources from a to-many relationship. Example: DELETE /articles/1/relationships/revisions HTTP/1.1 /// - /// The identifier of the primary resource. - /// The relationship to remove resources from. - /// The set of resources to remove from the relationship. - /// Propagates notification that request handling should be canceled. - public virtual async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken) + /// + /// The identifier of the primary resource. + /// + /// + /// The relationship to remove resources from. + /// + /// + /// The set of resources to remove from the relationship. + /// + /// + /// Propagates notification that request handling should be canceled. + /// + public virtual async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id, relationshipName, secondaryResourceIds}); + _traceWriter.LogMethodStart(new + { + id, + relationshipName, + secondaryResourceIds + }); ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); @@ -326,41 +372,32 @@ public virtual async Task DeleteRelationshipAsync(TId id, string } /// - public abstract class BaseJsonApiController : BaseJsonApiController where TResource : class, IIdentifiable + public abstract class BaseJsonApiController : BaseJsonApiController + where TResource : class, IIdentifiable { /// - protected BaseJsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) + protected BaseJsonApiController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService, resourceService) - { } + { + } /// - protected BaseJsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceQueryService queryService = null, + protected BaseJsonApiController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceQueryService queryService = null, IResourceCommandService commandService = null) : base(options, loggerFactory, queryService, commandService) - { } + { + } /// - protected BaseJsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetSecondaryService getSecondary = null, - IGetRelationshipService getRelationship = null, - ICreateService create = null, - IAddToRelationshipService addToRelationship = null, - IUpdateService update = null, - ISetRelationshipService setRelationship = null, - IDeleteService delete = null, + protected BaseJsonApiController(IJsonApiOptions options, ILoggerFactory loggerFactory, IGetAllService getAll = null, + IGetByIdService getById = null, IGetSecondaryService getSecondary = null, + IGetRelationshipService getRelationship = null, ICreateService create = null, + IAddToRelationshipService addToRelationship = null, IUpdateService update = null, + ISetRelationshipService setRelationship = null, IDeleteService delete = null, IRemoveFromRelationshipService removeFromRelationship = null) - : base(options, loggerFactory, getAll, getById, getSecondary, getRelationship, create, addToRelationship, update, - setRelationship, delete, removeFromRelationship) - { } + : base(options, loggerFactory, getAll, getById, getSecondary, getRelationship, create, addToRelationship, update, setRelationship, delete, + removeFromRelationship) + { + } } } diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs index 719b281db6..5da8b2b4f5 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs @@ -16,8 +16,8 @@ namespace JsonApiDotNetCore.Controllers { /// - /// Implements the foundational ASP.NET Core controller layer in the JsonApiDotNetCore architecture for handling atomic:operations requests. - /// See https://jsonapi.org/ext/atomic/ for details. Delegates work to . + /// Implements the foundational ASP.NET Core controller layer in the JsonApiDotNetCore architecture for handling atomic:operations requests. See + /// https://jsonapi.org/ext/atomic/ for details. Delegates work to . /// [PublicAPI] public abstract class BaseJsonApiOperationsController : CoreJsonApiController @@ -28,8 +28,8 @@ public abstract class BaseJsonApiOperationsController : CoreJsonApiController private readonly ITargetedFields _targetedFields; private readonly TraceLogWriter _traceWriter; - protected BaseJsonApiOperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IOperationsProcessor processor, IJsonApiRequest request, ITargetedFields targetedFields) + protected BaseJsonApiOperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IOperationsProcessor processor, + IJsonApiRequest request, ITargetedFields targetedFields) { ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); @@ -45,9 +45,8 @@ protected BaseJsonApiOperationsController(IJsonApiOptions options, ILoggerFactor } /// - /// Atomically processes a list of operations and returns a list of results. - /// All changes are reverted if processing fails. - /// If processing succeeds but none of the operations returns any data, then HTTP 201 is returned instead of 200. + /// Atomically processes a list of operations and returns a list of results. All changes are reverted if processing fails. If processing succeeds but + /// none of the operations returns any data, then HTTP 201 is returned instead of 200. /// /// /// The next example creates a new resource. @@ -66,7 +65,8 @@ protected BaseJsonApiOperationsController(IJsonApiOptions options, ILoggerFactor /// } /// }] /// } - /// ]]> + /// ]]> + /// /// /// The next example updates an existing resource. /// + /// ]]> + /// /// /// The next example deletes an existing resource. /// - public virtual async Task PostOperationsAsync([FromBody] IList operations, - CancellationToken cancellationToken) + /// ]]> + /// + public virtual async Task PostOperationsAsync([FromBody] IList operations, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {operations}); + _traceWriter.LogMethodStart(new + { + operations + }); ArgumentGuard.NotNull(operations, nameof(operations)); @@ -116,8 +120,8 @@ public virtual async Task PostOperationsAsync([FromBody] IList result != null) ? (IActionResult) Ok(results) : NoContent(); + IList results = await _processor.ProcessAsync(operations, cancellationToken); + return results.Any(result => result != null) ? (IActionResult)Ok(results) : NoContent(); } protected virtual void ValidateClientGeneratedIds(IEnumerable operations) @@ -125,7 +129,8 @@ protected virtual void ValidateClientGeneratedIds(IEnumerable operat var violations = new List(); int index = 0; - foreach (var operation in operations) + + foreach (OperationContainer operation in operations) { if (operation.Kind == OperationKind.CreateResource || operation.Kind == OperationKind.UpdateResource) { @@ -174,17 +180,18 @@ protected virtual void ValidateModelState(IEnumerable operat private static void AddValidationErrors(ModelStateDictionary modelState, Type resourceType, int operationIndex, List violations) { - foreach (var (propertyName, entry) in modelState) + foreach ((string propertyName, ModelStateEntry entry) in modelState) { AddValidationErrors(entry, propertyName, resourceType, operationIndex, violations); } } - private static void AddValidationErrors(ModelStateEntry entry, string propertyName, Type resourceType, int operationIndex, List violations) + private static void AddValidationErrors(ModelStateEntry entry, string propertyName, Type resourceType, int operationIndex, + List violations) { - foreach (var error in entry.Errors) + foreach (ModelError error in entry.Errors) { - var prefix = $"/atomic:operations[{operationIndex}]/data/attributes/"; + string prefix = $"/atomic:operations[{operationIndex}]/data/attributes/"; var violation = new ModelStateViolation(prefix, propertyName, resourceType, error); violations.Add(violation); diff --git a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs index 8406bdb55a..17db572550 100644 --- a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs @@ -24,7 +24,7 @@ protected IActionResult Error(IEnumerable errors) return new ObjectResult(document) { - StatusCode = (int) document.GetErrorStatusCode() + StatusCode = (int)document.GetErrorStatusCode() }; } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs index 1d6f801b06..cfc9399957 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiCommandController.cs @@ -10,23 +10,26 @@ namespace JsonApiDotNetCore.Controllers { /// - /// The base class to derive resource-specific write-only controllers from. - /// This class delegates all work to but adds attributes for routing templates. - /// If you want to provide routing templates yourself, you should derive from BaseJsonApiController directly. + /// The base class to derive resource-specific write-only controllers from. This class delegates all work to + /// but adds attributes for routing templates. If you want to provide routing templates yourself, + /// you should derive from BaseJsonApiController directly. /// - /// The resource type. - /// The resource identifier type. - public abstract class JsonApiCommandController : BaseJsonApiController where TResource : class, IIdentifiable + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// + public abstract class JsonApiCommandController : BaseJsonApiController + where TResource : class, IIdentifiable { /// /// Creates an instance from a write-only service. /// - protected JsonApiCommandController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceCommandService commandService) + protected JsonApiCommandController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceCommandService commandService) : base(options, loggerFactory, null, commandService) - { } + { + } /// [HttpPost] @@ -37,8 +40,8 @@ public override async Task PostAsync([FromBody] TResource resourc /// [HttpPost("{id}/relationships/{relationshipName}")] - public override async Task PostRelationshipAsync( - TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken) + public override async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + CancellationToken cancellationToken) { return await base.PostRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); } @@ -52,8 +55,8 @@ public override async Task PatchAsync(TId id, [FromBody] TResourc /// [HttpPatch("{id}/relationships/{relationshipName}")] - public override async Task PatchRelationshipAsync( - TId id, string relationshipName, [FromBody] object secondaryResourceIds, CancellationToken cancellationToken) + public override async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object secondaryResourceIds, + CancellationToken cancellationToken) { return await base.PatchRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); } @@ -67,21 +70,21 @@ public override async Task DeleteAsync(TId id, CancellationToken /// [HttpDelete("{id}/relationships/{relationshipName}")] - public override async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken) + public override async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + CancellationToken cancellationToken) { return await base.DeleteRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); } } /// - public abstract class JsonApiCommandController : JsonApiCommandController where TResource : class, IIdentifiable + public abstract class JsonApiCommandController : JsonApiCommandController + where TResource : class, IIdentifiable { /// - protected JsonApiCommandController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceCommandService commandService) + protected JsonApiCommandController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceCommandService commandService) : base(options, loggerFactory, commandService) - { } + { + } } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs index 793ba9cef2..8872e5447d 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs @@ -10,39 +10,35 @@ namespace JsonApiDotNetCore.Controllers { /// - /// The base class to derive resource-specific controllers from. - /// This class delegates all work to but adds attributes for routing templates. - /// If you want to provide routing templates yourself, you should derive from BaseJsonApiController directly. + /// The base class to derive resource-specific controllers from. This class delegates all work to + /// but adds attributes for routing templates. If you want to provide routing templates yourself, you should derive from BaseJsonApiController directly. /// - /// The resource type. - /// The resource identifier type. - public abstract class JsonApiController : BaseJsonApiController where TResource : class, IIdentifiable + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// + public abstract class JsonApiController : BaseJsonApiController + where TResource : class, IIdentifiable { /// - protected JsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) + protected JsonApiController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) - { } + { + } /// - protected JsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetSecondaryService getSecondary = null, - IGetRelationshipService getRelationship = null, - ICreateService create = null, - IAddToRelationshipService addToRelationship = null, - IUpdateService update = null, - ISetRelationshipService setRelationship = null, - IDeleteService delete = null, + protected JsonApiController(IJsonApiOptions options, ILoggerFactory loggerFactory, IGetAllService getAll = null, + IGetByIdService getById = null, IGetSecondaryService getSecondary = null, + IGetRelationshipService getRelationship = null, ICreateService create = null, + IAddToRelationshipService addToRelationship = null, IUpdateService update = null, + ISetRelationshipService setRelationship = null, IDeleteService delete = null, IRemoveFromRelationshipService removeFromRelationship = null) - : base(options, loggerFactory,getAll, getById, getSecondary, getRelationship, create, addToRelationship, update, - setRelationship, delete, removeFromRelationship) - { } + : base(options, loggerFactory, getAll, getById, getSecondary, getRelationship, create, addToRelationship, update, setRelationship, delete, + removeFromRelationship) + { + } /// [HttpGet] @@ -81,8 +77,8 @@ public override async Task PostAsync([FromBody] TResource resourc /// [HttpPost("{id}/relationships/{relationshipName}")] - public override async Task PostRelationshipAsync( - TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken) + public override async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + CancellationToken cancellationToken) { return await base.PostRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); } @@ -96,8 +92,8 @@ public override async Task PatchAsync(TId id, [FromBody] TResourc /// [HttpPatch("{id}/relationships/{relationshipName}")] - public override async Task PatchRelationshipAsync( - TId id, string relationshipName, [FromBody] object secondaryResourceIds, CancellationToken cancellationToken) + public override async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object secondaryResourceIds, + CancellationToken cancellationToken) { return await base.PatchRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); } @@ -111,39 +107,33 @@ public override async Task DeleteAsync(TId id, CancellationToken /// [HttpDelete("{id}/relationships/{relationshipName}")] - public override async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken) + public override async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet secondaryResourceIds, + CancellationToken cancellationToken) { return await base.DeleteRelationshipAsync(id, relationshipName, secondaryResourceIds, cancellationToken); } } /// - public abstract class JsonApiController : JsonApiController where TResource : class, IIdentifiable + public abstract class JsonApiController : JsonApiController + where TResource : class, IIdentifiable { /// - protected JsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceService resourceService) + protected JsonApiController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) - { } + { + } /// - protected JsonApiController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IGetAllService getAll = null, - IGetByIdService getById = null, - IGetSecondaryService getSecondary = null, - IGetRelationshipService getRelationship = null, - ICreateService create = null, - IAddToRelationshipService addToRelationship = null, - IUpdateService update = null, - ISetRelationshipService setRelationship = null, - IDeleteService delete = null, + protected JsonApiController(IJsonApiOptions options, ILoggerFactory loggerFactory, IGetAllService getAll = null, + IGetByIdService getById = null, IGetSecondaryService getSecondary = null, + IGetRelationshipService getRelationship = null, ICreateService create = null, + IAddToRelationshipService addToRelationship = null, IUpdateService update = null, + ISetRelationshipService setRelationship = null, IDeleteService delete = null, IRemoveFromRelationshipService removeFromRelationship = null) - : base(options, loggerFactory, getAll, getById, getSecondary, getRelationship, create, addToRelationship, update, - setRelationship, delete, removeFromRelationship) - { } + : base(options, loggerFactory, getAll, getById, getSecondary, getRelationship, create, addToRelationship, update, setRelationship, delete, + removeFromRelationship) + { + } } } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs index a2aed59e8f..7e8e4956ac 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs @@ -11,22 +11,20 @@ namespace JsonApiDotNetCore.Controllers { /// - /// The base class to derive atomic:operations controllers from. - /// This class delegates all work to but adds attributes for routing templates. - /// If you want to provide routing templates yourself, you should derive from BaseJsonApiOperationsController directly. + /// The base class to derive atomic:operations controllers from. This class delegates all work to but adds + /// attributes for routing templates. If you want to provide routing templates yourself, you should derive from BaseJsonApiOperationsController directly. /// public abstract class JsonApiOperationsController : BaseJsonApiOperationsController { - protected JsonApiOperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IOperationsProcessor processor, IJsonApiRequest request, ITargetedFields targetedFields) + protected JsonApiOperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request, + ITargetedFields targetedFields) : base(options, loggerFactory, processor, request, targetedFields) { } /// [HttpPost] - public override async Task PostOperationsAsync([FromBody] IList operations, - CancellationToken cancellationToken) + public override async Task PostOperationsAsync([FromBody] IList operations, CancellationToken cancellationToken) { return await base.PostOperationsAsync(operations, cancellationToken); } diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs index e6f222db50..7ab85612c8 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiQueryController.cs @@ -9,23 +9,26 @@ namespace JsonApiDotNetCore.Controllers { /// - /// The base class to derive resource-specific read-only controllers from. - /// This class delegates all work to but adds attributes for routing templates. - /// If you want to provide routing templates yourself, you should derive from BaseJsonApiController directly. + /// The base class to derive resource-specific read-only controllers from. This class delegates all work to + /// but adds attributes for routing templates. If you want to provide routing templates yourself, + /// you should derive from BaseJsonApiController directly. /// - /// The resource type. - /// The resource identifier type. - public abstract class JsonApiQueryController : BaseJsonApiController where TResource : class, IIdentifiable + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// + public abstract class JsonApiQueryController : BaseJsonApiController + where TResource : class, IIdentifiable { /// /// Creates an instance from a read-only service. /// - protected JsonApiQueryController( - IJsonApiOptions context, - ILoggerFactory loggerFactory, - IResourceQueryService queryService) + protected JsonApiQueryController(IJsonApiOptions context, ILoggerFactory loggerFactory, IResourceQueryService queryService) : base(context, loggerFactory, queryService) - { } + { + } /// [HttpGet] @@ -57,14 +60,13 @@ public override async Task GetRelationshipAsync(TId id, string re } /// - public abstract class JsonApiQueryController : JsonApiQueryController where TResource : class, IIdentifiable + public abstract class JsonApiQueryController : JsonApiQueryController + where TResource : class, IIdentifiable { /// - protected JsonApiQueryController( - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IResourceQueryService queryService) + protected JsonApiQueryController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceQueryService queryService) : base(options, loggerFactory, queryService) - { } + { + } } } diff --git a/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs b/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs index e0a647a8b2..f528ed6c96 100644 --- a/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs +++ b/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs @@ -10,13 +10,13 @@ namespace JsonApiDotNetCore.Errors [PublicAPI] public sealed class CannotClearRequiredRelationshipException : JsonApiException { - public CannotClearRequiredRelationshipException(string relationshipName, string resourceId, - string resourceType) : base(new Error(HttpStatusCode.BadRequest) - { - Title = "Failed to clear a required relationship.", - Detail = $"The relationship '{relationshipName}' of resource type '{resourceType}' " + - $"with ID '{resourceId}' cannot be cleared because it is a required relationship." - }) + public CannotClearRequiredRelationshipException(string relationshipName, string resourceId, string resourceType) + : base(new Error(HttpStatusCode.BadRequest) + { + Title = "Failed to clear a required relationship.", + Detail = $"The relationship '{relationshipName}' of resource type '{resourceType}' " + + $"with ID '{resourceId}' cannot be cleared because it is a required relationship." + }) { } } diff --git a/src/JsonApiDotNetCore/Errors/InvalidConfigurationException.cs b/src/JsonApiDotNetCore/Errors/InvalidConfigurationException.cs index 884ed046a7..ae46c9bad5 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidConfigurationException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidConfigurationException.cs @@ -9,7 +9,9 @@ namespace JsonApiDotNetCore.Errors [PublicAPI] public sealed class InvalidConfigurationException : Exception { - public InvalidConfigurationException(string message, Exception innerException = null) - : base(message, innerException) { } + public InvalidConfigurationException(string message, Exception innerException = null) + : base(message, innerException) + { + } } } diff --git a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs index a3db7f492b..de2e9deecb 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs @@ -19,12 +19,17 @@ namespace JsonApiDotNetCore.Errors [PublicAPI] public class InvalidModelStateException : JsonApiException { - public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType, - bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy) + public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType, bool includeExceptionStackTraceInErrors, + NamingStrategy namingStrategy) : this(FromModelStateDictionary(modelState, resourceType), includeExceptionStackTraceInErrors, namingStrategy) { } + public InvalidModelStateException(IEnumerable violations, bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy) + : base(FromModelStateViolations(violations, includeExceptionStackTraceInErrors, namingStrategy)) + { + } + private static IEnumerable FromModelStateDictionary(ModelStateDictionary modelState, Type resourceType) { ArgumentGuard.NotNull(modelState, nameof(modelState)); @@ -32,7 +37,7 @@ private static IEnumerable FromModelStateDictionary(ModelSt var violations = new List(); - foreach (var (propertyName, entry) in modelState) + foreach ((string propertyName, ModelStateEntry entry) in modelState) { AddValidationErrors(entry, propertyName, resourceType, violations); } @@ -40,8 +45,7 @@ private static IEnumerable FromModelStateDictionary(ModelSt return violations; } - private static void AddValidationErrors(ModelStateEntry entry, string propertyName, Type resourceType, - List violations) + private static void AddValidationErrors(ModelStateEntry entry, string propertyName, Type resourceType, List violations) { foreach (ModelError error in entry.Errors) { @@ -50,14 +54,8 @@ private static void AddValidationErrors(ModelStateEntry entry, string propertyNa } } - public InvalidModelStateException(IEnumerable violations, - bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy) - : base(FromModelStateViolations(violations, includeExceptionStackTraceInErrors, namingStrategy)) - { - } - - private static IEnumerable FromModelStateViolations(IEnumerable violations, - bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy) + private static IEnumerable FromModelStateViolations(IEnumerable violations, bool includeExceptionStackTraceInErrors, + NamingStrategy namingStrategy) { ArgumentGuard.NotNull(violations, nameof(violations)); ArgumentGuard.NotNull(namingStrategy, nameof(namingStrategy)); @@ -70,7 +68,7 @@ private static IEnumerable FromModelStateViolation(ModelStateViolation vi { if (violation.Error.Exception is JsonApiException jsonApiException) { - foreach (var error in jsonApiException.Errors) + foreach (Error error in jsonApiException.Errors) { yield return error; } @@ -78,16 +76,16 @@ private static IEnumerable FromModelStateViolation(ModelStateViolation vi else { string attributeName = GetDisplayNameForProperty(violation.PropertyName, violation.ResourceType, namingStrategy); - var attributePath = violation.Prefix + attributeName; + string attributePath = violation.Prefix + attributeName; yield return FromModelError(violation.Error, attributePath, includeExceptionStackTraceInErrors); } } - private static string GetDisplayNameForProperty(string propertyName, Type resourceType, - NamingStrategy namingStrategy) + private static string GetDisplayNameForProperty(string propertyName, Type resourceType, NamingStrategy namingStrategy) { PropertyInfo property = resourceType.GetProperty(propertyName); + if (property != null) { var attrAttribute = property.GetCustomAttribute(); @@ -97,8 +95,7 @@ private static string GetDisplayNameForProperty(string propertyName, Type resour return propertyName; } - private static Error FromModelError(ModelError modelError, string attributePath, - bool includeExceptionStackTraceInErrors) + private static Error FromModelError(ModelError modelError, string attributePath, bool includeExceptionStackTraceInErrors) { var error = new Error(HttpStatusCode.UnprocessableEntity) { diff --git a/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs b/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs index b776d2c683..5e704b8e3c 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.Errors { /// - /// The error that is thrown when translating a to Entity Framework Core fails. + /// The error that is thrown when translating a to Entity Framework Core fails. /// [PublicAPI] public sealed class InvalidQueryException : JsonApiException diff --git a/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs b/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs index f0e2b146d6..99a4eb381e 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs @@ -13,8 +13,7 @@ public sealed class InvalidQueryStringParameterException : JsonApiException { public string QueryParameterName { get; } - public InvalidQueryStringParameterException(string queryParameterName, string genericMessage, - string specificMessage, Exception innerException = null) + public InvalidQueryStringParameterException(string queryParameterName, string genericMessage, string specificMessage, Exception innerException = null) : base(new Error(HttpStatusCode.BadRequest) { Title = genericMessage, diff --git a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs index b50101f6dc..fe860ac0fd 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs @@ -15,9 +15,7 @@ public sealed class InvalidRequestBodyException : JsonApiException public InvalidRequestBodyException(string reason, string details, string requestBody, Exception innerException = null) : base(new Error(HttpStatusCode.UnprocessableEntity) { - Title = reason != null - ? "Failed to deserialize request body: " + reason - : "Failed to deserialize request body.", + Title = reason != null ? "Failed to deserialize request body: " + reason : "Failed to deserialize request body.", Detail = FormatErrorDetail(details, requestBody, innerException) }, innerException) { diff --git a/src/JsonApiDotNetCore/Errors/JsonApiException.cs b/src/JsonApiDotNetCore/Errors/JsonApiException.cs index 278aa1f4ab..55a88e287a 100644 --- a/src/JsonApiDotNetCore/Errors/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Errors/JsonApiException.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Errors { /// - /// The base class for an that represents one or more JSON:API error objects in an unsuccessful response. + /// The base class for an that represents one or more JSON:API error objects in an unsuccessful response. /// [PublicAPI] public class JsonApiException : Exception @@ -21,6 +21,8 @@ public class JsonApiException : Exception public IReadOnlyList Errors { get; } + public override string Message => "Errors = " + JsonConvert.SerializeObject(Errors, ErrorSerializerSettings); + public JsonApiException(Error error, Exception innerException = null) : base(null, innerException) { @@ -41,7 +43,5 @@ public JsonApiException(IEnumerable errors, Exception innerException = nu throw new ArgumentException("At least one error is required.", nameof(errors)); } } - - public override string Message => "Errors = " + JsonConvert.SerializeObject(Errors, ErrorSerializerSettings); } } diff --git a/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs b/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs index 980f6457fb..e092150ba5 100644 --- a/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs +++ b/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs @@ -5,8 +5,7 @@ namespace JsonApiDotNetCore.Errors { /// - /// The error that is thrown when accessing a repository that does not support transactions - /// during an atomic:operations request. + /// The error that is thrown when accessing a repository that does not support transactions during an atomic:operations request. /// [PublicAPI] public sealed class MissingTransactionSupportException : JsonApiException @@ -15,8 +14,7 @@ public MissingTransactionSupportException(string resourceType) : base(new Error(HttpStatusCode.UnprocessableEntity) { Title = "Unsupported resource type in atomic:operations request.", - Detail = $"Operations on resources of type '{resourceType}' " + - "cannot be used because transaction support is unavailable." + Detail = $"Operations on resources of type '{resourceType}' " + "cannot be used because transaction support is unavailable." }) { } diff --git a/src/JsonApiDotNetCore/Errors/NonSharedTransactionException.cs b/src/JsonApiDotNetCore/Errors/NonSharedTransactionException.cs index dbf699c301..5245c4344f 100644 --- a/src/JsonApiDotNetCore/Errors/NonSharedTransactionException.cs +++ b/src/JsonApiDotNetCore/Errors/NonSharedTransactionException.cs @@ -5,8 +5,7 @@ namespace JsonApiDotNetCore.Errors { /// - /// The error that is thrown when a repository does not participate in the overarching transaction - /// during an atomic:operations request. + /// The error that is thrown when a repository does not participate in the overarching transaction during an atomic:operations request. /// [PublicAPI] public sealed class NonSharedTransactionException : JsonApiException @@ -15,8 +14,7 @@ public NonSharedTransactionException() : base(new Error(HttpStatusCode.UnprocessableEntity) { Title = "Unsupported combination of resource types in atomic:operations request.", - Detail = "All operations need to participate in a single shared transaction, " + - "which is not the case for this request." + Detail = "All operations need to participate in a single shared transaction, " + "which is not the case for this request." }) { } diff --git a/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs b/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs index 091ff99586..864a0c7606 100644 --- a/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs @@ -10,11 +10,12 @@ namespace JsonApiDotNetCore.Errors [PublicAPI] public sealed class RelationshipNotFoundException : JsonApiException { - public RelationshipNotFoundException(string relationshipName, string resourceType) : base(new Error(HttpStatusCode.NotFound) - { - Title = "The requested relationship does not exist.", - Detail = $"Resource of type '{resourceType}' does not contain a relationship named '{relationshipName}'." - }) + public RelationshipNotFoundException(string relationshipName, string resourceType) + : base(new Error(HttpStatusCode.NotFound) + { + Title = "The requested relationship does not exist.", + Detail = $"Resource of type '{resourceType}' does not contain a relationship named '{relationshipName}'." + }) { } } diff --git a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs index efe5d80c72..4266f987b3 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs @@ -18,9 +18,7 @@ public ResourceIdInCreateResourceNotAllowedException(int? atomicOperationIndex = : "Specifying the resource ID in operations that create a resource is not allowed.", Source = { - Pointer = atomicOperationIndex != null - ? $"/atomic:operations[{atomicOperationIndex}]/data/id" - : "/data/id" + Pointer = atomicOperationIndex != null ? $"/atomic:operations[{atomicOperationIndex}]/data/id" : "/data/id" } }) { diff --git a/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs index b75366f102..721c17e7cd 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs @@ -14,8 +14,7 @@ public ResourceIdMismatchException(string bodyId, string endpointId, string requ : base(new Error(HttpStatusCode.Conflict) { Title = "Resource ID mismatch between request body and endpoint URL.", - Detail = $"Expected resource ID '{endpointId}' in PATCH request body " + - $"at endpoint '{requestPath}', instead of '{bodyId}'." + Detail = $"Expected resource ID '{endpointId}' in PATCH request body " + $"at endpoint '{requestPath}', instead of '{bodyId}'." }) { } diff --git a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs index 78f490c593..83f28a14f9 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs @@ -12,13 +12,13 @@ namespace JsonApiDotNetCore.Errors [PublicAPI] public sealed class ResourceTypeMismatchException : JsonApiException { - public ResourceTypeMismatchException(HttpMethod method, string requestPath, ResourceContext expected, ResourceContext actual) + public ResourceTypeMismatchException(HttpMethod method, string requestPath, ResourceContext expected, ResourceContext actual) : base(new Error(HttpStatusCode.Conflict) - { - Title = "Resource type mismatch between request body and endpoint URL.", - Detail = $"Expected resource of type '{expected.PublicName}' in {method} " + - $"request body at endpoint '{requestPath}', instead of '{actual?.PublicName}'." - }) + { + Title = "Resource type mismatch between request body and endpoint URL.", + Detail = $"Expected resource of type '{expected.PublicName}' in {method} " + + $"request body at endpoint '{requestPath}', instead of '{actual?.PublicName}'." + }) { } } diff --git a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs index 450ae4cdd5..94503ab343 100644 --- a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs @@ -22,8 +22,7 @@ private static Error CreateError(MissingResourceInRelationship missingResourceIn return new Error(HttpStatusCode.NotFound) { Title = "A related resource does not exist.", - Detail = - $"Related resource of type '{missingResourceInRelationship.ResourceType}' with ID '{missingResourceInRelationship.ResourceId}' " + + Detail = $"Related resource of type '{missingResourceInRelationship.ResourceType}' with ID '{missingResourceInRelationship.ResourceId}' " + $"in relationship '{missingResourceInRelationship.RelationshipName}' does not exist." }; } diff --git a/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs b/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs index 58c0219bf6..87e2a7856b 100644 --- a/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs +++ b/src/JsonApiDotNetCore/Errors/UnsuccessfulActionResultException.cs @@ -6,16 +6,16 @@ namespace JsonApiDotNetCore.Errors { /// - /// The error that is thrown when an with non-success status is returned from a controller method. + /// The error that is thrown when an with non-success status is returned from a controller method. /// [PublicAPI] public sealed class UnsuccessfulActionResultException : JsonApiException { - public UnsuccessfulActionResultException(HttpStatusCode status) + public UnsuccessfulActionResultException(HttpStatusCode status) : base(new Error(status) - { - Title = status.ToString() - }) + { + Title = status.ToString() + }) { } @@ -28,9 +28,7 @@ private static Error ToError(ProblemDetails problemDetails) { ArgumentGuard.NotNull(problemDetails, nameof(problemDetails)); - var status = problemDetails.Status != null - ? (HttpStatusCode) problemDetails.Status.Value - : HttpStatusCode.InternalServerError; + HttpStatusCode status = problemDetails.Status != null ? (HttpStatusCode)problemDetails.Status.Value : HttpStatusCode.InternalServerError; var error = new Error(status) { diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs index 720363cc82..e09d327fbc 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/HooksDiscovery.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using JetBrains.Annotations; using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Hooks.Internal.Execution; @@ -13,10 +14,12 @@ namespace JsonApiDotNetCore.Hooks.Internal.Discovery /// The default implementation for IHooksDiscovery /// [PublicAPI] - public class HooksDiscovery : IHooksDiscovery where TResource : class, IIdentifiable + public class HooksDiscovery : IHooksDiscovery + where TResource : class, IIdentifiable { private readonly Type _boundResourceDefinitionType = typeof(ResourceHooksDefinition); private readonly ResourceHook[] _allHooks; + private readonly ResourceHook[] _databaseValuesAttributeAllowed = { ResourceHook.BeforeUpdate, @@ -26,18 +29,17 @@ public class HooksDiscovery : IHooksDiscovery where TResou /// public ResourceHook[] ImplementedHooks { get; private set; } + public ResourceHook[] DatabaseValuesEnabledHooks { get; private set; } public ResourceHook[] DatabaseValuesDisabledHooks { get; private set; } public HooksDiscovery(IServiceProvider provider) { - _allHooks = Enum.GetValues(typeof(ResourceHook)) - .Cast() - .Where(h => h != ResourceHook.None) - .ToArray(); + _allHooks = Enum.GetValues(typeof(ResourceHook)).Cast().Where(h => h != ResourceHook.None).ToArray(); Type containerType; - using (var scope = provider.CreateScope()) + + using (IServiceScope scope = provider.CreateScope()) { containerType = scope.ServiceProvider.GetService(_boundResourceDefinitionType)?.GetType(); } @@ -48,7 +50,9 @@ public HooksDiscovery(IServiceProvider provider) /// /// Discovers the implemented hooks for a model. /// - /// The implemented hooks for model. + /// + /// The implemented hooks for model. + /// private void DiscoverImplementedHooks(Type containerType) { if (containerType == null || containerType == _boundResourceDefinitionType) @@ -58,18 +62,21 @@ private void DiscoverImplementedHooks(Type containerType) var implementedHooks = new List(); // this hook can only be used with enabled database values - var databaseValuesEnabledHooks = ResourceHook.BeforeImplicitUpdateRelationship.AsList(); + List databaseValuesEnabledHooks = ResourceHook.BeforeImplicitUpdateRelationship.AsList(); var databaseValuesDisabledHooks = new List(); - foreach (var hook in _allHooks) + + foreach (ResourceHook hook in _allHooks) { - var method = containerType.GetMethod(hook.ToString("G")); + MethodInfo method = containerType.GetMethod(hook.ToString("G")); + if (method == null || method.DeclaringType == _boundResourceDefinitionType) { continue; } implementedHooks.Add(hook); - var attr = method.GetCustomAttributes(true).OfType().SingleOrDefault(); + LoadDatabaseValuesAttribute attr = method.GetCustomAttributes(true).OfType().SingleOrDefault(); + if (attr != null) { if (!_databaseValuesAttributeAllowed.Contains(hook)) @@ -77,7 +84,8 @@ private void DiscoverImplementedHooks(Type containerType) throw new InvalidConfigurationException($"{nameof(LoadDatabaseValuesAttribute)} cannot be used on hook" + $"{hook:G} in resource definition {containerType.Name}"); } - var targetList = attr.Value ? databaseValuesEnabledHooks : databaseValuesDisabledHooks; + + List targetList = attr.Value ? databaseValuesEnabledHooks : databaseValuesDisabledHooks; targetList.Add(hook); } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs index 7fede4e57b..c45244b491 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Discovery/IHooksDiscovery.cs @@ -6,11 +6,11 @@ namespace JsonApiDotNetCore.Hooks.Internal.Discovery { /// - /// A singleton service for a particular TResource that stores a field of - /// enums that represents which resource hooks have been implemented for that + /// A singleton service for a particular TResource that stores a field of enums that represents which resource hooks have been implemented for that /// particular resource. /// - public interface IHooksDiscovery : IHooksDiscovery where TResource : class, IIdentifiable + public interface IHooksDiscovery : IHooksDiscovery + where TResource : class, IIdentifiable { } @@ -19,8 +19,11 @@ public interface IHooksDiscovery /// /// A list of the implemented hooks for resource TResource /// - /// The implemented hooks. + /// + /// The implemented hooks. + /// ResourceHook[] ImplementedHooks { get; } + ResourceHook[] DatabaseValuesEnabledHooks { get; } ResourceHook[] DatabaseValuesDisabledHooks { get; } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs index 574966af47..a61fb4b6d5 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/DiffableResourceHashSet.cs @@ -12,16 +12,15 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution { [PublicAPI] - public sealed class DiffableResourceHashSet : ResourceHashSet, IDiffableResourceHashSet where TResource : class, IIdentifiable + public sealed class DiffableResourceHashSet : ResourceHashSet, IDiffableResourceHashSet + where TResource : class, IIdentifiable { private readonly HashSet _databaseValues; private readonly bool _databaseValuesLoaded; private readonly Dictionary> _updatedAttributes; - public DiffableResourceHashSet(HashSet requestResources, - HashSet databaseResources, - Dictionary> relationships, - Dictionary> updatedAttributes) + public DiffableResourceHashSet(HashSet requestResources, HashSet databaseResources, + Dictionary> relationships, Dictionary> updatedAttributes) : base(requestResources, relationships) { _databaseValues = databaseResources; @@ -32,14 +31,15 @@ public DiffableResourceHashSet(HashSet requestResources, /// /// Used internally by the ResourceHookExecutor to make live a bit easier with generics /// - internal DiffableResourceHashSet(IEnumerable requestResources, - IEnumerable databaseResources, - Dictionary relationships, - ITargetedFields targetedFields) - : this((HashSet)requestResources, (HashSet)databaseResources, TypeHelper.ConvertRelationshipDictionary(relationships), - targetedFields.Attributes == null ? null : TypeHelper.ConvertAttributeDictionary(targetedFields.Attributes, (HashSet)requestResources)) - { } - + internal DiffableResourceHashSet(IEnumerable requestResources, IEnumerable databaseResources, + Dictionary relationships, ITargetedFields targetedFields) + : this((HashSet)requestResources, (HashSet)databaseResources, + TypeHelper.ConvertRelationshipDictionary(relationships), + targetedFields.Attributes == null + ? null + : TypeHelper.ConvertAttributeDictionary(targetedFields.Attributes, (HashSet)requestResources)) + { + } /// public IEnumerable> GetDiffs() @@ -49,7 +49,7 @@ public IEnumerable> GetDiffs() ThrowNoDbValuesError(); } - foreach (var resource in this) + foreach (TResource resource in this) { TResource currentValueInDatabase = _databaseValues.Single(e => resource.StringId == e.StringId); yield return new ResourceDiffPair(resource, currentValueInDatabase); @@ -61,8 +61,9 @@ public override HashSet GetAffected(Expression GetAffected(Expression resources) - ? resources - : new HashSet(); + return _updatedAttributes.TryGetValue(propertyInfo, out HashSet resources) ? resources : new HashSet(); } private void ThrowNoDbValuesError() diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs index 222c8754e9..0ba564f9da 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/HookExecutorHelper.cs @@ -49,6 +49,7 @@ public IResourceHookContainer GetResourceHookContainer(RightType targetResource, container = _genericProcessorFactory.Get(typeof(ResourceHooksDefinition<>), targetResource); _hookContainers[targetResource] = container; } + if (container == null) { return null; @@ -57,6 +58,7 @@ public IResourceHookContainer GetResourceHookContainer(RightType targetResource, // if there was a container, first check if it implements the hook we // want to use it for. IEnumerable targetHooks; + if (hook == ResourceHook.None) { CheckForTargetHookExistence(); @@ -74,36 +76,45 @@ public IResourceHookContainer GetResourceHookContainer(RightType targetResource, return container; } } + return null; } /// - public IResourceHookContainer GetResourceHookContainer(ResourceHook hook = ResourceHook.None) where TResource : class, IIdentifiable + public IResourceHookContainer GetResourceHookContainer(ResourceHook hook = ResourceHook.None) + where TResource : class, IIdentifiable { return (IResourceHookContainer)GetResourceHookContainer(typeof(TResource), hook); } - public IEnumerable LoadDbValues(LeftType resourceTypeForRepository, IEnumerable resources, ResourceHook hook, params RelationshipAttribute[] relationshipsToNextLayer) + public IEnumerable LoadDbValues(LeftType resourceTypeForRepository, IEnumerable resources, ResourceHook hook, + params RelationshipAttribute[] relationshipsToNextLayer) { - var idType = TypeHelper.GetIdType(resourceTypeForRepository); - var parameterizedGetWhere = GetType() - .GetMethod(nameof(GetWhereAndInclude), BindingFlags.NonPublic | BindingFlags.Instance)! - .MakeGenericMethod(resourceTypeForRepository, idType); - var cast = ((IEnumerable)resources).Cast(); - var ids = TypeHelper.CopyToList(cast.Select(i => i.GetTypedId()), idType); + LeftType idType = TypeHelper.GetIdType(resourceTypeForRepository); + + MethodInfo parameterizedGetWhere = + GetType().GetMethod(nameof(GetWhereAndInclude), BindingFlags.NonPublic | BindingFlags.Instance)!.MakeGenericMethod(resourceTypeForRepository, + idType); + + IEnumerable cast = ((IEnumerable)resources).Cast(); + IList ids = TypeHelper.CopyToList(cast.Select(i => i.GetTypedId()), idType); var values = (IEnumerable)parameterizedGetWhere.Invoke(this, ArrayFactory.Create(ids, relationshipsToNextLayer)); + if (values == null) { return null; } - return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(resourceTypeForRepository), TypeHelper.CopyToList(values, resourceTypeForRepository)); + return (IEnumerable)Activator.CreateInstance(typeof(HashSet<>).MakeGenericType(resourceTypeForRepository), + TypeHelper.CopyToList(values, resourceTypeForRepository)); } - public HashSet LoadDbValues(IEnumerable resources, ResourceHook hook, params RelationshipAttribute[] relationships) where TResource : class, IIdentifiable + public HashSet LoadDbValues(IEnumerable resources, ResourceHook hook, params RelationshipAttribute[] relationships) + where TResource : class, IIdentifiable { - var resourceType = typeof(TResource); - var dbValues = LoadDbValues(resourceType, resources, hook, relationships)?.Cast(); + Type resourceType = typeof(TResource); + IEnumerable dbValues = LoadDbValues(resourceType, resources, hook, relationships)?.Cast(); + if (dbValues == null) { return null; @@ -112,9 +123,10 @@ public HashSet LoadDbValues(IEnumerable resourc return new HashSet(dbValues); } - public bool ShouldLoadDbValues(Type resourceType, ResourceHook hook) + public bool ShouldLoadDbValues(LeftType resourceType, ResourceHook hook) { - var discovery = GetHookDiscovery(resourceType); + IHooksDiscovery discovery = GetHookDiscovery(resourceType); + if (discovery.DatabaseValuesDisabledHooks.Contains(hook)) { return false; @@ -130,7 +142,7 @@ public bool ShouldLoadDbValues(Type resourceType, ResourceHook hook) private bool ShouldExecuteHook(RightType resourceType, ResourceHook hook) { - var discovery = GetHookDiscovery(resourceType); + IHooksDiscovery discovery = GetHookDiscovery(resourceType); return discovery.ImplementedHooks.Contains(hook); } @@ -143,44 +155,48 @@ private void CheckForTargetHookExistence() } } - private IHooksDiscovery GetHookDiscovery(Type resourceType) + private IHooksDiscovery GetHookDiscovery(LeftType resourceType) { if (!_hookDiscoveries.TryGetValue(resourceType, out IHooksDiscovery discovery)) { discovery = _genericProcessorFactory.Get(typeof(IHooksDiscovery<>), resourceType); _hookDiscoveries[resourceType] = discovery; } + return discovery; } - private IEnumerable GetWhereAndInclude(IReadOnlyCollection ids, RelationshipAttribute[] relationshipsToNextLayer) where TResource : class, IIdentifiable + private IEnumerable GetWhereAndInclude(IReadOnlyCollection ids, RelationshipAttribute[] relationshipsToNextLayer) + where TResource : class, IIdentifiable { if (!ids.Any()) { return Array.Empty(); } - var resourceContext = _resourceContextProvider.GetResourceContext(); - var filterExpression = CreateFilterByIds(ids, resourceContext); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(); + FilterExpression filterExpression = CreateFilterByIds(ids, resourceContext); var queryLayer = new QueryLayer(resourceContext) { Filter = filterExpression }; - var chains = relationshipsToNextLayer.Select(relationship => new ResourceFieldChainExpression(relationship)).ToList(); + List chains = relationshipsToNextLayer.Select(relationship => new ResourceFieldChainExpression(relationship)) + .ToList(); + if (chains.Any()) { queryLayer.Include = IncludeChainConverter.FromRelationshipChains(chains); } - var repository = GetRepository(); + IResourceReadRepository repository = GetRepository(); return repository.GetAsync(queryLayer, CancellationToken.None).Result; } private static FilterExpression CreateFilterByIds(IReadOnlyCollection ids, ResourceContext resourceContext) { - var idAttribute = resourceContext.Attributes.Single(attr => attr.Property.Name == nameof(Identifiable.Id)); + AttrAttribute idAttribute = resourceContext.Attributes.Single(attr => attr.Property.Name == nameof(Identifiable.Id)); var idChain = new ResourceFieldChainExpression(idAttribute); if (ids.Count == 1) @@ -189,31 +205,32 @@ private static FilterExpression CreateFilterByIds(IReadOnlyCollection return new ComparisonExpression(ComparisonOperator.Equals, idChain, constant); } - var constants = ids.Select(id => new LiteralConstantExpression(id.ToString())).ToList(); + List constants = ids.Select(id => new LiteralConstantExpression(id.ToString())).ToList(); return new EqualsAnyOfExpression(idChain, constants); } - private IResourceReadRepository GetRepository() where TResource : class, IIdentifiable + private IResourceReadRepository GetRepository() + where TResource : class, IIdentifiable { return _genericProcessorFactory.Get>(typeof(IResourceReadRepository<,>), typeof(TResource), typeof(TId)); } - public Dictionary LoadImplicitlyAffected( - Dictionary leftResourcesByRelation, + public Dictionary LoadImplicitlyAffected(Dictionary leftResourcesByRelation, IEnumerable existingRightResources = null) { - var existingRightResourceList = existingRightResources?.Cast().ToList(); + List existingRightResourceList = existingRightResources?.Cast().ToList(); var implicitlyAffected = new Dictionary(); - foreach (var kvp in leftResourcesByRelation) + + foreach (KeyValuePair kvp in leftResourcesByRelation) { - if (IsHasManyThrough(kvp, out var lefts, out var relationship)) + if (IsHasManyThrough(kvp, out IEnumerable lefts, out RelationshipAttribute relationship)) { continue; } // note that we don't have to check if BeforeImplicitUpdate hook is implemented. If not, it wont ever get here. - var includedLefts = LoadDbValues(relationship.LeftType, lefts, ResourceHook.BeforeImplicitUpdateRelationship, relationship); + IEnumerable includedLefts = LoadDbValues(relationship.LeftType, lefts, ResourceHook.BeforeImplicitUpdateRelationship, relationship); AddToImplicitlyAffected(includedLefts, relationship, existingRightResourceList, implicitlyAffected); } @@ -227,7 +244,8 @@ private void AddToImplicitlyAffected(IEnumerable includedLefts, RelationshipAttr foreach (IIdentifiable ip in includedLefts) { IList dbRightResourceList = TypeHelper.CreateListFor(relationship.RightType); - var relationshipValue = relationship.GetValue(ip); + object relationshipValue = relationship.GetValue(ip); + if (!(relationshipValue is IEnumerable)) { if (relationshipValue != null) @@ -240,7 +258,8 @@ private void AddToImplicitlyAffected(IEnumerable includedLefts, RelationshipAttr AddToList(dbRightResourceList, (IEnumerable)relationshipValue); } - var dbRightResourceListCast = dbRightResourceList.Cast().ToList(); + List dbRightResourceListCast = dbRightResourceList.Cast().ToList(); + if (existingRightResourceList != null) { dbRightResourceListCast = dbRightResourceListCast.Except(existingRightResourceList, _comparer).ToList(); @@ -261,15 +280,13 @@ private void AddToImplicitlyAffected(IEnumerable includedLefts, RelationshipAttr private static void AddToList(IList list, IEnumerable itemsToAdd) { - foreach (var item in itemsToAdd) + foreach (object item in itemsToAdd) { list.Add(item); } } - private bool IsHasManyThrough(KeyValuePair kvp, - out IEnumerable resources, - out RelationshipAttribute attr) + private bool IsHasManyThrough(KeyValuePair kvp, out IEnumerable resources, out RelationshipAttribute attr) { attr = kvp.Key; resources = kvp.Value; @@ -277,4 +294,3 @@ private bool IsHasManyThrough(KeyValuePair k } } } - diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IByAffectedRelationships.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IByAffectedRelationships.cs index 689ac13260..2d77f065b0 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IByAffectedRelationships.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IByAffectedRelationships.cs @@ -7,12 +7,12 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution /// /// An interface that is implemented to expose a relationship dictionary on another class. /// - public interface IByAffectedRelationships : - IRelationshipGetters where TRightResource : class, IIdentifiable + public interface IByAffectedRelationships : IRelationshipGetters + where TRightResource : class, IIdentifiable { /// /// Gets a dictionary of affected resources grouped by affected relationships. /// Dictionary> AffectedRelationships { get; } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IDiffableResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IDiffableResourceHashSet.cs index 1a7d98fc14..ab712a0bc6 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IDiffableResourceHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IDiffableResourceHashSet.cs @@ -4,19 +4,16 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution { /// - /// A wrapper class that contains information about the resources that are updated by the request. - /// Contains the resources from the request and the corresponding database values. - /// - /// Also contains information about updated relationships through - /// implementation of IRelationshipsDictionary> + /// A wrapper class that contains information about the resources that are updated by the request. Contains the resources from the request and the + /// corresponding database values. Also contains information about updated relationships through implementation of IRelationshipsDictionary + /// > /// - public interface IDiffableResourceHashSet : IResourceHashSet where TResource : class, IIdentifiable + public interface IDiffableResourceHashSet : IResourceHashSet + where TResource : class, IIdentifiable { /// - /// Iterates over diffs, which is the affected resource from the request - /// with their associated current value from the database. + /// Iterates over diffs, which is the affected resource from the request with their associated current value from the database. /// IEnumerable> GetDiffs(); - } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookExecutorHelper.cs index 810a3f3dd5..56a7f635f9 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IHookExecutorHelper.cs @@ -7,55 +7,60 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution { /// - /// A helper class for retrieving meta data about hooks, - /// fetching database values and performing other recurring internal operations. - /// - /// Used internally by + /// A helper class for retrieving meta data about hooks, fetching database values and performing other recurring internal operations. Used internally by + /// /// internal interface IHookExecutorHelper { /// - /// For a particular ResourceHook and for a given model type, checks if - /// the ResourceHooksDefinition has an implementation for the hook - /// and if so, return it. - /// - /// Also caches the retrieves containers so we don't need to reflectively - /// instantiate them multiple times. + /// For a particular ResourceHook and for a given model type, checks if the ResourceHooksDefinition has an implementation for the hook and if so, return + /// it. Also caches the retrieves containers so we don't need to reflectively instantiate them multiple times. /// IResourceHookContainer GetResourceHookContainer(Type targetResource, ResourceHook hook = ResourceHook.None); /// - /// For a particular ResourceHook and for a given model type, checks if - /// the ResourceHooksDefinition has an implementation for the hook - /// and if so, return it. - /// - /// Also caches the retrieves containers so we don't need to reflectively - /// instantiate them multiple times. + /// For a particular ResourceHook and for a given model type, checks if the ResourceHooksDefinition has an implementation for the hook and if so, return + /// it. Also caches the retrieves containers so we don't need to reflectively instantiate them multiple times. /// - IResourceHookContainer GetResourceHookContainer(ResourceHook hook = ResourceHook.None) where TResource : class, IIdentifiable; + IResourceHookContainer GetResourceHookContainer(ResourceHook hook = ResourceHook.None) + where TResource : class, IIdentifiable; /// /// Load the implicitly affected resources from the database for a given set of target target resources and involved relationships /// - /// The implicitly affected resources by relationship - Dictionary LoadImplicitlyAffected(Dictionary leftResourcesByRelation, IEnumerable existingRightResources = null); + /// + /// The implicitly affected resources by relationship + /// + Dictionary LoadImplicitlyAffected(Dictionary leftResourcesByRelation, + IEnumerable existingRightResources = null); /// /// For a set of resources, loads current values from the database /// - /// type of the resources to be loaded - /// The set of resources to load the db values for - /// The hook in which the db values will be displayed. - /// Relationships that need to be included on resources. + /// + /// type of the resources to be loaded + /// + /// + /// The set of resources to load the db values for + /// + /// + /// The hook in which the db values will be displayed. + /// + /// + /// Relationships that need to be included on resources. + /// IEnumerable LoadDbValues(Type resourceTypeForRepository, IEnumerable resources, ResourceHook hook, params RelationshipAttribute[] relationships); /// - /// Checks if the display database values option is allowed for the targeted hook, and for - /// a given resource of type checks if this hook is implemented and if the - /// database values option is enabled. + /// Checks if the display database values option is allowed for the targeted hook, and for a given resource of type + /// checks if this hook is implemented and if the database values option is enabled. /// - /// true, if should load db values, false otherwise. - /// Container resource type. + /// + /// true, if should load db values, false otherwise. + /// + /// + /// Container resource type. + /// /// Hook. bool ShouldLoadDbValues(Type resourceType, ResourceHook hook); } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs index 7f07fc083c..ab3d80eca9 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipGetters.cs @@ -9,19 +9,22 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution /// /// A helper class that provides insights in which relationships have been updated for which resources. /// - public interface IRelationshipGetters where TLeftResource : class, IIdentifiable + public interface IRelationshipGetters + where TLeftResource : class, IIdentifiable { /// - /// Gets a dictionary of all resources that have an affected relationship to type + /// Gets a dictionary of all resources that have an affected relationship to type /// - Dictionary> GetByRelationship() where TRightResource : class, IIdentifiable; + Dictionary> GetByRelationship() + where TRightResource : class, IIdentifiable; + /// - /// Gets a dictionary of all resources that have an affected relationship to type + /// Gets a dictionary of all resources that have an affected relationship to type /// Dictionary> GetByRelationship(Type resourceType); + /// - /// Gets a collection of all the resources for the property within - /// has been affected by the request + /// Gets a collection of all the resources for the property within has been affected by the request /// HashSet GetAffected(Expression> navigationAction); } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs index 8de79a01ed..6f1496b01d 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IRelationshipsDictionary.cs @@ -7,14 +7,16 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution /// /// A dummy interface used internally by the hook executor. /// - public interface IRelationshipsDictionary { } + public interface IRelationshipsDictionary + { + } /// /// A helper class that provides insights in which relationships have been updated for which resources. /// - public interface IRelationshipsDictionary : - IRelationshipGetters, - IReadOnlyDictionary>, - IRelationshipsDictionary where TRightResource : class, IIdentifiable - { } + public interface IRelationshipsDictionary + : IRelationshipGetters, IReadOnlyDictionary>, IRelationshipsDictionary + where TRightResource : class, IIdentifiable + { + } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IResourceHashSet.cs index 8863ac4686..045880be3f 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/IResourceHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/IResourceHashSet.cs @@ -4,10 +4,11 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution { /// - /// Basically a enumerable of of resources that were affected by the request. - /// - /// Also contains information about updated relationships through - /// implementation of IAffectedRelationshipsDictionary> + /// Basically a enumerable of of resources that were affected by the request. Also contains information about updated + /// relationships through implementation of IAffectedRelationshipsDictionary> /// - public interface IResourceHashSet : IByAffectedRelationships, IReadOnlyCollection where TResource : class, IIdentifiable { } -} \ No newline at end of file + public interface IResourceHashSet : IByAffectedRelationships, IReadOnlyCollection + where TResource : class, IIdentifiable + { + } +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs index 0aa1b9406d..d738a6b162 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/RelationshipsDictionary.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -10,30 +11,35 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution { /// - /// Implementation of IAffectedRelationships{TRightResource} - /// - /// It is practically a ReadOnlyDictionary{RelationshipAttribute, HashSet{TRightResource}} dictionary - /// with the two helper methods defined on IAffectedRelationships{TRightResource}. + /// Implementation of IAffectedRelationships{TRightResource} It is practically a ReadOnlyDictionary{RelationshipAttribute, HashSet{TRightResource}} + /// dictionary with the two helper methods defined on IAffectedRelationships{TRightResource}. /// [PublicAPI] - public class RelationshipsDictionary : - Dictionary>, - IRelationshipsDictionary where TResource : class, IIdentifiable + public class RelationshipsDictionary : Dictionary>, IRelationshipsDictionary + where TResource : class, IIdentifiable { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// Relationships. - public RelationshipsDictionary(Dictionary> relationships) : base(relationships) { } + /// + /// Relationships. + /// + public RelationshipsDictionary(Dictionary> relationships) + : base(relationships) + { + } /// /// Used internally by the ResourceHookExecutor to make life a bit easier with generics /// internal RelationshipsDictionary(Dictionary relationships) - : this(TypeHelper.ConvertRelationshipDictionary(relationships)) { } + : this(TypeHelper.ConvertRelationshipDictionary(relationships)) + { + } /// - public Dictionary> GetByRelationship() where TRelatedResource : class, IIdentifiable + public Dictionary> GetByRelationship() + where TRelatedResource : class, IIdentifiable { return GetByRelationship(typeof(TRelatedResource)); } @@ -49,7 +55,7 @@ public HashSet GetAffected(Expression> naviga { ArgumentGuard.NotNull(navigationAction, nameof(navigationAction)); - var property = TypeHelper.ParseNavigationExpression(navigationAction); + PropertyInfo property = TypeHelper.ParseNavigationExpression(navigationAction); return this.Where(p => p.Key.Property.Name == property.Name).Select(p => p.Value).SingleOrDefault(); } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs index fe52b4f9b3..c338cbe612 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceDiffPair.cs @@ -4,25 +4,26 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution { /// - /// A wrapper that contains a resource that is affected by the request, - /// matched to its current database value + /// A wrapper that contains a resource that is affected by the request, matched to its current database value /// [PublicAPI] - public sealed class ResourceDiffPair where TResource : class, IIdentifiable + public sealed class ResourceDiffPair + where TResource : class, IIdentifiable { - public ResourceDiffPair(TResource resource, TResource databaseValue) - { - Resource = resource; - DatabaseValue = databaseValue; - } - /// /// The resource from the request matching the resource from the database. /// public TResource Resource { get; } + /// /// The resource from the database matching the resource from the request. /// public TResource DatabaseValue { get; } + + public ResourceDiffPair(TResource resource, TResource databaseValue) + { + Resource = resource; + DatabaseValue = databaseValue; + } } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs index 192bcdad21..40734b2168 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHashSet.cs @@ -9,23 +9,20 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution { /// - /// Implementation of IResourceHashSet{TResource}. - /// - /// Basically a enumerable of of resources that were affected by the request. - /// - /// Also contains information about updated relationships through - /// implementation of IRelationshipsDictionary> + /// Implementation of IResourceHashSet{TResource}. Basically a enumerable of of resources that were affected by the + /// request. Also contains information about updated relationships through implementation of IRelationshipsDictionary> /// [PublicAPI] - public class ResourceHashSet : HashSet, IResourceHashSet where TResource : class, IIdentifiable + public class ResourceHashSet : HashSet, IResourceHashSet + where TResource : class, IIdentifiable { + private readonly RelationshipsDictionary _relationships; + /// public Dictionary> AffectedRelationships => _relationships; - private readonly RelationshipsDictionary _relationships; - - public ResourceHashSet(HashSet resources, - Dictionary> relationships) : base(resources) + public ResourceHashSet(HashSet resources, Dictionary> relationships) + : base(resources) { _relationships = new RelationshipsDictionary(relationships); } @@ -33,10 +30,10 @@ public ResourceHashSet(HashSet resources, /// /// Used internally by the ResourceHookExecutor to make live a bit easier with generics /// - internal ResourceHashSet(IEnumerable resources, - Dictionary relationships) - : this((HashSet)resources, TypeHelper.ConvertRelationshipDictionary(relationships)) { } - + internal ResourceHashSet(IEnumerable resources, Dictionary relationships) + : this((HashSet)resources, TypeHelper.ConvertRelationshipDictionary(relationships)) + { + } /// public Dictionary> GetByRelationship(Type resourceType) @@ -45,7 +42,8 @@ public Dictionary> GetByRelationship(T } /// - public Dictionary> GetByRelationship() where TRightResource : class, IIdentifiable + public Dictionary> GetByRelationship() + where TRightResource : class, IIdentifiable { return GetByRelationship(typeof(TRightResource)); } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHook.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHook.cs index 310bdb808c..f63b1380ad 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHook.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourceHook.cs @@ -1,6 +1,5 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution { - /// /// A enum that represent the available resource hooks. /// @@ -20,5 +19,4 @@ public enum ResourceHook AfterDelete, AfterUpdateRelationship } - } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs index 3c256f1ad4..bdfc01aa78 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Execution/ResourcePipeline.cs @@ -5,9 +5,9 @@ namespace JsonApiDotNetCore.Hooks.Internal.Execution { /// - /// An enum that represents the initiator of a resource hook. Eg, when BeforeCreate() - /// is called from , it will be called - /// with parameter pipeline = ResourceAction.GetSingle. + /// An enum that represents the initiator of a resource hook. Eg, when BeforeCreate() is called from + /// , it will be called with parameter pipeline = + /// ResourceAction.GetSingle. /// [PublicAPI] public enum ResourcePipeline diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs index 11570d6212..a52e9a716c 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookContainer.cs @@ -8,42 +8,50 @@ namespace JsonApiDotNetCore.Hooks.Internal /// /// Create hooks container /// - public interface ICreateHookContainer where TResource : class, IIdentifiable + public interface ICreateHookContainer + where TResource : class, IIdentifiable { /// - /// Implement this hook to run custom logic in the - /// layer just before creation of resources of type . + /// Implement this hook to run custom logic in the layer just before creation of resources of type + /// . /// - /// For the pipeline, - /// will typically contain one entry. + /// For the pipeline, will typically contain one entry. /// - /// The returned may be a subset - /// of , in which case the operation of the - /// pipeline will not be executed for the omitted resources. The returned - /// set may also contain custom changes of the properties on the resources. + /// The returned may be a subset of , in which case the operation of the pipeline will + /// not be executed for the omitted resources. The returned set may also contain custom changes of the properties on the resources. /// - /// If new relationships are to be created with the to-be-created resources, - /// this will be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the - /// hook is fired after the execution of this hook. + /// If new relationships are to be created with the to-be-created resources, this will be reflected by the corresponding NavigationProperty being set. + /// For each of these relationships, the hook is fired after the execution of + /// this hook. /// - /// The transformed resource set - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. + /// + /// The transformed resource set + /// + /// + /// The unique set of affected resources. + /// + /// + /// An enum indicating from where the hook was triggered. + /// IEnumerable BeforeCreate(IResourceHashSet resources, ResourcePipeline pipeline); /// - /// Implement this hook to run custom logic in the - /// layer just after creation of resources of type . + /// Implement this hook to run custom logic in the layer just after creation of resources of type + /// . /// - /// If relationships were created with the created resources, this will - /// be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the + /// If relationships were created with the created resources, this will be reflected by the corresponding NavigationProperty being set. For each of these + /// relationships, the /// hook is fired after the execution of this hook. /// - /// The transformed resource set - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. + /// + /// The transformed resource set + /// + /// + /// The unique set of affected resources. + /// + /// + /// An enum indicating from where the hook was triggered. + /// void AfterCreate(HashSet resources, ResourcePipeline pipeline); } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs index 470a90d1db..c26c5099d8 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/ICreateHookExecutor.cs @@ -8,32 +8,47 @@ namespace JsonApiDotNetCore.Hooks.Internal public interface ICreateHookExecutor { /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. - /// The returned set will be used in the actual operation in . + /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. The returned set will be used in the actual operation in + /// . /// - /// Fires the - /// hook for values in parameter . + /// Fires the hook for values in parameter . /// - /// Fires the - /// hook for any secondary (nested) resource for values within parameter + /// Fires the hook for any secondary (nested) resource for values within + /// parameter /// - /// The transformed set - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - IEnumerable BeforeCreate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + /// + /// The transformed set + /// + /// + /// Target resources for the Before cycle. + /// + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// The type of the root resources + /// + IEnumerable BeforeCreate(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable; + /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. + /// Executes the After Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the - /// hook for values in parameter . + /// Fires the hook for values in parameter . /// - /// Fires the - /// hook for any secondary (nested) resource for values within parameter + /// Fires the hook for any secondary (nested) resource for values within + /// parameter /// - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - void AfterCreate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + /// + /// Target resources for the Before cycle. + /// + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// The type of the root resources + /// + void AfterCreate(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable; } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs index 7ab640fd49..595056ce07 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookContainer.cs @@ -8,36 +8,45 @@ namespace JsonApiDotNetCore.Hooks.Internal /// /// Delete hooks container /// - public interface IDeleteHookContainer where TResource : class, IIdentifiable + public interface IDeleteHookContainer + where TResource : class, IIdentifiable { /// - /// Implement this hook to run custom logic in the - /// layer just before deleting resources of type . + /// Implement this hook to run custom logic in the layer just before deleting resources of type + /// . /// - /// For the pipeline, - /// will typically contain one resource. + /// For the pipeline, will typically contain one resource. /// - /// The returned may be a subset - /// of , in which case the operation of the - /// pipeline will not be executed for the omitted resources. + /// The returned may be a subset of , in which case the operation of the pipeline will + /// not be executed for the omitted resources. /// - /// If by the deletion of these resources any other resources are affected - /// implicitly by the removal of their relationships (eg - /// in the case of an one-to-one relationship), the - /// hook is fired for these resources. + /// If by the deletion of these resources any other resources are affected implicitly by the removal of their relationships (eg in the case of an + /// one-to-one relationship), the hook is fired for these resources. /// - /// The transformed resource set - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. + /// + /// The transformed resource set + /// + /// + /// The unique set of affected resources. + /// + /// + /// An enum indicating from where the hook was triggered. + /// IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline); /// - /// Implement this hook to run custom logic in the - /// layer just after deletion of resources of type . + /// Implement this hook to run custom logic in the layer just after deletion of resources of type + /// . /// - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. - /// If set to true the deletion succeeded in the repository layer. + /// + /// The unique set of affected resources. + /// + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// If set to true the deletion succeeded in the repository layer. + /// void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs index efab29ac87..706477ad42 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IDeleteHookExecutor.cs @@ -8,33 +8,48 @@ namespace JsonApiDotNetCore.Hooks.Internal public interface IDeleteHookExecutor { /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. - /// The returned set will be used in the actual operation in . + /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. The returned set will be used in the actual operation in + /// . /// - /// Fires the - /// hook for values in parameter . + /// Fires the hook for values in parameter . /// - /// Fires the - /// hook for any resources that are indirectly (implicitly) affected by this operation. - /// Eg: when deleting a resource that has relationships set to other resources, - /// these other resources are implicitly affected by the delete operation. + /// Fires the hook for any resources that are indirectly (implicitly) + /// affected by this operation. Eg: when deleting a resource that has relationships set to other resources, these other resources are implicitly affected + /// by the delete operation. /// - /// The transformed set - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - IEnumerable BeforeDelete(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + /// + /// The transformed set + /// + /// + /// Target resources for the Before cycle. + /// + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// The type of the root resources + /// + IEnumerable BeforeDelete(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable; /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. + /// Executes the After Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the - /// hook for values in parameter . + /// Fires the hook for values in parameter . /// - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// If set to true the deletion succeeded. - /// The type of the root resources - void AfterDelete(IEnumerable resources, ResourcePipeline pipeline, bool succeeded) where TResource : class, IIdentifiable; + /// + /// Target resources for the Before cycle. + /// + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// If set to true the deletion succeeded. + /// + /// + /// The type of the root resources + /// + void AfterDelete(IEnumerable resources, ResourcePipeline pipeline, bool succeeded) + where TResource : class, IIdentifiable; } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookContainer.cs index 61db0db85f..94b60ead58 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookContainer.cs @@ -8,21 +8,26 @@ namespace JsonApiDotNetCore.Hooks.Internal /// /// On return hook container /// - public interface IOnReturnHookContainer where TResource : class, IIdentifiable + public interface IOnReturnHookContainer + where TResource : class, IIdentifiable { /// - /// Implement this hook to transform the result data just before returning - /// the resources of type from the - /// layer + /// Implement this hook to transform the result data just before returning the resources of type from the + /// layer /// - /// The returned may be a subset - /// of and may contain changes in properties - /// of the encapsulated resources. + /// The returned may be a subset of and may contain changes in properties of the + /// encapsulated resources. /// /// - /// The transformed resource set - /// The unique set of affected resources - /// An enum indicating from where the hook was triggered. + /// + /// The transformed resource set + /// + /// + /// The unique set of affected resources + /// + /// + /// An enum indicating from where the hook was triggered. + /// IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs index 30d10479f3..9dca66a068 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IOnReturnHookExecutor.cs @@ -10,15 +10,24 @@ namespace JsonApiDotNetCore.Hooks.Internal public interface IOnReturnHookExecutor { /// - /// Executes the On Cycle by firing the appropriate hooks if they are implemented. + /// Executes the On Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the for every unique - /// resource type occurring in parameter . + /// Fires the for every unique resource type occurring in parameter + /// . /// - /// The transformed set - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - IEnumerable OnReturn(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + /// + /// The transformed set + /// + /// + /// Target resources for the Before cycle. + /// + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// The type of the root resources + /// + IEnumerable OnReturn(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable; } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookContainer.cs index 9913f8f2d4..d7ad9117a6 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookContainer.cs @@ -8,24 +8,38 @@ namespace JsonApiDotNetCore.Hooks.Internal /// /// Read hooks container /// - public interface IReadHookContainer where TResource : class, IIdentifiable + public interface IReadHookContainer + where TResource : class, IIdentifiable { /// - /// Implement this hook to run custom logic in the - /// layer just before reading resources of type . + /// Implement this hook to run custom logic in the layer just before reading resources of type + /// . /// - /// An enum indicating from where the hook was triggered. - /// Indicates whether the to be queried resources are the primary request resources or if they were included - /// The string ID of the requested resource, in the case of + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// Indicates whether the to be queried resources are the primary request resources or if they were included + /// + /// + /// The string ID of the requested resource, in the case of + /// void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null); + /// - /// Implement this hook to run custom logic in the - /// layer just after reading resources of type . + /// Implement this hook to run custom logic in the layer just after reading resources of type + /// . /// - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. - /// A boolean to indicate whether the resources in this hook execution are the primary resources of the request, - /// or if they were included as a relationship + /// + /// The unique set of affected resources. + /// + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// A boolean to indicate whether the resources in this hook execution are the primary resources of the request, or if they were included as a + /// relationship + /// void AfterRead(HashSet resources, ResourcePipeline pipeline, bool isIncluded = false); } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs index 3ac67c0a93..a752d81e2d 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IReadHookExecutor.cs @@ -12,25 +12,38 @@ namespace JsonApiDotNetCore.Hooks.Internal public interface IReadHookExecutor { /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. + /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the - /// hook for the requested resources as well as any related relationship. + /// Fires the hook for the requested resources as well as any related relationship. /// - /// An enum indicating from where the hook was triggered. - /// StringId of the requested resource in the case of - /// . - /// The type of the request resource - void BeforeRead(ResourcePipeline pipeline, string stringId = null) where TResource : class, IIdentifiable; + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// StringId of the requested resource in the case of . + /// + /// + /// The type of the request resource + /// + void BeforeRead(ResourcePipeline pipeline, string stringId = null) + where TResource : class, IIdentifiable; + /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. + /// Executes the After Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the for every unique - /// resource type occurring in parameter . + /// Fires the for every unique resource type occurring in parameter + /// . /// - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - void AfterRead(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + /// + /// Target resources for the Before cycle. + /// + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// The type of the root resources + /// + void AfterRead(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable; } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs index b9cf89cd4d..f3bbc0697b 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookContainer.cs @@ -3,15 +3,19 @@ namespace JsonApiDotNetCore.Hooks.Internal { /// - /// Not meant for public usage. Used internally in the + /// Not meant for public usage. Used internally in the /// - public interface IResourceHookContainer { } + public interface IResourceHookContainer + { + } /// - /// Implement this interface to implement business logic hooks on . + /// Implement this interface to implement business logic hooks on . /// public interface IResourceHookContainer - : IReadHookContainer, IDeleteHookContainer, ICreateHookContainer, - IUpdateHookContainer, IOnReturnHookContainer, IResourceHookContainer - where TResource : class, IIdentifiable { } + : IReadHookContainer, IDeleteHookContainer, ICreateHookContainer, IUpdateHookContainer, + IOnReturnHookContainer, IResourceHookContainer + where TResource : class, IIdentifiable + { + } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs index 56a5cc975f..e3a62558f3 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IResourceHookExecutor.cs @@ -5,14 +5,12 @@ namespace JsonApiDotNetCore.Hooks.Internal { /// - /// Transient service responsible for executing Resource Hooks as defined - /// in . See methods in - /// , and - /// for more information. - /// - /// Uses for traversal of nested resource data structures. - /// Uses for retrieving meta data about hooks, - /// fetching database values and performing other recurring internal operations. + /// Transient service responsible for executing Resource Hooks as defined in . See methods in + /// , and for more information. Uses + /// for traversal of nested resource data structures. Uses for retrieving meta data + /// about hooks, fetching database values and performing other recurring internal operations. /// - public interface IResourceHookExecutor : IReadHookExecutor, IUpdateHookExecutor, ICreateHookExecutor, IDeleteHookExecutor, IOnReturnHookExecutor { } + public interface IResourceHookExecutor : IReadHookExecutor, IUpdateHookExecutor, ICreateHookExecutor, IDeleteHookExecutor, IOnReturnHookExecutor + { + } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs index dcf26d3fd0..bcae4b5f9b 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookContainer.cs @@ -8,96 +8,113 @@ namespace JsonApiDotNetCore.Hooks.Internal /// /// update hooks container /// - public interface IUpdateHookContainer where TResource : class, IIdentifiable + public interface IUpdateHookContainer + where TResource : class, IIdentifiable { /// - /// Implement this hook to run custom logic in the - /// layer just before updating resources of type . + /// Implement this hook to run custom logic in the layer just before updating resources of type + /// . /// - /// For the pipeline, the - /// will typically contain one resource. + /// For the pipeline, the will typically contain one resource. /// - /// The returned may be a subset - /// of the property in parameter , - /// in which case the operation of the pipeline will not be executed - /// for the omitted resources. The returned set may also contain custom - /// changes of the properties on the resources. + /// The returned may be a subset of the property in parameter + /// , in which case the operation of the pipeline will not be executed for the omitted resources. The returned set may also + /// contain custom changes of the properties on the resources. /// - /// If new relationships are to be created with the to-be-updated resources, - /// this will be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the - /// hook is fired after the execution of this hook. + /// If new relationships are to be created with the to-be-updated resources, this will be reflected by the corresponding NavigationProperty being set. + /// For each of these relationships, the hook is fired after the execution of + /// this hook. /// - /// If by the creation of these relationships, any other relationships (eg - /// in the case of an already populated one-to-one relationship) are implicitly - /// affected, the - /// hook is fired for these. + /// If by the creation of these relationships, any other relationships (eg in the case of an already populated one-to-one relationship) are implicitly + /// affected, the hook is fired for these. /// - /// The transformed resource set - /// The affected resources. - /// An enum indicating from where the hook was triggered. + /// + /// The transformed resource set + /// + /// + /// The affected resources. + /// + /// + /// An enum indicating from where the hook was triggered. + /// IEnumerable BeforeUpdate(IDiffableResourceHashSet resources, ResourcePipeline pipeline); /// - /// Implement this hook to run custom logic in the - /// layer just before updating relationships to resources of type . + /// Implement this hook to run custom logic in the layer just before updating relationships to + /// resources of type . /// - /// This hook is fired when a relationship is created to resources of type - /// from a dependent pipeline ( - /// or ). For example, If an Article was created - /// and its author relationship was set to an existing Person, this hook will be fired - /// for that particular Person. + /// This hook is fired when a relationship is created to resources of type from a dependent pipeline ( + /// or ). For example, If an Article was created and its author relationship + /// was set to an existing Person, this hook will be fired for that particular Person. /// - /// The returned may be a subset - /// of , in which case the operation of the - /// pipeline will not be executed for any resource whose ID was omitted + /// The returned may be a subset of , in which case the operation of the pipeline will not + /// be executed for any resource whose ID was omitted /// /// - /// The transformed set of ids - /// The unique set of ids - /// An enum indicating from where the hook was triggered. - /// A helper that groups the resources by the affected relationship - IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline); + /// + /// The transformed set of ids + /// + /// + /// The unique set of ids + /// + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// A helper that groups the resources by the affected relationship + /// + IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, + ResourcePipeline pipeline); /// - /// Implement this hook to run custom logic in the - /// layer just after updating resources of type . + /// Implement this hook to run custom logic in the layer just after updating resources of type + /// . /// - /// If relationships were updated with the updated resources, this will - /// be reflected by the corresponding NavigationProperty being set. - /// For each of these relationships, the + /// If relationships were updated with the updated resources, this will be reflected by the corresponding NavigationProperty being set. For each of these + /// relationships, the /// hook is fired after the execution of this hook. /// - /// The unique set of affected resources. - /// An enum indicating from where the hook was triggered. + /// + /// The unique set of affected resources. + /// + /// + /// An enum indicating from where the hook was triggered. + /// void AfterUpdate(HashSet resources, ResourcePipeline pipeline); /// - /// Implement this hook to run custom logic in the layer - /// just after a relationship was updated. + /// Implement this hook to run custom logic in the layer just after a relationship was updated. /// - /// Relationship helper. - /// An enum indicating from where the hook was triggered. + /// + /// Relationship helper. + /// + /// + /// An enum indicating from where the hook was triggered. + /// void AfterUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline); /// - /// Implement this hook to run custom logic in the - /// layer just before implicitly updating relationships to resources of type . + /// Implement this hook to run custom logic in the layer just before implicitly updating relationships + /// to resources of type . /// - /// This hook is fired when a relationship to resources of type - /// is implicitly affected from a dependent pipeline ( - /// or ). For example, if an Article was updated - /// by setting its author relationship (one-to-one) to an existing Person, - /// and by this the relationship to a different Person was implicitly removed, - /// this hook will be fired for the latter Person. + /// This hook is fired when a relationship to resources of type is implicitly affected from a dependent pipeline ( + /// or ). For example, if an Article was updated by setting its author + /// relationship (one-to-one) to an existing Person, and by this the relationship to a different Person was implicitly removed, this hook will be fired + /// for the latter Person. /// - /// See for information about - /// when this hook is fired. + /// See for information about when + /// this hook is fired. /// /// - /// The transformed set of ids - /// A helper that groups the resources by the affected relationship - /// An enum indicating from where the hook was triggered. + /// + /// The transformed set of ids + /// + /// + /// A helper that groups the resources by the affected relationship + /// + /// + /// An enum indicating from where the hook was triggered. + /// void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline); } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs index 9884e88260..67323ca1ea 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/IUpdateHookExecutor.cs @@ -11,38 +11,52 @@ namespace JsonApiDotNetCore.Hooks.Internal public interface IUpdateHookExecutor { /// - /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. - /// The returned set will be used in the actual operation in . + /// Executes the Before Cycle by firing the appropriate hooks if they are implemented. The returned set will be used in the actual operation in + /// . /// - /// Fires the - /// hook for values in parameter . + /// Fires the hook for values in + /// parameter . /// - /// Fires the - /// hook for any secondary (nested) resource for values within parameter + /// Fires the hook for any secondary (nested) resource for values within + /// parameter /// - /// Fires the - /// hook for any resources that are indirectly (implicitly) affected by this operation. - /// Eg: when updating a one-to-one relationship of a resource which already - /// had this relationship populated, then this update will indirectly affect - /// the existing relationship value. + /// Fires the hook for any resources that are indirectly (implicitly) + /// affected by this operation. Eg: when updating a one-to-one relationship of a resource which already had this relationship populated, then this update + /// will indirectly affect the existing relationship value. /// - /// The transformed set - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - IEnumerable BeforeUpdate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + /// + /// The transformed set + /// + /// + /// Target resources for the Before cycle. + /// + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// The type of the root resources + /// + IEnumerable BeforeUpdate(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable; + /// - /// Executes the After Cycle by firing the appropriate hooks if they are implemented. + /// Executes the After Cycle by firing the appropriate hooks if they are implemented. /// - /// Fires the - /// hook for values in parameter . + /// Fires the hook for values in parameter . /// - /// Fires the - /// hook for any secondary (nested) resource for values within parameter + /// Fires the hook for any secondary (nested) resource for values within + /// parameter /// - /// Target resources for the Before cycle. - /// An enum indicating from where the hook was triggered. - /// The type of the root resources - void AfterUpdate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable; + /// + /// Target resources for the Before cycle. + /// + /// + /// An enum indicating from where the hook was triggered. + /// + /// + /// The type of the root resources + /// + void AfterUpdate(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable; } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/NeverResourceHookExecutorFacade.cs b/src/JsonApiDotNetCore/Hooks/Internal/NeverResourceHookExecutorFacade.cs index 4de491aa53..aabff919c1 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/NeverResourceHookExecutorFacade.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/NeverResourceHookExecutorFacade.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.Hooks.Internal { /// - /// Facade for hooks that never executes any callbacks, which is used when is false. + /// Facade for hooks that never executes any callbacks, which is used when is false. /// public sealed class NeverResourceHookExecutorFacade : IResourceHookExecutorFacade { diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs index d2bfcb39a8..dcb3517027 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutor.cs @@ -27,12 +27,8 @@ internal sealed class ResourceHookExecutor : IResourceHookExecutor private readonly ITargetedFields _targetedFields; private readonly IResourceGraph _resourceGraph; - public ResourceHookExecutor( - IHookExecutorHelper executorHelper, - ITraversalHelper traversalHelper, - ITargetedFields targetedFields, - IEnumerable constraintProviders, - IResourceGraph resourceGraph) + public ResourceHookExecutor(IHookExecutorHelper executorHelper, ITraversalHelper traversalHelper, ITargetedFields targetedFields, + IEnumerable constraintProviders, IResourceGraph resourceGraph) { _executorHelper = executorHelper; _traversalHelper = traversalHelper; @@ -42,16 +38,17 @@ public ResourceHookExecutor( } /// - public void BeforeRead(ResourcePipeline pipeline, string stringId = null) where TResource : class, IIdentifiable + public void BeforeRead(ResourcePipeline pipeline, string stringId = null) + where TResource : class, IIdentifiable { - var hookContainer = _executorHelper.GetResourceHookContainer(ResourceHook.BeforeRead); + IResourceHookContainer hookContainer = _executorHelper.GetResourceHookContainer(ResourceHook.BeforeRead); hookContainer?.BeforeRead(pipeline, false, stringId); - var calledContainers = typeof(TResource).AsList(); + List calledContainers = typeof(TResource).AsList(); // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var includes = _constraintProviders + IncludeExpression[] includes = _constraintProviders .SelectMany(provider => provider.GetConstraints()) .Select(expressionInScope => expressionInScope.Expression) .OfType() @@ -60,19 +57,20 @@ public void BeforeRead(ResourcePipeline pipeline, string stringId = n // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - foreach (var chain in includes.SelectMany(IncludeChainConverter.GetRelationshipChains)) + foreach (ResourceFieldChainExpression chain in includes.SelectMany(IncludeChainConverter.GetRelationshipChains)) { RecursiveBeforeRead(chain.Fields.Cast().ToList(), pipeline, calledContainers); } } /// - public IEnumerable BeforeUpdate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable + public IEnumerable BeforeUpdate(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable { - if (GetHook(ResourceHook.BeforeUpdate, resources, out var container, out var node)) + if (GetHook(ResourceHook.BeforeUpdate, resources, out IResourceHookContainer container, out RootNode node)) { - var relationships = node.RelationshipsToNextLayer.Select(p => p.Attribute).ToArray(); - var dbValues = LoadDbValues(typeof(TResource), (IEnumerable)node.UniqueResources, ResourceHook.BeforeUpdate, relationships); + RelationshipAttribute[] relationships = node.RelationshipsToNextLayer.Select(p => p.Attribute).ToArray(); + IEnumerable dbValues = LoadDbValues(typeof(TResource), (IEnumerable)node.UniqueResources, ResourceHook.BeforeUpdate, relationships); var diff = new DiffableResourceHashSet(node.UniqueResources, dbValues, node.LeftsToNextLayer(), _targetedFields); IEnumerable updated = container.BeforeUpdate(diff, pipeline); node.UpdateUnique(updated); @@ -84,26 +82,33 @@ public IEnumerable BeforeUpdate(IEnumerable res } /// - public IEnumerable BeforeCreate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable + public IEnumerable BeforeCreate(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable { - if (GetHook(ResourceHook.BeforeCreate, resources, out var container, out var node)) + if (GetHook(ResourceHook.BeforeCreate, resources, out IResourceHookContainer container, out RootNode node)) { var affected = new ResourceHashSet((HashSet)node.UniqueResources, node.LeftsToNextLayer()); IEnumerable updated = container.BeforeCreate(affected, pipeline); node.UpdateUnique(updated); node.Reassign(resources); } + FireNestedBeforeUpdateHooks(pipeline, _traversalHelper.CreateNextLayer(node)); return resources; } /// - public IEnumerable BeforeDelete(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable + public IEnumerable BeforeDelete(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable { - if (GetHook(ResourceHook.BeforeDelete, resources, out var container, out var node)) + if (GetHook(ResourceHook.BeforeDelete, resources, out IResourceHookContainer container, out RootNode node)) { - var relationships = node.RelationshipsToNextLayer.Select(p => p.Attribute).ToArray(); - var targetResources = LoadDbValues(typeof(TResource), (IEnumerable)node.UniqueResources, ResourceHook.BeforeDelete, relationships) ?? node.UniqueResources; + RelationshipAttribute[] relationships = node.RelationshipsToNextLayer.Select(p => p.Attribute).ToArray(); + + IEnumerable targetResources = + LoadDbValues(typeof(TResource), (IEnumerable)node.UniqueResources, ResourceHook.BeforeDelete, relationships) ?? + node.UniqueResources; + var affected = new ResourceHashSet(targetResources, node.LeftsToNextLayer()); IEnumerable updated = container.BeforeDelete(affected, pipeline); @@ -115,19 +120,21 @@ public IEnumerable BeforeDelete(IEnumerable res // Here we're loading all relations onto the to-be-deleted article // if for that relation the BeforeImplicitUpdateHook is implemented, // and this hook is then executed - foreach (var entry in node.LeftsToNextLayerByRelationships()) + foreach (KeyValuePair> entry in node.LeftsToNextLayerByRelationships()) { - var rightType = entry.Key; - var implicitTargets = entry.Value; + Type rightType = entry.Key; + Dictionary implicitTargets = entry.Value; FireForAffectedImplicits(rightType, implicitTargets, pipeline); } + return resources; } /// - public IEnumerable OnReturn(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable + public IEnumerable OnReturn(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable { - if (GetHook(ResourceHook.OnReturn, resources, out var container, out var node)) + if (GetHook(ResourceHook.OnReturn, resources, out IResourceHookContainer container, out RootNode node)) { IEnumerable updated = container.OnReturn((HashSet)node.UniqueResources, pipeline); ValidateHookResponse(updated); @@ -137,17 +144,19 @@ public IEnumerable OnReturn(IEnumerable resourc Traverse(_traversalHelper.CreateNextLayer(node), ResourceHook.OnReturn, (nextContainer, nextNode) => { - var filteredUniqueSet = CallHook(nextContainer, ResourceHook.OnReturn, ArrayFactory.Create(nextNode.UniqueResources, pipeline)); + IEnumerable filteredUniqueSet = CallHook(nextContainer, ResourceHook.OnReturn, ArrayFactory.Create(nextNode.UniqueResources, pipeline)); nextNode.UpdateUnique(filteredUniqueSet); nextNode.Reassign(); }); + return resources; } /// - public void AfterRead(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable + public void AfterRead(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable { - if (GetHook(ResourceHook.AfterRead, resources, out var container, out var node)) + if (GetHook(ResourceHook.AfterRead, resources, out IResourceHookContainer container, out RootNode node)) { container.AfterRead((HashSet)node.UniqueResources, pipeline); } @@ -159,51 +168,53 @@ public void AfterRead(IEnumerable resources, ResourcePipel } /// - public void AfterCreate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable + public void AfterCreate(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable { - if (GetHook(ResourceHook.AfterCreate, resources, out var container, out var node)) + if (GetHook(ResourceHook.AfterCreate, resources, out IResourceHookContainer container, out RootNode node)) { container.AfterCreate((HashSet)node.UniqueResources, pipeline); } - Traverse(_traversalHelper.CreateNextLayer(node), - ResourceHook.AfterUpdateRelationship, + Traverse(_traversalHelper.CreateNextLayer(node), ResourceHook.AfterUpdateRelationship, (nextContainer, nextNode) => FireAfterUpdateRelationship(nextContainer, nextNode, pipeline)); } /// - public void AfterUpdate(IEnumerable resources, ResourcePipeline pipeline) where TResource : class, IIdentifiable + public void AfterUpdate(IEnumerable resources, ResourcePipeline pipeline) + where TResource : class, IIdentifiable { - if (GetHook(ResourceHook.AfterUpdate, resources, out var container, out var node)) + if (GetHook(ResourceHook.AfterUpdate, resources, out IResourceHookContainer container, out RootNode node)) { container.AfterUpdate((HashSet)node.UniqueResources, pipeline); } - Traverse(_traversalHelper.CreateNextLayer(node), - ResourceHook.AfterUpdateRelationship, + Traverse(_traversalHelper.CreateNextLayer(node), ResourceHook.AfterUpdateRelationship, (nextContainer, nextNode) => FireAfterUpdateRelationship(nextContainer, nextNode, pipeline)); } /// - public void AfterDelete(IEnumerable resources, ResourcePipeline pipeline, bool succeeded) where TResource : class, IIdentifiable + public void AfterDelete(IEnumerable resources, ResourcePipeline pipeline, bool succeeded) + where TResource : class, IIdentifiable { - if (GetHook(ResourceHook.AfterDelete, resources, out var container, out var node)) + if (GetHook(ResourceHook.AfterDelete, resources, out IResourceHookContainer container, out RootNode node)) { container.AfterDelete((HashSet)node.UniqueResources, pipeline, succeeded); } } /// - /// For a given target and for a given type - /// , gets the hook container if the target - /// hook was implemented and should be executed. + /// For a given target and for a given type , gets the hook container if the target hook was + /// implemented and should be executed. /// /// Along the way, creates a traversable node from the root resource set. /// - /// true, if hook was implemented, false otherwise. - private bool GetHook(ResourceHook target, IEnumerable resources, - out IResourceHookContainer container, - out RootNode node) where TResource : class, IIdentifiable + /// + /// true, if hook was implemented, false otherwise. + /// + private bool GetHook(ResourceHook target, IEnumerable resources, out IResourceHookContainer container, + out RootNode node) + where TResource : class, IIdentifiable { node = _traversalHelper.CreateRootNode(resources); container = _executorHelper.GetResourceHookContainer(target); @@ -211,11 +222,11 @@ private bool GetHook(ResourceHook target, IEnumerable reso } /// - /// Traverses the nodes in a . + /// Traverses the nodes in a . /// private void Traverse(NodeLayer currentLayer, ResourceHook target, Action action) { - var nextLayer = currentLayer; + NodeLayer nextLayer = currentLayer; while (true) { @@ -234,7 +245,7 @@ private void TraverseNextLayer(NodeLayer nextLayer, Action - /// Recursively goes through the included relationships from JsonApiContext, - /// translates them to the corresponding hook containers and fires the + /// Recursively goes through the included relationships from JsonApiContext, translates them to the corresponding hook containers and fires the /// BeforeRead hook (if implemented) /// private void RecursiveBeforeRead(List relationshipChain, ResourcePipeline pipeline, List calledContainers) { while (true) { - var relationship = relationshipChain.First(); + RelationshipAttribute relationship = relationshipChain.First(); if (!calledContainers.Contains(relationship.RightType)) { calledContainers.Add(relationship.RightType); - var container = _executorHelper.GetResourceHookContainer(relationship.RightType, ResourceHook.BeforeRead); + IResourceHookContainer container = _executorHelper.GetResourceHookContainer(relationship.RightType, ResourceHook.BeforeRead); if (container != null) { @@ -280,20 +290,18 @@ private void RecursiveBeforeRead(List relationshipChain, } /// - /// Fires the nested before hooks for resources in the current + /// Fires the nested before hooks for resources in the current /// /// - /// For example: consider the case when the owner of article1 (one-to-one) - /// is being updated from owner_old to owner_new, where owner_new is currently already - /// related to article2. Then, the following nested hooks need to be fired in the following order. - /// First the BeforeUpdateRelationship should be for owner1, then the - /// BeforeImplicitUpdateRelationship hook should be fired for - /// owner2, and lastly the BeforeImplicitUpdateRelationship for article2. + /// For example: consider the case when the owner of article1 (one-to-one) is being updated from owner_old to owner_new, where owner_new is currently + /// already related to article2. Then, the following nested hooks need to be fired in the following order. First the BeforeUpdateRelationship should be + /// for owner1, then the BeforeImplicitUpdateRelationship hook should be fired for owner2, and lastly the BeforeImplicitUpdateRelationship for article2. + /// private void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, NodeLayer layer) { foreach (IResourceNode node in layer) { - var nestedHookContainer = _executorHelper.GetResourceHookContainer(node.ResourceType, ResourceHook.BeforeUpdateRelationship); + IResourceHookContainer nestedHookContainer = _executorHelper.GetResourceHookContainer(node.ResourceType, ResourceHook.BeforeUpdateRelationship); IEnumerable uniqueResources = node.UniqueResources; RightType resourceType = node.ResourceType; Dictionary currentResourcesGrouped; @@ -304,8 +312,8 @@ private void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, NodeLayer la { if (uniqueResources.Cast().Any()) { - var relationships = node.RelationshipsToNextLayer.Select(p => p.Attribute).ToArray(); - var dbValues = LoadDbValues(resourceType, uniqueResources, ResourceHook.BeforeUpdateRelationship, relationships); + RelationshipAttribute[] relationships = node.RelationshipsToNextLayer.Select(p => p.Attribute).ToArray(); + IEnumerable dbValues = LoadDbValues(resourceType, uniqueResources, ResourceHook.BeforeUpdateRelationship, relationships); // these are the resources of the current node grouped by // RelationshipAttributes that occurred in the previous layer @@ -317,9 +325,12 @@ private void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, NodeLayer la currentResourcesGrouped = node.RelationshipsFromPreviousLayer.GetRightResources(); currentResourcesGroupedInverse = ReplaceKeysWithInverseRelationships(currentResourcesGrouped); - var resourcesByRelationship = CreateRelationshipHelper(resourceType, currentResourcesGroupedInverse, dbValues); - var allowedIds = CallHook(nestedHookContainer, ResourceHook.BeforeUpdateRelationship, ArrayFactory.Create(GetIds(uniqueResources), resourcesByRelationship, pipeline)).Cast(); - var updated = GetAllowedResources(uniqueResources, allowedIds); + IRelationshipsDictionary resourcesByRelationship = CreateRelationshipHelper(resourceType, currentResourcesGroupedInverse, dbValues); + + IEnumerable allowedIds = CallHook(nestedHookContainer, ResourceHook.BeforeUpdateRelationship, + ArrayFactory.Create(GetIds(uniqueResources), resourcesByRelationship, pipeline)).Cast(); + + HashSet updated = GetAllowedResources(uniqueResources, allowedIds); node.UpdateUnique(updated); node.Reassign(); } @@ -334,7 +345,8 @@ private void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, NodeLayer la // For this, we need to query the database for the HasOneAttribute:owner // relationship of article1, which is referred to as the // left side of the HasOneAttribute:owner relationship. - var leftResources = node.RelationshipsFromPreviousLayer.GetLeftResources(); + Dictionary leftResources = node.RelationshipsFromPreviousLayer.GetLeftResources(); + if (leftResources.Any()) { // owner_old is loaded, which is an "implicitly affected resource" @@ -346,6 +358,7 @@ private void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, NodeLayer la // For this, we need to query the database for the current owner // relationship value of owner_new. currentResourcesGrouped = node.RelationshipsFromPreviousLayer.GetRightResources(); + if (currentResourcesGrouped.Any()) { // rightResources is grouped by relationships from previous @@ -365,49 +378,61 @@ private void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, NodeLayer la } /// - /// replaces the keys of the dictionary - /// with its inverse relationship attribute. + /// replaces the keys of the dictionary with its inverse relationship attribute. /// - /// Resources grouped by relationship attribute - private Dictionary ReplaceKeysWithInverseRelationships(Dictionary resourcesByRelationship) + /// + /// Resources grouped by relationship attribute + /// + private Dictionary ReplaceKeysWithInverseRelationships( + Dictionary resourcesByRelationship) { // when Article has one Owner (HasOneAttribute:owner) is set, there is no guarantee // that the inverse attribute was also set (Owner has one Article: HasOneAttr:article). // If it isn't, JsonApiDotNetCore currently knows nothing about this relationship pointing back, and it // currently cannot fire hooks for resources resolved through inverse relationships. - var inversableRelationshipAttributes = resourcesByRelationship.Where(kvp => kvp.Key.InverseNavigationProperty != null); + IEnumerable> inversableRelationshipAttributes = + resourcesByRelationship.Where(kvp => kvp.Key.InverseNavigationProperty != null); + return inversableRelationshipAttributes.ToDictionary(kvp => _resourceGraph.GetInverseRelationship(kvp.Key), kvp => kvp.Value); } /// - /// Given a source of resources, gets the implicitly affected resources - /// from the database and calls the BeforeImplicitUpdateRelationship hook. + /// Given a source of resources, gets the implicitly affected resources from the database and calls the BeforeImplicitUpdateRelationship hook. /// - private void FireForAffectedImplicits(Type resourceTypeToInclude, Dictionary implicitsTarget, ResourcePipeline pipeline, IEnumerable existingImplicitResources = null) + private void FireForAffectedImplicits(Type resourceTypeToInclude, Dictionary implicitsTarget, + ResourcePipeline pipeline, IEnumerable existingImplicitResources = null) { - var container = _executorHelper.GetResourceHookContainer(resourceTypeToInclude, ResourceHook.BeforeImplicitUpdateRelationship); + IResourceHookContainer container = _executorHelper.GetResourceHookContainer(resourceTypeToInclude, ResourceHook.BeforeImplicitUpdateRelationship); + if (container == null) { return; } - var implicitAffected = _executorHelper.LoadImplicitlyAffected(implicitsTarget, existingImplicitResources); + Dictionary + implicitAffected = _executorHelper.LoadImplicitlyAffected(implicitsTarget, existingImplicitResources); + if (!implicitAffected.Any()) { return; } - var inverse = implicitAffected.ToDictionary(kvp => _resourceGraph.GetInverseRelationship(kvp.Key), kvp => kvp.Value); - var resourcesByRelationship = CreateRelationshipHelper(resourceTypeToInclude, inverse); + Dictionary inverse = + implicitAffected.ToDictionary(kvp => _resourceGraph.GetInverseRelationship(kvp.Key), kvp => kvp.Value); + + IRelationshipsDictionary resourcesByRelationship = CreateRelationshipHelper(resourceTypeToInclude, inverse); CallHook(container, ResourceHook.BeforeImplicitUpdateRelationship, ArrayFactory.Create(resourcesByRelationship, pipeline)); } /// - /// checks that the collection does not contain more than one item when - /// relevant (eg AfterRead from GetSingle pipeline). + /// checks that the collection does not contain more than one item when relevant (eg AfterRead from GetSingle pipeline). /// - /// The collection returned from the hook - /// The pipeline from which the hook was fired + /// + /// The collection returned from the hook + /// + /// + /// The pipeline from which the hook was fired + /// [AssertionMethod] private void ValidateHookResponse(IEnumerable returnedList, ResourcePipeline pipeline = 0) { @@ -419,11 +444,11 @@ private void ValidateHookResponse(IEnumerable returnedList, ResourcePipeli } /// - /// A helper method to call a hook on reflectively. + /// A helper method to call a hook on reflectively. /// private IEnumerable CallHook(IResourceHookContainer container, ResourceHook hook, object[] arguments) { - var method = container.GetType().GetMethod(hook.ToString("G")); + MethodInfo method = container.GetType().GetMethod(hook.ToString("G")); // note that some of the hooks return "void". When these hooks, the // are called reflectively with Invoke like here, the return value // is just null, so we don't have to worry about casting issues here. @@ -431,7 +456,7 @@ private IEnumerable CallHook(IResourceHookContainer container, ResourceHook hook } /// - /// If the method, unwrap and throw the actual exception. + /// If the method, unwrap and throw the actual exception. /// private object ThrowJsonApiExceptionOnError(Func action) { @@ -446,42 +471,46 @@ private object ThrowJsonApiExceptionOnError(Func action) } /// - /// Helper method to instantiate AffectedRelationships for a given - /// If are included, the values of the entries in need to be replaced with these values. + /// Helper method to instantiate AffectedRelationships for a given If are included, the + /// values of the entries in need to be replaced with these values. /// - /// The relationship helper. - private IRelationshipsDictionary CreateRelationshipHelper(RightType resourceType, Dictionary prevLayerRelationships, IEnumerable dbValues = null) + /// + /// The relationship helper. + /// + private IRelationshipsDictionary CreateRelationshipHelper(RightType resourceType, Dictionary prevLayerRelationships, + IEnumerable dbValues = null) { - var prevLayerRelationshipsWithDbValues = prevLayerRelationships; + Dictionary prevLayerRelationshipsWithDbValues = prevLayerRelationships; if (dbValues != null) { prevLayerRelationshipsWithDbValues = ReplaceWithDbValues(prevLayerRelationshipsWithDbValues, dbValues.Cast()); } - return (IRelationshipsDictionary)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipsDictionary<>), resourceType, true, prevLayerRelationshipsWithDbValues); + return (IRelationshipsDictionary)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipsDictionary<>), resourceType, true, + prevLayerRelationshipsWithDbValues); } /// - /// Replaces the resources in the values of the prevLayerRelationships dictionary - /// with the corresponding resources loaded from the db. + /// Replaces the resources in the values of the prevLayerRelationships dictionary with the corresponding resources loaded from the db. /// - private Dictionary ReplaceWithDbValues(Dictionary prevLayerRelationships, IEnumerable dbValues) + private Dictionary ReplaceWithDbValues(Dictionary prevLayerRelationships, + IEnumerable dbValues) { - foreach (var key in prevLayerRelationships.Keys.ToList()) + foreach (RelationshipAttribute key in prevLayerRelationships.Keys.ToList()) { - var source = prevLayerRelationships[key].Cast().Select(resource => + IEnumerable source = prevLayerRelationships[key].Cast().Select(resource => dbValues.Single(dbResource => dbResource.StringId == resource.StringId)); - var replaced = TypeHelper.CopyToList(source, key.LeftType); + IList replaced = TypeHelper.CopyToList(source, key.LeftType); prevLayerRelationships[key] = TypeHelper.CreateHashSetFor(key.LeftType, replaced); } + return prevLayerRelationships; } /// - /// Filter the source set by removing the resources with ID that are not - /// in . + /// Filter the source set by removing the resources with ID that are not in . /// private HashSet GetAllowedResources(IEnumerable source, IEnumerable allowedIds) { @@ -489,16 +518,26 @@ private HashSet GetAllowedResources(IEnumerable source, IEnumerab } /// - /// given the set of , it will load all the - /// values from the database of these resources. + /// given the set of , it will load all the values from the database of these resources. /// - /// The db values. - /// type of the resources to be loaded - /// The set of resources to load the db values for - /// The hook in which the db values will be displayed. - /// Relationships from to the next layer: - /// this indicates which relationships will be included on . - private IEnumerable LoadDbValues(Type resourceType, IEnumerable uniqueResources, ResourceHook targetHook, RelationshipAttribute[] relationshipsToNextLayer) + /// + /// The db values. + /// + /// + /// type of the resources to be loaded + /// + /// + /// The set of resources to load the db values for + /// + /// + /// The hook in which the db values will be displayed. + /// + /// + /// Relationships from to the next layer: this indicates which relationships will be included on + /// . + /// + private IEnumerable LoadDbValues(Type resourceType, IEnumerable uniqueResources, ResourceHook targetHook, + RelationshipAttribute[] relationshipsToNextLayer) { // We only need to load database values if the target hook of this hook execution // cycle is compatible with displaying database values and has this option enabled. @@ -515,21 +554,25 @@ private IEnumerable LoadDbValues(Type resourceType, IEnumerable uniqueResources, /// private void FireAfterUpdateRelationship(IResourceHookContainer container, IResourceNode node, ResourcePipeline pipeline) { - Dictionary currentResourcesGrouped = node.RelationshipsFromPreviousLayer.GetRightResources(); + // the relationships attributes in currentResourcesGrouped will be pointing from a // resource in the previous layer to a resource in the current (nested) layer. // For the nested hook we need to replace these attributes with their inverse. // See the FireNestedBeforeUpdateHooks method for a more detailed example. - var resourcesByRelationship = CreateRelationshipHelper(node.ResourceType, ReplaceKeysWithInverseRelationships(currentResourcesGrouped)); + IRelationshipsDictionary resourcesByRelationship = + CreateRelationshipHelper(node.ResourceType, ReplaceKeysWithInverseRelationships(currentResourcesGrouped)); + CallHook(container, ResourceHook.AfterUpdateRelationship, ArrayFactory.Create(resourcesByRelationship, pipeline)); } /// - /// Returns a list of StringIds from a list of IIdentifiable resources (). + /// Returns a list of StringIds from a list of IIdentifiable resources (). /// /// The ids. - /// IIdentifiable resources. + /// + /// IIdentifiable resources. + /// private HashSet GetIds(IEnumerable resources) { return new HashSet(resources.Cast().Select(e => e.StringId)); diff --git a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutorFacade.cs b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutorFacade.cs index 136666500f..21c03ab33f 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutorFacade.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/ResourceHookExecutorFacade.cs @@ -8,8 +8,8 @@ namespace JsonApiDotNetCore.Hooks.Internal { /// - /// Facade for hooks that invokes callbacks on , - /// which is used when is true. + /// Facade for hooks that invokes callbacks on , which is used when + /// is true. /// internal sealed class ResourceHookExecutorFacade : IResourceHookExecutorFacade { @@ -128,7 +128,7 @@ public object OnReturnRelationship(object resourceOrResources) if (resourceOrResources is IIdentifiable) { - var resources = ObjectExtensions.AsList((dynamic)resourceOrResources); + dynamic resources = ObjectExtensions.AsList((dynamic)resourceOrResources); return Enumerable.SingleOrDefault(_resourceHookExecutor.OnReturn(resources, ResourcePipeline.GetRelationship)); } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs index 8e0ee92706..7d9bbbe8a2 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ChildNode.cs @@ -10,7 +10,8 @@ namespace JsonApiDotNetCore.Hooks.Internal.Traversal /// Child node in the tree /// /// - internal sealed class ChildNode : IResourceNode where TResource : class, IIdentifiable + internal sealed class ChildNode : IResourceNode + where TResource : class, IIdentifiable { private readonly IdentifiableComparer _comparer = IdentifiableComparer.Instance; private readonly RelationshipsFromPreviousLayer _relationshipsFromPreviousLayer; @@ -44,7 +45,8 @@ public ChildNode(RelationshipProxy[] nextLayerRelationships, RelationshipsFromPr public void UpdateUnique(IEnumerable updated) { List cast = updated.Cast().ToList(); - foreach (var group in _relationshipsFromPreviousLayer) + + foreach (RelationshipGroup group in _relationshipsFromPreviousLayer) { group.RightResources = new HashSet(group.RightResources.Intersect(cast, _comparer).Cast()); } @@ -56,10 +58,11 @@ public void UpdateUnique(IEnumerable updated) public void Reassign(IEnumerable updated = null) { var unique = (HashSet)UniqueResources; - foreach (var group in _relationshipsFromPreviousLayer) + + foreach (RelationshipGroup group in _relationshipsFromPreviousLayer) { - var proxy = group.Proxy; - var leftResources = group.LeftResources; + RelationshipProxy proxy = group.Proxy; + HashSet leftResources = group.LeftResources; Reassign(leftResources, proxy, unique); } @@ -69,18 +72,20 @@ private void Reassign(IEnumerable leftResources, RelationshipProx { foreach (IIdentifiable left in leftResources) { - var currentValue = proxy.GetValue(left); + object currentValue = proxy.GetValue(left); if (currentValue is IEnumerable relationshipCollection) { - var intersection = relationshipCollection.Intersect(unique, _comparer); - IEnumerable typedCollection = - TypeHelper.CopyToTypedCollection(intersection, relationshipCollection.GetType()); + IEnumerable intersection = relationshipCollection.Intersect(unique, _comparer); + IEnumerable typedCollection = TypeHelper.CopyToTypedCollection(intersection, relationshipCollection.GetType()); proxy.SetValue(left, typedCollection); } else if (currentValue is IIdentifiable relationshipSingle) { - if (!unique.Intersect(new HashSet {relationshipSingle}, _comparer).Any()) + if (!unique.Intersect(new HashSet + { + relationshipSingle + }, _comparer).Any()) { proxy.SetValue(left, null); } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipGroup.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipGroup.cs index 42b20ced84..54d93c7905 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipGroup.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipGroup.cs @@ -8,4 +8,4 @@ internal interface IRelationshipGroup RelationshipProxy Proxy { get; } HashSet LeftResources { get; } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipsFromPreviousLayer.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipsFromPreviousLayer.cs index ed7b6fdf00..98886e1b10 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipsFromPreviousLayer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IRelationshipsFromPreviousLayer.cs @@ -12,12 +12,17 @@ internal interface IRelationshipsFromPreviousLayer /// /// Grouped by relationship to the previous layer, gets all the resources of the current layer /// - /// The right side resources. + /// + /// The right side resources. + /// Dictionary GetRightResources(); + /// /// Grouped by relationship to the previous layer, gets all the resources of the previous layer /// - /// The right side resources. + /// + /// The right side resources. + /// Dictionary GetLeftResources(); } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IResourceNode.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IResourceNode.cs index de364e8ccc..dcd350666c 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IResourceNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/IResourceNode.cs @@ -12,28 +12,33 @@ internal interface IResourceNode /// Each node represents the resources of a given type throughout a particular layer. /// RightType ResourceType { get; } + /// /// The unique set of resources in this node. Note that these are all of the same type. /// IEnumerable UniqueResources { get; } + /// /// Relationships to the next layer /// - /// The relationships to next layer. + /// + /// The relationships to next layer. + /// RelationshipProxy[] RelationshipsToNextLayer { get; } + /// /// Relationships to the previous layer /// IRelationshipsFromPreviousLayer RelationshipsFromPreviousLayer { get; } /// - /// A helper method to assign relationships to the previous layer after firing hooks. - /// Or, in case of the root node, to update the original source enumerable. + /// A helper method to assign relationships to the previous layer after firing hooks. Or, in case of the root node, to update the original source + /// enumerable. /// void Reassign(IEnumerable source = null); + /// - /// A helper method to internally update the unique set of resources as a result of - /// a filter action in a hook. + /// A helper method to internally update the unique set of resources as a result of a filter action in a hook. /// /// Updated. void UpdateUnique(IEnumerable updated); diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ITraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ITraversalHelper.cs index 4ca6ddfac0..a4db2e83f3 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ITraversalHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/ITraversalHelper.cs @@ -9,18 +9,26 @@ internal interface ITraversalHelper /// Crates the next layer /// NodeLayer CreateNextLayer(IResourceNode node); + /// /// Creates the next layer based on the nodes provided /// NodeLayer CreateNextLayer(IEnumerable nodes); + /// - /// Creates a root node for breadth-first-traversal (BFS). Note that typically, in - /// JsonApiDotNetCore, the root layer will be homogeneous. Also, because it is the first layer, - /// there can be no relationships to previous layers, only to next layers. + /// Creates a root node for breadth-first-traversal (BFS). Note that typically, in JsonApiDotNetCore, the root layer will be homogeneous. Also, because + /// it is the first layer, there can be no relationships to previous layers, only to next layers. /// - /// The root node. - /// Root resources. - /// The 1st type parameter. - RootNode CreateRootNode(IEnumerable rootResources) where TResource : class, IIdentifiable; + /// + /// The root node. + /// + /// + /// Root resources. + /// + /// + /// The 1st type parameter. + /// + RootNode CreateRootNode(IEnumerable rootResources) + where TResource : class, IIdentifiable; } } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/NodeLayer.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/NodeLayer.cs index 711ddd28fb..bad0938a20 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/NodeLayer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/NodeLayer.cs @@ -6,21 +6,21 @@ namespace JsonApiDotNetCore.Hooks.Internal.Traversal { /// - /// A helper class that represents all resources in the current layer that - /// are being traversed for which hooks will be executed (see IResourceHookExecutor) + /// A helper class that represents all resources in the current layer that are being traversed for which hooks will be executed (see + /// IResourceHookExecutor) /// internal sealed class NodeLayer : IEnumerable { private readonly List _collection; - public bool AnyResources() + public NodeLayer(List nodes) { - return _collection.Any(n => n.UniqueResources.Cast().Any()); + _collection = nodes; } - public NodeLayer(List nodes) + public bool AnyResources() { - _collection = nodes; + return _collection.Any(n => n.UniqueResources.Cast().Any()); } public IEnumerator GetEnumerator() @@ -33,4 +33,4 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipGroup.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipGroup.cs index 12ac8382cd..69a947f9f9 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipGroup.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipGroup.cs @@ -3,11 +3,13 @@ namespace JsonApiDotNetCore.Hooks.Internal.Traversal { - internal sealed class RelationshipGroup : IRelationshipGroup where TRight : class, IIdentifiable + internal sealed class RelationshipGroup : IRelationshipGroup + where TRight : class, IIdentifiable { public RelationshipProxy Proxy { get; } public HashSet LeftResources { get; } public HashSet RightResources { get; internal set; } + public RelationshipGroup(RelationshipProxy proxy, HashSet leftResources, HashSet rightResources) { Proxy = proxy; diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs index 86677d1907..1a95426557 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipProxy.cs @@ -7,15 +7,10 @@ namespace JsonApiDotNetCore.Hooks.Internal.Traversal { /// - /// A class used internally for resource hook execution. Not intended for developer use. - /// - /// A wrapper for RelationshipAttribute with an abstraction layer that works on the - /// getters and setters of relationships. These are different in the case of - /// HasMany vs HasManyThrough, and HasManyThrough. - /// It also depends on if the through type (eg ArticleTags) - /// is identifiable (in which case we will traverse through - /// it and fire hooks for it, if defined) or not (in which case we skip - /// ArticleTags and go directly to Tags. + /// A class used internally for resource hook execution. Not intended for developer use. A wrapper for RelationshipAttribute with an abstraction layer + /// that works on the getters and setters of relationships. These are different in the case of HasMany vs HasManyThrough, and HasManyThrough. It also + /// depends on if the through type (eg ArticleTags) is identifiable (in which case we will traverse through it and fire hooks for it, if defined) or not + /// (in which case we skip ArticleTags and go directly to Tags. /// internal sealed class RelationshipProxy { @@ -24,10 +19,8 @@ internal sealed class RelationshipProxy public Type LeftType => Attribute.LeftType; /// - /// The target type for this relationship attribute. - /// For HasOne has HasMany this is trivial: just the right-hand side. - /// For HasManyThrough it is either the ThroughProperty (when the through resource is - /// Identifiable) or it is the right-hand side (when the through resource is not identifiable) + /// The target type for this relationship attribute. For HasOne has HasMany this is trivial: just the right-hand side. For HasManyThrough it is either + /// the ThroughProperty (when the through resource is Identifiable) or it is the right-hand side (when the through resource is not identifiable) /// public Type RightType { get; } @@ -40,6 +33,7 @@ public RelationshipProxy(RelationshipAttribute attr, Type relatedType, bool isCo RightType = relatedType; Attribute = attr; IsContextRelation = isContextRelation; + if (attr is HasManyThroughAttribute throughAttr) { _skipThroughType |= RightType != throughAttr.ThroughType; @@ -47,12 +41,15 @@ public RelationshipProxy(RelationshipAttribute attr, Type relatedType, bool isCo } /// - /// Gets the relationship value for a given parent resource. - /// Internally knows how to do this depending on the type of RelationshipAttribute - /// that this RelationshipProxy encapsulates. + /// Gets the relationship value for a given parent resource. Internally knows how to do this depending on the type of RelationshipAttribute that this + /// RelationshipProxy encapsulates. /// - /// The relationship value. - /// Parent resource. + /// + /// The relationship value. + /// + /// + /// Parent resource. + /// public object GetValue(IIdentifiable resource) { if (Attribute is HasManyThroughAttribute hasManyThrough) @@ -61,16 +58,19 @@ public object GetValue(IIdentifiable resource) { return hasManyThrough.ThroughProperty.GetValue(resource); } + var collection = new List(); var throughResources = (IEnumerable)hasManyThrough.ThroughProperty.GetValue(resource); + if (throughResources == null) { return null; } - foreach (var throughResource in throughResources) + foreach (object throughResource in throughResources) { var rightResource = (IIdentifiable)hasManyThrough.RightProperty.GetValue(throughResource); + if (rightResource == null) { continue; @@ -81,16 +81,20 @@ public object GetValue(IIdentifiable resource) return collection; } + return Attribute.GetValue(resource); } /// - /// Set the relationship value for a given parent resource. - /// Internally knows how to do this depending on the type of RelationshipAttribute - /// that this RelationshipProxy encapsulates. + /// Set the relationship value for a given parent resource. Internally knows how to do this depending on the type of RelationshipAttribute that this + /// RelationshipProxy encapsulates. /// - /// Parent resource. - /// The relationship value. + /// + /// Parent resource. + /// + /// + /// The relationship value. + /// public void SetValue(IIdentifiable resource, object value) { if (Attribute is HasManyThroughAttribute hasManyThrough) @@ -104,8 +108,9 @@ public void SetValue(IIdentifiable resource, object value) var throughResources = (IEnumerable)hasManyThrough.ThroughProperty.GetValue(resource); var filteredList = new List(); - var rightResources = TypeHelper.CopyToList((IEnumerable)value, RightType); - foreach (var throughResource in throughResources ?? Array.Empty()) + IList rightResources = TypeHelper.CopyToList((IEnumerable)value, RightType); + + foreach (object throughResource in throughResources ?? Array.Empty()) { if (rightResources.Contains(hasManyThrough.RightProperty.GetValue(throughResource))) { @@ -113,7 +118,7 @@ public void SetValue(IIdentifiable resource, object value) } } - var collectionValue = TypeHelper.CopyToTypedCollection(filteredList, hasManyThrough.ThroughProperty.PropertyType); + IEnumerable collectionValue = TypeHelper.CopyToTypedCollection(filteredList, hasManyThrough.ThroughProperty.PropertyType); hasManyThrough.ThroughProperty.SetValue(resource, collectionValue); return; } diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipsFromPreviousLayer.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipsFromPreviousLayer.cs index d5bd5e416a..add2107d7e 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipsFromPreviousLayer.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RelationshipsFromPreviousLayer.cs @@ -6,7 +6,8 @@ namespace JsonApiDotNetCore.Hooks.Internal.Traversal { - internal sealed class RelationshipsFromPreviousLayer : IRelationshipsFromPreviousLayer, IEnumerable> where TRightResource : class, IIdentifiable + internal sealed class RelationshipsFromPreviousLayer : IRelationshipsFromPreviousLayer, IEnumerable> + where TRightResource : class, IIdentifiable { private readonly IEnumerable> _collection; diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs index 166b98f0a2..14b6296f6f 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/RootNode.cs @@ -8,10 +8,10 @@ namespace JsonApiDotNetCore.Hooks.Internal.Traversal { /// - /// The root node class of the breadth-first-traversal of resource data structures - /// as performed by the + /// The root node class of the breadth-first-traversal of resource data structures as performed by the /// - internal sealed class RootNode : IResourceNode where TResource : class, IIdentifiable + internal sealed class RootNode : IResourceNode + where TResource : class, IIdentifiable { private readonly IdentifiableComparer _comparer = IdentifiableComparer.Instance; private readonly RelationshipProxy[] _allRelationshipsToNextLayer; @@ -20,21 +20,6 @@ internal sealed class RootNode : IResourceNode where TResource : clas public IEnumerable UniqueResources => _uniqueResources; public RelationshipProxy[] RelationshipsToNextLayer { get; } - public Dictionary> LeftsToNextLayerByRelationships() - { - return _allRelationshipsToNextLayer - .GroupBy(proxy => proxy.RightType) - .ToDictionary(gdc => gdc.Key, gdc => gdc.ToDictionary(p => p.Attribute, _ => UniqueResources)); - } - - /// - /// The current layer resources grouped by affected relationship to the next layer - /// - public Dictionary LeftsToNextLayer() - { - return RelationshipsToNextLayer.ToDictionary(p => p.Attribute, _ => UniqueResources); - } - /// /// The root node does not have a parent layer and therefore does not have any relationships to any previous layer /// @@ -48,20 +33,34 @@ public RootNode(IEnumerable uniqueResources, RelationshipProxy[] popu _allRelationshipsToNextLayer = allRelationships; } + public Dictionary> LeftsToNextLayerByRelationships() + { + return _allRelationshipsToNextLayer.GroupBy(proxy => proxy.RightType) + .ToDictionary(gdc => gdc.Key, gdc => gdc.ToDictionary(p => p.Attribute, _ => UniqueResources)); + } + + /// + /// The current layer resources grouped by affected relationship to the next layer + /// + public Dictionary LeftsToNextLayer() + { + return RelationshipsToNextLayer.ToDictionary(p => p.Attribute, _ => UniqueResources); + } + /// - /// Update the internal list of affected resources. + /// Update the internal list of affected resources. /// /// Updated. public void UpdateUnique(IEnumerable updated) { - var cast = updated.Cast().ToList(); - var intersected = _uniqueResources.Intersect(cast, _comparer).Cast(); + List cast = updated.Cast().ToList(); + IEnumerable intersected = _uniqueResources.Intersect(cast, _comparer).Cast(); _uniqueResources = new HashSet(intersected); } public void Reassign(IEnumerable source = null) { - var ids = _uniqueResources.Select(ue => ue.StringId); + IEnumerable ids = _uniqueResources.Select(ue => ue.StringId); if (source is HashSet hashSet) { diff --git a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/TraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/TraversalHelper.cs index b27c0762ea..5e6451b9c1 100644 --- a/src/JsonApiDotNetCore/Hooks/Internal/Traversal/TraversalHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Internal/Traversal/TraversalHelper.cs @@ -1,4 +1,3 @@ -using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -12,59 +11,68 @@ namespace JsonApiDotNetCore.Hooks.Internal.Traversal { /// - /// A helper class used by the to traverse through - /// resource data structures (trees), allowing for a breadth-first-traversal - /// - /// It creates nodes for each layer. - /// Typically, the first layer is homogeneous (all resources have the same type), - /// and further nodes can be mixed. + /// A helper class used by the to traverse through resource data structures (trees), allowing for a + /// breadth-first-traversal It creates nodes for each layer. Typically, the first layer is homogeneous (all resources have the same type), and further + /// nodes can be mixed. /// internal sealed class TraversalHelper : ITraversalHelper { private readonly IdentifiableComparer _comparer = IdentifiableComparer.Instance; private readonly IResourceGraph _resourceGraph; private readonly ITargetedFields _targetedFields; + /// - /// Keeps track of which resources has already been traversed through, to prevent - /// infinite loops in eg cyclic data structures. + /// A mapper from to . See the latter for more details. /// - private Dictionary> _processedResources; + private readonly Dictionary _relationshipProxies = new Dictionary(); + /// - /// A mapper from to . - /// See the latter for more details. + /// Keeps track of which resources has already been traversed through, to prevent infinite loops in eg cyclic data structures. /// - private readonly Dictionary _relationshipProxies = new Dictionary(); - public TraversalHelper( - IResourceGraph resourceGraph, - ITargetedFields targetedFields) + private Dictionary> _processedResources; + + public TraversalHelper(IResourceGraph resourceGraph, ITargetedFields targetedFields) { _targetedFields = targetedFields; _resourceGraph = resourceGraph; } /// - /// Creates a root node for breadth-first-traversal. Note that typically, in - /// JsonApiDotNetCore, the root layer will be homogeneous. Also, because it is the first layer, - /// there can be no relationships to previous layers, only to next layers. + /// Creates a root node for breadth-first-traversal. Note that typically, in JsonApiDotNetCore, the root layer will be homogeneous. Also, because it is + /// the first layer, there can be no relationships to previous layers, only to next layers. /// - /// The root node. - /// Root resources. - /// The 1st type parameter. - public RootNode CreateRootNode(IEnumerable rootResources) where TResource : class, IIdentifiable + /// + /// The root node. + /// + /// + /// Root resources. + /// + /// + /// The 1st type parameter. + /// + public RootNode CreateRootNode(IEnumerable rootResources) + where TResource : class, IIdentifiable { _processedResources = new Dictionary>(); RegisterRelationshipProxies(typeof(TResource)); - var uniqueResources = ProcessResources(rootResources); - var populatedRelationshipsToNextLayer = GetPopulatedRelationships(typeof(TResource), uniqueResources); - var allRelationshipsFromType = _relationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.LeftType == typeof(TResource)).ToArray(); + HashSet uniqueResources = ProcessResources(rootResources); + RelationshipProxy[] populatedRelationshipsToNextLayer = GetPopulatedRelationships(typeof(TResource), uniqueResources); + + RelationshipProxy[] allRelationshipsFromType = + _relationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.LeftType == typeof(TResource)).ToArray(); + return new RootNode(uniqueResources, populatedRelationshipsToNextLayer, allRelationshipsFromType); } /// /// Create the first layer after the root layer (based on the root node) /// - /// The next layer. - /// Root node. + /// + /// The next layer. + /// + /// + /// Root node. + /// public NodeLayer CreateNextLayer(IResourceNode rootNode) { return CreateNextLayer(rootNode.AsEnumerable()); @@ -73,29 +81,32 @@ public NodeLayer CreateNextLayer(IResourceNode rootNode) /// /// Create a next layer from any previous layer /// - /// The next layer. + /// + /// The next layer. + /// /// Nodes. public NodeLayer CreateNextLayer(IEnumerable nodes) { // first extract resources by parsing populated relationships in the resources // of previous layer - var (lefts, rights) = ExtractResources(nodes); + (Dictionary> lefts, Dictionary> rights) = ExtractResources(nodes); // group them conveniently so we can make ChildNodes of them: // there might be several relationship attributes in rights dictionary // that point to the same right type. - var leftsGrouped = GroupByRightTypeOfRelationship(lefts); + Dictionary>>> leftsGrouped = GroupByRightTypeOfRelationship(lefts); // convert the groups into child nodes - var nextNodes = leftsGrouped.Select(entry => + List nextNodes = leftsGrouped.Select(entry => { - var nextNodeType = entry.Key; + RightType nextNodeType = entry.Key; RegisterRelationshipProxies(nextNodeType); var populatedRelationships = new List(); - var relationshipsToPreviousLayer = entry.Value.Select(grouped => + + List relationshipsToPreviousLayer = entry.Value.Select(grouped => { - var proxy = grouped.Key; + RelationshipProxy proxy = grouped.Key; populatedRelationships.AddRange(GetPopulatedRelationships(nextNodeType, rights[proxy])); return CreateRelationshipGroupInstance(nextNodeType, proxy, grouped.Value, rights[proxy]); }).ToList(); @@ -107,19 +118,20 @@ public NodeLayer CreateNextLayer(IEnumerable nodes) } /// - /// iterates through the dictionary and groups the values - /// by matching right type of the keys (which are relationship attributes) + /// iterates through the dictionary and groups the values by matching right type of the keys (which are relationship + /// attributes) /// - private Dictionary>>> GroupByRightTypeOfRelationship(Dictionary> relationships) + private Dictionary>>> GroupByRightTypeOfRelationship( + Dictionary> relationships) { return relationships.GroupBy(kvp => kvp.Key.RightType).ToDictionary(gdc => gdc.Key, gdc => gdc.ToList()); } /// - /// Extracts the resources for the current layer by going through all populated relationships - /// of the (left resources of the previous layer. + /// Extracts the resources for the current layer by going through all populated relationships of the (left resources of the previous layer. /// - private (Dictionary>, Dictionary>) ExtractResources(IEnumerable leftNodes) + private (Dictionary>, Dictionary>) ExtractResources( + IEnumerable leftNodes) { // RelationshipAttr_prevLayer->currentLayer => prevLayerResources var leftResourcesGrouped = new Dictionary>(); @@ -127,27 +139,28 @@ private DictionarycurrentLayer => currentLayerResources var rightResourcesGrouped = new Dictionary>(); - foreach (var node in leftNodes) + foreach (IResourceNode node in leftNodes) { - var leftResources = node.UniqueResources; - var relationships = node.RelationshipsToNextLayer; + IEnumerable leftResources = node.UniqueResources; + RelationshipProxy[] relationships = node.RelationshipsToNextLayer; ExtractLeftResources(leftResources, relationships, rightResourcesGrouped, leftResourcesGrouped); } - var processResourcesMethod = GetType().GetMethod(nameof(ProcessResources), BindingFlags.NonPublic | BindingFlags.Instance); - foreach (var kvp in rightResourcesGrouped) + MethodInfo processResourcesMethod = GetType().GetMethod(nameof(ProcessResources), BindingFlags.NonPublic | BindingFlags.Instance); + + foreach (KeyValuePair> kvp in rightResourcesGrouped) { - var type = kvp.Key.RightType; - var list = TypeHelper.CopyToList(kvp.Value, type); + RightType type = kvp.Key.RightType; + IList list = TypeHelper.CopyToList(kvp.Value, type); processResourcesMethod!.MakeGenericMethod(type).Invoke(this, ArrayFactory.Create(list)); } return (leftResourcesGrouped, rightResourcesGrouped); } - private void ExtractLeftResources(IEnumerable leftResources, RelationshipProxy[] relationships, Dictionary> rightResourcesGrouped, - Dictionary> leftResourcesGrouped) + private void ExtractLeftResources(IEnumerable leftResources, RelationshipProxy[] relationships, + Dictionary> rightResourcesGrouped, Dictionary> leftResourcesGrouped) { foreach (IIdentifiable leftResource in leftResources) { @@ -156,12 +169,12 @@ private void ExtractLeftResources(IEnumerable leftResources, RelationshipProxy[] } private void ExtractLeftResource(IIdentifiable leftResource, RelationshipProxy[] relationships, - Dictionary> rightResourcesGrouped, - Dictionary> leftResourcesGrouped) + Dictionary> rightResourcesGrouped, Dictionary> leftResourcesGrouped) { - foreach (var proxy in relationships) + foreach (RelationshipProxy proxy in relationships) { - var relationshipValue = proxy.GetValue(leftResource); + object relationshipValue = proxy.GetValue(leftResource); + // skip this relationship if it's not populated if (!proxy.IsContextRelation && relationshipValue == null) { @@ -172,7 +185,8 @@ private void ExtractLeftResource(IIdentifiable leftResource, RelationshipProxy[] { // in the case of a to-one relationship, the assigned value // will not be a list. We therefore first wrap it in a list. - var list = TypeHelper.CreateListFor(proxy.RightType); + IList list = TypeHelper.CreateListFor(proxy.RightType); + if (relationshipValue != null) { list.Add(relationshipValue); @@ -181,7 +195,8 @@ private void ExtractLeftResource(IIdentifiable leftResource, RelationshipProxy[] rightResources = list; } - var uniqueRightResources = UniqueInTree(rightResources.Cast(), proxy.RightType); + HashSet uniqueRightResources = UniqueInTree(rightResources.Cast(), proxy.RightType); + if (proxy.IsContextRelation || uniqueRightResources.Any()) { AddToRelationshipGroup(rightResourcesGrouped, proxy, uniqueRightResources); @@ -191,35 +206,46 @@ private void ExtractLeftResource(IIdentifiable leftResource, RelationshipProxy[] } /// - /// Get all populated relationships known in the current tree traversal from a - /// left type to any right type + /// Get all populated relationships known in the current tree traversal from a left type to any right type /// - /// The relationships. + /// + /// The relationships. + /// private RelationshipProxy[] GetPopulatedRelationships(LeftType leftType, IEnumerable lefts) { - var relationshipsFromLeftToRight = _relationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.LeftType == leftType); + IEnumerable relationshipsFromLeftToRight = + _relationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.LeftType == leftType); + return relationshipsFromLeftToRight.Where(proxy => proxy.IsContextRelation || lefts.Any(p => proxy.GetValue(p) != null)).ToArray(); } /// - /// Registers the resources as "seen" in the tree traversal, extracts any new s from it. + /// Registers the resources as "seen" in the tree traversal, extracts any new s from it. /// - /// The resources. - /// Incoming resources. - /// The 1st type parameter. - private HashSet ProcessResources(IEnumerable incomingResources) where TResource : class, IIdentifiable + /// + /// The resources. + /// + /// + /// Incoming resources. + /// + /// + /// The 1st type parameter. + /// + private HashSet ProcessResources(IEnumerable incomingResources) + where TResource : class, IIdentifiable { - Type type = typeof(TResource); - var newResources = UniqueInTree(incomingResources, type); + RightType type = typeof(TResource); + HashSet newResources = UniqueInTree(incomingResources, type); RegisterProcessedResources(newResources, type); return newResources; } /// - /// Parses all relationships from to - /// other models in the resource resourceGraphs by constructing RelationshipProxies . + /// Parses all relationships from to other models in the resource resourceGraphs by constructing RelationshipProxies . /// - /// The type to parse + /// + /// The type to parse + /// private void RegisterRelationshipProxies(RightType type) { foreach (RelationshipAttribute attr in _resourceGraph.GetRelationships(type)) @@ -233,7 +259,8 @@ private void RegisterRelationshipProxies(RightType type) { RightType rightType = GetRightTypeFromRelationship(attr); bool isContextRelation = false; - var relationshipsToUpdate = _targetedFields.Relationships; + ISet relationshipsToUpdate = _targetedFields.Relationships; + if (relationshipsToUpdate != null) { isContextRelation = relationshipsToUpdate.Contains(attr); @@ -248,94 +275,114 @@ private void RegisterRelationshipProxies(RightType type) /// /// Registers the processed resources in the dictionary grouped by type /// - /// Resources to register - /// Resource type. - private void RegisterProcessedResources(IEnumerable resources, Type resourceType) + /// + /// Resources to register + /// + /// + /// Resource type. + /// + private void RegisterProcessedResources(IEnumerable resources, RightType resourceType) { - var processedResources = GetProcessedResources(resourceType); + HashSet processedResources = GetProcessedResources(resourceType); processedResources.UnionWith(new HashSet(resources)); } /// /// Gets the processed resources for a given type, instantiates the collection if new. /// - /// The processed resources. - /// Resource type. - private HashSet GetProcessedResources(Type resourceType) + /// + /// The processed resources. + /// + /// + /// Resource type. + /// + private HashSet GetProcessedResources(RightType resourceType) { if (!_processedResources.TryGetValue(resourceType, out HashSet processedResources)) { processedResources = new HashSet(); _processedResources[resourceType] = processedResources; } + return processedResources; } /// - /// Using the register of processed resources, determines the unique and new - /// resources with respect to previous iterations. + /// Using the register of processed resources, determines the unique and new resources with respect to previous iterations. /// - /// The in tree. - private HashSet UniqueInTree(IEnumerable resources, Type resourceType) where TResource : class, IIdentifiable + /// + /// The in tree. + /// + private HashSet UniqueInTree(IEnumerable resources, RightType resourceType) + where TResource : class, IIdentifiable { - var newResources = resources.Except(GetProcessedResources(resourceType), _comparer).Cast(); + IEnumerable newResources = resources.Except(GetProcessedResources(resourceType), _comparer).Cast(); return new HashSet(newResources); } /// - /// Gets the type from relationship attribute. If the attribute is - /// HasManyThrough, and the through type is identifiable, then the target - /// type is the through type instead of the right type, because hooks might be - /// implemented for the through resource. + /// Gets the type from relationship attribute. If the attribute is HasManyThrough, and the through type is identifiable, then the target type is the + /// through type instead of the right type, because hooks might be implemented for the through resource. /// - /// The target type for traversal - /// Relationship attribute + /// + /// The target type for traversal + /// + /// + /// Relationship attribute + /// private RightType GetRightTypeFromRelationship(RelationshipAttribute attr) { if (attr is HasManyThroughAttribute throughAttr && TypeHelper.IsOrImplementsInterface(throughAttr.ThroughType, typeof(IIdentifiable))) { return throughAttr.ThroughType; } + return attr.RightType; } - private void AddToRelationshipGroup(Dictionary> target, RelationshipProxy proxy, IEnumerable newResources) + private void AddToRelationshipGroup(Dictionary> target, RelationshipProxy proxy, + IEnumerable newResources) { if (!target.TryGetValue(proxy, out List resources)) { resources = new List(); target[proxy] = resources; } + resources.AddRange(newResources); } /// - /// Reflective helper method to create an instance of ; + /// Reflective helper method to create an instance of ; /// - private IResourceNode CreateNodeInstance(RightType nodeType, RelationshipProxy[] relationshipsToNext, IEnumerable relationshipsFromPrev) + private IResourceNode CreateNodeInstance(RightType nodeType, RelationshipProxy[] relationshipsToNext, + IEnumerable relationshipsFromPrev) { IRelationshipsFromPreviousLayer prev = CreateRelationshipsFromInstance(nodeType, relationshipsFromPrev); return (IResourceNode)TypeHelper.CreateInstanceOfOpenType(typeof(ChildNode<>), nodeType, relationshipsToNext, prev); } /// - /// Reflective helper method to create an instance of ; + /// Reflective helper method to create an instance of ; /// private IRelationshipsFromPreviousLayer CreateRelationshipsFromInstance(RightType nodeType, IEnumerable relationshipsFromPrev) { - var relationshipsFromPrevList = relationshipsFromPrev.ToList(); - var cast = TypeHelper.CopyToList(relationshipsFromPrevList, relationshipsFromPrevList.First().GetType()); + List relationshipsFromPrevList = relationshipsFromPrev.ToList(); + IList cast = TypeHelper.CopyToList(relationshipsFromPrevList, relationshipsFromPrevList.First().GetType()); return (IRelationshipsFromPreviousLayer)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipsFromPreviousLayer<>), nodeType, cast); } /// - /// Reflective helper method to create an instance of ; + /// Reflective helper method to create an instance of ; /// - private IRelationshipGroup CreateRelationshipGroupInstance(Type thisLayerType, RelationshipProxy proxy, List leftResources, List rightResources) + private IRelationshipGroup CreateRelationshipGroupInstance(RightType thisLayerType, RelationshipProxy proxy, List leftResources, + List rightResources) { - var rightResourcesHashed = TypeHelper.CreateInstanceOfOpenType(typeof(HashSet<>), thisLayerType, TypeHelper.CopyToList(rightResources, thisLayerType)); - return (IRelationshipGroup)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipGroup<>), - thisLayerType, proxy, new HashSet(leftResources), rightResourcesHashed); + object rightResourcesHashed = + TypeHelper.CreateInstanceOfOpenType(typeof(HashSet<>), thisLayerType, TypeHelper.CopyToList(rightResources, thisLayerType)); + + return (IRelationshipGroup)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipGroup<>), thisLayerType, proxy, + new HashSet(leftResources), rightResourcesHashed); } } } diff --git a/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs index 63d74d18c0..f21dcad2b4 100644 --- a/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/AsyncConvertEmptyActionResultFilter.cs @@ -20,7 +20,10 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE { if (context.Result is IStatusCodeActionResult statusCodeResult) { - context.Result = new ObjectResult(null) {StatusCode = statusCodeResult.StatusCode}; + context.Result = new ObjectResult(null) + { + StatusCode = statusCodeResult.StatusCode + }; } } } diff --git a/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs index 9fa5df8366..54f9bfaf9f 100644 --- a/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -25,11 +26,11 @@ public Task OnExceptionAsync(ExceptionContext context) if (context.HttpContext.IsJsonApiRequest()) { - var errorDocument = _exceptionHandler.HandleException(context.Exception); + ErrorDocument errorDocument = _exceptionHandler.HandleException(context.Exception); context.Result = new ObjectResult(errorDocument) { - StatusCode = (int) errorDocument.GetErrorStatusCode() + StatusCode = (int)errorDocument.GetErrorStatusCode() }; } diff --git a/src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs index 65a7ac14b1..7a72874e5b 100644 --- a/src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/AsyncQueryStringActionFilter.cs @@ -10,7 +10,7 @@ namespace JsonApiDotNetCore.Middleware public sealed class AsyncQueryStringActionFilter : IAsyncQueryStringActionFilter { private readonly IQueryStringReader _queryStringReader; - + public AsyncQueryStringActionFilter(IQueryStringReader queryStringReader) { ArgumentGuard.NotNull(queryStringReader, nameof(queryStringReader)); diff --git a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs index 568cf6fed7..2006ad62de 100644 --- a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Net; using JetBrains.Annotations; @@ -38,9 +39,9 @@ public ErrorDocument HandleException(Exception exception) private void LogException(Exception exception) { - var level = GetLogLevel(exception); - var message = GetLogMessage(exception); - + LogLevel level = GetLogLevel(exception); + string message = GetLogMessage(exception); + _logger.Log(level, exception, message); } @@ -72,20 +73,17 @@ protected virtual ErrorDocument CreateErrorDocument(Exception exception) { ArgumentGuard.NotNull(exception, nameof(exception)); - var errors = exception is JsonApiException jsonApiException - ? jsonApiException.Errors - : exception is OperationCanceledException - ? new Error((HttpStatusCode) 499) - { - Title = "Request execution was canceled." - }.AsArray() - : new Error(HttpStatusCode.InternalServerError) - { - Title = "An unhandled error occurred while processing this request.", - Detail = exception.Message - }.AsArray(); - - foreach (var error in errors) + IReadOnlyList errors = exception is JsonApiException jsonApiException ? jsonApiException.Errors : + exception is OperationCanceledException ? new Error((HttpStatusCode)499) + { + Title = "Request execution was canceled." + }.AsArray() : new Error(HttpStatusCode.InternalServerError) + { + Title = "An unhandled error occurred while processing this request.", + Detail = exception.Message + }.AsArray(); + + foreach (Error error in errors) { ApplyOptions(error, exception); } diff --git a/src/JsonApiDotNetCore/Middleware/IAsyncConvertEmptyActionResultFilter.cs b/src/JsonApiDotNetCore/Middleware/IAsyncConvertEmptyActionResultFilter.cs index 9101c97c21..8e0f8b394e 100644 --- a/src/JsonApiDotNetCore/Middleware/IAsyncConvertEmptyActionResultFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IAsyncConvertEmptyActionResultFilter.cs @@ -10,9 +10,11 @@ namespace JsonApiDotNetCore.Middleware /// return NotFound() -> return NotFound(null) /// ]]> /// - /// This ensures our formatter is invoked, where we'll build a JSON:API compliant response. - /// For details, see: https://github.com/dotnet/aspnetcore/issues/16969 + /// This ensures our formatter is invoked, where we'll build a JSON:API compliant response. For details, see: + /// https://github.com/dotnet/aspnetcore/issues/16969 /// [PublicAPI] - public interface IAsyncConvertEmptyActionResultFilter : IAsyncAlwaysRunResultFilter { } + public interface IAsyncConvertEmptyActionResultFilter : IAsyncAlwaysRunResultFilter + { + } } diff --git a/src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs index 67b1985816..7ae6981c77 100644 --- a/src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IAsyncJsonApiExceptionFilter.cs @@ -4,8 +4,10 @@ namespace JsonApiDotNetCore.Middleware { /// - /// Application-wide exception filter that invokes for JSON:API requests. + /// Application-wide exception filter that invokes for JSON:API requests. /// [PublicAPI] - public interface IAsyncJsonApiExceptionFilter : IAsyncExceptionFilter { } + public interface IAsyncJsonApiExceptionFilter : IAsyncExceptionFilter + { + } } diff --git a/src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs b/src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs index c2a4effcce..4ad18fcf35 100644 --- a/src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/IAsyncQueryStringActionFilter.cs @@ -7,5 +7,7 @@ namespace JsonApiDotNetCore.Middleware /// Application-wide entry point for processing JSON:API request query strings. /// [PublicAPI] - public interface IAsyncQueryStringActionFilter : IAsyncActionFilter { } + public interface IAsyncQueryStringActionFilter : IAsyncActionFilter + { + } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs index 1aeb803be6..268c0a2697 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiInputFormatter.cs @@ -7,5 +7,7 @@ namespace JsonApiDotNetCore.Middleware /// Application-wide entry point for reading JSON:API request bodies. /// [PublicAPI] - public interface IJsonApiInputFormatter : IInputFormatter { } + public interface IJsonApiInputFormatter : IInputFormatter + { + } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs index 1afd8683f9..725accb03f 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiOutputFormatter.cs @@ -7,5 +7,7 @@ namespace JsonApiDotNetCore.Middleware /// Application-wide entry point for writing JSON:API response bodies. /// [PublicAPI] - public interface IJsonApiOutputFormatter : IOutputFormatter { } + public interface IJsonApiOutputFormatter : IOutputFormatter + { + } } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs index 84e56e1941..3888fafb74 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiRequest.cs @@ -15,7 +15,7 @@ public interface IJsonApiRequest public EndpointKind Kind { get; } /// - /// The request URL prefix. This may be an absolute or relative path, depending on . + /// The request URL prefix. This may be an absolute or relative path, depending on . /// /// /// - /// The ID of the primary (top-level) resource for this request. - /// This would be null in "/blogs", "123" in "/blogs/123" or "/blogs/123/author". + /// The ID of the primary (top-level) resource for this request. This would be null in "/blogs", "123" in "/blogs/123" or "/blogs/123/author". /// string PrimaryId { get; } /// - /// The primary (top-level) resource for this request. - /// This would be "blogs" in "/blogs", "/blogs/123" or "/blogs/123/author". + /// The primary (top-level) resource for this request. This would be "blogs" in "/blogs", "/blogs/123" or "/blogs/123/author". /// ResourceContext PrimaryResource { get; } /// - /// The secondary (nested) resource for this request. - /// This would be null in "/blogs", "/blogs/123" and "/blogs/123/unknownResource" or - /// "people" in "/blogs/123/author" and "/blogs/123/relationships/author". + /// The secondary (nested) resource for this request. This would be null in "/blogs", "/blogs/123" and "/blogs/123/unknownResource" or "people" in + /// "/blogs/123/author" and "/blogs/123/relationships/author". /// ResourceContext SecondaryResource { get; } /// - /// The relationship for this nested request. - /// This would be null in "/blogs", "/blogs/123" and "/blogs/123/unknownResource" or - /// "author" in "/blogs/123/author" and "/blogs/123/relationships/author". + /// The relationship for this nested request. This would be null in "/blogs", "/blogs/123" and "/blogs/123/unknownResource" or "author" in + /// "/blogs/123/author" and "/blogs/123/relationships/author". /// RelationshipAttribute Relationship { get; } diff --git a/src/JsonApiDotNetCore/Middleware/IJsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/IJsonApiRoutingConvention.cs index e1c941414c..d5db22efa6 100644 --- a/src/JsonApiDotNetCore/Middleware/IJsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/IJsonApiRoutingConvention.cs @@ -4,9 +4,10 @@ namespace JsonApiDotNetCore.Middleware { /// - /// Service for specifying which routing convention to use. This can be overridden to customize - /// the relation between controllers and mapped routes. + /// Service for specifying which routing convention to use. This can be overridden to customize the relation between controllers and mapped routes. /// [PublicAPI] - public interface IJsonApiRoutingConvention : IApplicationModelConvention, IControllerResourceMapping { } + public interface IJsonApiRoutingConvention : IApplicationModelConvention, IControllerResourceMapping + { + } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 46aded0e1f..27f9a2a46d 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; @@ -20,7 +21,7 @@ namespace JsonApiDotNetCore.Middleware { /// - /// Intercepts HTTP requests to populate injected instance for JSON:API requests. + /// Intercepts HTTP requests to populate injected instance for JSON:API requests. /// [PublicAPI] public sealed class JsonApiMiddleware @@ -35,11 +36,8 @@ public JsonApiMiddleware(RequestDelegate next) _next = next; } - public async Task InvokeAsync(HttpContext httpContext, - IControllerResourceMapping controllerResourceMapping, - IJsonApiOptions options, - IJsonApiRequest request, - IResourceContextProvider resourceContextProvider) + public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options, + IJsonApiRequest request, IResourceContextProvider resourceContextProvider) { ArgumentGuard.NotNull(httpContext, nameof(httpContext)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); @@ -47,12 +45,13 @@ public async Task InvokeAsync(HttpContext httpContext, ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); - var routeValues = httpContext.GetRouteData().Values; + RouteValueDictionary routeValues = httpContext.GetRouteData().Values; + + ResourceContext primaryResourceContext = CreatePrimaryResourceContext(routeValues, controllerResourceMapping, resourceContextProvider); - var primaryResourceContext = CreatePrimaryResourceContext(routeValues, controllerResourceMapping, resourceContextProvider); if (primaryResourceContext != null) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerSettings) || + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerSettings) || !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerSettings)) { return; @@ -64,7 +63,7 @@ public async Task InvokeAsync(HttpContext httpContext, } else if (IsOperationsRequest(routeValues)) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) || + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) || !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerSettings)) { return; @@ -78,13 +77,15 @@ public async Task InvokeAsync(HttpContext httpContext, await _next(httpContext); } - private static ResourceContext CreatePrimaryResourceContext(RouteValueDictionary routeValues, - IControllerResourceMapping controllerResourceMapping, IResourceContextProvider resourceContextProvider) + private static ResourceContext CreatePrimaryResourceContext(RouteValueDictionary routeValues, IControllerResourceMapping controllerResourceMapping, + IResourceContextProvider resourceContextProvider) { - var controllerName = (string) routeValues["controller"]; + string controllerName = (string)routeValues["controller"]; + if (controllerName != null) { - var resourceType = controllerResourceMapping.GetResourceTypeForController(controllerName); + Type resourceType = controllerResourceMapping.GetResourceTypeForController(controllerName); + if (resourceType != null) { return resourceContextProvider.GetResourceContext(resourceType); @@ -94,26 +95,30 @@ private static ResourceContext CreatePrimaryResourceContext(RouteValueDictionary return null; } - private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, JsonSerializerSettings serializerSettings) + private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, + JsonSerializerSettings serializerSettings) { - var contentType = httpContext.Request.ContentType; + string contentType = httpContext.Request.ContentType; + if (contentType != null && contentType != allowedContentType) { await FlushResponseAsync(httpContext.Response, serializerSettings, new Error(HttpStatusCode.UnsupportedMediaType) { Title = "The specified Content-Type header value is not supported.", - Detail = $"Please specify '{allowedContentType}' instead of '{contentType}' " + - "for the Content-Type header value." + Detail = $"Please specify '{allowedContentType}' instead of '{contentType}' " + "for the Content-Type header value." }); + return false; } return true; } - private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue allowedMediaTypeValue, HttpContext httpContext, JsonSerializerSettings serializerSettings) + private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue allowedMediaTypeValue, HttpContext httpContext, + JsonSerializerSettings serializerSettings) { StringValues acceptHeaders = httpContext.Request.Headers["Accept"]; + if (!acceptHeaders.Any()) { return true; @@ -121,9 +126,9 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a bool seenCompatibleMediaType = false; - foreach (var acceptHeader in acceptHeaders) + foreach (string acceptHeader in acceptHeaders) { - if (MediaTypeWithQualityHeaderValue.TryParse(acceptHeader, out var headerValue)) + if (MediaTypeWithQualityHeaderValue.TryParse(acceptHeader, out MediaTypeWithQualityHeaderValue headerValue)) { headerValue.Quality = null; @@ -148,6 +153,7 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a Title = "The specified Accept header value does not contain any supported media types.", Detail = $"Please include '{allowedMediaTypeValue}' in the Accept header values." }); + return false; } @@ -157,9 +163,9 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSerializerSettings serializerSettings, Error error) { httpResponse.ContentType = HeaderConstants.MediaType; - httpResponse.StatusCode = (int) error.StatusCode; + httpResponse.StatusCode = (int)error.StatusCode; - JsonSerializer serializer = JsonSerializer.CreateDefault(serializerSettings); + var serializer = JsonSerializer.CreateDefault(serializerSettings); serializer.ApplyErrorSettings(); // https://github.com/JamesNK/Newtonsoft.Json/issues/1193 @@ -178,9 +184,8 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSeri await httpResponse.Body.FlushAsync(); } - private static void SetupResourceRequest(JsonApiRequest request, ResourceContext primaryResourceContext, - RouteValueDictionary routeValues, IJsonApiOptions options, IResourceContextProvider resourceContextProvider, - HttpRequest httpRequest) + private static void SetupResourceRequest(JsonApiRequest request, ResourceContext primaryResourceContext, RouteValueDictionary routeValues, + IJsonApiOptions options, IResourceContextProvider resourceContextProvider, HttpRequest httpRequest) { request.IsReadOnly = httpRequest.Method == HttpMethod.Get.Method; request.Kind = EndpointKind.Primary; @@ -188,14 +193,14 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceContext request.PrimaryId = GetPrimaryRequestId(routeValues); request.BasePath = GetBasePath(primaryResourceContext.PublicName, options, httpRequest); - var relationshipName = GetRelationshipNameForSecondaryRequest(routeValues); + string relationshipName = GetRelationshipNameForSecondaryRequest(routeValues); + if (relationshipName != null) { request.Kind = IsRouteForRelationship(routeValues) ? EndpointKind.Relationship : EndpointKind.Secondary; - var requestRelationship = - primaryResourceContext.Relationships.SingleOrDefault(relationship => - relationship.PublicName == relationshipName); + RelationshipAttribute requestRelationship = + primaryResourceContext.Relationships.SingleOrDefault(relationship => relationship.PublicName == relationshipName); if (requestRelationship != null) { @@ -204,13 +209,13 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceContext } } - var isGetAll = request.PrimaryId == null && request.IsReadOnly; + bool isGetAll = request.PrimaryId == null && request.IsReadOnly; request.IsCollection = isGetAll || request.Relationship is HasManyAttribute; } private static string GetPrimaryRequestId(RouteValueDictionary routeValues) { - return routeValues.TryGetValue("id", out var id) ? (string) id : null; + return routeValues.TryGetValue("id", out object id) ? (string)id : null; } private static string GetBasePath(string resourceName, IJsonApiOptions options, HttpRequest httpRequest) @@ -230,6 +235,7 @@ private static string GetBasePath(string resourceName, IJsonApiOptions options, } string customRoute = GetCustomRoute(resourceName, options.Namespace, httpRequest.HttpContext); + if (!string.IsNullOrEmpty(customRoute)) { builder.Append('/'); @@ -248,14 +254,15 @@ private static string GetCustomRoute(string resourceName, string apiNamespace, H { if (resourceName != null) { - var endpoint = httpContext.GetEndpoint(); + Endpoint endpoint = httpContext.GetEndpoint(); var routeAttribute = endpoint.Metadata.GetMetadata(); + if (routeAttribute != null) { - var trimmedComponents = httpContext.Request.Path.Value.Trim('/').Split('/').ToList(); - var resourceNameIndex = trimmedComponents.FindIndex(c => c == resourceName); - var newComponents = trimmedComponents.Take(resourceNameIndex).ToArray(); - var customRoute = string.Join('/', newComponents); + List trimmedComponents = httpContext.Request.Path.Value.Trim('/').Split('/').ToList(); + int resourceNameIndex = trimmedComponents.FindIndex(c => c == resourceName); + string[] newComponents = trimmedComponents.Take(resourceNameIndex).ToArray(); + string customRoute = string.Join('/', newComponents); return customRoute == apiNamespace ? null : customRoute; } } @@ -265,18 +272,18 @@ private static string GetCustomRoute(string resourceName, string apiNamespace, H private static string GetRelationshipNameForSecondaryRequest(RouteValueDictionary routeValues) { - return routeValues.TryGetValue("relationshipName", out object routeValue) ? (string) routeValue : null; + return routeValues.TryGetValue("relationshipName", out object routeValue) ? (string)routeValue : null; } private static bool IsRouteForRelationship(RouteValueDictionary routeValues) { - var actionName = (string)routeValues["action"]; + string actionName = (string)routeValues["action"]; return actionName.EndsWith("Relationship", StringComparison.Ordinal); } private static bool IsOperationsRequest(RouteValueDictionary routeValues) { - var actionName = (string)routeValues["action"]; + string actionName = (string)routeValues["action"]; return actionName == "PostOperations"; } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs index ce2704aadf..53c86330b2 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRequest.cs @@ -11,25 +11,25 @@ public sealed class JsonApiRequest : IJsonApiRequest { /// public EndpointKind Kind { get; set; } - + /// public string BasePath { get; set; } - + /// public string PrimaryId { get; set; } - + /// public ResourceContext PrimaryResource { get; set; } - + /// public ResourceContext SecondaryResource { get; set; } - + /// public RelationshipAttribute Relationship { get; set; } - + /// public bool IsCollection { get; set; } - + /// public bool IsReadOnly { get; set; } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index 73468d4ef3..4d64bfe9d0 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -14,19 +14,18 @@ namespace JsonApiDotNetCore.Middleware { /// - /// The default routing convention registers the name of the resource as the route - /// using the serializer naming convention. The default for this is - /// a camel case formatter. If the controller directly inherits from and there is no - /// resource directly associated, it uses the name of the controller instead of the name of the type. + /// The default routing convention registers the name of the resource as the route using the serializer naming convention. The default for this is a + /// camel case formatter. If the controller directly inherits from and there is no resource directly associated, it + /// uses the name of the controller instead of the name of the type. /// /// { } // => /someResources/relationship/relatedResource - /// + /// /// public class RandomNameController : JsonApiController { } // => /someResources/relationship/relatedResource - /// + /// /// // when using kebab-case naming convention: /// public class SomeResourceController : JsonApiController { } // => /some-resources/relationship/related-resource - /// + /// /// public class SomeVeryCustomController : CoreJsonApiController { } // => /someVeryCustoms/relationship/relatedResource /// ]]> [PublicAPI] @@ -51,11 +50,11 @@ public Type GetResourceTypeForController(string controllerName) { ArgumentGuard.NotNull(controllerName, nameof(controllerName)); - if (_registeredResources.TryGetValue(controllerName, out var resourceContext)) + if (_registeredResources.TryGetValue(controllerName, out ResourceContext resourceContext)) { return resourceContext.ResourceType; } - + return null; } @@ -64,16 +63,17 @@ public void Apply(ApplicationModel application) { ArgumentGuard.NotNull(application, nameof(application)); - foreach (var controller in application.Controllers) + foreach (ControllerModel controller in application.Controllers) { bool isOperationsController = IsOperationsController(controller.ControllerType); + if (!isOperationsController) { - var resourceType = ExtractResourceTypeFromController(controller.ControllerType); + Type resourceType = ExtractResourceTypeFromController(controller.ControllerType); if (resourceType != null) { - var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); if (resourceContext != null) { @@ -87,14 +87,17 @@ public void Apply(ApplicationModel application) continue; } - var template = TemplateFromResource(controller) ?? TemplateFromController(controller); + string template = TemplateFromResource(controller) ?? TemplateFromController(controller); + if (template == null) { - throw new InvalidConfigurationException( - $"Controllers with overlapping route templates detected: {controller.ControllerType.FullName}"); + throw new InvalidConfigurationException($"Controllers with overlapping route templates detected: {controller.ControllerType.FullName}"); } - controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel { Template = template }; + controller.Selectors[0].AttributeRouteModel = new AttributeRouteModel + { + Template = template + }; } } @@ -109,9 +112,10 @@ private bool IsRoutingConventionEnabled(ControllerModel controller) /// private string TemplateFromResource(ControllerModel model) { - if (_registeredResources.TryGetValue(model.ControllerName, out var resourceContext)) + if (_registeredResources.TryGetValue(model.ControllerName, out ResourceContext resourceContext)) { - var template = $"{_options.Namespace}/{resourceContext.PublicName}"; + string template = $"{_options.Namespace}/{resourceContext.PublicName}"; + if (_registeredTemplates.Add(template)) { return template; @@ -127,13 +131,13 @@ private string TemplateFromResource(ControllerModel model) private string TemplateFromController(ControllerModel model) { string controllerName = _options.SerializerNamingStrategy.GetPropertyName(model.ControllerName, false); - var template = $"{_options.Namespace}/{controllerName}"; + string template = $"{_options.Namespace}/{controllerName}"; if (_registeredTemplates.Add(template)) { return template; } - + return null; } @@ -142,19 +146,19 @@ private string TemplateFromController(ControllerModel model) /// private Type ExtractResourceTypeFromController(Type type) { - var aspNetControllerType = typeof(ControllerBase); - var coreControllerType = typeof(CoreJsonApiController); - var baseControllerType = typeof(BaseJsonApiController<,>); - var currentType = type; + Type aspNetControllerType = typeof(ControllerBase); + Type coreControllerType = typeof(CoreJsonApiController); + Type baseControllerType = typeof(BaseJsonApiController<,>); + Type currentType = type; + while (!currentType.IsGenericType || currentType.GetGenericTypeDefinition() != baseControllerType) { - var nextBaseType = currentType.BaseType; + Type nextBaseType = currentType.BaseType; - if ((nextBaseType == aspNetControllerType || nextBaseType == coreControllerType) && - currentType.IsGenericType) + if ((nextBaseType == aspNetControllerType || nextBaseType == coreControllerType) && currentType.IsGenericType) { - var resourceType = currentType.GetGenericArguments() - .FirstOrDefault(t => TypeHelper.IsOrImplementsInterface(t, typeof(IIdentifiable))); + Type resourceType = currentType.GetGenericArguments().FirstOrDefault(t => TypeHelper.IsOrImplementsInterface(t, typeof(IIdentifiable))); + if (resourceType != null) { return resourceType; @@ -162,6 +166,7 @@ private Type ExtractResourceTypeFromController(Type type) } currentType = nextBaseType; + if (nextBaseType == null) { break; @@ -173,7 +178,7 @@ private Type ExtractResourceTypeFromController(Type type) private static bool IsOperationsController(Type type) { - var baseControllerType = typeof(BaseJsonApiOperationsController); + Type baseControllerType = typeof(BaseJsonApiOperationsController); return baseControllerType.IsAssignableFrom(type); } } diff --git a/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs index 7ba6490547..bd9d4f12ac 100644 --- a/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs +++ b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs @@ -54,7 +54,8 @@ private static void WriteProperties(StringBuilder builder, object propertyContai if (propertyContainer != null) { bool isFirstMember = true; - foreach (var property in propertyContainer.GetType().GetProperties()) + + foreach (PropertyInfo property in propertyContainer.GetType().GetProperties()) { if (isFirstMember) { @@ -75,7 +76,8 @@ private static void WriteProperty(StringBuilder builder, PropertyInfo property, builder.Append(property.Name); builder.Append(": "); - var value = property.GetValue(instance); + object value = property.GetValue(instance); + if (value == null) { builder.Append("null"); @@ -100,7 +102,7 @@ private static void WriteObject(StringBuilder builder, object value) } else { - var text = SerializeObject(value); + string text = SerializeObject(value); builder.Append(text); } } @@ -109,7 +111,8 @@ private static bool HasToStringOverload(Type type) { if (type != null) { - var toStringMethod = type.GetMethod("ToString", Array.Empty()); + MethodInfo toStringMethod = type.GetMethod("ToString", Array.Empty()); + if (toStringMethod != null && toStringMethod.DeclaringType != typeof(object)) { return true; diff --git a/src/JsonApiDotNetCore/ObjectExtensions.cs b/src/JsonApiDotNetCore/ObjectExtensions.cs index 7dfa76e02c..7df7cf3b02 100644 --- a/src/JsonApiDotNetCore/ObjectExtensions.cs +++ b/src/JsonApiDotNetCore/ObjectExtensions.cs @@ -11,12 +11,18 @@ public static IEnumerable AsEnumerable(this T element) public static T[] AsArray(this T element) { - return new[] {element}; + return new[] + { + element + }; } public static List AsList(this T element) { - return new List {element}; + return new List + { + element + }; } } } diff --git a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs index 5d848911dd..6bd2e5a870 100644 --- a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs +++ b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs @@ -1,6 +1,6 @@ using System.Runtime.CompilerServices; -[assembly:InternalsVisibleTo("Benchmarks")] -[assembly:InternalsVisibleTo("JsonApiDotNetCoreExampleTests")] -[assembly:InternalsVisibleTo("UnitTests")] -[assembly:InternalsVisibleTo("DiscoveryTests")] +[assembly: InternalsVisibleTo("Benchmarks")] +[assembly: InternalsVisibleTo("JsonApiDotNetCoreExampleTests")] +[assembly: InternalsVisibleTo("UnitTests")] +[assembly: InternalsVisibleTo("DiscoveryTests")] diff --git a/src/JsonApiDotNetCore/Properties/launchSettings.json b/src/JsonApiDotNetCore/Properties/launchSettings.json index d0f3094262..233fb4a18e 100644 --- a/src/JsonApiDotNetCore/Properties/launchSettings.json +++ b/src/JsonApiDotNetCore/Properties/launchSettings.json @@ -24,4 +24,4 @@ "applicationUrl": "http://localhost:63522/" } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs b/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs index 56f56468c1..5475baed85 100644 --- a/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs +++ b/src/JsonApiDotNetCore/Queries/ExpressionInScope.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Queries { /// - /// Represents an expression coming from query string. The scope determines at which depth in the to apply its expression. + /// Represents an expression coming from query string. The scope determines at which depth in the to apply its expression. /// [PublicAPI] public class ExpressionInScope diff --git a/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs index 47bdde4bda..1d398192f8 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/CollectionNotEmptyExpression.cs @@ -40,7 +40,7 @@ public override bool Equals(object obj) return false; } - var other = (CollectionNotEmptyExpression) obj; + var other = (CollectionNotEmptyExpression)obj; return TargetCollection.Equals(other.TargetCollection); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs index 16063c9c1f..ab06961738 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/ComparisonExpression.cs @@ -46,7 +46,7 @@ public override bool Equals(object obj) return false; } - var other = (ComparisonExpression) obj; + var other = (ComparisonExpression)obj; return Operator == other.Operator && Left.Equals(other.Left) && Right.Equals(other.Right); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs index 26459b943f..57163936f7 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/CountExpression.cs @@ -40,7 +40,7 @@ public override bool Equals(object obj) return false; } - var other = (CountExpression) obj; + var other = (CountExpression)obj; return TargetCollection.Equals(other.TargetCollection); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs index 69076e2d39..5e723bb5bf 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/EqualsAnyOfExpression.cs @@ -16,8 +16,7 @@ public class EqualsAnyOfExpression : FilterExpression public ResourceFieldChainExpression TargetAttribute { get; } public IReadOnlyCollection Constants { get; } - public EqualsAnyOfExpression(ResourceFieldChainExpression targetAttribute, - IReadOnlyCollection constants) + public EqualsAnyOfExpression(ResourceFieldChainExpression targetAttribute, IReadOnlyCollection constants) { ArgumentGuard.NotNull(targetAttribute, nameof(targetAttribute)); ArgumentGuard.NotNull(constants, nameof(constants)); @@ -62,7 +61,7 @@ public override bool Equals(object obj) return false; } - var other = (EqualsAnyOfExpression) obj; + var other = (EqualsAnyOfExpression)obj; return TargetAttribute.Equals(other.TargetAttribute) && Constants.SequenceEqual(other.Constants); } @@ -72,7 +71,7 @@ public override int GetHashCode() var hashCode = new HashCode(); hashCode.Add(TargetAttribute); - foreach (var constant in Constants) + foreach (LiteralConstantExpression constant in Constants) { hashCode.Add(constant); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs index 6814166c4f..57357fa6ec 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs @@ -5,8 +5,7 @@ namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Converts includes between tree and chain formats. - /// Exists for backwards compatibility, subject to be removed in the future. + /// Converts includes between tree and chain formats. Exists for backwards compatibility, subject to be removed in the future. /// internal static class IncludeChainConverter { @@ -14,8 +13,7 @@ internal static class IncludeChainConverter /// Converts a tree of inclusions into a set of relationship chains. /// /// - /// Input tree: - /// - /// Output chains: + /// ]]> Output chains: /// Blog, /// Article -> Revisions -> Author @@ -35,7 +32,7 @@ public static IReadOnlyCollection GetRelationshipC { ArgumentGuard.NotNull(include, nameof(include)); - IncludeToChainsConverter converter = new IncludeToChainsConverter(); + var converter = new IncludeToChainsConverter(); converter.Visit(include, null); return converter.Chains; @@ -45,12 +42,10 @@ public static IReadOnlyCollection GetRelationshipC /// Converts a set of relationship chains into a tree of inclusions. /// /// - /// Input chains: - /// Blog, /// Article -> Revisions -> Author - /// ]]> - /// Output tree: + /// ]]> Output tree: /// elements = ConvertChainsToElements(chains); return elements.Any() ? new IncludeExpression(elements) : IncludeExpression.Empty; } @@ -86,7 +81,7 @@ private static void ConvertChainToElement(ResourceFieldChainExpression chain, Mu { MutableIncludeNode currentNode = rootNode; - foreach (var relationship in chain.Fields.OfType()) + foreach (RelationshipAttribute relationship in chain.Fields.OfType()) { if (!currentNode.Children.ContainsKey(relationship)) { @@ -156,7 +151,7 @@ public MutableIncludeNode(RelationshipAttribute relationship) public IncludeElementExpression ToExpression() { - var elementChildren = Children.Values.Select(child => child.ToExpression()).ToArray(); + IncludeElementExpression[] elementChildren = Children.Values.Select(child => child.ToExpression()).ToArray(); return new IncludeElementExpression(_relationship, elementChildren); } } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs index 2b9be3d95d..f279f4ede4 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeElementExpression.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Represents an element in . + /// Represents an element in . /// [PublicAPI] public class IncludeElementExpression : QueryExpression @@ -46,7 +46,7 @@ public override string ToString() builder.Append(string.Join(",", Children.Select(child => child.ToString()))); builder.Append('}'); } - + return builder.ToString(); } @@ -62,7 +62,7 @@ public override bool Equals(object obj) return false; } - var other = (IncludeElementExpression) obj; + var other = (IncludeElementExpression)obj; return Relationship.Equals(other.Relationship) == Children.SequenceEqual(other.Children); } @@ -72,7 +72,7 @@ public override int GetHashCode() var hashCode = new HashCode(); hashCode.Add(Relationship); - foreach (var child in Children) + foreach (IncludeElementExpression child in Children) { hashCode.Add(child); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs index a96065db56..538d698193 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/IncludeExpression.cs @@ -11,14 +11,8 @@ namespace JsonApiDotNetCore.Queries.Expressions [PublicAPI] public class IncludeExpression : QueryExpression { - public IReadOnlyCollection Elements { get; } - public static readonly IncludeExpression Empty = new IncludeExpression(); - - private IncludeExpression() - { - Elements = Array.Empty(); - } + public IReadOnlyCollection Elements { get; } public IncludeExpression(IReadOnlyCollection elements) { @@ -32,6 +26,11 @@ public IncludeExpression(IReadOnlyCollection elements) Elements = elements; } + private IncludeExpression() + { + Elements = Array.Empty(); + } + public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) { return visitor.VisitInclude(this, argument); @@ -39,7 +38,7 @@ public override TResult Accept(QueryExpressionVisitor chains = IncludeChainConverter.GetRelationshipChains(this); return string.Join(",", chains.Select(child => child.ToString())); } @@ -55,7 +54,7 @@ public override bool Equals(object obj) return false; } - var other = (IncludeExpression) obj; + var other = (IncludeExpression)obj; return Elements.SequenceEqual(other.Elements); } @@ -64,7 +63,7 @@ public override int GetHashCode() { var hashCode = new HashCode(); - foreach (var element in Elements) + foreach (IncludeElementExpression element in Elements) { hashCode.Add(element); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs index e59299ba3d..019301e40c 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/LiteralConstantExpression.cs @@ -40,7 +40,7 @@ public override bool Equals(object obj) return false; } - var other = (LiteralConstantExpression) obj; + var other = (LiteralConstantExpression)obj; return Value == other.Value; } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs index ef093e204f..ee368235ce 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/LogicalExpression.cs @@ -58,7 +58,7 @@ public override bool Equals(object obj) return false; } - var other = (LogicalExpression) obj; + var other = (LogicalExpression)obj; return Operator == other.Operator && Terms.SequenceEqual(other.Terms); } @@ -68,7 +68,7 @@ public override int GetHashCode() var hashCode = new HashCode(); hashCode.Add(Operator); - foreach (var term in Terms) + foreach (QueryExpression term in Terms) { hashCode.Add(term); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs index 6c026d0258..0df64dbb44 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/MatchTextExpression.cs @@ -15,8 +15,7 @@ public class MatchTextExpression : FilterExpression public LiteralConstantExpression TextValue { get; } public TextMatchKind MatchKind { get; } - public MatchTextExpression(ResourceFieldChainExpression targetAttribute, LiteralConstantExpression textValue, - TextMatchKind matchKind) + public MatchTextExpression(ResourceFieldChainExpression targetAttribute, LiteralConstantExpression textValue, TextMatchKind matchKind) { ArgumentGuard.NotNull(targetAttribute, nameof(targetAttribute)); ArgumentGuard.NotNull(textValue, nameof(textValue)); @@ -55,7 +54,7 @@ public override bool Equals(object obj) return false; } - var other = (MatchTextExpression) obj; + var other = (MatchTextExpression)obj; return TargetAttribute.Equals(other.TargetAttribute) && TextValue.Equals(other.TextValue) && MatchKind == other.MatchKind; } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs index 23a9637045..da790bcfbe 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/NotExpression.cs @@ -40,7 +40,7 @@ public override bool Equals(object obj) return false; } - var other = (NotExpression) obj; + var other = (NotExpression)obj; return Child.Equals(other.Child); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs index 29dbf72d99..d62ca621e0 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationElementQueryStringValueExpression.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Represents an element in . + /// Represents an element in . /// [PublicAPI] public class PaginationElementQueryStringValueExpression : QueryExpression @@ -40,7 +40,7 @@ public override bool Equals(object obj) return false; } - var other = (PaginationElementQueryStringValueExpression) obj; + var other = (PaginationElementQueryStringValueExpression)obj; return Equals(Scope, other.Scope) && Value == other.Value; } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs index 593e2ca1b8..3d8f2c5870 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationExpression.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Represents a pagination, produced from . + /// Represents a pagination, produced from . /// [PublicAPI] public class PaginationExpression : QueryExpression @@ -43,7 +43,7 @@ public override bool Equals(object obj) return false; } - var other = (PaginationExpression) obj; + var other = (PaginationExpression)obj; return PageNumber.Equals(other.PageNumber) && Equals(PageSize, other.PageSize); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs index 5e054d000e..7046c7b1ad 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/PaginationQueryStringValueExpression.cs @@ -13,8 +13,7 @@ public class PaginationQueryStringValueExpression : QueryExpression { public IReadOnlyCollection Elements { get; } - public PaginationQueryStringValueExpression( - IReadOnlyCollection elements) + public PaginationQueryStringValueExpression(IReadOnlyCollection elements) { ArgumentGuard.NotNull(elements, nameof(elements)); @@ -26,8 +25,7 @@ public PaginationQueryStringValueExpression( Elements = elements; } - public override TResult Accept(QueryExpressionVisitor visitor, - TArgument argument) + public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) { return visitor.PaginationQueryStringValue(this, argument); } @@ -49,7 +47,7 @@ public override bool Equals(object obj) return false; } - var other = (PaginationQueryStringValueExpression) obj; + var other = (PaginationQueryStringValueExpression)obj; return Elements.SequenceEqual(other.Elements); } @@ -58,7 +56,7 @@ public override int GetHashCode() { var hashCode = new HashCode(); - foreach (var element in Elements) + foreach (PaginationElementQueryStringValueExpression element in Elements) { hashCode.Add(element); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpression.cs index 6fc8467a93..44d5311b19 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpression.cs @@ -3,8 +3,8 @@ namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Represents the base data structure for immutable types that query string parameters are converted into. - /// This intermediate structure is later transformed into system trees that are handled by Entity Framework Core. + /// Represents the base data structure for immutable types that query string parameters are converted into. This intermediate structure is later + /// transformed into system trees that are handled by Entity Framework Core. /// public abstract class QueryExpression { diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs index 6ff2e777aa..e3107db41a 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionRewriter.cs @@ -28,8 +28,8 @@ public override QueryExpression VisitComparison(ComparisonExpression expression, return null; } - var newLeft = Visit(expression.Left, argument); - var newRight = Visit(expression.Right, argument); + QueryExpression newLeft = Visit(expression.Left, argument); + QueryExpression newRight = Visit(expression.Right, argument); var newExpression = new ComparisonExpression(expression.Operator, newLeft, newRight); return newExpression.Equals(expression) ? expression : newExpression; @@ -54,7 +54,7 @@ public override QueryExpression VisitLogical(LogicalExpression expression, TArgu { if (expression != null) { - var newTerms = VisitSequence(expression.Terms, argument); + IReadOnlyCollection newTerms = VisitSequence(expression.Terms, argument); if (newTerms.Count == 1) { @@ -75,7 +75,7 @@ public override QueryExpression VisitNot(NotExpression expression, TArgument arg { if (expression != null) { - var newChild = Visit(expression.Child, argument); + QueryExpression newChild = Visit(expression.Child, argument); if (newChild != null) { @@ -135,7 +135,7 @@ public override QueryExpression VisitSort(SortExpression expression, TArgument a { if (expression != null) { - var newElements = VisitSequence(expression.Elements, argument); + IReadOnlyCollection newElements = VisitSequence(expression.Elements, argument); if (newElements.Count != 0) { @@ -185,7 +185,7 @@ public override QueryExpression VisitEqualsAnyOf(EqualsAnyOfExpression expressio if (expression != null) { var newTargetAttribute = Visit(expression.TargetAttribute, argument) as ResourceFieldChainExpression; - var newConstants = VisitSequence(expression.Constants, argument); + IReadOnlyCollection newConstants = VisitSequence(expression.Constants, argument); var newExpression = new EqualsAnyOfExpression(newTargetAttribute, newConstants); return newExpression.Equals(expression) ? expression : newExpression; @@ -200,7 +200,7 @@ public override QueryExpression VisitSparseFieldTable(SparseFieldTableExpression { var newTable = new Dictionary(); - foreach (var (resourceContext, sparseFieldSet) in expression.Table) + foreach ((ResourceContext resourceContext, SparseFieldSetExpression sparseFieldSet) in expression.Table) { if (Visit(sparseFieldSet, argument) is SparseFieldSetExpression newSparseFieldSet) { @@ -229,9 +229,7 @@ public override QueryExpression VisitQueryStringParameterScope(QueryStringParame { var newParameterName = Visit(expression.ParameterName, argument) as LiteralConstantExpression; - var newScope = expression.Scope != null - ? Visit(expression.Scope, argument) as ResourceFieldChainExpression - : null; + ResourceFieldChainExpression newScope = expression.Scope != null ? Visit(expression.Scope, argument) as ResourceFieldChainExpression : null; var newExpression = new QueryStringParameterScopeExpression(newParameterName, newScope); return newExpression.Equals(expression) ? expression : newExpression; @@ -244,7 +242,7 @@ public override QueryExpression PaginationQueryStringValue(PaginationQueryString { if (expression != null) { - var newElements = VisitSequence(expression.Elements, argument); + IReadOnlyCollection newElements = VisitSequence(expression.Elements, argument); var newExpression = new PaginationQueryStringValueExpression(newElements); return newExpression.Equals(expression) ? expression : newExpression; @@ -253,14 +251,11 @@ public override QueryExpression PaginationQueryStringValue(PaginationQueryString return null; } - public override QueryExpression PaginationElementQueryStringValue(PaginationElementQueryStringValueExpression expression, - TArgument argument) + public override QueryExpression PaginationElementQueryStringValue(PaginationElementQueryStringValueExpression expression, TArgument argument) { if (expression != null) { - var newScope = expression.Scope != null - ? Visit(expression.Scope, argument) as ResourceFieldChainExpression - : null; + ResourceFieldChainExpression newScope = expression.Scope != null ? Visit(expression.Scope, argument) as ResourceFieldChainExpression : null; var newExpression = new PaginationElementQueryStringValueExpression(newScope, expression.Value); return newExpression.Equals(expression) ? expression : newExpression; @@ -273,7 +268,7 @@ public override QueryExpression VisitInclude(IncludeExpression expression, TArgu { if (expression != null) { - var newElements = VisitSequence(expression.Elements, argument); + IReadOnlyCollection newElements = VisitSequence(expression.Elements, argument); if (newElements.Count == 0) { @@ -291,7 +286,7 @@ public override QueryExpression VisitIncludeElement(IncludeElementExpression exp { if (expression != null) { - var newElements = VisitSequence(expression.Children, argument); + IReadOnlyCollection newElements = VisitSequence(expression.Children, argument); var newExpression = new IncludeElementExpression(expression.Relationship, newElements); return newExpression.Equals(expression) ? expression : newExpression; @@ -305,8 +300,7 @@ public override QueryExpression VisitQueryableHandler(QueryableHandlerExpression return expression; } - protected virtual IReadOnlyCollection VisitSequence(IEnumerable elements, - TArgument argument) + protected virtual IReadOnlyCollection VisitSequence(IEnumerable elements, TArgument argument) where TExpression : QueryExpression { var newElements = new List(); diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs index 6b7da6210d..f16a7b0424 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryExpressionVisitor.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Implements the visitor design pattern that enables traversing a tree. + /// Implements the visitor design pattern that enables traversing a tree. /// [PublicAPI] public abstract class QueryExpressionVisitor diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs index ca514c6638..7a6071e450 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryStringParameterScopeExpression.cs @@ -42,7 +42,7 @@ public override bool Equals(object obj) return false; } - var other = (QueryStringParameterScopeExpression) obj; + var other = (QueryStringParameterScopeExpression)obj; return ParameterName.Equals(other.ParameterName) && Equals(Scope, other.Scope); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs index dd47117388..9ebf5f0c2b 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/QueryableHandlerExpression.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Holds an expression, used for custom query string handlers from s. + /// Holds an expression, used for custom query string handlers from s. /// [PublicAPI] public class QueryableHandlerExpression : QueryExpression @@ -26,7 +26,7 @@ public QueryableHandlerExpression(object queryableHandler, StringValues paramete public IQueryable Apply(IQueryable query) where TResource : class, IIdentifiable { - var handler = (Func, StringValues, IQueryable>) _queryableHandler; + var handler = (Func, StringValues, IQueryable>)_queryableHandler; return handler(query, _parameterValue); } @@ -52,7 +52,7 @@ public override bool Equals(object obj) return false; } - var other = (QueryableHandlerExpression) obj; + var other = (QueryableHandlerExpression)obj; return _queryableHandler == other._queryableHandler && _parameterValue.Equals(other._parameterValue); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs index 40c30113d9..156c7cd091 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/ResourceFieldChainExpression.cs @@ -33,8 +33,7 @@ public ResourceFieldChainExpression(IReadOnlyCollection Fields = fields; } - public override TResult Accept(QueryExpressionVisitor visitor, - TArgument argument) + public override TResult Accept(QueryExpressionVisitor visitor, TArgument argument) { return visitor.VisitResourceFieldChain(this, argument); } @@ -56,7 +55,7 @@ public override bool Equals(object obj) return false; } - var other = (ResourceFieldChainExpression) obj; + var other = (ResourceFieldChainExpression)obj; return Fields.SequenceEqual(other.Fields); } @@ -65,7 +64,7 @@ public override int GetHashCode() { var hashCode = new HashCode(); - foreach (var field in Fields) + foreach (ResourceFieldAttribute field in Fields) { hashCode.Add(field); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs index 4711bd0cdc..d84564ae9b 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SortElementExpression.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Queries.Expressions { /// - /// Represents an element in . + /// Represents an element in . /// [PublicAPI] public class SortElementExpression : QueryExpression @@ -68,7 +68,7 @@ public override bool Equals(object obj) return false; } - var other = (SortElementExpression) obj; + var other = (SortElementExpression)obj; return Equals(TargetAttribute, other.TargetAttribute) && Equals(Count, other.Count) && IsAscending == other.IsAscending; } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs index 43e25df7e3..ab4d4a6f43 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SortExpression.cs @@ -47,7 +47,7 @@ public override bool Equals(object obj) return false; } - var other = (SortExpression) obj; + var other = (SortExpression)obj; return Elements.SequenceEqual(other.Elements); } @@ -56,7 +56,7 @@ public override int GetHashCode() { var hashCode = new HashCode(); - foreach (var element in Elements) + foreach (SortElementExpression element in Elements) { hashCode.Add(element); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs index db55b1ee29..32ba68c45e 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpression.cs @@ -48,7 +48,7 @@ public override bool Equals(object obj) return false; } - var other = (SparseFieldSetExpression) obj; + var other = (SparseFieldSetExpression)obj; return Fields.SequenceEqual(other.Fields); } @@ -57,7 +57,7 @@ public override int GetHashCode() { var hashCode = new HashCode(); - foreach (var field in Fields) + foreach (ResourceFieldAttribute field in Fields) { hashCode.Add(field); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs index 7375326189..62010b7e11 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldSetExpressionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using JetBrains.Annotations; @@ -18,9 +19,9 @@ public static SparseFieldSetExpression Including(this SparseFieldSetE ArgumentGuard.NotNull(fieldSelector, nameof(fieldSelector)); ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - var newSparseFieldSet = sparseFieldSet; + SparseFieldSetExpression newSparseFieldSet = sparseFieldSet; - foreach (var field in resourceGraph.GetFields(fieldSelector)) + foreach (ResourceFieldAttribute field in resourceGraph.GetFields(fieldSelector)) { newSparseFieldSet = IncludeField(newSparseFieldSet, field); } @@ -35,7 +36,7 @@ private static SparseFieldSetExpression IncludeField(SparseFieldSetExpression sp return sparseFieldSet; } - var fieldSet = sparseFieldSet.Fields.ToHashSet(); + HashSet fieldSet = sparseFieldSet.Fields.ToHashSet(); fieldSet.Add(fieldToInclude); return new SparseFieldSetExpression(fieldSet); } @@ -47,9 +48,9 @@ public static SparseFieldSetExpression Excluding(this SparseFieldSetE ArgumentGuard.NotNull(fieldSelector, nameof(fieldSelector)); ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - var newSparseFieldSet = sparseFieldSet; + SparseFieldSetExpression newSparseFieldSet = sparseFieldSet; - foreach (var field in resourceGraph.GetFields(fieldSelector)) + foreach (ResourceFieldAttribute field in resourceGraph.GetFields(fieldSelector)) { newSparseFieldSet = ExcludeField(newSparseFieldSet, field); } @@ -69,7 +70,7 @@ private static SparseFieldSetExpression ExcludeField(SparseFieldSetExpression sp return sparseFieldSet; } - var fieldSet = sparseFieldSet.Fields.ToHashSet(); + HashSet fieldSet = sparseFieldSet.Fields.ToHashSet(); fieldSet.Remove(fieldToExclude); return new SparseFieldSetExpression(fieldSet); } diff --git a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs index fcbdd2fd82..48c86a1cba 100644 --- a/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs +++ b/src/JsonApiDotNetCore/Queries/Expressions/SparseFieldTableExpression.cs @@ -36,7 +36,7 @@ public override string ToString() { var builder = new StringBuilder(); - foreach (var (resource, fields) in Table) + foreach ((ResourceContext resource, SparseFieldSetExpression fields) in Table) { if (builder.Length > 0) { @@ -64,7 +64,7 @@ public override bool Equals(object obj) return false; } - var other = (SparseFieldTableExpression) obj; + var other = (SparseFieldTableExpression)obj; return Table.SequenceEqual(other.Table); } @@ -73,7 +73,7 @@ public override int GetHashCode() { var hashCode = new HashCode(); - foreach (var (resourceContext, sparseFieldSet) in Table) + foreach ((ResourceContext resourceContext, SparseFieldSetExpression sparseFieldSet) in Table) { hashCode.Add(resourceContext); hashCode.Add(sparseFieldSet); diff --git a/src/JsonApiDotNetCore/Queries/IPaginationContext.cs b/src/JsonApiDotNetCore/Queries/IPaginationContext.cs index e80199de7c..fb249cfbec 100644 --- a/src/JsonApiDotNetCore/Queries/IPaginationContext.cs +++ b/src/JsonApiDotNetCore/Queries/IPaginationContext.cs @@ -8,32 +8,30 @@ namespace JsonApiDotNetCore.Queries public interface IPaginationContext { /// - /// The value 1, unless specified from query string. Never null. - /// Cannot be higher than options.MaximumPageNumber. + /// The value 1, unless specified from query string. Never null. Cannot be higher than options.MaximumPageNumber. /// PageNumber PageNumber { get; set; } /// - /// The default page size from options, unless specified in query string. Can be null, which means no paging. - /// Cannot be higher than options.MaximumPageSize. + /// The default page size from options, unless specified in query string. Can be null, which means no paging. Cannot be higher than + /// options.MaximumPageSize. /// PageSize PageSize { get; set; } /// - /// Indicates whether the number of resources on the current page equals the page size. - /// When true, a subsequent page might exist (assuming is unknown). + /// Indicates whether the number of resources on the current page equals the page size. When true, a subsequent page might exist (assuming + /// is unknown). /// bool IsPageFull { get; set; } /// - /// The total number of resources. - /// null when is set to false. + /// The total number of resources. null when is set to false. /// int? TotalResourceCount { get; set; } /// - /// The total number of resource pages. - /// null when is set to false or is null. + /// The total number of resource pages. null when is set to false or + /// is null. /// int? TotalPageCount { get; } } diff --git a/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs index 1f1628edd0..c3fa8428e4 100644 --- a/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/IQueryLayerComposer.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.Queries { /// - /// Takes scoped expressions from s and transforms them. + /// Takes scoped expressions from s and transforms them. /// public interface IQueryLayerComposer { @@ -17,12 +17,12 @@ public interface IQueryLayerComposer FilterExpression GetTopFilterFromConstraints(ResourceContext resourceContext); /// - /// Collects constraints and builds a out of them, used to retrieve the actual resources. + /// Collects constraints and builds a out of them, used to retrieve the actual resources. /// QueryLayer ComposeFromConstraints(ResourceContext requestResource); /// - /// Collects constraints and builds a out of them, used to retrieve one resource. + /// Collects constraints and builds a out of them, used to retrieve one resource. /// QueryLayer ComposeForGetById(TId id, ResourceContext resourceContext, TopFieldSelection fieldSelection); @@ -34,11 +34,12 @@ public interface IQueryLayerComposer /// /// Wraps a layer for a secondary endpoint into a primary layer, rewriting top-level includes. /// - QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, ResourceContext primaryResourceContext, - TId primaryId, RelationshipAttribute secondaryRelationship); + QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, ResourceContext primaryResourceContext, TId primaryId, + RelationshipAttribute secondaryRelationship); /// - /// Builds a query that retrieves the primary resource, including all of its attributes and all targeted relationships, during a create/update/delete request. + /// Builds a query that retrieves the primary resource, including all of its attributes and all targeted relationships, during a create/update/delete + /// request. /// QueryLayer ComposeForUpdate(TId id, ResourceContext primaryResource); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FieldChainRequirements.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FieldChainRequirements.cs index 443c63a3c0..5864c18d3e 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FieldChainRequirements.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FieldChainRequirements.cs @@ -4,30 +4,30 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing { /// - /// Used internally when parsing subexpressions in the query string parsers to indicate requirements when resolving a chain of fields. - /// Note these may be interpreted differently or even discarded completely by the various parser implementations, - /// as they tend to better understand the characteristics of the entire expression being parsed. + /// Used internally when parsing subexpressions in the query string parsers to indicate requirements when resolving a chain of fields. Note these may be + /// interpreted differently or even discarded completely by the various parser implementations, as they tend to better understand the characteristics of + /// the entire expression being parsed. /// [Flags] public enum FieldChainRequirements { /// - /// Indicates a single , optionally preceded by a chain of s. + /// Indicates a single , optionally preceded by a chain of s. /// EndsInAttribute = 1, /// - /// Indicates a single , optionally preceded by a chain of s. + /// Indicates a single , optionally preceded by a chain of s. /// EndsInToOne = 2, /// - /// Indicates a single , optionally preceded by a chain of s. + /// Indicates a single , optionally preceded by a chain of s. /// EndsInToMany = 4, /// - /// Indicates one or a chain of s. + /// Indicates one or a chain of s. /// IsRelationship = EndsInToOne | EndsInToMany } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs index aeb5358249..cb2a9ec329 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs @@ -36,7 +36,7 @@ public FilterExpression Parse(string source, ResourceContext resourceContextInSc Tokenize(source); - var expression = ParseFilter(); + FilterExpression expression = ParseFilter(); AssertTokenStackIsEmpty(); @@ -135,7 +135,7 @@ protected ComparisonExpression ParseComparison(string operatorName) EatSingleCharacterToken(TokenKind.OpenParen); // Allow equality comparison of a HasOne relationship with null. - var leftChainRequirements = comparisonOperator == ComparisonOperator.Equals + FieldChainRequirements leftChainRequirements = comparisonOperator == ComparisonOperator.Equals ? FieldChainRequirements.EndsInAttribute | FieldChainRequirements.EndsInToOne : FieldChainRequirements.EndsInAttribute; @@ -149,14 +149,14 @@ protected ComparisonExpression ParseComparison(string operatorName) if (leftTerm is ResourceFieldChainExpression leftChain) { - if (leftChainRequirements.HasFlag(FieldChainRequirements.EndsInToOne) && - !(rightTerm is NullConstantExpression)) + if (leftChainRequirements.HasFlag(FieldChainRequirements.EndsInToOne) && !(rightTerm is NullConstantExpression)) { // Run another pass over left chain to have it fail when chain ends in relationship. OnResolveFieldChain(leftChain.ToString(), FieldChainRequirements.EndsInAttribute); } PropertyInfo leftProperty = leftChain.Fields.Last().Property; + if (leftProperty.Name == nameof(Identifiable.Id) && rightTerm is LiteralConstantExpression rightConstant) { string id = DeObfuscateStringId(leftProperty.ReflectedType, rightConstant.Value); @@ -214,6 +214,7 @@ protected EqualsAnyOfExpression ParseAny() EatSingleCharacterToken(TokenKind.CloseParen); PropertyInfo targetAttributeProperty = targetAttribute.Fields.Last().Property; + if (targetAttributeProperty.Name == nameof(Identifiable.Id)) { for (int index = 0; index < constants.Count; index++) @@ -302,7 +303,7 @@ protected LiteralConstantExpression ParseConstant() private string DeObfuscateStringId(Type resourceType, string stringId) { - var tempResource = _resourceFactory.CreateInstance(resourceType); + IIdentifiable tempResource = _resourceFactory.CreateInstance(resourceType); tempResource.StringId = stringId; return tempResource.GetTypedId().ToString(); } @@ -319,8 +320,7 @@ protected override IReadOnlyCollection OnResolveFieldCha return ChainResolver.ResolveToOneChainEndingInAttribute(_resourceContextInScope, path, _validateSingleFieldCallback); } - if (chainRequirements.HasFlag(FieldChainRequirements.EndsInAttribute) && - chainRequirements.HasFlag(FieldChainRequirements.EndsInToOne)) + if (chainRequirements.HasFlag(FieldChainRequirements.EndsInAttribute) && chainRequirements.HasFlag(FieldChainRequirements.EndsInToOne)) { return ChainResolver.ResolveToOneChainEndingInAttributeOrToOne(_resourceContextInScope, path, _validateSingleFieldCallback); } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs index d1b7d73321..448bff8762 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs @@ -29,7 +29,7 @@ public IncludeExpression Parse(string source, ResourceContext resourceContextInS Tokenize(source); - var expression = ParseInclude(maximumDepth); + IncludeExpression expression = ParseInclude(maximumDepth); AssertTokenStackIsEmpty(); @@ -38,16 +38,15 @@ public IncludeExpression Parse(string source, ResourceContext resourceContextInS protected IncludeExpression ParseInclude(int? maximumDepth) { - ResourceFieldChainExpression firstChain = - ParseFieldChain(FieldChainRequirements.IsRelationship, "Relationship name expected."); + ResourceFieldChainExpression firstChain = ParseFieldChain(FieldChainRequirements.IsRelationship, "Relationship name expected."); - var chains = firstChain.AsList(); + List chains = firstChain.AsList(); while (TokenStack.Any()) { EatSingleCharacterToken(TokenKind.Comma); - var nextChain = ParseFieldChain(FieldChainRequirements.IsRelationship, "Relationship name expected."); + ResourceFieldChainExpression nextChain = ParseFieldChain(FieldChainRequirements.IsRelationship, "Relationship name expected."); chains.Add(nextChain); } @@ -60,11 +59,11 @@ private static void ValidateMaximumIncludeDepth(int? maximumDepth, IReadOnlyColl { if (maximumDepth != null) { - foreach (var chain in chains) + foreach (ResourceFieldChainExpression chain in chains) { if (chain.Fields.Count > maximumDepth) { - var path = string.Join('.', chain.Fields.Select(field => field.PublicName)); + string path = string.Join('.', chain.Fields.Select(field => field.PublicName)); throw new QueryParseException($"Including '{path}' exceeds the maximum inclusion depth of {maximumDepth}."); } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs index c4744876a7..156c5281dc 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs @@ -29,7 +29,7 @@ public PaginationQueryStringValueExpression Parse(string source, ResourceContext Tokenize(source); - var expression = ParsePagination(); + PaginationQueryStringValueExpression expression = ParsePagination(); AssertTokenStackIsEmpty(); @@ -40,7 +40,7 @@ protected PaginationQueryStringValueExpression ParsePagination() { var elements = new List(); - var element = ParsePaginationElement(); + PaginationElementQueryStringValueExpression element = ParsePaginationElement(); elements.Add(element); while (TokenStack.Any()) @@ -56,17 +56,19 @@ protected PaginationQueryStringValueExpression ParsePagination() protected PaginationElementQueryStringValueExpression ParsePaginationElement() { - var number = TryParseNumber(); + int? number = TryParseNumber(); + if (number != null) { return new PaginationElementQueryStringValueExpression(null, number.Value); } - var scope = ParseFieldChain(FieldChainRequirements.EndsInToMany, "Number or relationship name expected."); + ResourceFieldChainExpression scope = ParseFieldChain(FieldChainRequirements.EndsInToMany, "Number or relationship name expected."); EatSingleCharacterToken(TokenKind.Colon); number = TryParseNumber(); + if (number == null) { throw new QueryParseException("Number expected."); @@ -85,8 +87,7 @@ protected PaginationElementQueryStringValueExpression ParsePaginationElement() { TokenStack.Pop(); - if (TokenStack.TryPop(out Token token) && token.Kind == TokenKind.Text && - int.TryParse(token.Value, out number)) + if (TokenStack.TryPop(out Token token) && token.Kind == TokenKind.Text && int.TryParse(token.Value, out number)) { return -number; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs index 15b189c7f7..48fcb44a07 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs @@ -11,15 +11,14 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing /// The base class for parsing query string parameters, using the Recursive Descent algorithm. /// /// - /// Uses a tokenizer to populate a stack of tokens, which is then manipulated from the various parsing routines for subexpressions. - /// Implementations should throw on invalid input. + /// Uses a tokenizer to populate a stack of tokens, which is then manipulated from the various parsing routines for subexpressions. Implementations + /// should throw on invalid input. /// [PublicAPI] public abstract class QueryExpressionParser { - private protected ResourceFieldChainResolver ChainResolver { get; } - protected Stack TokenStack { get; private set; } + private protected ResourceFieldChainResolver ChainResolver { get; } protected QueryExpressionParser(IResourceContextProvider resourceContextProvider) { @@ -41,7 +40,8 @@ protected ResourceFieldChainExpression ParseFieldChain(FieldChainRequirements ch { if (TokenStack.TryPop(out Token token) && token.Kind == TokenKind.Text) { - var chain = OnResolveFieldChain(token.Value, chainRequirements); + IReadOnlyCollection chain = OnResolveFieldChain(token.Value, chainRequirements); + if (chain.Any()) { return new ResourceFieldChainExpression(chain); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs index da2d85f25a..26010e3528 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryParseException.cs @@ -6,7 +6,8 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing [PublicAPI] public sealed class QueryParseException : Exception { - public QueryParseException(string message) : base(message) + public QueryParseException(string message) + : base(message) { } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs index 33965facd4..4944f090b1 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs @@ -14,7 +14,7 @@ public class QueryStringParameterScopeParser : QueryExpressionParser private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContextInScope; - public QueryStringParameterScopeParser(IResourceContextProvider resourceContextProvider, FieldChainRequirements chainRequirements, + public QueryStringParameterScopeParser(IResourceContextProvider resourceContextProvider, FieldChainRequirements chainRequirements, Action validateSingleFieldCallback = null) : base(resourceContextProvider) { @@ -30,7 +30,7 @@ public QueryStringParameterScopeExpression Parse(string source, ResourceContext Tokenize(source); - var expression = ParseQueryStringParameterScope(); + QueryStringParameterScopeExpression expression = ParseQueryStringParameterScope(); AssertTokenStackIsEmpty(); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs index 0084fa7bc2..04feabd60d 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryTokenizer.cs @@ -8,8 +8,8 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing [PublicAPI] public sealed class QueryTokenizer { - public static readonly IReadOnlyDictionary SingleCharacterToTokenKinds = - new ReadOnlyDictionary(new Dictionary + public static readonly IReadOnlyDictionary SingleCharacterToTokenKinds = new ReadOnlyDictionary( + new Dictionary { ['('] = TokenKind.OpenParen, [')'] = TokenKind.CloseParen, diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs index b0a4af334f..c4c0d1c546 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs @@ -28,12 +28,12 @@ public IReadOnlyCollection ResolveToManyChain(ResourceCo { var chain = new List(); - var publicNameParts = path.Split("."); - var nextResourceContext = resourceContext; + string[] publicNameParts = path.Split("."); + ResourceContext nextResourceContext = resourceContext; foreach (string publicName in publicNameParts[..^1]) { - var relationship = GetRelationship(publicName, nextResourceContext, path); + RelationshipAttribute relationship = GetRelationship(publicName, nextResourceContext, path); validateCallback?.Invoke(relationship, nextResourceContext, path); @@ -42,7 +42,7 @@ public IReadOnlyCollection ResolveToManyChain(ResourceCo } string lastName = publicNameParts[^1]; - var lastToManyRelationship = GetToManyRelationship(lastName, nextResourceContext, path); + RelationshipAttribute lastToManyRelationship = GetToManyRelationship(lastName, nextResourceContext, path); validateCallback?.Invoke(lastToManyRelationship, nextResourceContext, path); @@ -62,15 +62,15 @@ public IReadOnlyCollection ResolveToManyChain(ResourceCo /// articles.revisions.author /// /// - public IReadOnlyCollection ResolveRelationshipChain(ResourceContext resourceContext, string path, + public IReadOnlyCollection ResolveRelationshipChain(ResourceContext resourceContext, string path, Action validateCallback = null) { var chain = new List(); - var nextResourceContext = resourceContext; + ResourceContext nextResourceContext = resourceContext; foreach (string publicName in path.Split(".")) { - var relationship = GetRelationship(publicName, nextResourceContext, path); + RelationshipAttribute relationship = GetRelationship(publicName, nextResourceContext, path); validateCallback?.Invoke(relationship, nextResourceContext, path); @@ -86,21 +86,19 @@ public IReadOnlyCollection ResolveRelationshipChain(Reso /// /// author.address.country.name /// - /// - /// name - /// + /// name /// public IReadOnlyCollection ResolveToOneChainEndingInAttribute(ResourceContext resourceContext, string path, Action validateCallback = null) { - List chain = new List(); + var chain = new List(); - var publicNameParts = path.Split("."); - var nextResourceContext = resourceContext; + string[] publicNameParts = path.Split("."); + ResourceContext nextResourceContext = resourceContext; foreach (string publicName in publicNameParts[..^1]) { - var toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); + RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); @@ -109,7 +107,7 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr } string lastName = publicNameParts[^1]; - var lastAttribute = GetAttribute(lastName, nextResourceContext, path); + AttrAttribute lastAttribute = GetAttribute(lastName, nextResourceContext, path); validateCallback?.Invoke(lastAttribute, nextResourceContext, path); @@ -129,14 +127,14 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr public IReadOnlyCollection ResolveToOneChainEndingInToMany(ResourceContext resourceContext, string path, Action validateCallback = null) { - List chain = new List(); + var chain = new List(); - var publicNameParts = path.Split("."); - var nextResourceContext = resourceContext; + string[] publicNameParts = path.Split("."); + ResourceContext nextResourceContext = resourceContext; foreach (string publicName in publicNameParts[..^1]) { - var toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); + RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); @@ -146,7 +144,7 @@ public IReadOnlyCollection ResolveToOneChainEndingInToMa string lastName = publicNameParts[^1]; - var toManyRelationship = GetToManyRelationship(lastName, nextResourceContext, path); + RelationshipAttribute toManyRelationship = GetToManyRelationship(lastName, nextResourceContext, path); validateCallback?.Invoke(toManyRelationship, nextResourceContext, path); @@ -166,14 +164,14 @@ public IReadOnlyCollection ResolveToOneChainEndingInToMa public IReadOnlyCollection ResolveToOneChainEndingInAttributeOrToOne(ResourceContext resourceContext, string path, Action validateCallback = null) { - List chain = new List(); + var chain = new List(); - var publicNameParts = path.Split("."); - var nextResourceContext = resourceContext; + string[] publicNameParts = path.Split("."); + ResourceContext nextResourceContext = resourceContext; foreach (string publicName in publicNameParts[..^1]) { - var toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); + RelationshipAttribute toOneRelationship = GetToOneRelationship(publicName, nextResourceContext, path); validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); @@ -182,7 +180,7 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr } string lastName = publicNameParts[^1]; - var lastField = GetField(lastName, nextResourceContext, path); + ResourceFieldAttribute lastField = GetField(lastName, nextResourceContext, path); if (lastField is HasManyAttribute) { @@ -199,7 +197,7 @@ public IReadOnlyCollection ResolveToOneChainEndingInAttr private RelationshipAttribute GetRelationship(string publicName, ResourceContext resourceContext, string path) { - var relationship = resourceContext.Relationships.FirstOrDefault(r => r.PublicName == publicName); + RelationshipAttribute relationship = resourceContext.Relationships.FirstOrDefault(r => r.PublicName == publicName); if (relationship == null) { @@ -213,7 +211,7 @@ private RelationshipAttribute GetRelationship(string publicName, ResourceContext private RelationshipAttribute GetToManyRelationship(string publicName, ResourceContext resourceContext, string path) { - var relationship = GetRelationship(publicName, resourceContext, path); + RelationshipAttribute relationship = GetRelationship(publicName, resourceContext, path); if (!(relationship is HasManyAttribute)) { @@ -227,7 +225,7 @@ private RelationshipAttribute GetToManyRelationship(string publicName, ResourceC private RelationshipAttribute GetToOneRelationship(string publicName, ResourceContext resourceContext, string path) { - var relationship = GetRelationship(publicName, resourceContext, path); + RelationshipAttribute relationship = GetRelationship(publicName, resourceContext, path); if (!(relationship is HasOneAttribute)) { @@ -241,7 +239,7 @@ private RelationshipAttribute GetToOneRelationship(string publicName, ResourceCo private AttrAttribute GetAttribute(string publicName, ResourceContext resourceContext, string path) { - var attribute = resourceContext.Attributes.FirstOrDefault(a => a.PublicName == publicName); + AttrAttribute attribute = resourceContext.Attributes.FirstOrDefault(a => a.PublicName == publicName); if (attribute == null) { @@ -255,7 +253,7 @@ private AttrAttribute GetAttribute(string publicName, ResourceContext resourceCo public ResourceFieldAttribute GetField(string publicName, ResourceContext resourceContext, string path) { - var field = resourceContext.Fields.FirstOrDefault(a => a.PublicName == publicName); + ResourceFieldAttribute field = resourceContext.Fields.FirstOrDefault(a => a.PublicName == publicName); if (field == null) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs index 2b00f686ef..3b146b1785 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs @@ -40,7 +40,7 @@ protected SortExpression ParseSort() { SortElementExpression firstElement = ParseSortElement(); - var elements = firstElement.AsList(); + List elements = firstElement.AsList(); while (TokenStack.Any()) { @@ -64,12 +64,13 @@ protected SortElementExpression ParseSortElement() } CountExpression count = TryParseCount(); + if (count != null) { return new SortElementExpression(count, isAscending); } - var errorMessage = isAscending ? "-, count function or field name expected." : "Count function or field name expected."; + string errorMessage = isAscending ? "-, count function or field name expected." : "Count function or field name expected."; ResourceFieldChainExpression targetAttribute = ParseFieldChain(FieldChainRequirements.EndsInAttribute, errorMessage); return new SortElementExpression(targetAttribute, isAscending); } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs index aea883f43e..52fbe6610b 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs @@ -14,7 +14,8 @@ public class SparseFieldSetParser : QueryExpressionParser private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContext; - public SparseFieldSetParser(IResourceContextProvider resourceContextProvider, Action validateSingleFieldCallback = null) + public SparseFieldSetParser(IResourceContextProvider resourceContextProvider, + Action validateSingleFieldCallback = null) : base(resourceContextProvider) { _validateSingleFieldCallback = validateSingleFieldCallback; @@ -28,7 +29,7 @@ public SparseFieldSetExpression Parse(string source, ResourceContext resourceCon Tokenize(source); - var expression = ParseSparseFieldSet(); + SparseFieldSetExpression expression = ParseSparseFieldSet(); AssertTokenStackIsEmpty(); @@ -57,7 +58,7 @@ protected SparseFieldSetExpression ParseSparseFieldSet() protected override IReadOnlyCollection OnResolveFieldChain(string path, FieldChainRequirements chainRequirements) { - var field = ChainResolver.GetField(path, _resourceContext, path); + ResourceFieldAttribute field = ChainResolver.GetField(path, _resourceContext, path); _validateSingleFieldCallback?.Invoke(field, _resourceContext, path); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs index 18fd966167..361710e345 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs @@ -21,7 +21,7 @@ public ResourceContext Parse(string source) { Tokenize(source); - var resourceContext = ParseSparseFieldTarget(); + ResourceContext resourceContext = ParseSparseFieldTarget(); AssertTokenStackIsEmpty(); @@ -37,7 +37,7 @@ private ResourceContext ParseSparseFieldTarget() EatSingleCharacterToken(TokenKind.OpenBracket); - var resourceContext = ParseResourceName(); + ResourceContext resourceContext = ParseResourceName(); EatSingleCharacterToken(TokenKind.CloseBracket); @@ -56,7 +56,8 @@ private ResourceContext ParseResourceName() private ResourceContext GetResourceContext(string resourceName) { - var resourceContext = _resourceContextProvider.GetResourceContext(resourceName); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceName); + if (resourceContext == null) { throw new QueryParseException($"Resource type '{resourceName}' does not exist."); diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index eac149391a..8476ecfcb4 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -21,12 +21,8 @@ public class QueryLayerComposer : IQueryLayerComposer private readonly ITargetedFields _targetedFields; private readonly SparseFieldSetCache _sparseFieldSetCache; - public QueryLayerComposer( - IEnumerable constraintProviders, - IResourceContextProvider resourceContextProvider, - IResourceDefinitionAccessor resourceDefinitionAccessor, - IJsonApiOptions options, - IPaginationContext paginationContext, + public QueryLayerComposer(IEnumerable constraintProviders, IResourceContextProvider resourceContextProvider, + IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiOptions options, IPaginationContext paginationContext, ITargetedFields targetedFields) { ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); @@ -48,12 +44,12 @@ public QueryLayerComposer( /// public FilterExpression GetTopFilterFromConstraints(ResourceContext resourceContext) { - var constraints = _constraintProviders.SelectMany(provider => provider.GetConstraints()).ToArray(); + ExpressionInScope[] constraints = _constraintProviders.SelectMany(provider => provider.GetConstraints()).ToArray(); // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var filtersInTopScope = constraints + FilterExpression[] filtersInTopScope = constraints .Where(constraint => constraint.Scope == null) .Select(constraint => constraint.Expression) .OfType() @@ -70,9 +66,9 @@ public QueryLayer ComposeFromConstraints(ResourceContext requestResource) { ArgumentGuard.NotNull(requestResource, nameof(requestResource)); - var constraints = _constraintProviders.SelectMany(provider => provider.GetConstraints()).ToArray(); + ExpressionInScope[] constraints = _constraintProviders.SelectMany(provider => provider.GetConstraints()).ToArray(); - var topLayer = ComposeTopLayer(constraints, requestResource); + QueryLayer topLayer = ComposeTopLayer(constraints, requestResource); topLayer.Include = ComposeChildren(topLayer, constraints); return topLayer; @@ -83,7 +79,7 @@ private QueryLayer ComposeTopLayer(IEnumerable constraints, R // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var expressionsInTopScope = constraints + QueryExpression[] expressionsInTopScope = constraints .Where(constraint => constraint.Scope == null) .Select(constraint => constraint.Expression) .ToArray(); @@ -91,7 +87,8 @@ private QueryLayer ComposeTopLayer(IEnumerable constraints, R // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - var topPagination = GetPagination(expressionsInTopScope, resourceContext); + PaginationExpression topPagination = GetPagination(expressionsInTopScope, resourceContext); + if (topPagination != null) { _paginationContext.PageSize = topPagination.PageSize; @@ -112,7 +109,7 @@ private IncludeExpression ComposeChildren(QueryLayer topLayer, ICollection constraint.Scope == null) .Select(constraint => constraint.Expression) .OfType() @@ -121,7 +118,7 @@ private IncludeExpression ComposeChildren(QueryLayer topLayer, ICollection includeElements = ProcessIncludeSet(include.Elements, topLayer, new List(), constraints); return !ReferenceEquals(includeElements, include.Elements) @@ -132,11 +129,12 @@ private IncludeExpression ComposeChildren(QueryLayer topLayer, ICollection ProcessIncludeSet(IReadOnlyCollection includeElements, QueryLayer parentLayer, ICollection parentRelationshipChain, ICollection constraints) { - var includeElementsEvaluated = GetIncludeElements(includeElements, parentLayer.ResourceContext) ?? Array.Empty(); + IReadOnlyCollection includeElementsEvaluated = + GetIncludeElements(includeElements, parentLayer.ResourceContext) ?? Array.Empty(); var updatesInChildren = new Dictionary>(); - foreach (var includeElement in includeElementsEvaluated) + foreach (IncludeElementExpression includeElement in includeElementsEvaluated) { parentLayer.Projection ??= new Dictionary(); @@ -150,7 +148,7 @@ private IReadOnlyCollection ProcessIncludeSet(IReadOnl // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var expressionsInCurrentScope = constraints + QueryExpression[] expressionsInCurrentScope = constraints .Where(constraint => constraint.Scope != null && constraint.Scope.Fields.SequenceEqual(relationshipChain)) .Select(constraint => constraint.Expression) @@ -159,16 +157,13 @@ private IReadOnlyCollection ProcessIncludeSet(IReadOnl // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - var resourceContext = - _resourceContextProvider.GetResourceContext(includeElement.Relationship.RightType); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(includeElement.Relationship.RightType); var child = new QueryLayer(resourceContext) { Filter = GetFilter(expressionsInCurrentScope, resourceContext), Sort = GetSort(expressionsInCurrentScope, resourceContext), - Pagination = ((JsonApiOptions)_options).DisableChildrenPagination - ? null - : GetPagination(expressionsInCurrentScope, resourceContext), + Pagination = ((JsonApiOptions)_options).DisableChildrenPagination ? null : GetPagination(expressionsInCurrentScope, resourceContext), Projection = GetProjectionForSparseAttributeSet(resourceContext) }; @@ -176,7 +171,8 @@ private IReadOnlyCollection ProcessIncludeSet(IReadOnl if (includeElement.Children.Any()) { - var updatedChildren = ProcessIncludeSet(includeElement.Children, child, relationshipChain, constraints); + IReadOnlyCollection updatedChildren = + ProcessIncludeSet(includeElement.Children, child, relationshipChain, constraints); if (!ReferenceEquals(includeElement.Children, updatedChildren)) { @@ -192,11 +188,11 @@ private IReadOnlyCollection ProcessIncludeSet(IReadOnl private static IReadOnlyCollection ApplyIncludeElementUpdates(IEnumerable includeElements, IDictionary> updatesInChildren) { - var newIncludeElements = includeElements.ToList(); + List newIncludeElements = includeElements.ToList(); - foreach (var (existingElement, updatedChildren) in updatesInChildren) + foreach ((IncludeElementExpression existingElement, IReadOnlyCollection updatedChildren) in updatesInChildren) { - var existingIndex = newIncludeElements.IndexOf(existingElement); + int existingIndex = newIncludeElements.IndexOf(existingElement); newIncludeElements[existingIndex] = new IncludeElementExpression(existingElement.Relationship, updatedChildren); } @@ -208,9 +204,9 @@ public QueryLayer ComposeForGetById(TId id, ResourceContext resourceContext { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - var idAttribute = GetIdAttribute(resourceContext); + AttrAttribute idAttribute = GetIdAttribute(resourceContext); - var queryLayer = ComposeFromConstraints(resourceContext); + QueryLayer queryLayer = ComposeFromConstraints(resourceContext); queryLayer.Sort = null; queryLayer.Pagination = null; queryLayer.Filter = CreateFilterByIds(id.AsArray(), idAttribute, queryLayer.Filter); @@ -239,7 +235,7 @@ public QueryLayer ComposeSecondaryLayerForRelationship(ResourceContext secondary { ArgumentGuard.NotNull(secondaryResourceContext, nameof(secondaryResourceContext)); - var secondaryLayer = ComposeFromConstraints(secondaryResourceContext); + QueryLayer secondaryLayer = ComposeFromConstraints(secondaryResourceContext); secondaryLayer.Projection = GetProjectionForRelationship(secondaryResourceContext); secondaryLayer.Include = null; @@ -248,27 +244,31 @@ public QueryLayer ComposeSecondaryLayerForRelationship(ResourceContext secondary private IDictionary GetProjectionForRelationship(ResourceContext secondaryResourceContext) { - var secondaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(secondaryResourceContext); + IReadOnlyCollection secondaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(secondaryResourceContext); return secondaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); } /// - public QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, ResourceContext primaryResourceContext, TId primaryId, RelationshipAttribute secondaryRelationship) + public QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, ResourceContext primaryResourceContext, TId primaryId, + RelationshipAttribute secondaryRelationship) { ArgumentGuard.NotNull(secondaryLayer, nameof(secondaryLayer)); ArgumentGuard.NotNull(primaryResourceContext, nameof(primaryResourceContext)); ArgumentGuard.NotNull(secondaryRelationship, nameof(secondaryRelationship)); - var innerInclude = secondaryLayer.Include; + IncludeExpression innerInclude = secondaryLayer.Include; secondaryLayer.Include = null; - var primaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(primaryResourceContext); - var primaryProjection = primaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); + IReadOnlyCollection primaryAttributeSet = _sparseFieldSetCache.GetIdAttributeSetForRelationshipQuery(primaryResourceContext); + + Dictionary primaryProjection = + primaryAttributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); + primaryProjection[secondaryRelationship] = secondaryLayer; - var primaryFilter = GetFilter(Array.Empty(), primaryResourceContext); - var primaryIdAttribute = GetIdAttribute(primaryResourceContext); + FilterExpression primaryFilter = GetFilter(Array.Empty(), primaryResourceContext); + AttrAttribute primaryIdAttribute = GetIdAttribute(primaryResourceContext); return new QueryLayer(primaryResourceContext) { @@ -280,7 +280,7 @@ public QueryLayer WrapLayerForSecondaryEndpoint(QueryLayer secondaryLayer, private IncludeExpression RewriteIncludeForSecondaryEndpoint(IncludeExpression relativeInclude, RelationshipAttribute secondaryRelationship) { - var parentElement = relativeInclude != null + IncludeElementExpression parentElement = relativeInclude != null ? new IncludeElementExpression(secondaryRelationship, relativeInclude.Elements) : new IncludeElementExpression(secondaryRelationship); @@ -300,7 +300,7 @@ private FilterExpression CreateFilterByIds(ICollection ids, AttrAttrib } else if (ids.Count > 1) { - var constants = ids.Select(id => new LiteralConstantExpression(id.ToString())).ToList(); + List constants = ids.Select(id => new LiteralConstantExpression(id.ToString())).ToList(); filter = new EqualsAnyOfExpression(idChain, constants); } @@ -320,12 +320,12 @@ public QueryLayer ComposeForUpdate(TId id, ResourceContext primaryResource) { ArgumentGuard.NotNull(primaryResource, nameof(primaryResource)); - var includeElements = _targetedFields.Relationships + IncludeElementExpression[] includeElements = _targetedFields.Relationships .Select(relationship => new IncludeElementExpression(relationship)).ToArray(); - var primaryIdAttribute = GetIdAttribute(primaryResource); + AttrAttribute primaryIdAttribute = GetIdAttribute(primaryResource); - var primaryLayer = ComposeTopLayer(Array.Empty(), primaryResource); + QueryLayer primaryLayer = ComposeTopLayer(Array.Empty(), primaryResource); primaryLayer.Include = includeElements.Any() ? new IncludeExpression(includeElements) : IncludeExpression.Empty; primaryLayer.Sort = null; primaryLayer.Pagination = null; @@ -340,14 +340,14 @@ public QueryLayer ComposeForUpdate(TId id, ResourceContext primaryResource) { ArgumentGuard.NotNull(primaryResource, nameof(primaryResource)); - foreach (var relationship in _targetedFields.Relationships) + foreach (RelationshipAttribute relationship in _targetedFields.Relationships) { object rightValue = relationship.GetValue(primaryResource); ICollection rightResourceIds = TypeHelper.ExtractResources(rightValue); if (rightResourceIds.Any()) { - var queryLayer = ComposeForGetRelationshipRightIds(relationship, rightResourceIds); + QueryLayer queryLayer = ComposeForGetRelationshipRightIds(relationship, rightResourceIds); yield return (queryLayer, relationship); } } @@ -359,13 +359,13 @@ public QueryLayer ComposeForGetRelationshipRightIds(RelationshipAttribute relati ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); - var rightResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); - var rightIdAttribute = GetIdAttribute(rightResourceContext); + ResourceContext rightResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + AttrAttribute rightIdAttribute = GetIdAttribute(rightResourceContext); - var typedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); + object[] typedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); - var baseFilter = GetFilter(Array.Empty(), rightResourceContext); - var filter = CreateFilterByIds(typedIds, rightIdAttribute, baseFilter); + FilterExpression baseFilter = GetFilter(Array.Empty(), rightResourceContext); + FilterExpression filter = CreateFilterByIds(typedIds, rightIdAttribute, baseFilter); return new QueryLayer(rightResourceContext) { @@ -384,15 +384,15 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T ArgumentGuard.NotNull(hasManyRelationship, nameof(hasManyRelationship)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); - var leftResourceContext = _resourceContextProvider.GetResourceContext(hasManyRelationship.LeftType); - var leftIdAttribute = GetIdAttribute(leftResourceContext); + ResourceContext leftResourceContext = _resourceContextProvider.GetResourceContext(hasManyRelationship.LeftType); + AttrAttribute leftIdAttribute = GetIdAttribute(leftResourceContext); - var rightResourceContext = _resourceContextProvider.GetResourceContext(hasManyRelationship.RightType); - var rightIdAttribute = GetIdAttribute(rightResourceContext); - var rightTypedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); + ResourceContext rightResourceContext = _resourceContextProvider.GetResourceContext(hasManyRelationship.RightType); + AttrAttribute rightIdAttribute = GetIdAttribute(rightResourceContext); + object[] rightTypedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); - var leftFilter = CreateFilterByIds(leftId.AsArray(), leftIdAttribute, null); - var rightFilter = CreateFilterByIds(rightTypedIds, rightIdAttribute, null); + FilterExpression leftFilter = CreateFilterByIds(leftId.AsArray(), leftIdAttribute, null); + FilterExpression rightFilter = CreateFilterByIds(rightTypedIds, rightIdAttribute, null); return new QueryLayer(leftResourceContext) { @@ -413,7 +413,8 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T }; } - protected virtual IReadOnlyCollection GetIncludeElements(IReadOnlyCollection includeElements, ResourceContext resourceContext) + protected virtual IReadOnlyCollection GetIncludeElements(IReadOnlyCollection includeElements, + ResourceContext resourceContext) { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); @@ -425,11 +426,9 @@ protected virtual FilterExpression GetFilter(IReadOnlyCollection().ToArray(); + FilterExpression[] filters = expressionsInScope.OfType().ToArray(); - var filter = filters.Length > 1 - ? new LogicalExpression(LogicalOperator.And, filters) - : filters.FirstOrDefault(); + FilterExpression filter = filters.Length > 1 ? new LogicalExpression(LogicalOperator.And, filters) : filters.FirstOrDefault(); return _resourceDefinitionAccessor.OnApplyFilter(resourceContext.ResourceType, filter); } @@ -439,13 +438,13 @@ protected virtual SortExpression GetSort(IReadOnlyCollection ex ArgumentGuard.NotNull(expressionsInScope, nameof(expressionsInScope)); ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - var sort = expressionsInScope.OfType().FirstOrDefault(); + SortExpression sort = expressionsInScope.OfType().FirstOrDefault(); sort = _resourceDefinitionAccessor.OnApplySort(resourceContext.ResourceType, sort); if (sort == null) { - var idAttribute = GetIdAttribute(resourceContext); + AttrAttribute idAttribute = GetIdAttribute(resourceContext); sort = new SortExpression(new SortElementExpression(new ResourceFieldChainExpression(idAttribute), true).AsArray()); } @@ -457,7 +456,7 @@ protected virtual PaginationExpression GetPagination(IReadOnlyCollection().FirstOrDefault(); + PaginationExpression pagination = expressionsInScope.OfType().FirstOrDefault(); pagination = _resourceDefinitionAccessor.OnApplyPagination(resourceContext.ResourceType, pagination); @@ -470,14 +469,15 @@ protected virtual IDictionary GetProjectionF { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - var fieldSet = _sparseFieldSetCache.GetSparseFieldSetForQuery(resourceContext); + IReadOnlyCollection fieldSet = _sparseFieldSetCache.GetSparseFieldSetForQuery(resourceContext); + if (!fieldSet.Any()) { return null; } - var attributeSet = fieldSet.OfType().ToHashSet(); - var idAttribute = GetIdAttribute(resourceContext); + HashSet attributeSet = fieldSet.OfType().ToHashSet(); + AttrAttribute idAttribute = GetIdAttribute(resourceContext); attributeSet.Add(idAttribute); return attributeSet.ToDictionary(key => (ResourceFieldAttribute)key, _ => (QueryLayer)null); diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs index 2571b4931f..424ff9962e 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs @@ -10,7 +10,7 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// - /// Transforms into calls. + /// Transforms into calls. /// [PublicAPI] public class IncludeClauseBuilder : QueryClauseBuilder @@ -41,7 +41,7 @@ public Expression ApplyInclude(IncludeExpression include) public override Expression VisitInclude(IncludeExpression expression, object argument) { - var source = ApplyEagerLoads(_source, _resourceContext.EagerLoads, null); + Expression source = ApplyEagerLoads(_source, _resourceContext.EagerLoads, null); foreach (ResourceFieldChainExpression chain in IncludeChainConverter.GetRelationshipChains(expression)) { @@ -54,13 +54,13 @@ public override Expression VisitInclude(IncludeExpression expression, object arg private Expression ProcessRelationshipChain(ResourceFieldChainExpression chain, Expression source) { string path = null; - var result = source; + Expression result = source; - foreach (var relationship in chain.Fields.Cast()) + foreach (RelationshipAttribute relationship in chain.Fields.Cast()) { path = path == null ? relationship.RelationshipPath : path + "." + relationship.RelationshipPath; - var resourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); result = ApplyEagerLoads(result, resourceContext.EagerLoads, path); } @@ -69,9 +69,9 @@ private Expression ProcessRelationshipChain(ResourceFieldChainExpression chain, private Expression ApplyEagerLoads(Expression source, IEnumerable eagerLoads, string pathPrefix) { - var result = source; + Expression result = source; - foreach (var eagerLoad in eagerLoads) + foreach (EagerLoadAttribute eagerLoad in eagerLoads) { string path = pathPrefix != null ? pathPrefix + "." + eagerLoad.Property.Name : eagerLoad.Property.Name; result = IncludeExtensionMethodCall(result, path); diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/OrderClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/OrderClauseBuilder.cs index 2ddfb60e47..543a89d673 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/OrderClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/OrderClauseBuilder.cs @@ -9,7 +9,8 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// - /// Transforms into calls. + /// Transforms into + /// calls. /// [PublicAPI] public class OrderClauseBuilder : QueryClauseBuilder @@ -48,9 +49,7 @@ public override Expression VisitSort(SortExpression expression, Expression argum public override Expression VisitSortElement(SortElementExpression expression, Expression previousExpression) { - Expression body = expression.Count != null - ? Visit(expression.Count, null) - : Visit(expression.TargetAttribute, null); + Expression body = expression.Count != null ? Visit(expression.Count, null) : Visit(expression.TargetAttribute, null); LambdaExpression lambda = Expression.Lambda(body, LambdaScope.Parameter); @@ -69,16 +68,15 @@ private static string GetOperationName(bool hasPrecedingSort, bool isAscending) return isAscending ? "OrderBy" : "OrderByDescending"; } - private Expression ExtensionMethodCall(Expression source, string operationName, Type keyType, - LambdaExpression keySelector) + private Expression ExtensionMethodCall(Expression source, string operationName, Type keyType, LambdaExpression keySelector) { - var typeArguments = ArrayFactory.Create(LambdaScope.Parameter.Type, keyType); + Type[] typeArguments = ArrayFactory.Create(LambdaScope.Parameter.Type, keyType); return Expression.Call(_extensionType, operationName, typeArguments, source, keySelector); } protected override MemberExpression CreatePropertyExpressionForFieldChain(IReadOnlyCollection chain, Expression source) { - var components = chain.Select(GetPropertyName).ToArray(); + string[] components = chain.Select(GetPropertyName).ToArray(); return CreatePropertyExpressionFromComponents(LambdaScope.Accessor, components); } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs index 60909aa272..519a800cc2 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryClauseBuilder.cs @@ -9,7 +9,7 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// - /// Base class for transforming trees into system trees. + /// Base class for transforming trees into system trees. /// public abstract class QueryClauseBuilder : QueryExpressionVisitor { @@ -24,9 +24,10 @@ protected QueryClauseBuilder(LambdaScope lambdaScope) public override Expression VisitCount(CountExpression expression, TArgument argument) { - var collectionExpression = Visit(expression.TargetCollection, argument); + Expression collectionExpression = Visit(expression.TargetCollection, argument); + + Expression propertyExpression = TryGetCollectionCount(collectionExpression); - var propertyExpression = TryGetCollectionCount(collectionExpression); if (propertyExpression == null) { throw new InvalidOperationException($"Field '{expression.TargetCollection}' must be a collection."); @@ -38,15 +39,16 @@ public override Expression VisitCount(CountExpression expression, TArgument argu private static Expression TryGetCollectionCount(Expression collectionExpression) { var properties = new HashSet(collectionExpression.Type.GetProperties()); + if (collectionExpression.Type.IsInterface) { - foreach (var item in collectionExpression.Type.GetInterfaces().SelectMany(i => i.GetProperties())) + foreach (PropertyInfo item in collectionExpression.Type.GetInterfaces().SelectMany(i => i.GetProperties())) { properties.Add(item); } } - foreach (var property in properties) + foreach (PropertyInfo property in properties) { if (property.Name == "Count" || property.Name == "Length") { @@ -64,9 +66,9 @@ public override Expression VisitResourceFieldChain(ResourceFieldChainExpression protected virtual MemberExpression CreatePropertyExpressionForFieldChain(IReadOnlyCollection chain, Expression source) { - var components = chain.Select(field => - field is RelationshipAttribute relationship ? relationship.RelationshipPath : field.Property.Name).ToArray(); - + string[] components = chain.Select(field => field is RelationshipAttribute relationship ? relationship.RelationshipPath : field.Property.Name) + .ToArray(); + return CreatePropertyExpressionFromComponents(source, components); } @@ -74,19 +76,16 @@ protected static MemberExpression CreatePropertyExpressionFromComponents(Express { MemberExpression property = null; - foreach (var propertyName in components) + foreach (string propertyName in components) { Type parentType = property == null ? source.Type : property.Type; if (parentType.GetProperty(propertyName) == null) { - throw new InvalidOperationException( - $"Type '{parentType.Name}' does not contain a property named '{propertyName}'."); + throw new InvalidOperationException($"Type '{parentType.Name}' does not contain a property named '{propertyName}'."); } - property = property == null - ? Expression.Property(source, propertyName) - : Expression.Property(property, propertyName); + property = property == null ? Expression.Property(source, propertyName) : Expression.Property(property, propertyName); } return property; diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs index 2faa590dc1..4d4187b84f 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs @@ -12,7 +12,7 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// - /// Drives conversion from into system trees. + /// Drives conversion from into system trees. /// [PublicAPI] public class QueryableBuilder @@ -84,7 +84,7 @@ public virtual Expression ApplyQuery(QueryLayer layer) protected virtual Expression ApplyInclude(Expression source, IncludeExpression include, ResourceContext resourceContext) { - using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); + using LambdaScope lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); var builder = new IncludeClauseBuilder(source, lambdaScope, resourceContext, _resourceContextProvider); return builder.ApplyInclude(include); @@ -92,7 +92,7 @@ protected virtual Expression ApplyInclude(Expression source, IncludeExpression i protected virtual Expression ApplyFilter(Expression source, FilterExpression filter) { - using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); + using LambdaScope lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); var builder = new WhereClauseBuilder(source, lambdaScope, _extensionType); return builder.ApplyWhere(filter); @@ -100,7 +100,7 @@ protected virtual Expression ApplyFilter(Expression source, FilterExpression fil protected virtual Expression ApplySort(Expression source, SortExpression sort) { - using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); + using LambdaScope lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); var builder = new OrderClauseBuilder(source, lambdaScope, _extensionType); return builder.ApplyOrderBy(sort); @@ -108,15 +108,16 @@ protected virtual Expression ApplySort(Expression source, SortExpression sort) protected virtual Expression ApplyPagination(Expression source, PaginationExpression pagination) { - using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); + using LambdaScope lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); var builder = new SkipTakeClauseBuilder(source, lambdaScope, _extensionType); return builder.ApplySkipTake(pagination); } - protected virtual Expression ApplyProjection(Expression source, IDictionary projection, ResourceContext resourceContext) + protected virtual Expression ApplyProjection(Expression source, IDictionary projection, + ResourceContext resourceContext) { - using var lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); + using LambdaScope lambdaScope = _lambdaScopeFactory.CreateScope(_elementType); var builder = new SelectClauseBuilder(source, lambdaScope, _entityModel, _extensionType, _nameFactory, _resourceFactory, _resourceContextProvider); return builder.ApplySelect(projection, resourceContext); diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs index 09f1d3c3ed..79545277ef 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs @@ -15,7 +15,8 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// - /// Transforms into calls. + /// Transforms into + /// calls. /// [PublicAPI] public class SelectClauseBuilder : QueryClauseBuilder @@ -29,8 +30,8 @@ public class SelectClauseBuilder : QueryClauseBuilder private readonly IResourceFactory _resourceFactory; private readonly IResourceContextProvider _resourceContextProvider; - public SelectClauseBuilder(Expression source, LambdaScope lambdaScope, IModel entityModel, Type extensionType, - LambdaParameterNameFactory nameFactory, IResourceFactory resourceFactory, IResourceContextProvider resourceContextProvider) + public SelectClauseBuilder(Expression source, LambdaScope lambdaScope, IModel entityModel, Type extensionType, LambdaParameterNameFactory nameFactory, + IResourceFactory resourceFactory, IResourceContextProvider resourceContextProvider) : base(lambdaScope) { ArgumentGuard.NotNull(source, nameof(source)); @@ -67,8 +68,10 @@ public Expression ApplySelect(IDictionary se private Expression CreateLambdaBodyInitializer(IDictionary selectors, ResourceContext resourceContext, LambdaScope lambdaScope, bool lambdaAccessorRequiresTestForNull) { - var propertySelectors = ToPropertySelectors(selectors, resourceContext, lambdaScope.Accessor.Type); - MemberBinding[] propertyAssignments = propertySelectors.Select(selector => CreatePropertyAssignment(selector, lambdaScope)).Cast().ToArray(); + ICollection propertySelectors = ToPropertySelectors(selectors, resourceContext, lambdaScope.Accessor.Type); + + MemberBinding[] propertyAssignments = + propertySelectors.Select(selector => CreatePropertyAssignment(selector, lambdaScope)).Cast().ToArray(); NewExpression newExpression = _resourceFactory.CreateNewExpression(lambdaScope.Accessor.Type); Expression memberInit = Expression.MemberInit(newExpression, propertyAssignments); @@ -89,10 +92,10 @@ private Expression CreateLambdaBodyInitializer(IDictionary ToPropertySelectors(IDictionary resourceFieldSelectors, + private ICollection ToPropertySelectors(IDictionary resourceFieldSelectors, ResourceContext resourceContext, Type elementType) { - Dictionary propertySelectors = new Dictionary(); + var propertySelectors = new Dictionary(); // If a read-only attribute is selected, its value likely depends on another property, so select all resource properties. bool includesReadOnlyAttribute = resourceFieldSelectors.Any(selector => @@ -100,9 +103,10 @@ private ICollection ToPropertySelectors(IDictionary selector.Key is RelationshipAttribute); - foreach (var fieldSelector in resourceFieldSelectors) + foreach (KeyValuePair fieldSelector in resourceFieldSelectors) { var propertySelector = new PropertySelector(fieldSelector.Key, fieldSelector.Value); + if (propertySelector.Property.SetMethod != null) { propertySelectors[propertySelector.Property] = propertySelector; @@ -111,12 +115,13 @@ private ICollection ToPropertySelectors(IDictionary type.ClrType == elementType); + IEntityType entityModel = _entityModel.GetEntityTypes().Single(type => type.ClrType == elementType); IEnumerable entityProperties = entityModel.GetProperties().Where(p => !p.IsShadowProperty()).ToArray(); - foreach (var entityProperty in entityProperties) + foreach (IProperty entityProperty in entityProperties) { var propertySelector = new PropertySelector(entityProperty.PropertyInfo); + if (propertySelector.Property.SetMethod != null) { propertySelectors[propertySelector.Property] = propertySelector; @@ -124,7 +129,7 @@ private ICollection ToPropertySelectors(IDictionary - /// Transforms into and calls. + /// Transforms into and calls. /// [PublicAPI] public class SkipTakeClauseBuilder : QueryClauseBuilder diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs index 25da976355..6ce2a920b2 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/WhereClauseBuilder.cs @@ -11,7 +11,8 @@ namespace JsonApiDotNetCore.Queries.Internal.QueryableBuilding { /// - /// Transforms into calls. + /// Transforms into + /// calls. /// [PublicAPI] public class WhereClauseBuilder : QueryClauseBuilder @@ -127,8 +128,7 @@ public override Expression VisitLogical(LogicalExpression expression, Type argum throw new InvalidOperationException($"Unknown logical operator '{expression.Operator}'."); } - private static BinaryExpression Compose(Queue argumentQueue, - Func applyOperator) + private static BinaryExpression Compose(Queue argumentQueue, Func applyOperator) { Expression left = argumentQueue.Dequeue(); Expression right = argumentQueue.Dequeue(); @@ -186,7 +186,7 @@ public override Expression VisitComparison(ComparisonExpression expression, Type private Type TryResolveCommonType(QueryExpression left, QueryExpression right) { - var leftType = ResolveFixedType(left); + Type leftType = ResolveFixedType(left); if (TypeHelper.CanContainNull(leftType)) { @@ -198,7 +198,8 @@ private Type TryResolveCommonType(QueryExpression left, QueryExpression right) return typeof(Nullable<>).MakeGenericType(leftType); } - var rightType = TryResolveFixedType(right); + Type rightType = TryResolveFixedType(right); + if (rightType != null && TypeHelper.CanContainNull(rightType)) { return rightType; @@ -209,7 +210,7 @@ private Type TryResolveCommonType(QueryExpression left, QueryExpression right) private Type ResolveFixedType(QueryExpression expression) { - var result = Visit(expression, null); + Expression result = Visit(expression, null); return result.Type; } @@ -233,9 +234,7 @@ private static Expression WrapInConvert(Expression expression, Type targetType) { try { - return targetType != null && expression.Type != targetType - ? Expression.Convert(expression, targetType) - : expression; + return targetType != null && expression.Type != targetType ? Expression.Convert(expression, targetType) : expression; } catch (InvalidOperationException exception) { @@ -250,9 +249,7 @@ public override Expression VisitNullConstant(NullConstantExpression expression, public override Expression VisitLiteralConstant(LiteralConstantExpression expression, Type expressionType) { - var convertedValue = expressionType != null - ? ConvertTextToTargetType(expression.Value, expressionType) - : expression.Value; + object convertedValue = expressionType != null ? ConvertTextToTargetType(expression.Value, expressionType) : expression.Value; return CreateTupleAccessExpressionForConstant(convertedValue, expressionType ?? typeof(string)); } @@ -271,7 +268,7 @@ private static object ConvertTextToTargetType(string text, Type targetType) protected override MemberExpression CreatePropertyExpressionForFieldChain(IReadOnlyCollection chain, Expression source) { - var components = chain.Select(GetPropertyName).ToArray(); + string[] components = chain.Select(GetPropertyName).ToArray(); return CreatePropertyExpressionFromComponents(LambdaScope.Accessor, components); } diff --git a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs index 9f355696d3..35b7f4ac3f 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs @@ -10,7 +10,8 @@ namespace JsonApiDotNetCore.Queries.Internal { /// - /// Takes sparse fieldsets from s and invokes on them. + /// Takes sparse fieldsets from s and invokes + /// on them. /// [PublicAPI] public sealed class SparseFieldSetCache @@ -34,7 +35,7 @@ private static IDictionary> Bui // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var sparseFieldTables = constraintProviders + KeyValuePair[] sparseFieldTables = constraintProviders .SelectMany(provider => provider.GetConstraints()) .Where(constraint => constraint.Scope == null) .Select(constraint => constraint.Expression) @@ -48,7 +49,7 @@ private static IDictionary> Bui var mergedTable = new Dictionary>(); - foreach (var (resourceContext, sparseFieldSet) in sparseFieldTables) + foreach ((ResourceContext resourceContext, SparseFieldSetExpression sparseFieldSet) in sparseFieldTables) { if (!mergedTable.ContainsKey(resourceContext)) { @@ -61,10 +62,9 @@ private static IDictionary> Bui return mergedTable; } - private static void AddSparseFieldsToSet(IReadOnlyCollection sparseFieldsToAdd, - HashSet sparseFieldSet) + private static void AddSparseFieldsToSet(IReadOnlyCollection sparseFieldsToAdd, HashSet sparseFieldSet) { - foreach (var field in sparseFieldsToAdd) + foreach (ResourceFieldAttribute field in sparseFieldsToAdd) { sparseFieldSet.Add(field); } @@ -76,13 +76,13 @@ public IReadOnlyCollection GetSparseFieldSetForQuery(Res if (!_visitedTable.ContainsKey(resourceContext)) { - var inputExpression = _lazySourceTable.Value.ContainsKey(resourceContext) + SparseFieldSetExpression inputExpression = _lazySourceTable.Value.ContainsKey(resourceContext) ? new SparseFieldSetExpression(_lazySourceTable.Value[resourceContext]) : null; - var outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); + SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); - var outputFields = outputExpression == null + HashSet outputFields = outputExpression == null ? new HashSet() : outputExpression.Fields.ToHashSet(); @@ -96,13 +96,13 @@ public IReadOnlyCollection GetIdAttributeSetForRelationshipQuery( { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - var idAttribute = resourceContext.Attributes.Single(attr => attr.Property.Name == nameof(Identifiable.Id)); + AttrAttribute idAttribute = resourceContext.Attributes.Single(attr => attr.Property.Name == nameof(Identifiable.Id)); var inputExpression = new SparseFieldSetExpression(idAttribute.AsArray()); // Intentionally not cached, as we are fetching ID only (ignoring any sparse fieldset that came from query string). - var outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); + SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); - var outputAttributes = outputExpression == null + HashSet outputAttributes = outputExpression == null ? new HashSet() : outputExpression.Fields.OfType().ToHashSet(); @@ -116,14 +116,15 @@ public IReadOnlyCollection GetSparseFieldSetForSerialize if (!_visitedTable.ContainsKey(resourceContext)) { - var inputFields = _lazySourceTable.Value.ContainsKey(resourceContext) + HashSet inputFields = _lazySourceTable.Value.ContainsKey(resourceContext) ? _lazySourceTable.Value[resourceContext] : GetResourceFields(resourceContext); var inputExpression = new SparseFieldSetExpression(inputFields); - var outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); + SparseFieldSetExpression outputExpression = _resourceDefinitionAccessor.OnApplySparseFieldSet(resourceContext.ResourceType, inputExpression); HashSet outputFields; + if (outputExpression == null) { outputFields = GetResourceFields(resourceContext); @@ -146,12 +147,12 @@ private HashSet GetResourceFields(ResourceContext resour var fieldSet = new HashSet(); - foreach (var attribute in resourceContext.Attributes.Where(attr => attr.Capabilities.HasFlag(AttrCapabilities.AllowView))) + foreach (AttrAttribute attribute in resourceContext.Attributes.Where(attr => attr.Capabilities.HasFlag(AttrCapabilities.AllowView))) { fieldSet.Add(attribute); } - foreach (var relationship in resourceContext.Relationships) + foreach (RelationshipAttribute relationship in resourceContext.Relationships) { fieldSet.Add(relationship); } diff --git a/src/JsonApiDotNetCore/Queries/PaginationContext.cs b/src/JsonApiDotNetCore/Queries/PaginationContext.cs index 0ca1e25076..beb760555c 100644 --- a/src/JsonApiDotNetCore/Queries/PaginationContext.cs +++ b/src/JsonApiDotNetCore/Queries/PaginationContext.cs @@ -19,8 +19,7 @@ internal sealed class PaginationContext : IPaginationContext public int? TotalResourceCount { get; set; } /// - public int? TotalPageCount => TotalResourceCount == null || PageSize == null - ? null - : (int?) Math.Ceiling((decimal) TotalResourceCount.Value / PageSize.Value); + public int? TotalPageCount => + TotalResourceCount == null || PageSize == null ? null : (int?)Math.Ceiling((decimal)TotalResourceCount.Value / PageSize.Value); } } diff --git a/src/JsonApiDotNetCore/Queries/QueryLayer.cs b/src/JsonApiDotNetCore/Queries/QueryLayer.cs index c50146a329..dfc022f164 100644 --- a/src/JsonApiDotNetCore/Queries/QueryLayer.cs +++ b/src/JsonApiDotNetCore/Queries/QueryLayer.cs @@ -10,7 +10,7 @@ namespace JsonApiDotNetCore.Queries { /// - /// A nested data structure that contains constraints per resource type. + /// A nested data structure that contains constraints per resource type. /// [PublicAPI] public sealed class QueryLayer @@ -69,9 +69,10 @@ private static void WriteLayer(IndentingStringWriter writer, QueryLayer layer, s if (layer.Projection != null && layer.Projection.Any()) { writer.WriteLine(nameof(Projection)); + using (writer.Indent()) { - foreach (var (field, nextLayer) in layer.Projection) + foreach ((ResourceFieldAttribute field, QueryLayer nextLayer) in layer.Projection) { if (nextLayer == null) { diff --git a/src/JsonApiDotNetCore/QueryStrings/IQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IQueryStringParameterReader.cs index b07399b98c..a7688bf8f7 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IQueryStringParameterReader.cs @@ -9,7 +9,7 @@ namespace JsonApiDotNetCore.QueryStrings public interface IQueryStringParameterReader { /// - /// Indicates whether usage of this query string parameter is blocked using on a controller. + /// Indicates whether usage of this query string parameter is blocked using on a controller. /// bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute); diff --git a/src/JsonApiDotNetCore/QueryStrings/IQueryStringReader.cs b/src/JsonApiDotNetCore/QueryStrings/IQueryStringReader.cs index c26b14bd1e..39c07ec036 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IQueryStringReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IQueryStringReader.cs @@ -11,7 +11,7 @@ public interface IQueryStringReader /// Reads and processes the key/value pairs from the request query string. /// /// - /// The if set on the controller that is targeted by the current request. + /// The if set on the controller that is targeted by the current request. /// void ReadAll(DisableQueryStringAttribute disableQueryStringAttribute); } diff --git a/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs index 58e1f18d40..baea3e2938 100644 --- a/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/IResourceDefinitionQueryableParameterReader.cs @@ -5,8 +5,8 @@ namespace JsonApiDotNetCore.QueryStrings { /// - /// Reads custom query string parameters for which handlers on are registered - /// and produces a set of query constraints from it. + /// Reads custom query string parameters for which handlers on are registered and produces a set of + /// query constraints from it. /// [PublicAPI] public interface IResourceDefinitionQueryableParameterReader : IQueryStringParameterReader, IQueryConstraintProvider diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs index bf88cfb9e5..0faa4d6ff4 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs @@ -30,7 +30,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); return _options.AllowQueryStringOverrideForSerializerDefaultValueHandling && - !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Defaults); + !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Defaults); } /// @@ -42,10 +42,9 @@ public virtual bool CanRead(string parameterName) /// public virtual void Read(string parameterName, StringValues parameterValue) { - if (!bool.TryParse(parameterValue, out var result)) + if (!bool.TryParse(parameterValue, out bool result)) { - throw new InvalidQueryStringParameterException(parameterName, - "The specified defaults is invalid.", + throw new InvalidQueryStringParameterException(parameterName, "The specified defaults is invalid.", $"The value '{parameterValue}' must be 'true' or 'false'."); } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs index 3565f9beb1..c39c5e6f47 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs @@ -25,11 +25,14 @@ public class FilterQueryStringParameterReader : QueryStringParameterReader, IFil private readonly FilterParser _filterParser; private readonly List _filtersInGlobalScope = new List(); - private readonly Dictionary> _filtersPerScope = new Dictionary>(); + + private readonly Dictionary> _filtersPerScope = + new Dictionary>(); + private string _lastParameterName; - public FilterQueryStringParameterReader(IJsonApiRequest request, - IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, IJsonApiOptions options) + public FilterQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, + IJsonApiOptions options) : base(request, resourceContextProvider) { ArgumentGuard.NotNull(options, nameof(options)); @@ -53,8 +56,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr { ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - return !IsAtomicOperationsRequest && - !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Filter); + return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Filter); } /// @@ -62,7 +64,7 @@ public virtual bool CanRead(string parameterName) { ArgumentGuard.NotNull(parameterName, nameof(parameterName)); - var isNested = parameterName.StartsWith("filter[", StringComparison.Ordinal) && parameterName.EndsWith("]", StringComparison.Ordinal); + bool isNested = parameterName.StartsWith("filter[", StringComparison.Ordinal) && parameterName.EndsWith("]", StringComparison.Ordinal); return parameterName == "filter" || isNested; } @@ -117,7 +119,7 @@ private void ReadSingleValue(string parameterName, string parameterValue) private ResourceFieldChainExpression GetScope(string parameterName) { - var parameterScope = _scopeParser.Parse(parameterName, RequestResource); + QueryStringParameterScopeExpression parameterScope = _scopeParser.Parse(parameterName, RequestResource); if (parameterScope.Scope == null) { @@ -160,13 +162,13 @@ private IEnumerable EnumerateFiltersInScopes() { if (_filtersInGlobalScope.Any()) { - var filter = MergeFilters(_filtersInGlobalScope); + FilterExpression filter = MergeFilters(_filtersInGlobalScope); yield return new ExpressionInScope(null, filter); } - foreach (var (scope, filters) in _filtersPerScope) + foreach ((ResourceFieldChainExpression scope, List filters) in _filtersPerScope) { - var filter = MergeFilters(filters); + FilterExpression filter = MergeFilters(filters); yield return new ExpressionInScope(scope, filter); } } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs index f9dc9d30d7..c0fbb4a60d 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs @@ -34,8 +34,7 @@ protected void ValidateSingleRelationship(RelationshipAttribute relationship, Re { if (!relationship.CanInclude) { - throw new InvalidQueryStringParameterException(_lastParameterName, - "Including the requested relationship is not allowed.", + throw new InvalidQueryStringParameterException(_lastParameterName, "Including the requested relationship is not allowed.", path == relationship.PublicName ? $"Including the relationship '{relationship.PublicName}' on '{resourceContext.PublicName}' is not allowed." : $"Including the relationship '{relationship.PublicName}' in '{path}' on '{resourceContext.PublicName}' is not allowed."); @@ -47,8 +46,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr { ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - return !IsAtomicOperationsRequest && - !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Include); + return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Include); } /// @@ -68,8 +66,7 @@ public virtual void Read(string parameterName, StringValues parameterValue) } catch (QueryParseException exception) { - throw new InvalidQueryStringParameterException(parameterName, "The specified include is invalid.", - exception.Message, exception); + throw new InvalidQueryStringParameterException(parameterName, "The specified include is invalid.", exception.Message, exception); } } @@ -81,7 +78,7 @@ private IncludeExpression GetInclude(string parameterValue) /// public virtual IReadOnlyCollection GetConstraints() { - var expressionInScope = _includeExpression != null + ExpressionInScope expressionInScope = _includeExpression != null ? new ExpressionInScope(null, _includeExpression) : new ExpressionInScope(null, IncludeExpression.Empty); diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs index ae8e7a2230..ff4a47f845 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs @@ -31,8 +31,7 @@ public IEnumerable ExtractConditions(string parameterValue) { ArgumentGuard.NotNull(parameterValue, nameof(parameterValue)); - if (parameterValue.StartsWith(ExpressionPrefix, StringComparison.Ordinal) || - parameterValue.StartsWith(InPrefix, StringComparison.Ordinal) || + if (parameterValue.StartsWith(ExpressionPrefix, StringComparison.Ordinal) || parameterValue.StartsWith(InPrefix, StringComparison.Ordinal) || parameterValue.StartsWith(NotInPrefix, StringComparison.Ordinal)) { yield return parameterValue; @@ -57,13 +56,13 @@ public IEnumerable ExtractConditions(string parameterValue) return (parameterName, expression); } - var attributeName = ExtractAttributeName(parameterName); + string attributeName = ExtractAttributeName(parameterName); - foreach (var (prefix, keyword) in PrefixConversionTable) + foreach ((string prefix, string keyword) in PrefixConversionTable) { if (parameterValue.StartsWith(prefix, StringComparison.Ordinal)) { - var value = parameterValue.Substring(prefix.Length); + string value = parameterValue.Substring(prefix.Length); string escapedValue = EscapeQuotes(value); string expression = $"{keyword}({attributeName},'{escapedValue}')"; @@ -73,7 +72,7 @@ public IEnumerable ExtractConditions(string parameterValue) if (parameterValue.StartsWith(NotEqualsPrefix, StringComparison.Ordinal)) { - var value = parameterValue.Substring(NotEqualsPrefix.Length); + string value = parameterValue.Substring(NotEqualsPrefix.Length); string escapedValue = EscapeQuotes(value); string expression = $"{Keywords.Not}({Keywords.Equals}({attributeName},'{escapedValue}'))"; @@ -83,7 +82,7 @@ public IEnumerable ExtractConditions(string parameterValue) if (parameterValue.StartsWith(InPrefix, StringComparison.Ordinal)) { string[] valueParts = parameterValue.Substring(InPrefix.Length).Split(","); - var valueList = "'" + string.Join("','", valueParts) + "'"; + string valueList = "'" + string.Join("','", valueParts) + "'"; string expression = $"{Keywords.Any}({attributeName},{valueList})"; return (OutputParameterName, expression); @@ -92,7 +91,7 @@ public IEnumerable ExtractConditions(string parameterValue) if (parameterValue.StartsWith(NotInPrefix, StringComparison.Ordinal)) { string[] valueParts = parameterValue.Substring(NotInPrefix.Length).Split(","); - var valueList = "'" + string.Join("','", valueParts) + "'"; + string valueList = "'" + string.Join("','", valueParts) + "'"; string expression = $"{Keywords.Not}({Keywords.Any}({attributeName},{valueList}))"; return (OutputParameterName, expression); @@ -120,7 +119,8 @@ public IEnumerable ExtractConditions(string parameterValue) private static string ExtractAttributeName(string parameterName) { - if (parameterName.StartsWith(ParameterNamePrefix, StringComparison.Ordinal) && parameterName.EndsWith(ParameterNameSuffix, StringComparison.Ordinal)) + if (parameterName.StartsWith(ParameterNamePrefix, StringComparison.Ordinal) && + parameterName.EndsWith(ParameterNameSuffix, StringComparison.Ordinal)) { string attributeName = parameterName.Substring(ParameterNamePrefix.Length, parameterName.Length - ParameterNamePrefix.Length - ParameterNameSuffix.Length); diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs index 98658bd356..3f20c6ba2a 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs @@ -30,7 +30,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); return _options.AllowQueryStringOverrideForSerializerNullValueHandling && - !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Nulls); + !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Nulls); } /// @@ -42,10 +42,9 @@ public virtual bool CanRead(string parameterName) /// public virtual void Read(string parameterName, StringValues parameterValue) { - if (!bool.TryParse(parameterValue, out var result)) + if (!bool.TryParse(parameterValue, out bool result)) { - throw new InvalidQueryStringParameterException(parameterName, - "The specified nulls is invalid.", + throw new InvalidQueryStringParameterException(parameterName, "The specified nulls is invalid.", $"The value '{parameterValue}' must be 'true' or 'false'."); } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs index 0eb3a71b49..c9b9813a5c 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs @@ -39,8 +39,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr { ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - return !IsAtomicOperationsRequest && - !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Page); + return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Page); } /// @@ -54,7 +53,7 @@ public virtual void Read(string parameterName, StringValues parameterValue) { try { - var constraint = GetPageConstraint(parameterValue); + PaginationQueryStringValueExpression constraint = GetPageConstraint(parameterValue); if (constraint.Elements.Any(element => element.Scope == null)) { @@ -107,8 +106,7 @@ protected virtual void ValidatePageSize(PaginationQueryStringValueExpression con [AssertionMethod] protected virtual void ValidatePageNumber(PaginationQueryStringValueExpression constraint) { - if (_options.MaximumPageNumber != null && - constraint.Elements.Any(element => element.Value > _options.MaximumPageNumber.OneBasedValue)) + if (_options.MaximumPageNumber != null && constraint.Elements.Any(element => element.Value > _options.MaximumPageNumber.OneBasedValue)) { throw new QueryParseException($"Page number cannot be higher than {_options.MaximumPageNumber}."); } @@ -124,16 +122,18 @@ public virtual IReadOnlyCollection GetConstraints() { var context = new PaginationContext(); - foreach (var element in _pageSizeConstraint?.Elements ?? Array.Empty()) + foreach (PaginationElementQueryStringValueExpression element in _pageSizeConstraint?.Elements ?? + Array.Empty()) { - var entry = context.ResolveEntryInScope(element.Scope); + MutablePaginationEntry entry = context.ResolveEntryInScope(element.Scope); entry.PageSize = element.Value == 0 ? null : new PageSize(element.Value); entry.HasSetPageSize = true; } - foreach (var element in _pageNumberConstraint?.Elements ?? Array.Empty()) + foreach (PaginationElementQueryStringValueExpression element in _pageNumberConstraint?.Elements ?? + Array.Empty()) { - var entry = context.ResolveEntryInScope(element.Scope); + MutablePaginationEntry entry = context.ResolveEntryInScope(element.Scope); entry.PageNumber = new PageNumber(element.Value); } @@ -145,7 +145,9 @@ public virtual IReadOnlyCollection GetConstraints() private sealed class PaginationContext { private readonly MutablePaginationEntry _globalScope = new MutablePaginationEntry(); - private readonly Dictionary _nestedScopes = new Dictionary(); + + private readonly Dictionary _nestedScopes = + new Dictionary(); public MutablePaginationEntry ResolveEntryInScope(ResourceFieldChainExpression scope) { @@ -166,7 +168,7 @@ public void ApplyOptions(IJsonApiOptions options) { ApplyOptionsInEntry(_globalScope, options); - foreach (var (_, entry) in _nestedScopes) + foreach ((_, MutablePaginationEntry entry) in _nestedScopes) { ApplyOptionsInEntry(entry, options); } @@ -191,7 +193,7 @@ private IEnumerable EnumerateExpressionsInScope() { yield return new ExpressionInScope(null, new PaginationExpression(_globalScope.PageNumber, _globalScope.PageSize)); - foreach (var (scope, entry) in _nestedScopes) + foreach ((ResourceFieldChainExpression scope, MutablePaginationEntry entry) in _nestedScopes) { yield return new ExpressionInScope(scope, new PaginationExpression(entry.PageNumber, entry.PageSize)); } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs index 496c179fcc..79c698627e 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; @@ -33,8 +34,8 @@ protected ResourceContext GetResourceContextForScope(ResourceFieldChainExpressio return RequestResource; } - var lastField = scope.Fields.Last(); - var type = lastField is RelationshipAttribute relationship ? relationship.RightType : lastField.Property.PropertyType; + ResourceFieldAttribute lastField = scope.Fields.Last(); + Type type = lastField is RelationshipAttribute relationship ? relationship.RightType : lastField.Property.PropertyType; return _resourceContextProvider.GetResourceContext(type); } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs index b7528624d2..ce7ac0288d 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Controllers.Annotations; using JsonApiDotNetCore.Errors; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; namespace JsonApiDotNetCore.QueryStrings.Internal { @@ -34,22 +35,21 @@ public QueryStringReader(IJsonApiOptions options, IRequestQueryStringAccessor qu /// public virtual void ReadAll(DisableQueryStringAttribute disableQueryStringAttribute) { - var disableQueryStringAttributeNotNull = disableQueryStringAttribute ?? DisableQueryStringAttribute.Empty; + DisableQueryStringAttribute disableQueryStringAttributeNotNull = disableQueryStringAttribute ?? DisableQueryStringAttribute.Empty; - foreach (var (parameterName, parameterValue) in _queryStringAccessor.Query) + foreach ((string parameterName, StringValues parameterValue) in _queryStringAccessor.Query) { if (string.IsNullOrEmpty(parameterValue)) { - throw new InvalidQueryStringParameterException(parameterName, - "Missing query string parameter value.", + throw new InvalidQueryStringParameterException(parameterName, "Missing query string parameter value.", $"Missing value for '{parameterName}' query string parameter."); } - var reader = _parameterReaders.FirstOrDefault(r => r.CanRead(parameterName)); + IQueryStringParameterReader reader = _parameterReaders.FirstOrDefault(r => r.CanRead(parameterName)); + if (reader != null) { - _logger.LogDebug( - $"Query string parameter '{parameterName}' with value '{parameterValue}' was accepted by {reader.GetType().Name}."); + _logger.LogDebug($"Query string parameter '{parameterName}' with value '{parameterValue}' was accepted by {reader.GetType().Name}."); if (!reader.IsEnabled(disableQueryStringAttributeNotNull)) { diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs index 868551fa26..b73777855f 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using JetBrains.Annotations; using JsonApiDotNetCore.Controllers.Annotations; @@ -41,27 +42,26 @@ public virtual bool CanRead(string parameterName) return false; } - var queryableHandler = GetQueryableHandler(parameterName); + object queryableHandler = GetQueryableHandler(parameterName); return queryableHandler != null; } /// public virtual void Read(string parameterName, StringValues parameterValue) { - var queryableHandler = GetQueryableHandler(parameterName); + object queryableHandler = GetQueryableHandler(parameterName); var expressionInScope = new ExpressionInScope(null, new QueryableHandlerExpression(queryableHandler, parameterValue)); _constraints.Add(expressionInScope); } private object GetQueryableHandler(string parameterName) { - var resourceType = _request.PrimaryResource.ResourceType; - var handler = _resourceDefinitionAccessor.GetQueryableHandlerForQueryStringParameter(resourceType, parameterName); + Type resourceType = _request.PrimaryResource.ResourceType; + object handler = _resourceDefinitionAccessor.GetQueryableHandlerForQueryStringParameter(resourceType, parameterName); if (handler != null && _request.Kind != EndpointKind.Primary) { - throw new InvalidQueryStringParameterException(parameterName, - "Custom query string parameters cannot be used on nested resource endpoints.", + throw new InvalidQueryStringParameterException(parameterName, "Custom query string parameters cannot be used on nested resource endpoints.", $"Query string parameter '{parameterName}' cannot be used on a nested resource endpoint."); } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs index e9ca858acf..0271d3f95d 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs @@ -42,8 +42,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr { ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - return !IsAtomicOperationsRequest && - !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Sort); + return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Sort); } /// @@ -51,7 +50,7 @@ public virtual bool CanRead(string parameterName) { ArgumentGuard.NotNull(parameterName, nameof(parameterName)); - var isNested = parameterName.StartsWith("sort[", StringComparison.Ordinal) && parameterName.EndsWith("]", StringComparison.Ordinal); + bool isNested = parameterName.StartsWith("sort[", StringComparison.Ordinal) && parameterName.EndsWith("]", StringComparison.Ordinal); return parameterName == "sort" || isNested; } @@ -76,7 +75,7 @@ public virtual void Read(string parameterName, StringValues parameterValue) private ResourceFieldChainExpression GetScope(string parameterName) { - var parameterScope = _scopeParser.Parse(parameterName, RequestResource); + QueryStringParameterScopeExpression parameterScope = _scopeParser.Parse(parameterName, RequestResource); if (parameterScope.Scope == null) { diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs index b1cc88afdc..6a0cbf9244 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs @@ -43,8 +43,7 @@ public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttr { ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - return !IsAtomicOperationsRequest && - !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Fields); + return !IsAtomicOperationsRequest && !disableQueryStringAttribute.ContainsParameter(StandardQueryStringParameters.Fields); } /// @@ -62,15 +61,14 @@ public virtual void Read(string parameterName, StringValues parameterValue) try { - var targetResource = GetSparseFieldType(parameterName); - var sparseFieldSet = GetSparseFieldSet(parameterValue, targetResource); + ResourceContext targetResource = GetSparseFieldType(parameterName); + SparseFieldSetExpression sparseFieldSet = GetSparseFieldSet(parameterValue, targetResource); _sparseFieldTable[targetResource] = sparseFieldSet; } catch (QueryParseException exception) { - throw new InvalidQueryStringParameterException(parameterName, "The specified fieldset is invalid.", - exception.Message, exception); + throw new InvalidQueryStringParameterException(parameterName, "The specified fieldset is invalid.", exception.Message, exception); } } diff --git a/src/JsonApiDotNetCore/QueryStrings/StandardQueryStringParameters.cs b/src/JsonApiDotNetCore/QueryStrings/StandardQueryStringParameters.cs index f488e823f6..521a9af37b 100644 --- a/src/JsonApiDotNetCore/QueryStrings/StandardQueryStringParameters.cs +++ b/src/JsonApiDotNetCore/QueryStrings/StandardQueryStringParameters.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCore.QueryStrings { /// - /// Lists query string parameters used by . + /// Lists query string parameters used by . /// [Flags] public enum StandardQueryStringParameters diff --git a/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs b/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs index 5adfbc2c8f..a6d95560a2 100644 --- a/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs +++ b/src/JsonApiDotNetCore/Repositories/DbContextExtensions.cs @@ -12,7 +12,7 @@ namespace JsonApiDotNetCore.Repositories public static class DbContextExtensions { /// - /// If not already tracked, attaches the specified resource to the change tracker in state. + /// If not already tracked, attaches the specified resource to the change tracker in state. /// public static IIdentifiable GetTrackedOrAttach(this DbContext dbContext, IIdentifiable resource) { @@ -20,6 +20,7 @@ public static IIdentifiable GetTrackedOrAttach(this DbContext dbContext, IIdenti ArgumentGuard.NotNull(resource, nameof(resource)); var trackedIdentifiable = (IIdentifiable)dbContext.GetTrackedIdentifiable(resource); + if (trackedIdentifiable == null) { dbContext.Entry(resource).State = EntityState.Unchanged; @@ -30,25 +31,24 @@ public static IIdentifiable GetTrackedOrAttach(this DbContext dbContext, IIdenti } /// - /// Searches the change tracker for an entity that matches the type and ID of . + /// Searches the change tracker for an entity that matches the type and ID of . /// public static object GetTrackedIdentifiable(this DbContext dbContext, IIdentifiable identifiable) { ArgumentGuard.NotNull(dbContext, nameof(dbContext)); ArgumentGuard.NotNull(identifiable, nameof(identifiable)); - var resourceType = identifiable.GetType(); + Type resourceType = identifiable.GetType(); string stringId = identifiable.StringId; - var entityEntry = dbContext.ChangeTracker.Entries() - .FirstOrDefault(entry => IsResource(entry, resourceType, stringId)); + EntityEntry entityEntry = dbContext.ChangeTracker.Entries().FirstOrDefault(entry => IsResource(entry, resourceType, stringId)); return entityEntry?.Entity; } private static bool IsResource(EntityEntry entry, Type resourceType, string stringId) { - return entry.Entity.GetType() == resourceType && ((IIdentifiable) entry.Entity).StringId == stringId; + return entry.Entity.GetType() == resourceType && ((IIdentifiable)entry.Entity).StringId == stringId; } /// diff --git a/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs b/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs index 9b8f1327ce..4e1c7b4552 100644 --- a/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs +++ b/src/JsonApiDotNetCore/Repositories/DbContextResolver.cs @@ -17,8 +17,14 @@ public DbContextResolver(TDbContext context) _context = context; } - public DbContext GetContext() => _context; - - public TDbContext GetTypedContext() => _context; + public DbContext GetContext() + { + return _context; + } + + public TDbContext GetTypedContext() + { + return _context; + } } } diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs index a87155cda1..b305938bb0 100644 --- a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -37,13 +38,8 @@ public class EntityFrameworkCoreRepository : IResourceRepository /// public virtual Guid? TransactionId => _dbContext.Database.CurrentTransaction?.TransactionId; - public EntityFrameworkCoreRepository( - ITargetedFields targetedFields, - IDbContextResolver contextResolver, - IResourceGraph resourceGraph, - IResourceFactory resourceFactory, - IEnumerable constraintProviders, - ILoggerFactory loggerFactory) + public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) { ArgumentGuard.NotNull(contextResolver, nameof(contextResolver)); ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); @@ -63,7 +59,10 @@ public EntityFrameworkCoreRepository( /// public virtual async Task> GetAsync(QueryLayer layer, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {layer}); + _traceWriter.LogMethodStart(new + { + layer + }); ArgumentGuard.NotNull(layer, nameof(layer)); @@ -74,9 +73,13 @@ public virtual async Task> GetAsync(QueryLayer la /// public virtual async Task CountAsync(FilterExpression topFilter, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {topFilter}); + _traceWriter.LogMethodStart(new + { + topFilter + }); + + ResourceContext resourceContext = _resourceGraph.GetResourceContext(); - var resourceContext = _resourceGraph.GetResourceContext(); var layer = new QueryLayer(resourceContext) { Filter = topFilter @@ -88,11 +91,15 @@ public virtual async Task CountAsync(FilterExpression topFilter, Cancellati protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) { - _traceWriter.LogMethodStart(new {layer}); + _traceWriter.LogMethodStart(new + { + layer + }); ArgumentGuard.NotNull(layer, nameof(layer)); QueryLayer rewrittenLayer = layer; + if (EntityFrameworkCoreSupport.Version.Major < 5) { var writer = new MemoryLeakDetectionBugRewriter(); @@ -104,7 +111,7 @@ protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var queryableHandlers = _constraintProviders + QueryableHandlerExpression[] queryableHandlers = _constraintProviders .SelectMany(provider => provider.GetConstraints()) .Where(expressionInScope => expressionInScope.Scope == null) .Select(expressionInScope => expressionInScope.Expression) @@ -114,15 +121,17 @@ protected virtual IQueryable ApplyQueryLayer(QueryLayer layer) // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - foreach (var queryableHandler in queryableHandlers) + foreach (QueryableHandlerExpression queryableHandler in queryableHandlers) { source = queryableHandler.Apply(source); } var nameFactory = new LambdaParameterNameFactory(); - var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, _resourceGraph, _dbContext.Model); - var expression = builder.ApplyQuery(rewrittenLayer); + var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, _resourceGraph, + _dbContext.Model); + + Expression expression = builder.ApplyQuery(rewrittenLayer); return source.Provider.CreateQuery(expression); } @@ -143,25 +152,29 @@ public virtual Task GetForCreateAsync(TId id, CancellationToken cance /// public virtual async Task CreateAsync(TResource resourceFromRequest, TResource resourceForDatabase, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {resourceFromRequest, resourceForDatabase}); + _traceWriter.LogMethodStart(new + { + resourceFromRequest, + resourceForDatabase + }); ArgumentGuard.NotNull(resourceFromRequest, nameof(resourceFromRequest)); ArgumentGuard.NotNull(resourceForDatabase, nameof(resourceForDatabase)); using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); - foreach (var relationship in _targetedFields.Relationships) + foreach (RelationshipAttribute relationship in _targetedFields.Relationships) { - var rightResources = relationship.GetValue(resourceFromRequest); + object rightResources = relationship.GetValue(resourceFromRequest); await UpdateRelationshipAsync(relationship, resourceForDatabase, rightResources, collector, cancellationToken); } - foreach (var attribute in _targetedFields.Attributes) + foreach (AttrAttribute attribute in _targetedFields.Attributes) { attribute.SetValue(resourceForDatabase, attribute.GetValue(resourceFromRequest)); } - var dbSet = _dbContext.Set(); + DbSet dbSet = _dbContext.Set(); await dbSet.AddAsync(resourceForDatabase, cancellationToken); await SaveChangesAsync(cancellationToken); @@ -170,30 +183,34 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r /// public virtual async Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken) { - var resources = await GetAsync(queryLayer, cancellationToken); + IReadOnlyCollection resources = await GetAsync(queryLayer, cancellationToken); return resources.FirstOrDefault(); } /// public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource resourceFromDatabase, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {resourceFromRequest, resourceFromDatabase}); + _traceWriter.LogMethodStart(new + { + resourceFromRequest, + resourceFromDatabase + }); ArgumentGuard.NotNull(resourceFromRequest, nameof(resourceFromRequest)); ArgumentGuard.NotNull(resourceFromDatabase, nameof(resourceFromDatabase)); using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); - foreach (var relationship in _targetedFields.Relationships) + foreach (RelationshipAttribute relationship in _targetedFields.Relationships) { - var rightResources = relationship.GetValue(resourceFromRequest); + object rightResources = relationship.GetValue(resourceFromRequest); AssertIsNotClearingRequiredRelationship(relationship, resourceFromDatabase, rightResources); await UpdateRelationshipAsync(relationship, resourceFromDatabase, rightResources, collector, cancellationToken); } - foreach (var attribute in _targetedFields.Attributes) + foreach (AttrAttribute attribute in _targetedFields.Attributes) { attribute.SetValue(resourceFromDatabase, attribute.GetValue(resourceFromRequest)); } @@ -207,17 +224,17 @@ protected void AssertIsNotClearingRequiredRelationship(RelationshipAttribute rel if (!(relationship is HasManyThroughAttribute)) { - var navigation = TryGetNavigation(relationship); + INavigation navigation = TryGetNavigation(relationship); relationshipIsRequired = navigation?.ForeignKey?.IsRequired ?? false; } - var relationshipIsBeingCleared = relationship is HasOneAttribute + bool relationshipIsBeingCleared = relationship is HasOneAttribute ? rightValue == null : IsToManyRelationshipBeingCleared(relationship, leftResource, rightValue); - + if (relationshipIsRequired && relationshipIsBeingCleared) { - var resourceType = _resourceGraph.GetResourceContext().PublicName; + string resourceType = _resourceGraph.GetResourceContext().PublicName; throw new CannotClearRequiredRelationshipException(relationship.PublicName, leftResource.StringId, resourceType); } } @@ -226,8 +243,8 @@ private static bool IsToManyRelationshipBeingCleared(RelationshipAttribute relat { ICollection newRightResourceIds = TypeHelper.ExtractResources(valueToAssign); - var existingRightValue = relationship.GetValue(leftResource); - var existingRightResourceIds = TypeHelper.ExtractResources(existingRightValue).ToHashSet(IdentifiableComparer.Instance); + object existingRightValue = relationship.GetValue(leftResource); + HashSet existingRightResourceIds = TypeHelper.ExtractResources(existingRightValue).ToHashSet(IdentifiableComparer.Instance); existingRightResourceIds.ExceptWith(newRightResourceIds); @@ -237,18 +254,21 @@ private static bool IsToManyRelationshipBeingCleared(RelationshipAttribute relat /// public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id}); + _traceWriter.LogMethodStart(new + { + id + }); using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); - var resource = collector.CreateForId(id); + TResource resource = collector.CreateForId(id); - foreach (var relationship in _resourceGraph.GetRelationships()) + foreach (RelationshipAttribute relationship in _resourceGraph.GetRelationships()) { // Loads the data of the relationship, if in EF Core it is configured in such a way that loading the related // entities into memory is required for successfully executing the selected deletion behavior. if (RequiresLoadOfRelationshipForDeletion(relationship)) { - var navigation = GetNavigationEntry(resource, relationship); + NavigationEntry navigation = GetNavigationEntry(resource, relationship); await navigation.LoadAsync(cancellationToken); } } @@ -281,7 +301,7 @@ private NavigationEntry GetNavigationEntry(TResource resource, RelationshipAttri private bool RequiresLoadOfRelationshipForDeletion(RelationshipAttribute relationship) { - var navigation = TryGetNavigation(relationship); + INavigation navigation = TryGetNavigation(relationship); bool isClearOfForeignKeyRequired = navigation?.ForeignKey.DeleteBehavior == DeleteBehavior.ClientSetNull; bool hasForeignKeyAtLeftSide = HasForeignKeyAtLeftSide(relationship); @@ -291,7 +311,7 @@ private bool RequiresLoadOfRelationshipForDeletion(RelationshipAttribute relatio private INavigation TryGetNavigation(RelationshipAttribute relationship) { - var entityType = _dbContext.Model.FindEntityType(typeof(TResource)); + IEntityType entityType = _dbContext.Model.FindEntityType(typeof(TResource)); return entityType?.FindNavigation(relationship.Property.Name); } @@ -299,7 +319,7 @@ private bool HasForeignKeyAtLeftSide(RelationshipAttribute relationship) { if (relationship is HasOneAttribute) { - var navigation = TryGetNavigation(relationship); + INavigation navigation = TryGetNavigation(relationship); return navigation?.IsDependentToPrincipal() ?? false; } @@ -309,9 +329,13 @@ private bool HasForeignKeyAtLeftSide(RelationshipAttribute relationship) /// public virtual async Task SetRelationshipAsync(TResource primaryResource, object secondaryResourceIds, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {primaryResource, secondaryResourceIds}); + _traceWriter.LogMethodStart(new + { + primaryResource, + secondaryResourceIds + }); - var relationship = _targetedFields.Relationships.Single(); + RelationshipAttribute relationship = _targetedFields.Relationships.Single(); AssertIsNotClearingRequiredRelationship(relationship, primaryResource, secondaryResourceIds); @@ -324,16 +348,20 @@ public virtual async Task SetRelationshipAsync(TResource primaryResource, object /// public virtual async Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {primaryId, secondaryResourceIds}); + _traceWriter.LogMethodStart(new + { + primaryId, + secondaryResourceIds + }); ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); - var relationship = _targetedFields.Relationships.Single(); + RelationshipAttribute relationship = _targetedFields.Relationships.Single(); if (secondaryResourceIds.Any()) { using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext); - var primaryResource = collector.CreateForId(primaryId); + TResource primaryResource = collector.CreateForId(primaryId); await UpdateRelationshipAsync(relationship, primaryResource, secondaryResourceIds, collector, cancellationToken); @@ -342,17 +370,22 @@ public virtual async Task AddToToManyRelationshipAsync(TId primaryId, ISet - public virtual async Task RemoveFromToManyRelationshipAsync(TResource primaryResource, ISet secondaryResourceIds, CancellationToken cancellationToken) + public virtual async Task RemoveFromToManyRelationshipAsync(TResource primaryResource, ISet secondaryResourceIds, + CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {primaryResource, secondaryResourceIds}); + _traceWriter.LogMethodStart(new + { + primaryResource, + secondaryResourceIds + }); ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); var relationship = (HasManyAttribute)_targetedFields.Relationships.Single(); - var rightValue = relationship.GetValue(primaryResource); + object rightValue = relationship.GetValue(primaryResource); - var rightResourceIds= TypeHelper.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); + HashSet rightResourceIds = TypeHelper.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); rightResourceIds.ExceptWith(secondaryResourceIds); AssertIsNotClearingRequiredRelationship(relationship, primaryResource, rightResourceIds); @@ -363,15 +396,15 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource primaryRes await SaveChangesAsync(cancellationToken); } - protected async Task UpdateRelationshipAsync(RelationshipAttribute relationship, TResource leftResource, - object valueToAssign, PlaceholderResourceCollector collector, CancellationToken cancellationToken) + protected async Task UpdateRelationshipAsync(RelationshipAttribute relationship, TResource leftResource, object valueToAssign, + PlaceholderResourceCollector collector, CancellationToken cancellationToken) { - var trackedValueToAssign = EnsureRelationshipValueToAssignIsTracked(valueToAssign, relationship.Property.PropertyType, collector); + object trackedValueToAssign = EnsureRelationshipValueToAssignIsTracked(valueToAssign, relationship.Property.PropertyType, collector); if (RequireLoadOfInverseRelationship(relationship, trackedValueToAssign)) { - var entityEntry = _dbContext.Entry(trackedValueToAssign); - var inversePropertyName = relationship.InverseNavigationProperty.Name; + EntityEntry entityEntry = _dbContext.Entry(trackedValueToAssign); + string inversePropertyName = relationship.InverseNavigationProperty.Name; await entityEntry.Reference(inversePropertyName).LoadAsync(cancellationToken); } @@ -379,19 +412,18 @@ protected async Task UpdateRelationshipAsync(RelationshipAttribute relationship, relationship.SetValue(leftResource, trackedValueToAssign); } - private object EnsureRelationshipValueToAssignIsTracked(object rightValue, Type relationshipPropertyType, - PlaceholderResourceCollector collector) + private object EnsureRelationshipValueToAssignIsTracked(object rightValue, Type relationshipPropertyType, PlaceholderResourceCollector collector) { if (rightValue == null) { return null; } - var rightResources = TypeHelper.ExtractResources(rightValue); - var rightResourcesTracked = rightResources.Select(collector.CaptureExisting).ToArray(); + ICollection rightResources = TypeHelper.ExtractResources(rightValue); + IIdentifiable[] rightResourcesTracked = rightResources.Select(collector.CaptureExisting).ToArray(); return rightValue is IEnumerable - ? (object) TypeHelper.CopyToTypedCollection(rightResourcesTracked, relationshipPropertyType) + ? (object)TypeHelper.CopyToTypedCollection(rightResourcesTracked, relationshipPropertyType) : rightResourcesTracked.Single(); } @@ -405,7 +437,7 @@ private static bool IsOneToOneRelationship(RelationshipAttribute relationship) { if (relationship is HasOneAttribute hasOneRelationship) { - var elementType = TypeHelper.TryGetCollectionElementType(hasOneRelationship.InverseNavigationProperty.PropertyType); + Type elementType = TypeHelper.TryGetCollectionElementType(hasOneRelationship.InverseNavigationProperty.PropertyType); return elementType == null; } @@ -439,13 +471,8 @@ protected virtual async Task SaveChangesAsync(CancellationToken cancellationToke public class EntityFrameworkCoreRepository : EntityFrameworkCoreRepository, IResourceRepository where TResource : class, IIdentifiable { - public EntityFrameworkCoreRepository( - ITargetedFields targetedFields, - IDbContextResolver contextResolver, - IResourceGraph resourceGraph, - IResourceFactory resourceFactory, - IEnumerable constraintProviders, - ILoggerFactory loggerFactory) + public EntityFrameworkCoreRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) { } diff --git a/src/JsonApiDotNetCore/Repositories/IDbContextResolver.cs b/src/JsonApiDotNetCore/Repositories/IDbContextResolver.cs index 693724ed90..0a38f2dfcc 100644 --- a/src/JsonApiDotNetCore/Repositories/IDbContextResolver.cs +++ b/src/JsonApiDotNetCore/Repositories/IDbContextResolver.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCore.Repositories { /// - /// Provides a method to resolve a . + /// Provides a method to resolve a . /// public interface IDbContextResolver { diff --git a/src/JsonApiDotNetCore/Repositories/IRepositorySupportsTransaction.cs b/src/JsonApiDotNetCore/Repositories/IRepositorySupportsTransaction.cs index fb1f026d31..15e65f3bda 100644 --- a/src/JsonApiDotNetCore/Repositories/IRepositorySupportsTransaction.cs +++ b/src/JsonApiDotNetCore/Repositories/IRepositorySupportsTransaction.cs @@ -4,7 +4,7 @@ namespace JsonApiDotNetCore.Repositories { /// - /// Used to indicate that an supports execution inside a transaction. + /// Used to indicate that an supports execution inside a transaction. /// [PublicAPI] public interface IRepositorySupportsTransaction diff --git a/src/JsonApiDotNetCore/Repositories/IResourceReadRepository.cs b/src/JsonApiDotNetCore/Repositories/IResourceReadRepository.cs index ec373094db..a75d95c213 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceReadRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceReadRepository.cs @@ -9,16 +9,20 @@ namespace JsonApiDotNetCore.Repositories { /// - public interface IResourceReadRepository - : IResourceReadRepository - where TResource : class, IIdentifiable - { } + public interface IResourceReadRepository : IResourceReadRepository + where TResource : class, IIdentifiable + { + } /// /// Groups read operations. /// - /// The resource type. - /// The resource identifier type. + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// [PublicAPI] public interface IResourceReadRepository where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/Repositories/IResourceRepository.cs b/src/JsonApiDotNetCore/Repositories/IResourceRepository.cs index 266672cf77..d43e355f06 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceRepository.cs @@ -6,7 +6,9 @@ namespace JsonApiDotNetCore.Repositories /// /// Represents the foundational Resource Repository layer in the JsonApiDotNetCore architecture that provides data access to an underlying store. /// - /// The resource type. + /// + /// The resource type. + /// [PublicAPI] public interface IResourceRepository : IResourceRepository, IResourceReadRepository, IResourceWriteRepository @@ -17,10 +19,13 @@ public interface IResourceRepository /// /// Represents the foundational Resource Repository layer in the JsonApiDotNetCore architecture that provides data access to an underlying store. /// - /// The resource type. - /// The resource identifier type. - public interface IResourceRepository - : IResourceReadRepository, IResourceWriteRepository + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// + public interface IResourceRepository : IResourceReadRepository, IResourceWriteRepository where TResource : class, IIdentifiable { } diff --git a/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs index 93f9640038..607512c242 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs @@ -9,73 +9,74 @@ namespace JsonApiDotNetCore.Repositories { /// - /// Retrieves an instance from the D/I container and invokes a method on it. + /// Retrieves an instance from the D/I container and invokes a method on it. /// public interface IResourceRepositoryAccessor { /// - /// Invokes . + /// Invokes . /// Task> GetAsync(QueryLayer layer, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes for the specified resource type. + /// Invokes for the specified resource type. /// Task> GetAsync(Type resourceType, QueryLayer layer, CancellationToken cancellationToken); /// - /// Invokes for the specified resource type. + /// Invokes for the specified resource type. /// Task CountAsync(FilterExpression topFilter, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes . + /// Invokes . /// Task GetForCreateAsync(TId id, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes . + /// Invokes . /// Task CreateAsync(TResource resourceFromRequest, TResource resourceForDatabase, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes . + /// Invokes . /// Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes . + /// Invokes . /// Task UpdateAsync(TResource resourceFromRequest, TResource resourceFromDatabase, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes for the specified resource type. + /// Invokes for the specified resource type. /// Task DeleteAsync(TId id, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes . + /// Invokes . /// Task SetRelationshipAsync(TResource primaryResource, object secondaryResourceIds, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes for the specified resource type. + /// Invokes for the specified resource type. /// Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken) where TResource : class, IIdentifiable; /// - /// Invokes . + /// Invokes . /// - Task RemoveFromToManyRelationshipAsync(TResource primaryResource, ISet secondaryResourceIds, CancellationToken cancellationToken) + Task RemoveFromToManyRelationshipAsync(TResource primaryResource, ISet secondaryResourceIds, + CancellationToken cancellationToken) where TResource : class, IIdentifiable; } } diff --git a/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs b/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs index 35f7ab1459..049e02ce8c 100644 --- a/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/IResourceWriteRepository.cs @@ -8,22 +8,26 @@ namespace JsonApiDotNetCore.Repositories { /// - public interface IResourceWriteRepository - : IResourceWriteRepository + public interface IResourceWriteRepository : IResourceWriteRepository where TResource : class, IIdentifiable - { } + { + } /// /// Groups write operations. /// - /// The resource type. - /// The resource identifier type. + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// [PublicAPI] public interface IResourceWriteRepository where TResource : class, IIdentifiable { /// - /// Creates a new resource instance, in preparation for . + /// Creates a new resource instance, in preparation for . /// /// /// This method can be overridden to assign resource-specific required relationships. @@ -36,7 +40,7 @@ public interface IResourceWriteRepository Task CreateAsync(TResource resourceFromRequest, TResource resourceForDatabase, CancellationToken cancellationToken); /// - /// Retrieves a resource with all of its attributes, including the set of targeted relationships, in preparation for . + /// Retrieves a resource with all of its attributes, including the set of targeted relationships, in preparation for . /// Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken); diff --git a/src/JsonApiDotNetCore/Repositories/MemoryLeakDetectionBugRewriter.cs b/src/JsonApiDotNetCore/Repositories/MemoryLeakDetectionBugRewriter.cs index 0b7509fc28..9d6c58a0ca 100644 --- a/src/JsonApiDotNetCore/Repositories/MemoryLeakDetectionBugRewriter.cs +++ b/src/JsonApiDotNetCore/Repositories/MemoryLeakDetectionBugRewriter.cs @@ -8,12 +8,12 @@ namespace JsonApiDotNetCore.Repositories { /// - /// Removes projections from a when its resource type uses injected parameters, - /// as a workaround for EF Core bug https://github.com/dotnet/efcore/issues/20502, which exists in versions below v5. + /// Removes projections from a when its resource type uses injected parameters, as a workaround for EF Core bug + /// https://github.com/dotnet/efcore/issues/20502, which exists in versions below v5. /// /// - /// Note that by using this workaround, nested filtering, paging and sorting all remain broken in EF Core 3.1 when using injected parameters in resources. - /// But at least it enables simple top-level queries to succeed without an exception. + /// Note that by using this workaround, nested filtering, paging and sorting all remain broken in EF Core 3.1 when using injected parameters in + /// resources. But at least it enables simple top-level queries to succeed without an exception. /// [PublicAPI] public sealed class MemoryLeakDetectionBugRewriter @@ -35,7 +35,8 @@ private QueryLayer RewriteLayer(QueryLayer queryLayer) return queryLayer; } - private IDictionary RewriteProjection(IDictionary projection, ResourceContext resourceContext) + private IDictionary RewriteProjection(IDictionary projection, + ResourceContext resourceContext) { if (projection == null || projection.Count == 0) { @@ -43,9 +44,10 @@ private IDictionary RewriteProjection(IDicti } var newProjection = new Dictionary(); - foreach (var (field, layer) in projection) + + foreach ((ResourceFieldAttribute field, QueryLayer layer) in projection) { - var newLayer = RewriteLayer(layer); + QueryLayer newLayer = RewriteLayer(layer); newProjection.Add(field, newLayer); } diff --git a/src/JsonApiDotNetCore/Repositories/PlaceholderResourceCollector.cs b/src/JsonApiDotNetCore/Repositories/PlaceholderResourceCollector.cs index 3dfa3c9d99..d28ab4a6c0 100644 --- a/src/JsonApiDotNetCore/Repositories/PlaceholderResourceCollector.cs +++ b/src/JsonApiDotNetCore/Repositories/PlaceholderResourceCollector.cs @@ -7,9 +7,9 @@ namespace JsonApiDotNetCore.Repositories { /// - /// Creates placeholder resource instances (with only their ID property set), which are added to the - /// Entity Framework Core change tracker so they can be used in relationship updates without fetching the resource. - /// On disposal, the created placeholders are detached, leaving the change tracker in a clean state for reuse. + /// Creates placeholder resource instances (with only their ID property set), which are added to the Entity Framework Core change tracker so they can be + /// used in relationship updates without fetching the resource. On disposal, the created placeholders are detached, leaving the change tracker in a clean + /// state for reuse. /// [PublicAPI] public sealed class PlaceholderResourceCollector : IDisposable @@ -28,8 +28,8 @@ public PlaceholderResourceCollector(IResourceFactory resourceFactory, DbContext } /// - /// Creates a new placeholder resource, assigns the specified ID, adds it to the change tracker - /// in state and registers it for detachment. + /// Creates a new placeholder resource, assigns the specified ID, adds it to the change tracker in state and + /// registers it for detachment. /// public TResource CreateForId(TId id) where TResource : IIdentifiable @@ -41,13 +41,13 @@ public TResource CreateForId(TId id) } /// - /// Takes an existing placeholder resource, adds it to the change tracker - /// in state and registers it for detachment. + /// Takes an existing placeholder resource, adds it to the change tracker in state and registers it for detachment. /// public TResource CaptureExisting(TResource placeholderResource) where TResource : IIdentifiable { - var resourceTracked = (TResource) _dbContext.GetTrackedOrAttach(placeholderResource); + var resourceTracked = (TResource)_dbContext.GetTrackedOrAttach(placeholderResource); + if (ReferenceEquals(resourceTracked, placeholderResource)) { _resources.Add(resourceTracked); @@ -68,7 +68,7 @@ public void Dispose() private void Detach(IEnumerable resources) { - foreach (var resource in resources) + foreach (object resource in resources) { _dbContext.Entry(resource).State = EntityState.Detached; } diff --git a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs index 8dfe3971e3..887ebb06a6 100644 --- a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs @@ -37,7 +37,7 @@ public async Task> GetAsync(QueryLayer where TResource : class, IIdentifiable { dynamic repository = ResolveReadRepository(typeof(TResource)); - return (IReadOnlyCollection) await repository.GetAsync(layer, cancellationToken); + return (IReadOnlyCollection)await repository.GetAsync(layer, cancellationToken); } /// @@ -46,7 +46,7 @@ public async Task> GetAsync(Type resourceType ArgumentGuard.NotNull(resourceType, nameof(resourceType)); dynamic repository = ResolveReadRepository(resourceType); - return (IReadOnlyCollection) await repository.GetAsync(layer, cancellationToken); + return (IReadOnlyCollection)await repository.GetAsync(layer, cancellationToken); } /// @@ -54,7 +54,7 @@ public async Task CountAsync(FilterExpression topFilter, Cancell where TResource : class, IIdentifiable { dynamic repository = ResolveReadRepository(typeof(TResource)); - return (int) await repository.CountAsync(topFilter, cancellationToken); + return (int)await repository.CountAsync(topFilter, cancellationToken); } /// @@ -106,7 +106,8 @@ public async Task SetRelationshipAsync(TResource primaryResource, obj } /// - public async Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, CancellationToken cancellationToken) + public async Task AddToToManyRelationshipAsync(TId primaryId, ISet secondaryResourceIds, + CancellationToken cancellationToken) where TResource : class, IIdentifiable { dynamic repository = GetWriteRepository(typeof(TResource)); @@ -114,7 +115,8 @@ public async Task AddToToManyRelationshipAsync(TId primaryId, IS } /// - public async Task RemoveFromToManyRelationshipAsync(TResource primaryResource, ISet secondaryResourceIds, CancellationToken cancellationToken) + public async Task RemoveFromToManyRelationshipAsync(TResource primaryResource, ISet secondaryResourceIds, + CancellationToken cancellationToken) where TResource : class, IIdentifiable { dynamic repository = GetWriteRepository(typeof(TResource)); @@ -123,12 +125,12 @@ public async Task RemoveFromToManyRelationshipAsync(TResource primary protected virtual object ResolveReadRepository(Type resourceType) { - var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); if (resourceContext.IdentityType == typeof(int)) { - var intRepositoryType = typeof(IResourceReadRepository<>).MakeGenericType(resourceContext.ResourceType); - var intRepository = _serviceProvider.GetService(intRepositoryType); + Type intRepositoryType = typeof(IResourceReadRepository<>).MakeGenericType(resourceContext.ResourceType); + object intRepository = _serviceProvider.GetService(intRepositoryType); if (intRepository != null) { @@ -136,19 +138,19 @@ protected virtual object ResolveReadRepository(Type resourceType) } } - var resourceDefinitionType = typeof(IResourceReadRepository<,>).MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); + Type resourceDefinitionType = typeof(IResourceReadRepository<,>).MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); return _serviceProvider.GetRequiredService(resourceDefinitionType); } private object GetWriteRepository(Type resourceType) { - var writeRepository = ResolveWriteRepository(resourceType); + object writeRepository = ResolveWriteRepository(resourceType); if (_request.TransactionId != null) { if (!(writeRepository is IRepositorySupportsTransaction repository)) { - var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); throw new MissingTransactionSupportException(resourceContext.PublicName); } @@ -163,12 +165,12 @@ private object GetWriteRepository(Type resourceType) protected virtual object ResolveWriteRepository(Type resourceType) { - var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); if (resourceContext.IdentityType == typeof(int)) { - var intRepositoryType = typeof(IResourceWriteRepository<>).MakeGenericType(resourceContext.ResourceType); - var intRepository = _serviceProvider.GetService(intRepositoryType); + Type intRepositoryType = typeof(IResourceWriteRepository<>).MakeGenericType(resourceContext.ResourceType); + object intRepository = _serviceProvider.GetService(intRepositoryType); if (intRepository != null) { @@ -176,7 +178,7 @@ protected virtual object ResolveWriteRepository(Type resourceType) } } - var resourceDefinitionType = typeof(IResourceWriteRepository<,>).MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); + Type resourceDefinitionType = typeof(IResourceWriteRepository<,>).MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); return _serviceProvider.GetRequiredService(resourceDefinitionType); } } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs index bd19f5e850..1987eb4d94 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/AttrAttribute.cs @@ -15,8 +15,8 @@ public sealed class AttrAttribute : ResourceFieldAttribute internal bool HasExplicitCapabilities => _capabilities != null; /// - /// The set of capabilities that are allowed to be performed on this attribute. - /// When not explicitly assigned, the configured default set of capabilities is used. + /// The set of capabilities that are allowed to be performed on this attribute. When not explicitly assigned, the configured default set of capabilities + /// is used. /// /// /// @@ -34,8 +34,7 @@ public AttrCapabilities Capabilities } /// - /// Get the value of the attribute for the given object. - /// Throws if the attribute does not belong to the provided object. + /// Get the value of the attribute for the given object. Throws if the attribute does not belong to the provided object. /// public object GetValue(object resource) { @@ -58,11 +57,10 @@ public void SetValue(object resource, object newValue) if (Property.SetMethod == null) { - throw new InvalidOperationException( - $"Property '{Property.DeclaringType?.Name}.{Property.Name}' is read-only."); + throw new InvalidOperationException($"Property '{Property.DeclaringType?.Name}.{Property.Name}' is read-only."); } - var convertedValue = TypeHelper.ConvertType(newValue, Property.PropertyType); + object convertedValue = TypeHelper.ConvertType(newValue, Property.PropertyType); Property.SetValue(resource, convertedValue); } @@ -78,7 +76,7 @@ public override bool Equals(object obj) return false; } - var other = (AttrAttribute) obj; + var other = (AttrAttribute)obj; return Capabilities == other.Capabilities && base.Equals(other); } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/AttrCapabilities.cs b/src/JsonApiDotNetCore/Resources/Annotations/AttrCapabilities.cs index af1448ce43..9eb93c9377 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/AttrCapabilities.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/AttrCapabilities.cs @@ -3,7 +3,7 @@ namespace JsonApiDotNetCore.Resources.Annotations { /// - /// Indicates capabilities that can be performed on an . + /// Indicates capabilities that can be performed on an . /// [Flags] public enum AttrCapabilities @@ -11,32 +11,27 @@ public enum AttrCapabilities None = 0, /// - /// Whether or not GET requests can retrieve the attribute. - /// Attempts to retrieve when disabled will return an HTTP 400 response. + /// Whether or not GET requests can retrieve the attribute. Attempts to retrieve when disabled will return an HTTP 400 response. /// AllowView = 1, /// - /// Whether or not POST requests can assign the attribute value. - /// Attempts to assign when disabled will return an HTTP 422 response. + /// Whether or not POST requests can assign the attribute value. Attempts to assign when disabled will return an HTTP 422 response. /// AllowCreate = 2, /// - /// Whether or not PATCH requests can update the attribute value. - /// Attempts to update when disabled will return an HTTP 422 response. + /// Whether or not PATCH requests can update the attribute value. Attempts to update when disabled will return an HTTP 422 response. /// AllowChange = 4, /// - /// Whether or not an attribute can be filtered on via a query string parameter. - /// Attempts to filter when disabled will return an HTTP 400 response. + /// Whether or not an attribute can be filtered on via a query string parameter. Attempts to filter when disabled will return an HTTP 400 response. /// AllowFilter = 8, /// - /// Whether or not an attribute can be sorted on via a query string parameter. - /// Attempts to sort when disabled will return an HTTP 400 response. + /// Whether or not an attribute can be sorted on via a query string parameter. Attempts to sort when disabled will return an HTTP 400 response. /// AllowSort = 16, diff --git a/src/JsonApiDotNetCore/Resources/Annotations/HasManyAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/HasManyAttribute.cs index 869e322ee8..b2f51f4a47 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/HasManyAttribute.cs @@ -3,7 +3,8 @@ namespace JsonApiDotNetCore.Resources.Annotations { /// - /// Used to expose a property on a resource class as a JSON:API to-many relationship (https://jsonapi.org/format/#document-resource-object-relationships). + /// Used to expose a property on a resource class as a JSON:API to-many relationship + /// (https://jsonapi.org/format/#document-resource-object-relationships). /// /// /// /// - /// In the following example, we expose a relationship named "tags" - /// through the navigation property `ArticleTags`. - /// The `Tags` property is decorated with `NotMapped` so that EF does not try - /// to map this to a database relationship. + /// In the following example, we expose a relationship named "tags" through the navigation property `ArticleTags`. The `Tags` property is decorated with + /// `NotMapped` so that EF does not try to map this to a database relationship. /// Tags { get; set; } /// public ISet ArticleTags { get; set; } /// } - /// + /// /// public class Tag : Identifiable /// { /// [Attr] /// public string Name { get; set; } /// } - /// + /// /// public sealed class ArticleTag /// { /// public int ArticleId { get; set; } /// public Article Article { get; set; } - /// + /// /// public int TagId { get; set; } /// public Tag Tag { get; set; } /// } @@ -48,83 +46,77 @@ namespace JsonApiDotNetCore.Resources.Annotations public sealed class HasManyThroughAttribute : HasManyAttribute { /// - /// The name of the join property on the parent resource. - /// In the example described above, this would be "ArticleTags". + /// The name of the join property on the parent resource. In the example described above, this would be "ArticleTags". /// public string ThroughPropertyName { get; } /// - /// The join type. - /// In the example described above, this would be `ArticleTag`. + /// The join type. In the example described above, this would be `ArticleTag`. /// public Type ThroughType { get; internal set; } /// - /// The navigation property back to the parent resource from the through type. - /// In the example described above, this would point to the `Article.ArticleTags.Article` property. + /// The navigation property back to the parent resource from the through type. In the example described above, this would point to the + /// `Article.ArticleTags.Article` property. /// public PropertyInfo LeftProperty { get; internal set; } /// - /// The ID property back to the parent resource from the through type. - /// In the example described above, this would point to the `Article.ArticleTags.ArticleId` property. + /// The ID property back to the parent resource from the through type. In the example described above, this would point to the + /// `Article.ArticleTags.ArticleId` property. /// public PropertyInfo LeftIdProperty { get; internal set; } /// - /// The navigation property to the related resource from the through type. - /// In the example described above, this would point to the `Article.ArticleTags.Tag` property. + /// The navigation property to the related resource from the through type. In the example described above, this would point to the + /// `Article.ArticleTags.Tag` property. /// public PropertyInfo RightProperty { get; internal set; } /// - /// The ID property to the related resource from the through type. - /// In the example described above, this would point to the `Article.ArticleTags.TagId` property. + /// The ID property to the related resource from the through type. In the example described above, this would point to the `Article.ArticleTags.TagId` + /// property. /// public PropertyInfo RightIdProperty { get; internal set; } /// - /// The join resource property on the parent resource. - /// In the example described above, this would point to the `Article.ArticleTags` property. + /// The join resource property on the parent resource. In the example described above, this would point to the `Article.ArticleTags` property. /// public PropertyInfo ThroughProperty { get; internal set; } /// - /// The internal navigation property path to the related resource. - /// In the example described above, this would contain "ArticleTags.Tag". + /// The internal navigation property path to the related resource. In the example described above, this would contain "ArticleTags.Tag". /// public override string RelationshipPath => $"{ThroughProperty.Name}.{RightProperty.Name}"; /// - /// Required for a self-referencing many-to-many relationship. - /// Contains the name of the property back to the parent resource from the through type. + /// Required for a self-referencing many-to-many relationship. Contains the name of the property back to the parent resource from the through type. /// public string LeftPropertyName { get; set; } /// - /// Required for a self-referencing many-to-many relationship. - /// Contains the name of the property to the related resource from the through type. + /// Required for a self-referencing many-to-many relationship. Contains the name of the property to the related resource from the through type. /// public string RightPropertyName { get; set; } /// - /// Optional. Can be used to indicate a non-default name for the ID property back to the parent resource from the through type. - /// Defaults to the name of suffixed with "Id". - /// In the example described above, this would be "ArticleId". + /// Optional. Can be used to indicate a non-default name for the ID property back to the parent resource from the through type. Defaults to the name of + /// suffixed with "Id". In the example described above, this would be "ArticleId". /// public string LeftIdPropertyName { get; set; } /// - /// Optional. Can be used to indicate a non-default name for the ID property to the related resource from the through type. - /// Defaults to the name of suffixed with "Id". - /// In the example described above, this would be "TagId". + /// Optional. Can be used to indicate a non-default name for the ID property to the related resource from the through type. Defaults to the name of + /// suffixed with "Id". In the example described above, this would be "TagId". /// public string RightIdPropertyName { get; set; } /// /// Creates a HasMany relationship through a many-to-many join relationship. /// - /// The name of the navigation property that will be used to access the join relationship. + /// + /// The name of the navigation property that will be used to access the join relationship. + /// public HasManyThroughAttribute(string throughPropertyName) { ArgumentGuard.NotNull(throughPropertyName, nameof(throughPropertyName)); @@ -133,29 +125,28 @@ public HasManyThroughAttribute(string throughPropertyName) } /// - /// Traverses through the provided resource and returns the value of the relationship on the other side of the through type. - /// In the example described above, this would be the value of "Articles.ArticleTags.Tag". + /// Traverses through the provided resource and returns the value of the relationship on the other side of the through type. In the example described + /// above, this would be the value of "Articles.ArticleTags.Tag". /// public override object GetValue(object resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - var throughEntity = ThroughProperty.GetValue(resource); + object throughEntity = ThroughProperty.GetValue(resource); + if (throughEntity == null) { return null; } - IEnumerable rightResources = ((IEnumerable) throughEntity) - .Cast() - .Select(rightResource => RightProperty.GetValue(rightResource)); + IEnumerable rightResources = ((IEnumerable)throughEntity).Cast().Select(rightResource => RightProperty.GetValue(rightResource)); return TypeHelper.CopyToTypedCollection(rightResources, Property.PropertyType); } /// - /// Traverses through the provided resource and sets the value of the relationship on the other side of the through type. - /// In the example described above, this would be the value of "Articles.ArticleTags.Tag". + /// Traverses through the provided resource and sets the value of the relationship on the other side of the through type. In the example described above, + /// this would be the value of "Articles.ArticleTags.Tag". /// public override void SetValue(object resource, object newValue) { @@ -169,17 +160,18 @@ public override void SetValue(object resource, object newValue) } else { - List throughResources = new List(); + var throughResources = new List(); + foreach (IIdentifiable rightResource in (IEnumerable)newValue) { - var throughEntity = TypeHelper.CreateInstance(ThroughType); + object throughEntity = TypeHelper.CreateInstance(ThroughType); LeftProperty.SetValue(throughEntity, resource); RightProperty.SetValue(throughEntity, rightResource); throughResources.Add(throughEntity); } - var typedCollection = TypeHelper.CopyToTypedCollection(throughResources, ThroughProperty.PropertyType); + IEnumerable typedCollection = TypeHelper.CopyToTypedCollection(throughResources, ThroughProperty.PropertyType); ThroughProperty.SetValue(resource, typedCollection); } } @@ -196,18 +188,17 @@ public override bool Equals(object obj) return false; } - var other = (HasManyThroughAttribute) obj; + var other = (HasManyThroughAttribute)obj; - return ThroughPropertyName == other.ThroughPropertyName && ThroughType == other.ThroughType && - LeftProperty == other.LeftProperty && LeftIdProperty == other.LeftIdProperty && - RightProperty == other.RightProperty && RightIdProperty == other.RightIdProperty && - ThroughProperty == other.ThroughProperty && base.Equals(other); + return ThroughPropertyName == other.ThroughPropertyName && ThroughType == other.ThroughType && LeftProperty == other.LeftProperty && + LeftIdProperty == other.LeftIdProperty && RightProperty == other.RightProperty && RightIdProperty == other.RightIdProperty && + ThroughProperty == other.ThroughProperty && base.Equals(other); } public override int GetHashCode() { - return HashCode.Combine(ThroughPropertyName, ThroughType, LeftProperty, LeftIdProperty, RightProperty, - RightIdProperty, ThroughProperty, base.GetHashCode()); + return HashCode.Combine(ThroughPropertyName, ThroughType, LeftProperty, LeftIdProperty, RightProperty, RightIdProperty, ThroughProperty, + base.GetHashCode()); } } } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs index cd7188b3a5..bb46646faa 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/RelationshipAttribute.cs @@ -14,8 +14,7 @@ namespace JsonApiDotNetCore.Resources.Annotations public abstract class RelationshipAttribute : ResourceFieldAttribute { /// - /// The property name of the EF Core inverse navigation, which may or may not exist. - /// Even if it exists, it may not be exposed as a JSON:API relationship. + /// The property name of the EF Core inverse navigation, which may or may not exist. Even if it exists, it may not be exposed as a JSON:API relationship. /// /// /// /// - /// In all cases except for relationships, this equals the property name. + /// In all cases except for relationships, this equals the property name. /// public virtual string RelationshipPath => Property.Name; /// - /// The child resource type. This does not necessarily match the navigation property type. - /// In the case of a relationship, this value will be the collection element type. + /// The child resource type. This does not necessarily match the navigation property type. In the case of a relationship, + /// this value will be the collection element type. /// /// /// - /// Configures which links to show in the - /// object for this relationship. - /// Defaults to , which falls back to - /// and then falls back to . + /// Configures which links to show in the object for this relationship. Defaults to + /// , which falls back to and then falls back to + /// . /// public LinkTypes Links { get; set; } = LinkTypes.NotConfigured; /// - /// Whether or not this relationship can be included using the ?include=publicName query string parameter. - /// This is true by default. + /// Whether or not this relationship can be included using the + /// + /// ?include=publicName + /// + /// query string parameter. This is true by default. /// public bool CanInclude { get; set; } = true; @@ -104,10 +105,9 @@ public override bool Equals(object obj) return false; } - var other = (RelationshipAttribute) obj; + var other = (RelationshipAttribute)obj; - return LeftType == other.LeftType && RightType == other.RightType && Links == other.Links && - CanInclude == other.CanInclude && base.Equals(other); + return LeftType == other.LeftType && RightType == other.RightType && Links == other.Links && CanInclude == other.CanInclude && base.Equals(other); } public override int GetHashCode() diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs index df3227acf1..db6f7c8621 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceAttribute.cs @@ -11,8 +11,8 @@ namespace JsonApiDotNetCore.Resources.Annotations public sealed class ResourceAttribute : Attribute { /// - /// The publicly exposed name of this resource type. - /// When not explicitly assigned, the configured naming convention is applied on the pluralized resource class name. + /// The publicly exposed name of this resource type. When not explicitly assigned, the configured naming convention is applied on the pluralized resource + /// class name. /// public string PublicName { get; } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs index c538c6a12c..93efe49696 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceFieldAttribute.cs @@ -7,8 +7,8 @@ namespace JsonApiDotNetCore.Resources.Annotations { /// - /// Used to expose a property on a resource class as a JSON:API field (attribute or relationship). - /// See https://jsonapi.org/format/#document-resource-object-fields. + /// Used to expose a property on a resource class as a JSON:API field (attribute or relationship). See + /// https://jsonapi.org/format/#document-resource-object-fields. /// [PublicAPI] public abstract class ResourceFieldAttribute : Attribute @@ -16,8 +16,7 @@ public abstract class ResourceFieldAttribute : Attribute private string _publicName; /// - /// The publicly exposed name of this JSON:API field. - /// When not explicitly assigned, the configured naming convention is applied on the property name. + /// The publicly exposed name of this JSON:API field. When not explicitly assigned, the configured naming convention is applied on the property name. /// public string PublicName { @@ -28,6 +27,7 @@ public string PublicName { throw new ArgumentException("Exposed name cannot be null, empty or contain only whitespace.", nameof(value)); } + _publicName = value; } } @@ -54,7 +54,7 @@ public override bool Equals(object obj) return false; } - var other = (ResourceFieldAttribute) obj; + var other = (ResourceFieldAttribute)obj; return PublicName == other.PublicName && Property == other.Property; } diff --git a/src/JsonApiDotNetCore/Resources/Annotations/ResourceLinksAttribute.cs b/src/JsonApiDotNetCore/Resources/Annotations/ResourceLinksAttribute.cs index ede3a402be..f4d3a1ffc7 100644 --- a/src/JsonApiDotNetCore/Resources/Annotations/ResourceLinksAttribute.cs +++ b/src/JsonApiDotNetCore/Resources/Annotations/ResourceLinksAttribute.cs @@ -12,24 +12,21 @@ namespace JsonApiDotNetCore.Resources.Annotations public sealed class ResourceLinksAttribute : Attribute { /// - /// Configures which links to show in the - /// object for this resource type. - /// Defaults to , which falls back to . + /// Configures which links to show in the object for this resource type. Defaults to + /// , which falls back to . /// public LinkTypes TopLevelLinks { get; set; } = LinkTypes.NotConfigured; /// - /// Configures which links to show in the - /// object for this resource type. - /// Defaults to , which falls back to . + /// Configures which links to show in the object for this resource type. Defaults to + /// , which falls back to . /// public LinkTypes ResourceLinks { get; set; } = LinkTypes.NotConfigured; - + /// - /// Configures which links to show in the - /// object for all relationships of this resource type. - /// Defaults to , which falls back to . - /// This can be overruled per relationship by setting . + /// Configures which links to show in the object for all relationships of this resource type. + /// Defaults to , which falls back to . This can be overruled per + /// relationship by setting . /// public LinkTypes RelationshipLinks { get; set; } = LinkTypes.NotConfigured; } diff --git a/src/JsonApiDotNetCore/Resources/IIdentifiable.cs b/src/JsonApiDotNetCore/Resources/IIdentifiable.cs index 6fa523c617..99559870a4 100644 --- a/src/JsonApiDotNetCore/Resources/IIdentifiable.cs +++ b/src/JsonApiDotNetCore/Resources/IIdentifiable.cs @@ -1,8 +1,8 @@ namespace JsonApiDotNetCore.Resources { /// - /// When implemented by a class, indicates to JsonApiDotNetCore that the class represents a JSON:API resource. - /// Note that JsonApiDotNetCore also assumes that a property named 'Id' exists. + /// When implemented by a class, indicates to JsonApiDotNetCore that the class represents a JSON:API resource. Note that JsonApiDotNetCore also assumes + /// that a property named 'Id' exists. /// public interface IIdentifiable { @@ -20,7 +20,9 @@ public interface IIdentifiable /// /// When implemented by a class, indicates to JsonApiDotNetCore that the class represents a JSON:API resource. /// - /// The resource identifier type. + /// + /// The resource identifier type. + /// public interface IIdentifiable : IIdentifiable { /// diff --git a/src/JsonApiDotNetCore/Resources/IResourceChangeTracker.cs b/src/JsonApiDotNetCore/Resources/IResourceChangeTracker.cs index cceb0994d1..79eca2ed6a 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceChangeTracker.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceChangeTracker.cs @@ -3,11 +3,12 @@ namespace JsonApiDotNetCore.Resources /// /// Used to determine whether additional changes to a resource (side effects), not specified in a POST or PATCH request, have been applied. /// - public interface IResourceChangeTracker where TResource : class, IIdentifiable + public interface IResourceChangeTracker + where TResource : class, IIdentifiable { /// - /// Sets the exposed resource attributes as stored in database, before applying the PATCH operation. - /// For POST operations, this sets exposed resource attributes to their default value. + /// Sets the exposed resource attributes as stored in database, before applying the PATCH operation. For POST operations, this sets exposed resource + /// attributes to their default value. /// void SetInitiallyStoredAttributeValues(TResource resource); @@ -22,8 +23,8 @@ public interface IResourceChangeTracker where TResource : class, I void SetFinallyStoredAttributeValues(TResource resource); /// - /// Validates if any exposed resource attributes that were not in the POST or PATCH request have been changed. - /// And validates if the values from the request are stored without modification. + /// Validates if any exposed resource attributes that were not in the POST or PATCH request have been changed. And validates if the values from the + /// request are stored without modification. /// /// /// true if the attribute values from the POST or PATCH request were the only changes; false, otherwise. diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs index 0ca5e77950..8688a412ec 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinition.cs @@ -6,10 +6,12 @@ namespace JsonApiDotNetCore.Resources { /// - /// Provides a resource-centric extensibility point for executing custom code when something happens with a resource. - /// The goal here is to reduce the need for overriding the service and repository layers. + /// Provides a resource-centric extensibility point for executing custom code when something happens with a resource. The goal here is to reduce the need + /// for overriding the service and repository layers. /// - /// The resource type. + /// + /// The resource type. + /// [PublicAPI] public interface IResourceDefinition : IResourceDefinition where TResource : class, IIdentifiable @@ -17,11 +19,15 @@ public interface IResourceDefinition : IResourceDefinition - /// Provides a resource-centric extensibility point for executing custom code when something happens with a resource. - /// The goal here is to reduce the need for overriding the service and repository layers. + /// Provides a resource-centric extensibility point for executing custom code when something happens with a resource. The goal here is to reduce the need + /// for overriding the service and repository layers. /// - /// The resource type. - /// The resource identifier type. + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// [PublicAPI] public interface IResourceDefinition where TResource : class, IIdentifiable @@ -47,10 +53,10 @@ public interface IResourceDefinition /// The new filter, or null to disable the existing filter. /// FilterExpression OnApplyFilter(FilterExpression existingFilter); - + /// - /// Enables to extend, replace or remove a sort order that is being applied on a set of this resource type. - /// Tip: Use to build from a lambda expression. + /// Enables to extend, replace or remove a sort order that is being applied on a set of this resource type. Tip: Use + /// to build from a lambda expression. /// /// /// An optional existing sort order, coming from query string. Can be null. @@ -59,7 +65,7 @@ public interface IResourceDefinition /// The new sort order, or null to disable the existing sort order and sort by ID. /// SortExpression OnApplySort(SortExpression existingSort); - + /// /// Enables to extend, replace or remove pagination that is being applied on a set of this resource type. /// @@ -67,24 +73,24 @@ public interface IResourceDefinition /// An optional existing pagination, coming from query string. Can be null. /// /// - /// The changed pagination, or null to use the first page with default size from options. - /// To disable paging, set to null. + /// The changed pagination, or null to use the first page with default size from options. To disable paging, set + /// to null. /// PaginationExpression OnApplyPagination(PaginationExpression existingPagination); - + /// - /// Enables to extend, replace or remove a sparse fieldset that is being applied on a set of this resource type. - /// Tip: Use and - /// to safely change the fieldset without worrying about nulls. + /// Enables to extend, replace or remove a sparse fieldset that is being applied on a set of this resource type. Tip: Use + /// and to + /// safely change the fieldset without worrying about nulls. /// /// - /// This method executes twice for a single request: first to select which fields to retrieve from the data store and then to - /// select which fields to serialize. Including extra fields from this method will retrieve them, but not include them in the json output. - /// This enables you to expose calculated properties whose value depends on a field that is not in the sparse fieldset. + /// This method executes twice for a single request: first to select which fields to retrieve from the data store and then to select which fields to + /// serialize. Including extra fields from this method will retrieve them, but not include them in the json output. This enables you to expose calculated + /// properties whose value depends on a field that is not in the sparse fieldset. /// - /// The incoming sparse fieldset from query string. - /// At query execution time, this is null if the query string contains no sparse fieldset. - /// At serialization time, this contains all viewable fields if the query string contains no sparse fieldset. + /// + /// The incoming sparse fieldset from query string. At query execution time, this is null if the query string contains no sparse fieldset. At + /// serialization time, this contains all viewable fields if the query string contains no sparse fieldset. /// /// /// The new sparse fieldset, or null to discard the existing sparse fieldset and select all viewable fields. @@ -92,8 +98,8 @@ public interface IResourceDefinition SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet); /// - /// Enables to adapt the Entity Framework Core query, based on custom query string parameters. - /// Note this only works on primary resource requests, such as /articles, but not on /blogs/1/articles or /blogs?include=articles. + /// Enables to adapt the Entity Framework Core query, based on custom query string parameters. Note this only works on + /// primary resource requests, such as /articles, but not on /blogs/1/articles or /blogs?include=articles. /// /// /// /// ["isHighRisk"] = FilterByHighRisk /// }; /// } - /// + /// /// private static IQueryable FilterByHighRisk(IQueryable source, StringValues parameterValue) /// { /// bool isFilterOnHighRisk = bool.Parse(parameterValue); diff --git a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs index b3b744ba1f..90910c8d5b 100644 --- a/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/IResourceDefinitionAccessor.cs @@ -6,43 +6,43 @@ namespace JsonApiDotNetCore.Resources { /// - /// Retrieves an instance from the D/I container and invokes a callback on it. + /// Retrieves an instance from the D/I container and invokes a callback on it. /// public interface IResourceDefinitionAccessor { /// - /// Invokes for the specified resource type. + /// Invokes for the specified resource type. /// IReadOnlyCollection OnApplyIncludes(Type resourceType, IReadOnlyCollection existingIncludes); /// - /// Invokes for the specified resource type. + /// Invokes for the specified resource type. /// FilterExpression OnApplyFilter(Type resourceType, FilterExpression existingFilter); /// - /// Invokes for the specified resource type. + /// Invokes for the specified resource type. /// SortExpression OnApplySort(Type resourceType, SortExpression existingSort); /// - /// Invokes for the specified resource type. + /// Invokes for the specified resource type. /// PaginationExpression OnApplyPagination(Type resourceType, PaginationExpression existingPagination); /// - /// Invokes for the specified resource type. + /// Invokes for the specified resource type. /// SparseFieldSetExpression OnApplySparseFieldSet(Type resourceType, SparseFieldSetExpression existingSparseFieldSet); /// - /// Invokes for the specified resource type, - /// then returns the expression for the specified parameter name. + /// Invokes for the specified resource type, then + /// returns the expression for the specified parameter name. /// object GetQueryableHandlerForQueryStringParameter(Type resourceType, string parameterName); /// - /// Invokes for the specified resource. + /// Invokes for the specified resource. /// IDictionary GetMeta(Type resourceType, IIdentifiable resourceInstance); } diff --git a/src/JsonApiDotNetCore/Resources/Identifiable.cs b/src/JsonApiDotNetCore/Resources/Identifiable.cs index 6b95285da4..69875bfdac 100644 --- a/src/JsonApiDotNetCore/Resources/Identifiable.cs +++ b/src/JsonApiDotNetCore/Resources/Identifiable.cs @@ -5,12 +5,15 @@ namespace JsonApiDotNetCore.Resources { /// public abstract class Identifiable : Identifiable - { } + { + } /// - /// A convenient basic implementation of that provides conversion between and . + /// A convenient basic implementation of that provides conversion between and . /// - /// The resource identifier type. + /// + /// The resource identifier type. + /// public abstract class Identifiable : IIdentifiable { /// diff --git a/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs b/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs index 21782057db..332995b294 100644 --- a/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs +++ b/src/JsonApiDotNetCore/Resources/IdentifiableComparer.cs @@ -5,8 +5,8 @@ namespace JsonApiDotNetCore.Resources { /// - /// Compares `IIdentifiable` instances with each other based on their type and , - /// falling back to when both StringIds are null. + /// Compares `IIdentifiable` instances with each other based on their type and , falling back to + /// when both StringIds are null. /// [PublicAPI] public sealed class IdentifiableComparer : IEqualityComparer diff --git a/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs b/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs index b89e8ec580..807d6c3f23 100644 --- a/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs +++ b/src/JsonApiDotNetCore/Resources/IdentifiableExtensions.cs @@ -10,7 +10,7 @@ public static object GetTypedId(this IIdentifiable identifiable) ArgumentGuard.NotNull(identifiable, nameof(identifiable)); PropertyInfo property = identifiable.GetType().GetProperty(nameof(Identifiable.Id)); - + if (property == null) { throw new InvalidOperationException($"Resource of type '{identifiable.GetType()}' does not have an 'Id' property."); diff --git a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs index 1d3e945c00..06987f123a 100644 --- a/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs @@ -6,19 +6,22 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.Resources { /// - /// Provides a resource-centric extensibility point for executing custom code when something happens with a resource. - /// The goal here is to reduce the need for overriding the service and repository layers. + /// Provides a resource-centric extensibility point for executing custom code when something happens with a resource. The goal here is to reduce the need + /// for overriding the service and repository layers. /// - /// The resource type. + /// + /// The resource type. + /// [PublicAPI] public class JsonApiResourceDefinition : JsonApiResourceDefinition, IResourceDefinition where TResource : class, IIdentifiable { - public JsonApiResourceDefinition(IResourceGraph resourceGraph) + public JsonApiResourceDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } @@ -57,7 +60,7 @@ public virtual SortExpression OnApplySort(SortExpression existingSort) } /// - /// Creates a from a lambda expression. + /// Creates a from a lambda expression. /// /// /// sortElements = new List(); + var sortElements = new List(); - foreach (var (keySelector, sortDirection) in keySelectors) + foreach ((Expression> keySelector, ListSortDirection sortDirection) in keySelectors) { bool isAscending = sortDirection == ListSortDirection.Ascending; - var attribute = ResourceGraph.GetAttributes(keySelector).Single(); + AttrAttribute attribute = ResourceGraph.GetAttributes(keySelector).Single(); var sortElement = new SortElementExpression(new ResourceFieldChainExpression(attribute), isAscending); sortElements.Add(sortElement); @@ -111,8 +114,8 @@ public virtual IDictionary GetMeta(TResource resource) } /// - /// This is an alias type intended to simplify the implementation's method signature. - /// See for usage details. + /// This is an alias type intended to simplify the implementation's method signature. See for usage + /// details. /// public sealed class PropertySortOrder : List<(Expression> KeySelector, ListSortDirection SortDirection)> { diff --git a/src/JsonApiDotNetCore/Resources/OperationContainer.cs b/src/JsonApiDotNetCore/Resources/OperationContainer.cs index 85000dff0e..33dd890f39 100644 --- a/src/JsonApiDotNetCore/Resources/OperationContainer.cs +++ b/src/JsonApiDotNetCore/Resources/OperationContainer.cs @@ -17,8 +17,7 @@ public sealed class OperationContainer public ITargetedFields TargetedFields { get; } public IJsonApiRequest Request { get; } - public OperationContainer(OperationKind kind, IIdentifiable resource, ITargetedFields targetedFields, - IJsonApiRequest request) + public OperationContainer(OperationKind kind, IIdentifiable resource, ITargetedFields targetedFields, IJsonApiRequest request) { ArgumentGuard.NotNull(resource, nameof(resource)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); @@ -32,7 +31,7 @@ public OperationContainer(OperationKind kind, IIdentifiable resource, ITargetedF public void SetTransactionId(Guid transactionId) { - ((JsonApiRequest) Request).TransactionId = transactionId; + ((JsonApiRequest)Request).TransactionId = transactionId; } public OperationContainer WithResource(IIdentifiable resource) @@ -46,7 +45,7 @@ public ISet GetSecondaryResources() { var secondaryResources = new HashSet(IdentifiableComparer.Instance); - foreach (var relationship in TargetedFields.Relationships) + foreach (RelationshipAttribute relationship in TargetedFields.Relationships) { AddSecondaryResources(relationship, secondaryResources); } @@ -56,9 +55,9 @@ public ISet GetSecondaryResources() private void AddSecondaryResources(RelationshipAttribute relationship, HashSet secondaryResources) { - var rightValue = relationship.GetValue(Resource); + object rightValue = relationship.GetValue(Resource); - foreach (var rightResource in TypeHelper.ExtractResources(rightValue)) + foreach (IIdentifiable rightResource in TypeHelper.ExtractResources(rightValue)) { secondaryResources.Add(rightResource); } diff --git a/src/JsonApiDotNetCore/Resources/QueryStringParameterHandlers.cs b/src/JsonApiDotNetCore/Resources/QueryStringParameterHandlers.cs index 5914fd174f..1f5d412252 100644 --- a/src/JsonApiDotNetCore/Resources/QueryStringParameterHandlers.cs +++ b/src/JsonApiDotNetCore/Resources/QueryStringParameterHandlers.cs @@ -6,8 +6,8 @@ namespace JsonApiDotNetCore.Resources { /// - /// This is an alias type intended to simplify the implementation's method signature. - /// See for usage details. + /// This is an alias type intended to simplify the implementation's method signature. See + /// for usage details. /// public sealed class QueryStringParameterHandlers : Dictionary, StringValues, IQueryable>> { diff --git a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs index f4e88c8c27..1dfc5a09a7 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs @@ -8,7 +8,8 @@ namespace JsonApiDotNetCore.Resources { /// [PublicAPI] - public sealed class ResourceChangeTracker : IResourceChangeTracker where TResource : class, IIdentifiable + public sealed class ResourceChangeTracker : IResourceChangeTracker + where TResource : class, IIdentifiable { private readonly IJsonApiOptions _options; private readonly IResourceContextProvider _resourceContextProvider; @@ -18,8 +19,7 @@ public sealed class ResourceChangeTracker : IResourceChangeTracker _requestedAttributeValues; private IDictionary _finallyStoredAttributeValues; - public ResourceChangeTracker(IJsonApiOptions options, IResourceContextProvider resourceContextProvider, - ITargetedFields targetedFields) + public ResourceChangeTracker(IJsonApiOptions options, IResourceContextProvider resourceContextProvider, ITargetedFields targetedFields) { ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); @@ -35,7 +35,7 @@ public void SetInitiallyStoredAttributeValues(TResource resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - var resourceContext = _resourceContextProvider.GetResourceContext(); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(); _initiallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); } @@ -52,19 +52,18 @@ public void SetFinallyStoredAttributeValues(TResource resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - var resourceContext = _resourceContextProvider.GetResourceContext(); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(); _finallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); } - private IDictionary CreateAttributeDictionary(TResource resource, - IEnumerable attributes) + private IDictionary CreateAttributeDictionary(TResource resource, IEnumerable attributes) { var result = new Dictionary(); - foreach (var attribute in attributes) + foreach (AttrAttribute attribute in attributes) { object value = attribute.GetValue(resource); - var json = JsonConvert.SerializeObject(value, _options.SerializerSettings); + string json = JsonConvert.SerializeObject(value, _options.SerializerSettings); result.Add(attribute.PublicName, json); } @@ -74,12 +73,12 @@ private IDictionary CreateAttributeDictionary(TResource resource /// public bool HasImplicitChanges() { - foreach (var key in _initiallyStoredAttributeValues.Keys) + foreach (string key in _initiallyStoredAttributeValues.Keys) { if (_requestedAttributeValues.ContainsKey(key)) { - var requestedValue = _requestedAttributeValues[key]; - var actualValue = _finallyStoredAttributeValues[key]; + string requestedValue = _requestedAttributeValues[key]; + string actualValue = _finallyStoredAttributeValues[key]; if (requestedValue != actualValue) { @@ -88,8 +87,8 @@ public bool HasImplicitChanges() } else { - var initiallyStoredValue = _initiallyStoredAttributeValues[key]; - var finallyStoredValue = _finallyStoredAttributeValues[key]; + string initiallyStoredValue = _initiallyStoredAttributeValues[key]; + string finallyStoredValue = _finallyStoredAttributeValues[key]; if (initiallyStoredValue != finallyStoredValue) { diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs index fe5dc40088..958e735e4d 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -75,7 +75,7 @@ public object GetQueryableHandlerForQueryStringParameter(Type resourceType, stri ArgumentGuard.NotNull(parameterName, nameof(parameterName)); dynamic resourceDefinition = ResolveResourceDefinition(resourceType); - var handlers = resourceDefinition.OnRegisterQueryableHandlersForQueryStringParameters(); + dynamic handlers = resourceDefinition.OnRegisterQueryableHandlersForQueryStringParameters(); return handlers != null && handlers.ContainsKey(parameterName) ? handlers[parameterName] : null; } @@ -86,25 +86,25 @@ public IDictionary GetMeta(Type resourceType, IIdentifiable reso ArgumentGuard.NotNull(resourceType, nameof(resourceType)); dynamic resourceDefinition = ResolveResourceDefinition(resourceType); - return resourceDefinition.GetMeta((dynamic) resourceInstance); + return resourceDefinition.GetMeta((dynamic)resourceInstance); } protected virtual object ResolveResourceDefinition(Type resourceType) { - var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); if (resourceContext.IdentityType == typeof(int)) { - var intResourceDefinitionType = typeof(IResourceDefinition<>).MakeGenericType(resourceContext.ResourceType); - var intResourceDefinition = _serviceProvider.GetService(intResourceDefinitionType); - + Type intResourceDefinitionType = typeof(IResourceDefinition<>).MakeGenericType(resourceContext.ResourceType); + object intResourceDefinition = _serviceProvider.GetService(intResourceDefinitionType); + if (intResourceDefinition != null) { return intResourceDefinition; } } - var resourceDefinitionType = typeof(IResourceDefinition<,>).MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); + Type resourceDefinitionType = typeof(IResourceDefinition<,>).MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); return _serviceProvider.GetRequiredService(resourceDefinitionType); } } diff --git a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs index 76e96cb6f5..cb802a4b3b 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs @@ -31,7 +31,7 @@ public IIdentifiable CreateInstance(Type resourceType) public TResource CreateInstance() where TResource : IIdentifiable { - return (TResource) InnerCreateInstance(typeof(TResource), _serviceProvider); + return (TResource)InnerCreateInstance(typeof(TResource), _serviceProvider); } private static IIdentifiable InnerCreateInstance(Type type, IServiceProvider serviceProvider) @@ -46,10 +46,10 @@ private static IIdentifiable InnerCreateInstance(Type type, IServiceProvider ser } catch (Exception exception) { - throw new InvalidOperationException(hasSingleConstructorWithoutParameters + throw new InvalidOperationException( + hasSingleConstructorWithoutParameters ? $"Failed to create an instance of '{type.FullName}' using its default constructor." - : $"Failed to create an instance of '{type.FullName}' using injected constructor parameters.", - exception); + : $"Failed to create an instance of '{type.FullName}' using injected constructor parameters.", exception); } } @@ -63,18 +63,17 @@ public NewExpression CreateNewExpression(Type resourceType) return Expression.New(resourceType); } - List constructorArguments = new List(); + var constructorArguments = new List(); + + ConstructorInfo longestConstructor = GetLongestConstructor(resourceType); - var longestConstructor = GetLongestConstructor(resourceType); foreach (ParameterInfo constructorParameter in longestConstructor.GetParameters()) { try { - object constructorArgument = - ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, constructorParameter.ParameterType); + object constructorArgument = ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, constructorParameter.ParameterType); - var argumentExpression = - CreateTupleAccessExpressionForConstant(constructorArgument, constructorArgument.GetType()); + Expression argumentExpression = CreateTupleAccessExpressionForConstant(constructorArgument, constructorArgument.GetType()); constructorArguments.Add(argumentExpression); } @@ -123,8 +122,9 @@ private static ConstructorInfo GetLongestConstructor(Type type) for (int index = 1; index < constructors.Length; index++) { - var constructor = constructors[index]; + ConstructorInfo constructor = constructors[index]; int length = constructor.GetParameters().Length; + if (length > maxParameterLength) { bestMatch = constructor; diff --git a/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs b/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs index 6b20919de2..60b9e67505 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceHooksDefinition.cs @@ -7,13 +7,16 @@ namespace JsonApiDotNetCore.Resources { /// - /// Provides a resource-specific extensibility point for API developers to be notified of various events and influence behavior using custom code. - /// It is intended to improve the developer experience and reduce boilerplate for commonly required features. - /// The goal of this class is to reduce the frequency with which developers have to override the service and repository layers. + /// Provides a resource-specific extensibility point for API developers to be notified of various events and influence behavior using custom code. It is + /// intended to improve the developer experience and reduce boilerplate for commonly required features. The goal of this class is to reduce the frequency + /// with which developers have to override the service and repository layers. /// - /// The resource type. + /// + /// The resource type. + /// [PublicAPI] - public class ResourceHooksDefinition : IResourceHookContainer where TResource : class, IIdentifiable + public class ResourceHooksDefinition : IResourceHookContainer + where TResource : class, IIdentifiable { protected IResourceGraph ResourceGraph { get; } @@ -25,51 +28,105 @@ public ResourceHooksDefinition(IResourceGraph resourceGraph) } /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void AfterCreate(HashSet resources, ResourcePipeline pipeline) { } - + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual void AfterCreate(HashSet resources, ResourcePipeline pipeline) + { + } + /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void AfterRead(HashSet resources, ResourcePipeline pipeline, bool isIncluded = false) { } - + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual void AfterRead(HashSet resources, ResourcePipeline pipeline, bool isIncluded = false) + { + } + /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void AfterUpdate(HashSet resources, ResourcePipeline pipeline) { } - + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual void AfterUpdate(HashSet resources, ResourcePipeline pipeline) + { + } + /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) { } - + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual void AfterDelete(HashSet resources, ResourcePipeline pipeline, bool succeeded) + { + } + /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void AfterUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { } - + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual void AfterUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) + { + } + /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual IEnumerable BeforeCreate(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } - + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual IEnumerable BeforeCreate(IResourceHashSet resources, ResourcePipeline pipeline) + { + return resources; + } + /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) { } - + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) + { + } + /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual IEnumerable BeforeUpdate(IDiffableResourceHashSet resources, ResourcePipeline pipeline) { return resources; } - + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual IEnumerable BeforeUpdate(IDiffableResourceHashSet resources, ResourcePipeline pipeline) + { + return resources; + } + /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) { return resources; } - + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual IEnumerable BeforeDelete(IResourceHashSet resources, ResourcePipeline pipeline) + { + return resources; + } + /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { return ids; } - + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, + ResourcePipeline pipeline) + { + return ids; + } + /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { } - + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) + { + } + /// - /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. - public virtual IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) { return resources; } + /// + /// This method is part of Resource Hooks, which is an experimental feature and subject to change in future versions. + /// + public virtual IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline) + { + return resources; + } } } diff --git a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs index b9692bfac2..aeb773dd88 100644 --- a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs @@ -5,13 +5,14 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Serialization { /// - /// Server serializer implementation of for atomic:operations responses. + /// Server serializer implementation of for atomic:operations responses. /// [PublicAPI] public sealed class AtomicOperationsResponseSerializer : BaseSerializer, IJsonApiSerializer @@ -25,9 +26,8 @@ public sealed class AtomicOperationsResponseSerializer : BaseSerializer, IJsonAp /// public string ContentType { get; } = HeaderConstants.AtomicOperationsMediaType; - public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectBuilder, - IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IFieldsToSerialize fieldsToSerialize, - IJsonApiRequest request, IJsonApiOptions options) + public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectBuilder, IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, + IFieldsToSerialize fieldsToSerialize, IJsonApiRequest request, IJsonApiOptions options) : base(resourceObjectBuilder) { ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); @@ -79,9 +79,9 @@ private AtomicResultObject SerializeOperation(OperationContainer operation) _request.CopyFrom(operation.Request); _fieldsToSerialize.ResetCache(); - var resourceType = operation.Resource.GetType(); - var attributes = _fieldsToSerialize.GetAttributes(resourceType); - var relationships = _fieldsToSerialize.GetRelationships(resourceType); + Type resourceType = operation.Resource.GetType(); + IReadOnlyCollection attributes = _fieldsToSerialize.GetAttributes(resourceType); + IReadOnlyCollection relationships = _fieldsToSerialize.GetRelationships(resourceType); resourceObject = ResourceObjectBuilder.Build(operation.Resource, attributes, relationships); } @@ -99,7 +99,10 @@ private AtomicResultObject SerializeOperation(OperationContainer operation) private string SerializeErrorDocument(ErrorDocument errorDocument) { - return SerializeObject(errorDocument, _options.SerializerSettings, serializer => { serializer.ApplyErrorSettings(); }); + return SerializeObject(errorDocument, _options.SerializerSettings, serializer => + { + serializer.ApplyErrorSettings(); + }); } } } diff --git a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs index 070f501d01..c5a393efe9 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -14,8 +15,8 @@ namespace JsonApiDotNetCore.Serialization { /// - /// Abstract base class for deserialization. Deserializes JSON content into s - /// and constructs instances of the resource(s) in the document body. + /// Abstract base class for deserialization. Deserializes JSON content into s and constructs instances of the resource(s) + /// in the document body. /// [PublicAPI] public abstract class BaseDeserializer @@ -36,25 +37,30 @@ protected BaseDeserializer(IResourceContextProvider resourceContextProvider, IRe } /// - /// This method is called each time a is constructed - /// from the serialized content, which is used to do additional processing + /// This method is called each time a is constructed from the serialized content, which is used to do additional processing /// depending on the type of deserializer. /// /// - /// See the implementation of this method in - /// and for examples. + /// See the implementation of this method in and for examples. /// - /// The resource that was constructed from the document's body. - /// The metadata for the exposed field. - /// Relationship data for . Is null when is not a . + /// + /// The resource that was constructed from the document's body. + /// + /// + /// The metadata for the exposed field. + /// + /// + /// Relationship data for . Is null when is not a . + /// protected abstract void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null); protected object DeserializeBody(string body) { ArgumentGuard.NotNull(body, nameof(body)); - var bodyJToken = LoadJToken(body); + JToken bodyJToken = LoadJToken(body); Document = bodyJToken.ToObject(); + if (Document != null) { if (Document.IsManyData) @@ -74,10 +80,17 @@ protected object DeserializeBody(string body) /// /// Sets the attributes on a parsed resource. /// - /// The parsed resource. - /// Attributes and their values, as in the serialized content. - /// Exposed attributes for . - protected IIdentifiable SetAttributes(IIdentifiable resource, IDictionary attributeValues, IReadOnlyCollection attributes) + /// + /// The parsed resource. + /// + /// + /// Attributes and their values, as in the serialized content. + /// + /// + /// Exposed attributes for . + /// + protected IIdentifiable SetAttributes(IIdentifiable resource, IDictionary attributeValues, + IReadOnlyCollection attributes) { ArgumentGuard.NotNull(resource, nameof(resource)); ArgumentGuard.NotNull(attributes, nameof(attributes)); @@ -87,17 +100,17 @@ protected IIdentifiable SetAttributes(IIdentifiable resource, IDictionary /// Sets the relationships on a parsed resource. /// - /// The parsed resource. - /// Relationships and their values, as in the serialized content. - /// Exposed relationships for . - protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictionary relationshipValues, IReadOnlyCollection relationshipAttributes) + /// + /// The parsed resource. + /// + /// + /// Relationships and their values, as in the serialized content. + /// + /// + /// Exposed relationships for . + /// + protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictionary relationshipValues, + IReadOnlyCollection relationshipAttributes) { ArgumentGuard.NotNull(resource, nameof(resource)); ArgumentGuard.NotNull(relationshipAttributes, nameof(relationshipAttributes)); @@ -122,9 +142,10 @@ protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictio return resource; } - foreach (var attr in relationshipAttributes) + foreach (RelationshipAttribute attr in relationshipAttributes) { - var relationshipIsProvided = relationshipValues.TryGetValue(attr.PublicName, out RelationshipEntry relationshipData); + bool relationshipIsProvided = relationshipValues.TryGetValue(attr.PublicName, out RelationshipEntry relationshipData); + if (!relationshipIsProvided || !relationshipData.IsPopulated) { continue; @@ -139,7 +160,7 @@ protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictio SetHasManyRelationship(resource, hasManyAttribute, relationshipData); } } - + return resource; } @@ -155,10 +176,11 @@ protected JToken LoadJToken(string body) } /// - /// Creates an instance of the referenced type in - /// and sets its attributes and relationships. + /// Creates an instance of the referenced type in and sets its attributes and relationships. /// - /// The parsed resource. + /// + /// The parsed resource. + /// protected IIdentifiable ParseResourceObject(ResourceObject data) { AssertHasType(data, null); @@ -168,8 +190,8 @@ protected IIdentifiable ParseResourceObject(ResourceObject data) AssertHasNoLid(data); } - var resourceContext = GetExistingResourceContext(data.Type); - var resource = ResourceFactory.CreateInstance(resourceContext.ResourceType); + ResourceContext resourceContext = GetExistingResourceContext(data.Type); + IIdentifiable resource = ResourceFactory.CreateInstance(resourceContext.ResourceType); resource = SetAttributes(resource, data.Attributes, resourceContext.Attributes); resource = SetRelationships(resource, data.Relationships, resourceContext.Relationships); @@ -186,11 +208,12 @@ protected IIdentifiable ParseResourceObject(ResourceObject data) protected ResourceContext GetExistingResourceContext(string publicName) { - var resourceContext = ResourceContextProvider.GetResourceContext(publicName); + ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(publicName); + if (resourceContext == null) { - throw new JsonApiSerializationException("Request body includes unknown resource type.", - $"Resource type '{publicName}' does not exist.", atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Request body includes unknown resource type.", $"Resource type '{publicName}' does not exist.", + atomicOperationIndex: AtomicOperationIndex); } return resourceContext; @@ -203,12 +226,11 @@ private void SetHasOneRelationship(IIdentifiable resource, HasOneAttribute hasOn { if (relationshipData.ManyData != null) { - throw new JsonApiSerializationException("Expected single data element for to-one relationship.", - $"Expected single data element for '{hasOneRelationship.PublicName}' relationship.", - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Expected single data element for to-one relationship.", + $"Expected single data element for '{hasOneRelationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } - var rightResource = CreateRightResource(hasOneRelationship, relationshipData.SingleData); + IIdentifiable rightResource = CreateRightResource(hasOneRelationship, relationshipData.SingleData); hasOneRelationship.SetValue(resource, rightResource); // depending on if this base parser is used client-side or server-side, @@ -219,40 +241,34 @@ private void SetHasOneRelationship(IIdentifiable resource, HasOneAttribute hasOn /// /// Sets a HasMany relationship. /// - private void SetHasManyRelationship( - IIdentifiable resource, - HasManyAttribute hasManyRelationship, - RelationshipEntry relationshipData) + private void SetHasManyRelationship(IIdentifiable resource, HasManyAttribute hasManyRelationship, RelationshipEntry relationshipData) { if (relationshipData.ManyData == null) { - throw new JsonApiSerializationException("Expected data[] element for to-many relationship.", - $"Expected data[] element for '{hasManyRelationship.PublicName}' relationship.", - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Expected data[] element for to-many relationship.", + $"Expected data[] element for '{hasManyRelationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } - var rightResources = relationshipData.ManyData - .Select(rio => CreateRightResource(hasManyRelationship, rio)) + HashSet rightResources = relationshipData.ManyData.Select(rio => CreateRightResource(hasManyRelationship, rio)) .ToHashSet(IdentifiableComparer.Instance); - var convertedCollection = TypeHelper.CopyToTypedCollection(rightResources, hasManyRelationship.Property.PropertyType); + IEnumerable convertedCollection = TypeHelper.CopyToTypedCollection(rightResources, hasManyRelationship.Property.PropertyType); hasManyRelationship.SetValue(resource, convertedCollection); AfterProcessField(resource, hasManyRelationship, relationshipData); } - private IIdentifiable CreateRightResource(RelationshipAttribute relationship, - ResourceIdentifierObject resourceIdentifierObject) + private IIdentifiable CreateRightResource(RelationshipAttribute relationship, ResourceIdentifierObject resourceIdentifierObject) { if (resourceIdentifierObject != null) { AssertHasType(resourceIdentifierObject, relationship); AssertHasIdOrLid(resourceIdentifierObject, relationship); - var rightResourceContext = GetExistingResourceContext(resourceIdentifierObject.Type); + ResourceContext rightResourceContext = GetExistingResourceContext(resourceIdentifierObject.Type); AssertRightTypeIsCompatible(rightResourceContext, relationship); - var rightInstance = ResourceFactory.CreateInstance(rightResourceContext.ResourceType); + IIdentifiable rightInstance = ResourceFactory.CreateInstance(rightResourceContext.ResourceType); rightInstance.StringId = resourceIdentifierObject.Id; rightInstance.LocalId = resourceIdentifierObject.Lid; @@ -267,12 +283,11 @@ private void AssertHasType(ResourceIdentifierObject resourceIdentifierObject, Re { if (resourceIdentifierObject.Type == null) { - var details = relationship != null + string details = relationship != null ? $"Expected 'type' element in '{relationship.PublicName}' relationship." : "Expected 'type' element in 'data' element."; - throw new JsonApiSerializationException("Request body must include 'type' element.", details, - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Request body must include 'type' element.", details, atomicOperationIndex: AtomicOperationIndex); } } @@ -286,8 +301,7 @@ private void AssertHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, if (hasNone || hasBoth) { throw new JsonApiSerializationException("Request body must include 'id' or 'lid' element.", - $"Expected 'id' or 'lid' element in '{relationship.PublicName}' relationship.", - atomicOperationIndex: AtomicOperationIndex); + $"Expected 'id' or 'lid' element in '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } } else @@ -295,8 +309,7 @@ private void AssertHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, if (resourceIdentifierObject.Id == null) { throw new JsonApiSerializationException("Request body must include 'id' element.", - $"Expected 'id' element in '{relationship.PublicName}' relationship.", - atomicOperationIndex: AtomicOperationIndex); + $"Expected 'id' element in '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } AssertHasNoLid(resourceIdentifierObject); @@ -308,8 +321,7 @@ private void AssertHasNoLid(ResourceIdentifierObject resourceIdentifierObject) { if (resourceIdentifierObject.Lid != null) { - throw new JsonApiSerializationException("Local IDs cannot be used at this endpoint.", null, - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Local IDs cannot be used at this endpoint.", null, atomicOperationIndex: AtomicOperationIndex); } } @@ -332,7 +344,7 @@ private object ConvertAttrValue(object newValue, Type targetType) } // the attribute value is a native C# type. - var convertedValue = TypeHelper.ConvertType(newValue, targetType); + object convertedValue = TypeHelper.ConvertType(newValue, targetType); return convertedValue; } diff --git a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs index d9248c56d7..33ebf99f64 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs @@ -10,8 +10,8 @@ namespace JsonApiDotNetCore.Serialization { /// - /// Abstract base class for serialization. - /// Uses to convert resources into s and wraps them in a . + /// Abstract base class for serialization. Uses to convert resources into s and wraps + /// them in a . /// public abstract class BaseSerializer { @@ -25,49 +25,74 @@ protected BaseSerializer(IResourceObjectBuilder resourceObjectBuilder) } /// - /// Builds a for . - /// Adds the attributes and relationships that are enlisted in and . + /// Builds a for . Adds the attributes and relationships that are enlisted in + /// and . /// - /// Resource to build a for. - /// Attributes to include in the building process. - /// Relationships to include in the building process. - /// The resource object that was built. - protected Document Build(IIdentifiable resource, IReadOnlyCollection attributes, IReadOnlyCollection relationships) + /// + /// Resource to build a for. + /// + /// + /// Attributes to include in the building process. + /// + /// + /// Relationships to include in the building process. + /// + /// + /// The resource object that was built. + /// + protected Document Build(IIdentifiable resource, IReadOnlyCollection attributes, + IReadOnlyCollection relationships) { if (resource == null) { return new Document(); } - return new Document { Data = ResourceObjectBuilder.Build(resource, attributes, relationships) }; + return new Document + { + Data = ResourceObjectBuilder.Build(resource, attributes, relationships) + }; } /// - /// Builds a for . - /// Adds the attributes and relationships that are enlisted in and . + /// Builds a for . Adds the attributes and relationships that are enlisted in + /// and . /// - /// Resource to build a for. - /// Attributes to include in the building process. - /// Relationships to include in the building process. - /// The resource object that was built. - protected Document Build(IReadOnlyCollection resources, IReadOnlyCollection attributes, IReadOnlyCollection relationships) + /// + /// Resource to build a for. + /// + /// + /// Attributes to include in the building process. + /// + /// + /// Relationships to include in the building process. + /// + /// + /// The resource object that was built. + /// + protected Document Build(IReadOnlyCollection resources, IReadOnlyCollection attributes, + IReadOnlyCollection relationships) { ArgumentGuard.NotNull(resources, nameof(resources)); var data = new List(); + foreach (IIdentifiable resource in resources) { data.Add(ResourceObjectBuilder.Build(resource, attributes, relationships)); } - return new Document { Data = data }; + return new Document + { + Data = data + }; } protected string SerializeObject(object value, JsonSerializerSettings defaultSettings, Action changeSerializer = null) { ArgumentGuard.NotNull(defaultSettings, nameof(defaultSettings)); - JsonSerializer serializer = JsonSerializer.CreateDefault(defaultSettings); + var serializer = JsonSerializer.CreateDefault(defaultSettings); changeSerializer?.Invoke(serializer); using var stringWriter = new StringWriter(); diff --git a/src/JsonApiDotNetCore/Serialization/Building/IIncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IIncludedResourceObjectBuilder.cs index 88a1b21afc..3a49f0d413 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IIncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IIncludedResourceObjectBuilder.cs @@ -13,8 +13,8 @@ public interface IIncludedResourceObjectBuilder IList Build(); /// - /// Extracts the included resources from using the - /// (arbitrarily deeply nested) included relationships in . + /// Extracts the included resources from using the (arbitrarily deeply nested) included relationships in + /// . /// void IncludeRelationshipChain(IReadOnlyCollection inclusionChain, IIdentifiable rootResource); } diff --git a/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs index 39137d2612..17a492f9b2 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ILinkBuilder.cs @@ -20,7 +20,7 @@ public interface ILinkBuilder ResourceLinks GetResourceLinks(string resourceName, string id); /// - /// Builds the links object that is included in the values of the . + /// Builds the links object that is included in the values of the . /// RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable parent); } diff --git a/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs index 0a83126407..1e668feca5 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IMetaBuilder.cs @@ -10,8 +10,8 @@ namespace JsonApiDotNetCore.Serialization.Building public interface IMetaBuilder { /// - /// Merges the specified dictionary with existing key/value pairs. In the event of a key collision, - /// the value from the specified dictionary will overwrite the existing one. + /// Merges the specified dictionary with existing key/value pairs. In the event of a key collision, the value from the specified dictionary will + /// overwrite the existing one. /// void Add(IReadOnlyDictionary values); diff --git a/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilder.cs index 4dea18abdb..ff182c2dab 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilder.cs @@ -6,19 +6,26 @@ namespace JsonApiDotNetCore.Serialization.Building { /// - /// Responsible for converting resources into s - /// given a collection of attributes and relationships. - /// + /// Responsible for converting resources into s given a collection of attributes and relationships. + /// public interface IResourceObjectBuilder { /// - /// Converts into a . - /// Adds the attributes and relationships that are enlisted in and . + /// Converts into a . Adds the attributes and relationships that are enlisted in + /// and . /// - /// Resource to build a for. - /// Attributes to include in the building process. - /// Relationships to include in the building process. - /// The resource object that was built. + /// + /// Resource to build a for. + /// + /// + /// Attributes to include in the building process. + /// + /// + /// Relationships to include in the building process. + /// + /// + /// The resource object that was built. + /// ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes, IReadOnlyCollection relationships); } } diff --git a/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs index 5edc8801d4..0fcaefd32a 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs @@ -1,7 +1,7 @@ namespace JsonApiDotNetCore.Serialization.Building { /// - /// Service that provides the server serializer with . + /// Service that provides the server serializer with . /// public interface IResourceObjectBuilderSettingsProvider { diff --git a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs index 99ee0c2e7d..7db926c6da 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -20,12 +21,9 @@ public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedRes private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly SparseFieldSetCache _sparseFieldSetCache; - public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, - ILinkBuilder linkBuilder, - IResourceContextProvider resourceContextProvider, - IEnumerable constraintProviders, - IResourceDefinitionAccessor resourceDefinitionAccessor, - IResourceObjectBuilderSettingsProvider settingsProvider) + public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILinkBuilder linkBuilder, IResourceContextProvider resourceContextProvider, + IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, + IResourceObjectBuilderSettingsProvider settingsProvider) : base(resourceContextProvider, settingsProvider.Get()) { ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); @@ -46,7 +44,7 @@ public IList Build() if (_included.Any()) { // cleans relationship dictionaries and adds links of resources. - foreach (var resourceObject in _included) + foreach (ResourceObject resourceObject in _included) { if (resourceObject.Relationships != null) { @@ -55,17 +53,19 @@ public IList Build() resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); } + return _included.ToArray(); } + return null; } private void UpdateRelationships(ResourceObject resourceObject) { - foreach (var relationshipName in resourceObject.Relationships.Keys.ToArray()) + foreach (string relationshipName in resourceObject.Relationships.Keys.ToArray()) { - var resourceContext = ResourceContextProvider.GetResourceContext(resourceObject.Type); - var relationship = resourceContext.Relationships.Single(rel => rel.PublicName == relationshipName); + ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(resourceObject.Type); + RelationshipAttribute relationship = resourceContext.Relationships.Single(rel => rel.PublicName == relationshipName); if (!IsRelationshipInSparseFieldSet(relationship)) { @@ -78,26 +78,25 @@ private void UpdateRelationships(ResourceObject resourceObject) private static IDictionary PruneRelationshipEntries(ResourceObject resourceObject) { - var pruned = resourceObject.Relationships - .Where(pair => pair.Value.IsPopulated || pair.Value.Links != null) + Dictionary pruned = resourceObject.Relationships.Where(pair => pair.Value.IsPopulated || pair.Value.Links != null) .ToDictionary(pair => pair.Key, pair => pair.Value); - + return !pruned.Any() ? null : pruned; } private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) { - var resourceContext = ResourceContextProvider.GetResourceContext(relationship.LeftType); + ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(relationship.LeftType); - var fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); + IReadOnlyCollection fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); return fieldSet.Contains(relationship); } - /// + /// public override ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) { - var resourceObject = base.Build(resource, attributes, relationships); + ResourceObject resourceObject = base.Build(resource, attributes, relationships); resourceObject.Meta = _resourceDefinitionAccessor.GetMeta(resource.GetType(), resource); @@ -113,9 +112,9 @@ public void IncludeRelationshipChain(IReadOnlyCollection // We don't have to build a resource object for the root resource because // this one is already encoded in the documents primary data, so we process the chain // starting from the first related resource. - var relationship = inclusionChain.First(); - var chainRemainder = ShiftChain(inclusionChain); - var related = relationship.GetValue(rootResource); + RelationshipAttribute relationship = inclusionChain.First(); + IList chainRemainder = ShiftChain(inclusionChain); + object related = relationship.GetValue(rootResource); ProcessChain(related, chainRemainder); } @@ -137,20 +136,22 @@ private void ProcessChain(object related, IList inclusion private void ProcessRelationship(IIdentifiable parent, IList inclusionChain) { // get the resource object for parent. - var resourceObject = GetOrBuildResourceObject(parent); + ResourceObject resourceObject = GetOrBuildResourceObject(parent); + if (!inclusionChain.Any()) { return; } - var nextRelationship = inclusionChain.First(); - var chainRemainder = inclusionChain.ToList(); + RelationshipAttribute nextRelationship = inclusionChain.First(); + List chainRemainder = inclusionChain.ToList(); chainRemainder.RemoveAt(0); - var nextRelationshipName = nextRelationship.PublicName; - var relationshipsObject = resourceObject.Relationships; + string nextRelationshipName = nextRelationship.PublicName; + IDictionary relationshipsObject = resourceObject.Relationships; + // add the relationship entry in the relationship object. - if (!relationshipsObject.TryGetValue(nextRelationshipName, out var relationshipEntry)) + if (!relationshipsObject.TryGetValue(nextRelationshipName, out RelationshipEntry relationshipEntry)) { relationshipEntry = GetRelationshipData(nextRelationship, parent); relationshipsObject[nextRelationshipName] = relationshipEntry; @@ -161,44 +162,48 @@ private void ProcessRelationship(IIdentifiable parent, IList ShiftChain(IReadOnlyCollection chain) { - var chainRemainder = chain.ToList(); + List chainRemainder = chain.ToList(); chainRemainder.RemoveAt(0); return chainRemainder; } /// - /// We only need an empty relationship object entry here. It will be populated in the - /// ProcessRelationships method. + /// We only need an empty relationship object entry here. It will be populated in the ProcessRelationships method. /// protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(resource, nameof(resource)); - return new RelationshipEntry { Links = _linkBuilder.GetRelationshipLinks(relationship, resource) }; + return new RelationshipEntry + { + Links = _linkBuilder.GetRelationshipLinks(relationship, resource) + }; } /// - /// Gets the resource object for by searching the included list. - /// If it was not already built, it is constructed and added to the inclusion list. + /// Gets the resource object for by searching the included list. If it was not already built, it is constructed and added to + /// the inclusion list. /// private ResourceObject GetOrBuildResourceObject(IIdentifiable parent) { - var type = parent.GetType(); - var resourceName = ResourceContextProvider.GetResourceContext(type).PublicName; - var entry = _included.SingleOrDefault(ro => ro.Type == resourceName && ro.Id == parent.StringId); + Type type = parent.GetType(); + string resourceName = ResourceContextProvider.GetResourceContext(type).PublicName; + ResourceObject entry = _included.SingleOrDefault(ro => ro.Type == resourceName && ro.Id == parent.StringId); + if (entry == null) { entry = Build(parent, _fieldsToSerialize.GetAttributes(type), _fieldsToSerialize.GetRelationships(type)); _included.Add(entry); } + return entry; } } diff --git a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs index db15f31300..50ac9f5851 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs @@ -28,11 +28,8 @@ public class LinkBuilder : ILinkBuilder private readonly IJsonApiRequest _request; private readonly IPaginationContext _paginationContext; - public LinkBuilder(IJsonApiOptions options, - IJsonApiRequest request, - IPaginationContext paginationContext, - IResourceContextProvider provider, - IRequestQueryStringAccessor queryStringAccessor) + public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPaginationContext paginationContext, IResourceContextProvider provider, + IRequestQueryStringAccessor queryStringAccessor) { ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(request, nameof(request)); @@ -53,19 +50,23 @@ public TopLevelLinks GetTopLevelLinks() ResourceContext resourceContext = _request.PrimaryResource; TopLevelLinks topLevelLinks = null; + if (ShouldAddTopLevelLink(resourceContext, LinkTypes.Self)) { - topLevelLinks = new TopLevelLinks {Self = GetSelfTopLevelLink(resourceContext, null)}; + topLevelLinks = new TopLevelLinks + { + Self = GetSelfTopLevelLink(resourceContext, null) + }; } if (ShouldAddTopLevelLink(resourceContext, LinkTypes.Related) && _request.Kind == EndpointKind.Relationship) - { + { topLevelLinks ??= new TopLevelLinks(); topLevelLinks.Related = GetRelatedRelationshipLink(_request.PrimaryResource.PublicName, _request.PrimaryId, _request.Relationship.PublicName); } if (ShouldAddTopLevelLink(resourceContext, LinkTypes.Paging) && _paginationContext.PageSize != null && _request.IsCollection) - { + { SetPageLinks(resourceContext, topLevelLinks ??= new TopLevelLinks()); } @@ -73,9 +74,8 @@ public TopLevelLinks GetTopLevelLinks() } /// - /// Checks if the top-level should be added by first checking - /// configuration on the , and if not configured, by checking with the - /// global configuration in . + /// Checks if the top-level should be added by first checking configuration on the , and if not + /// configured, by checking with the global configuration in . /// private bool ShouldAddTopLevelLink(ResourceContext resourceContext, LinkTypes link) { @@ -142,7 +142,7 @@ private string GetSelfTopLevelLink(ResourceContext resourceContext, Action> updateAction) { - var parameters = _queryStringAccessor.Query.ToDictionary(pair => pair.Key, pair => pair.Value.ToString()); + Dictionary parameters = _queryStringAccessor.Query.ToDictionary(pair => pair.Key, pair => pair.Value.ToString()); updateAction?.Invoke(parameters); string queryString = QueryString.Create(parameters).Value; @@ -158,13 +158,12 @@ private string GetPageLink(ResourceContext resourceContext, int pageOffset, Page { return GetSelfTopLevelLink(resourceContext, parameters => { - var existingPageSizeParameterValue = parameters.ContainsKey(PageSizeParameterName) - ? parameters[PageSizeParameterName] - : null; + string existingPageSizeParameterValue = parameters.ContainsKey(PageSizeParameterName) ? parameters[PageSizeParameterName] : null; PageSize newTopPageSize = Equals(pageSize, _options.DefaultPageSize) ? null : pageSize; string newPageSizeParameterValue = ChangeTopPageSize(existingPageSizeParameterValue, newTopPageSize); + if (newPageSizeParameterValue == null) { parameters.Remove(PageSizeParameterName); @@ -187,8 +186,8 @@ private string GetPageLink(ResourceContext resourceContext, int pageOffset, Page private string ChangeTopPageSize(string pageSizeParameterValue, PageSize topPageSize) { - var elements = ParsePageSizeExpression(pageSizeParameterValue); - var elementInTopScopeIndex = elements.FindIndex(expression => expression.Scope == null); + List elements = ParsePageSizeExpression(pageSizeParameterValue); + int elementInTopScopeIndex = elements.FindIndex(expression => expression.Scope == null); if (topPageSize != null) { @@ -211,7 +210,7 @@ private string ChangeTopPageSize(string pageSizeParameterValue, PageSize topPage } } - var parameterValue = string.Join(',', + string parameterValue = string.Join(',', elements.Select(expression => expression.Scope == null ? expression.Value.ToString() : $"{expression.Scope}:{expression.Value}")); return parameterValue == string.Empty ? null : parameterValue; @@ -224,10 +223,10 @@ private List ParsePageSizeExpressio return new List(); } - var requestResource = _request.SecondaryResource ?? _request.PrimaryResource; + ResourceContext requestResource = _request.SecondaryResource ?? _request.PrimaryResource; var parser = new PaginationParser(_provider); - var paginationExpression = parser.Parse(pageSizeParameterValue, requestResource); + PaginationQueryStringValueExpression paginationExpression = parser.Parse(pageSizeParameterValue, requestResource); return paginationExpression.Elements.ToList(); } @@ -238,10 +237,14 @@ public ResourceLinks GetResourceLinks(string resourceName, string id) ArgumentGuard.NotNull(resourceName, nameof(resourceName)); ArgumentGuard.NotNull(id, nameof(id)); - var resourceContext = _provider.GetResourceContext(resourceName); + ResourceContext resourceContext = _provider.GetResourceContext(resourceName); + if (ShouldAddResourceLink(resourceContext, LinkTypes.Self)) { - return new ResourceLinks { Self = GetSelfResourceLink(resourceName, id) }; + return new ResourceLinks + { + Self = GetSelfResourceLink(resourceName, id) + }; } return null; @@ -253,12 +256,16 @@ public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(parent, nameof(parent)); - var parentResourceContext = _provider.GetResourceContext(parent.GetType()); - var childNavigation = relationship.PublicName; + ResourceContext parentResourceContext = _provider.GetResourceContext(parent.GetType()); + string childNavigation = relationship.PublicName; RelationshipLinks links = null; + if (ShouldAddRelationshipLink(parentResourceContext, relationship, LinkTypes.Related)) { - links = new RelationshipLinks { Related = GetRelatedRelationshipLink(parentResourceContext.PublicName, parent.StringId, childNavigation) }; + links = new RelationshipLinks + { + Related = GetRelatedRelationshipLink(parentResourceContext.PublicName, parent.StringId, childNavigation) + }; } if (ShouldAddRelationshipLink(parentResourceContext, relationship, LinkTypes.Self)) @@ -270,7 +277,6 @@ public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship return links; } - private string GetSelfRelationshipLink(string parent, string parentId, string navigation) { return $"{_request.BasePath}/{parent}/{parentId}/relationships/{navigation}"; @@ -287,9 +293,8 @@ private string GetRelatedRelationshipLink(string parent, string parentId, string } /// - /// Checks if the resource object level should be added by first checking - /// configuration on the , and if not configured, by checking with the - /// global configuration in . + /// Checks if the resource object level should be added by first checking configuration on the , + /// and if not configured, by checking with the global configuration in . /// private bool ShouldAddResourceLink(ResourceContext resourceContext, LinkTypes link) { @@ -302,14 +307,14 @@ private bool ShouldAddResourceLink(ResourceContext resourceContext, LinkTypes li { return resourceContext.ResourceLinks.HasFlag(link); } + return _options.ResourceLinks.HasFlag(link); } /// - /// Checks if the resource object level should be added by first checking - /// configuration on the attribute, if not configured by checking - /// the , and if not configured by checking with the - /// global configuration in . + /// Checks if the resource object level should be added by first checking configuration on the + /// attribute, if not configured by checking the , and if not configured by checking with the global configuration in + /// . /// private bool ShouldAddRelationshipLink(ResourceContext resourceContext, RelationshipAttribute relationship, LinkTypes link) { @@ -317,6 +322,7 @@ private bool ShouldAddRelationshipLink(ResourceContext resourceContext, Relation { return relationship.Links.HasFlag(link); } + if (resourceContext.RelationshipLinks != LinkTypes.NotConfigured) { return resourceContext.RelationshipLinks.HasFlag(link); diff --git a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs index d641910969..6d35b8385b 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs @@ -32,9 +32,7 @@ public void Add(IReadOnlyDictionary values) { ArgumentGuard.NotNull(values, nameof(values)); - _meta = values.Keys.Union(_meta.Keys) - .ToDictionary(key => key, - key => values.ContainsKey(key) ? values[key] : _meta[key]); + _meta = values.Keys.Union(_meta.Keys).ToDictionary(key => key, key => values.ContainsKey(key) ? values[key] : _meta[key]); } /// @@ -47,7 +45,8 @@ public IDictionary Build() _meta.Add(key, _paginationContext.TotalResourceCount); } - var extraMeta = _responseMeta.GetMeta(); + IReadOnlyDictionary extraMeta = _responseMeta.GetMeta(); + if (extraMeta != null) { Add(extraMeta); diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs index b67e04e410..3e1c782c18 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs @@ -9,12 +9,12 @@ namespace JsonApiDotNetCore.Serialization.Building { - /// + /// [PublicAPI] public class ResourceObjectBuilder : IResourceObjectBuilder { - protected IResourceContextProvider ResourceContextProvider { get; } private readonly ResourceObjectBuilderSettings _settings; + protected IResourceContextProvider ResourceContextProvider { get; } public ResourceObjectBuilder(IResourceContextProvider resourceContextProvider, ResourceObjectBuilderSettings settings) { @@ -25,20 +25,26 @@ public ResourceObjectBuilder(IResourceContextProvider resourceContextProvider, R _settings = settings; } - /// - public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) + /// + public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, + IReadOnlyCollection relationships = null) { ArgumentGuard.NotNull(resource, nameof(resource)); - var resourceContext = ResourceContextProvider.GetResourceContext(resource.GetType()); + ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(resource.GetType()); // populating the top-level "type" and "id" members. - var resourceObject = new ResourceObject { Type = resourceContext.PublicName, Id = resource.StringId }; + var resourceObject = new ResourceObject + { + Type = resourceContext.PublicName, + Id = resource.StringId + }; // populating the top-level "attribute" member of a resource object. never include "id" as an attribute if (attributes != null) { - var attributesWithoutId = attributes.Where(attr => attr.Property.Name != nameof(Identifiable.Id)).ToArray(); + AttrAttribute[] attributesWithoutId = attributes.Where(attr => attr.Property.Name != nameof(Identifiable.Id)).ToArray(); + if (attributesWithoutId.Any()) { ProcessAttributes(resource, attributesWithoutId, resourceObject); @@ -55,22 +61,23 @@ public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection< } /// - /// Builds the entries of the "relationships - /// objects". The default behavior is to just construct a resource linkage - /// with the "data" field populated with "single" or "many" data. - /// Depending on the requirements of the implementation (server or client serializer), - /// this may be overridden. + /// Builds the entries of the "relationships objects". The default behavior is to just construct a resource linkage with + /// the "data" field populated with "single" or "many" data. Depending on the requirements of the implementation (server or client serializer), this may + /// be overridden. /// protected virtual RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(resource, nameof(resource)); - return new RelationshipEntry { Data = GetRelatedResourceLinkage(relationship, resource) }; + return new RelationshipEntry + { + Data = GetRelatedResourceLinkage(relationship, resource) + }; } /// - /// Gets the value for the property. + /// Gets the value for the property. /// protected object GetRelatedResourceLinkage(RelationshipAttribute relationship, IIdentifiable resource) { @@ -78,12 +85,12 @@ protected object GetRelatedResourceLinkage(RelationshipAttribute relationship, I ArgumentGuard.NotNull(resource, nameof(resource)); return relationship is HasOneAttribute hasOne - ? (object) GetRelatedResourceLinkageForHasOne(hasOne, resource) - : GetRelatedResourceLinkageForHasMany((HasManyAttribute) relationship, resource); + ? (object)GetRelatedResourceLinkageForHasOne(hasOne, resource) + : GetRelatedResourceLinkageForHasMany((HasManyAttribute)relationship, resource); } /// - /// Builds a for a HasOne relationship. + /// Builds a for a HasOne relationship. /// private ResourceIdentifierObject GetRelatedResourceLinkageForHasOne(HasOneAttribute relationship, IIdentifiable resource) { @@ -98,17 +105,18 @@ private ResourceIdentifierObject GetRelatedResourceLinkageForHasOne(HasOneAttrib } /// - /// Builds the s for a HasMany relationship. + /// Builds the s for a HasMany relationship. /// private IList GetRelatedResourceLinkageForHasMany(HasManyAttribute relationship, IIdentifiable resource) { - var value = relationship.GetValue(resource); - var relatedResources = TypeHelper.ExtractResources(value); - + object value = relationship.GetValue(resource); + ICollection relatedResources = TypeHelper.ExtractResources(value); + var manyData = new List(); + if (relatedResources != null) { - foreach (var relatedResource in relatedResources) + foreach (IIdentifiable relatedResource in relatedResources) { manyData.Add(GetResourceIdentifier(relatedResource)); } @@ -118,11 +126,12 @@ private IList GetRelatedResourceLinkageForHasMany(HasM } /// - /// Creates a from . + /// Creates a from . /// private ResourceIdentifierObject GetResourceIdentifier(IIdentifiable resource) { - var resourceName = ResourceContextProvider.GetResourceContext(resource.GetType()).PublicName; + string resourceName = ResourceContextProvider.GetResourceContext(resource.GetType()).PublicName; + return new ResourceIdentifierObject { Type = resourceName, @@ -135,9 +144,10 @@ private ResourceIdentifierObject GetResourceIdentifier(IIdentifiable resource) /// private void ProcessRelationships(IIdentifiable resource, IEnumerable relationships, ResourceObject ro) { - foreach (var rel in relationships) + foreach (RelationshipAttribute rel in relationships) { - var relData = GetRelationshipData(rel, resource); + RelationshipEntry relData = GetRelationshipData(rel, resource); + if (relData != null) { (ro.Relationships ??= new Dictionary()).Add(rel.PublicName, relData); @@ -151,7 +161,8 @@ private void ProcessRelationships(IIdentifiable resource, IEnumerable attributes, ResourceObject ro) { ro.Attributes = new Dictionary(); - foreach (var attr in attributes) + + foreach (AttrAttribute attr in attributes) { object value = attr.GetValue(resource); @@ -160,7 +171,8 @@ private void ProcessAttributes(IIdentifiable resource, IEnumerable - /// Options used to configure how fields of a model get serialized into - /// a JSON:API . + /// Options used to configure how fields of a model get serialized into a JSON:API . /// [PublicAPI] public sealed class ResourceObjectBuilderSettings @@ -14,8 +13,7 @@ public sealed class ResourceObjectBuilderSettings public NullValueHandling SerializerNullValueHandling { get; } public DefaultValueHandling SerializerDefaultValueHandling { get; } - public ResourceObjectBuilderSettings( - NullValueHandling serializerNullValueHandling = NullValueHandling.Include, + public ResourceObjectBuilderSettings(NullValueHandling serializerNullValueHandling = NullValueHandling.Include, DefaultValueHandling serializerDefaultValueHandling = DefaultValueHandling.Include) { SerializerNullValueHandling = serializerNullValueHandling; diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs index 83422527ed..d28a7591eb 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs @@ -4,8 +4,8 @@ namespace JsonApiDotNetCore.Serialization.Building { /// - /// This implementation of the behavior provider reads the defaults/nulls query string parameters that - /// can, if provided, override the settings in . + /// This implementation of the behavior provider reads the defaults/nulls query string parameters that can, if provided, override the settings in + /// . /// public sealed class ResourceObjectBuilderSettingsProvider : IResourceObjectBuilderSettingsProvider { diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs index eb3e445c94..e05e4478ee 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs @@ -22,12 +22,9 @@ public class ResponseResourceObjectBuilder : ResourceObjectBuilder private readonly SparseFieldSetCache _sparseFieldSetCache; private RelationshipAttribute _requestRelationship; - public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, - IIncludedResourceObjectBuilder includedBuilder, - IEnumerable constraintProviders, - IResourceContextProvider resourceContextProvider, - IResourceDefinitionAccessor resourceDefinitionAccessor, - IResourceObjectBuilderSettingsProvider settingsProvider) + public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, + IEnumerable constraintProviders, IResourceContextProvider resourceContextProvider, + IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectBuilderSettingsProvider settingsProvider) : base(resourceContextProvider, settingsProvider.Get()) { ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); @@ -51,11 +48,11 @@ public RelationshipEntry Build(IIdentifiable resource, RelationshipAttribute req return GetRelationshipData(requestRelationship, resource); } - /// + /// public override ResourceObject Build(IIdentifiable resource, IReadOnlyCollection attributes = null, IReadOnlyCollection relationships = null) { - var resourceObject = base.Build(resource, attributes, relationships); + ResourceObject resourceObject = base.Build(resource, attributes, relationships); resourceObject.Meta = _resourceDefinitionAccessor.GetMeta(resource.GetType(), resource); @@ -63,11 +60,9 @@ public override ResourceObject Build(IIdentifiable resource, IReadOnlyCollection } /// - /// Builds the values of the relationships object on a resource object. - /// The server serializer only populates the "data" member when the relationship is included, - /// and adds links unless these are turned off. This means that if a relationship is not included - /// and links are turned off, the entry would be completely empty, ie { }, which is not conform - /// JSON:API spec. In that case we return null which will omit the entry from the output. + /// Builds the values of the relationships object on a resource object. The server serializer only populates the "data" member when the relationship is + /// included, and adds links unless these are turned off. This means that if a relationship is not included and links are turned off, the entry would be + /// completely empty, ie { }, which is not conform JSON:API spec. In that case we return null which will omit the entry from the output. /// protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { @@ -76,12 +71,14 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r RelationshipEntry relationshipEntry = null; List> relationshipChains = null; + if (Equals(relationship, _requestRelationship) || ShouldInclude(relationship, out relationshipChains)) { relationshipEntry = base.GetRelationshipData(relationship, resource); + if (relationshipChains != null && relationshipEntry.HasResource) { - foreach (var chain in relationshipChains) + foreach (IReadOnlyCollection chain in relationshipChains) { // traverses (recursively) and extracts all (nested) related resources for the current inclusion chain. _includedBuilder.IncludeRelationshipChain(chain, resource); @@ -94,7 +91,8 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r return null; } - var links = _linkBuilder.GetRelationshipLinks(relationship, resource); + RelationshipLinks links = _linkBuilder.GetRelationshipLinks(relationship, resource); + if (links != null) { // if relationshipLinks should be built for this entry, populate the "links" field. @@ -109,22 +107,22 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) { - var resourceContext = ResourceContextProvider.GetResourceContext(relationship.LeftType); + ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(relationship.LeftType); - var fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); + IReadOnlyCollection fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); return fieldSet.Contains(relationship); } /// - /// Inspects the included relationship chains (see - /// to see if should be included or not. + /// Inspects the included relationship chains (see to see if should be + /// included or not. /// private bool ShouldInclude(RelationshipAttribute relationship, out List> inclusionChain) { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var chains = _constraintProviders + ResourceFieldChainExpression[] chains = _constraintProviders .SelectMany(provider => provider.GetConstraints()) .Select(expressionInScope => expressionInScope.Expression) .OfType() @@ -136,7 +134,7 @@ private bool ShouldInclude(RelationshipAttribute relationship, out List>(); - foreach (var chain in chains) + foreach (ResourceFieldChainExpression chain in chains) { if (chain.Fields.First().Equals(relationship)) { diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs index 14587a1cb2..5f7186ddcc 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs @@ -5,7 +5,10 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal { /// Base class for "single data" and "many data" deserialized responses. - /// TODO: Currently and + /// TODO: Currently + /// + /// and + /// /// information is ignored by the serializer. This is out of scope for now because /// it is not considered mission critical for v4. [PublicAPI] diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs index 86e2b8bcbf..d58d7d9957 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs @@ -7,36 +7,37 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal { /// - /// Interface for client serializer that can be used to register with the DI container, for usage in - /// custom services or repositories. + /// Interface for client serializer that can be used to register with the DI container, for usage in custom services or repositories. /// [PublicAPI] public interface IRequestSerializer { /// - /// Creates and serializes a document for a single resource. + /// Sets the attributes that will be included in the serialized request body. You can use to + /// conveniently access the desired instances. /// - /// The serialized content - string Serialize(IIdentifiable resource); + public IReadOnlyCollection AttributesToSerialize { get; set; } /// - /// Creates and serializes a document for a collection of resources. + /// Sets the relationships that will be included in the serialized request body. You can use to + /// conveniently access the desired instances. /// - /// The serialized content - string Serialize(IReadOnlyCollection resources); + public IReadOnlyCollection RelationshipsToSerialize { get; set; } /// - /// Sets the attributes that will be included in the serialized request body. - /// You can use - /// to conveniently access the desired instances. + /// Creates and serializes a document for a single resource. /// - public IReadOnlyCollection AttributesToSerialize { get; set; } + /// + /// The serialized content + /// + string Serialize(IIdentifiable resource); /// - /// Sets the relationships that will be included in the serialized request body. - /// You can use - /// to conveniently access the desired instances. + /// Creates and serializes a document for a collection of resources. /// - public IReadOnlyCollection RelationshipsToSerialize { get; set; } + /// + /// The serialized content + /// + string Serialize(IReadOnlyCollection resources); } } diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs index 6d29d2779d..0a41306523 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs @@ -4,9 +4,8 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal { /// - /// Client deserializer. Currently not used internally in JsonApiDotNetCore, - /// except for in the tests. Exposed publicly to make testing easier or to implement - /// server-to-server communication. + /// Client deserializer. Currently not used internally in JsonApiDotNetCore, except for in the tests. Exposed publicly to make testing easier or to + /// implement server-to-server communication. /// [PublicAPI] public interface IResponseDeserializer @@ -14,15 +13,25 @@ public interface IResponseDeserializer /// /// Deserializes a response with a single resource (or null) as data. /// - /// The type of the resources in the primary data. - /// The JSON to be deserialized. - SingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable; + /// + /// The type of the resources in the primary data. + /// + /// + /// The JSON to be deserialized. + /// + SingleResponse DeserializeSingle(string body) + where TResource : class, IIdentifiable; /// /// Deserializes a response with an (empty) collection of resources as data. /// - /// The type of the resources in the primary data. - /// The JSON to be deserialized. - ManyResponse DeserializeMany(string body) where TResource : class, IIdentifiable; + /// + /// The type of the resources in the primary data. + /// + /// + /// The JSON to be deserialized. + /// + ManyResponse DeserializeMany(string body) + where TResource : class, IIdentifiable; } } diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs index c48a3f54b9..659e33d0dd 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs @@ -7,9 +7,12 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal /// /// Represents a deserialized document with "many data". /// - /// Type of the resource(s) in the primary data. + /// + /// Type of the resource(s) in the primary data. + /// [PublicAPI] - public sealed class ManyResponse : DeserializedResponseBase where TResource : class, IIdentifiable + public sealed class ManyResponse : DeserializedResponseBase + where TResource : class, IIdentifiable { public IReadOnlyCollection Data { get; set; } } diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs index 61d3c5a2fe..73f0964a9f 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs @@ -12,17 +12,22 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal { /// - /// Client serializer implementation of . + /// Client serializer implementation of . /// [PublicAPI] public class RequestSerializer : BaseSerializer, IRequestSerializer { - private Type _currentTargetedResource; private readonly IResourceGraph _resourceGraph; private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings(); + private Type _currentTargetedResource; + + /// + public IReadOnlyCollection AttributesToSerialize { get; set; } + + /// + public IReadOnlyCollection RelationshipsToSerialize { get; set; } - public RequestSerializer(IResourceGraph resourceGraph, - IResourceObjectBuilder resourceObjectBuilder) + public RequestSerializer(IResourceGraph resourceGraph, IResourceObjectBuilder resourceObjectBuilder) : base(resourceObjectBuilder) { ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); @@ -35,12 +40,12 @@ public string Serialize(IIdentifiable resource) { if (resource == null) { - var empty = Build((IIdentifiable) null, Array.Empty(), Array.Empty()); + Document empty = Build((IIdentifiable)null, Array.Empty(), Array.Empty()); return SerializeObject(empty, _jsonSerializerSettings); } _currentTargetedResource = resource.GetType(); - var document = Build(resource, GetAttributesToSerialize(resource), RelationshipsToSerialize); + Document document = Build(resource, GetAttributesToSerialize(resource), RelationshipsToSerialize); _currentTargetedResource = null; return SerializeObject(document, _jsonSerializerSettings); @@ -54,6 +59,7 @@ public string Serialize(IReadOnlyCollection resources) IIdentifiable firstResource = resources.FirstOrDefault(); Document document; + if (firstResource == null) { document = Build(resources, Array.Empty(), Array.Empty()); @@ -61,7 +67,7 @@ public string Serialize(IReadOnlyCollection resources) else { _currentTargetedResource = firstResource.GetType(); - var attributes = GetAttributesToSerialize(firstResource); + IReadOnlyCollection attributes = GetAttributesToSerialize(firstResource); document = Build(resources, attributes, RelationshipsToSerialize); _currentTargetedResource = null; @@ -70,20 +76,14 @@ public string Serialize(IReadOnlyCollection resources) return SerializeObject(document, _jsonSerializerSettings); } - /// - public IReadOnlyCollection AttributesToSerialize { get; set; } - - /// - public IReadOnlyCollection RelationshipsToSerialize { get; set; } - /// - /// By default, the client serializer includes all attributes in the result, - /// unless a list of allowed attributes was supplied using the - /// method. For any related resources, attributes are never exposed. + /// By default, the client serializer includes all attributes in the result, unless a list of allowed attributes was supplied using the + /// method. For any related resources, attributes are never exposed. /// private IReadOnlyCollection GetAttributesToSerialize(IIdentifiable resource) { - var currentResourceType = resource.GetType(); + Type currentResourceType = resource.GetType(); + if (_currentTargetedResource != currentResourceType) { // We're dealing with a relationship that is being serialized, for which diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs index a9e4232076..83c0c94059 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; @@ -10,53 +11,65 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal { /// - /// Client deserializer implementation of the . + /// Client deserializer implementation of the . /// [PublicAPI] public class ResponseDeserializer : BaseDeserializer, IResponseDeserializer { - public ResponseDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory) : base(resourceContextProvider, resourceFactory) { } + public ResponseDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory) + : base(resourceContextProvider, resourceFactory) + { + } /// - public SingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable + public SingleResponse DeserializeSingle(string body) + where TResource : class, IIdentifiable { ArgumentGuard.NotNull(body, nameof(body)); - var resource = DeserializeBody(body); + object resource = DeserializeBody(body); + return new SingleResponse { Links = Document.Links, Meta = Document.Meta, - Data = (TResource) resource, + Data = (TResource)resource, JsonApi = null, Errors = null }; } /// - public ManyResponse DeserializeMany(string body) where TResource : class, IIdentifiable + public ManyResponse DeserializeMany(string body) + where TResource : class, IIdentifiable { ArgumentGuard.NotNull(body, nameof(body)); - var resources = DeserializeBody(body); + object resources = DeserializeBody(body); + return new ManyResponse { Links = Document.Links, Meta = Document.Meta, - Data = ((ICollection) resources)?.Cast().ToArray(), + Data = ((ICollection)resources)?.Cast().ToArray(), JsonApi = null, Errors = null }; } /// - /// Additional processing required for client deserialization, responsible - /// for parsing the property. When a relationship value is parsed, - /// it goes through the included list to set its attributes and relationships. + /// Additional processing required for client deserialization, responsible for parsing the property. When a relationship + /// value is parsed, it goes through the included list to set its attributes and relationships. /// - /// The resource that was constructed from the document's body. - /// The metadata for the exposed field. - /// Relationship data for . Is null when is not a . + /// + /// The resource that was constructed from the document's body. + /// + /// + /// The metadata for the exposed field. + /// + /// + /// Relationship data for . Is null when is not a . + /// protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) { ArgumentGuard.NotNull(resource, nameof(resource)); @@ -79,14 +92,14 @@ protected override void AfterProcessField(IIdentifiable resource, ResourceFieldA if (field is HasOneAttribute hasOneAttr) { // add attributes and relationships of a parsed HasOne relationship - var rio = data.SingleData; + ResourceIdentifierObject rio = data.SingleData; hasOneAttr.SetValue(resource, rio == null ? null : ParseIncludedRelationship(rio)); } else if (field is HasManyAttribute hasManyAttr) { // add attributes and relationships of a parsed HasMany relationship - var items = data.ManyData.Select(ParseIncludedRelationship); - var values = TypeHelper.CopyToTypedCollection(items, hasManyAttr.Property.PropertyType); + IEnumerable items = data.ManyData.Select(ParseIncludedRelationship); + IEnumerable values = TypeHelper.CopyToTypedCollection(items, hasManyAttr.Property.PropertyType); hasManyAttr.SetValue(resource, values); } } @@ -97,24 +110,24 @@ protected override void AfterProcessField(IIdentifiable resource, ResourceFieldA /// private IIdentifiable ParseIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier) { - var relatedResourceContext = ResourceContextProvider.GetResourceContext(relatedResourceIdentifier.Type); + ResourceContext relatedResourceContext = ResourceContextProvider.GetResourceContext(relatedResourceIdentifier.Type); if (relatedResourceContext == null) { throw new InvalidOperationException($"Included type '{relatedResourceIdentifier.Type}' is not a registered JSON:API resource."); } - - var relatedInstance = ResourceFactory.CreateInstance(relatedResourceContext.ResourceType); + + IIdentifiable relatedInstance = ResourceFactory.CreateInstance(relatedResourceContext.ResourceType); relatedInstance.StringId = relatedResourceIdentifier.Id; - var includedResource = GetLinkedResource(relatedResourceIdentifier); + ResourceObject includedResource = GetLinkedResource(relatedResourceIdentifier); if (includedResource != null) { SetAttributes(relatedInstance, includedResource.Attributes, relatedResourceContext.Attributes); SetRelationships(relatedInstance, includedResource.Relationships, relatedResourceContext.Relationships); } - + return relatedInstance; } @@ -126,8 +139,9 @@ private ResourceObject GetLinkedResource(ResourceIdentifierObject relatedResourc } catch (InvalidOperationException e) { - throw new InvalidOperationException("A compound document MUST NOT include more than one resource object for each type and ID pair." - + $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", e); + throw new InvalidOperationException( + "A compound document MUST NOT include more than one resource object for each type and ID pair." + + $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", e); } } } diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs index d40421567b..3359cafae6 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs @@ -6,10 +6,13 @@ namespace JsonApiDotNetCore.Serialization.Client.Internal /// /// Represents a deserialized document with "single data". /// - /// Type of the resource in the primary data. + /// + /// Type of the resource in the primary data. + /// [PublicAPI] - public sealed class SingleResponse : DeserializedResponseBase where TResource : class, IIdentifiable - { - public TResource Data { get; set; } + public sealed class SingleResponse : DeserializedResponseBase + where TResource : class, IIdentifiable + { + public TResource Data { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs index aea6a2d1e7..763c41020a 100644 --- a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs +++ b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs @@ -19,11 +19,8 @@ public class FieldsToSerialize : IFieldsToSerialize private readonly IJsonApiRequest _request; private readonly SparseFieldSetCache _sparseFieldSetCache; - public FieldsToSerialize( - IResourceContextProvider resourceContextProvider, - IEnumerable constraintProviders, - IResourceDefinitionAccessor resourceDefinitionAccessor, - IJsonApiRequest request) + public FieldsToSerialize(IResourceContextProvider resourceContextProvider, IEnumerable constraintProviders, + IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiRequest request) { ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); ArgumentGuard.NotNull(request, nameof(request)); @@ -43,18 +40,17 @@ public IReadOnlyCollection GetAttributes(Type resourceType) return Array.Empty(); } - var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); - var fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + IReadOnlyCollection fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); return fieldSet.OfType().ToArray(); } /// /// - /// Note: this method does NOT check if a relationship is included to determine - /// if it should be serialized. This is because completely hiding a relationship - /// is not the same as not including. In the case of the latter, - /// we may still want to add the relationship to expose the navigation link to the client. + /// Note: this method does NOT check if a relationship is included to determine if it should be serialized. This is because completely hiding a + /// relationship is not the same as not including. In the case of the latter, we may still want to add the relationship to expose the navigation link to + /// the client. /// public IReadOnlyCollection GetRelationships(Type resourceType) { @@ -65,7 +61,7 @@ public IReadOnlyCollection GetRelationships(Type resource return Array.Empty(); } - var resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); return resourceContext.Relationships; } diff --git a/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs index aca991a49c..7f2775c021 100644 --- a/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs +++ b/src/JsonApiDotNetCore/Serialization/IFieldsToSerialize.cs @@ -5,19 +5,18 @@ namespace JsonApiDotNetCore.Serialization { /// - /// Responsible for getting the set of fields that are to be included for a - /// given type in the serialization result. Typically combines various sources - /// of information, like application-wide and request-wide sparse fieldsets. + /// Responsible for getting the set of fields that are to be included for a given type in the serialization result. Typically combines various sources of + /// information, like application-wide and request-wide sparse fieldsets. /// public interface IFieldsToSerialize { /// - /// Gets the collection of attributes that are to be serialized for resources of type . + /// Gets the collection of attributes that are to be serialized for resources of type . /// IReadOnlyCollection GetAttributes(Type resourceType); /// - /// Gets the collection of relationships that are to be serialized for resources of type . + /// Gets the collection of relationships that are to be serialized for resources of type . /// IReadOnlyCollection GetRelationships(Type resourceType); diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs index b8dea76109..3c363cbd4b 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs @@ -8,10 +8,12 @@ namespace JsonApiDotNetCore.Serialization public interface IJsonApiDeserializer { /// - /// Deserializes JSON into a or and constructs resources - /// from . + /// Deserializes JSON into a or and constructs resources from + /// . /// - /// The JSON to be deserialized. + /// + /// The JSON to be deserialized. + /// object Deserialize(string body); } } diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs index 9313d5b8c6..dbc851a492 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiReader.cs @@ -5,8 +5,7 @@ namespace JsonApiDotNetCore.Serialization { /// - /// The deserializer of the body, used in ASP.NET Core internally - /// to process `FromBody`. + /// The deserializer of the body, used in ASP.NET Core internally to process `FromBody`. /// [PublicAPI] public interface IJsonApiReader diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs index ebdf401213..97f0a15747 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiSerializer.cs @@ -6,13 +6,13 @@ namespace JsonApiDotNetCore.Serialization public interface IJsonApiSerializer { /// - /// Serializes a single resource or a collection of resources. + /// Gets the Content-Type HTTP header value. /// - string Serialize(object content); + string ContentType { get; } /// - /// Gets the Content-Type HTTP header value. + /// Serializes a single resource or a collection of resources. /// - string ContentType { get; } + string Serialize(object content); } } diff --git a/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs b/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs index 57e6304fc3..2561da2543 100644 --- a/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/IResponseMeta.cs @@ -5,8 +5,8 @@ namespace JsonApiDotNetCore.Serialization { /// - /// Provides a method to obtain global JSON:API meta, which is added at top-level to a response . - /// Use to specify nested metadata per individual resource. + /// Provides a method to obtain global JSON:API meta, which is added at top-level to a response . Use + /// to specify nested metadata per individual resource. /// public interface IResponseMeta { diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index 91d4fc80b9..c2b32d0381 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -29,9 +29,7 @@ public class JsonApiReader : IJsonApiReader private readonly IResourceContextProvider _resourceContextProvider; private readonly TraceLogWriter _traceWriter; - public JsonApiReader(IJsonApiDeserializer deserializer, - IJsonApiRequest request, - IResourceContextProvider resourceContextProvider, + public JsonApiReader(IJsonApiDeserializer deserializer, IJsonApiRequest request, IResourceContextProvider resourceContextProvider, ILoggerFactory loggerFactory) { ArgumentGuard.NotNull(deserializer, nameof(deserializer)); @@ -55,6 +53,7 @@ public async Task ReadAsync(InputFormatterContext context) _traceWriter.LogMessage(() => $"Received request at '{url}' with body: <<{body}>>"); object model = null; + if (!string.IsNullOrWhiteSpace(body)) { try @@ -93,17 +92,15 @@ private InvalidRequestBodyException ToInvalidRequestBodyException(JsonApiSeriali { if (_request.Kind != EndpointKind.AtomicOperations) { - return new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, body, - exception); + return new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, body, exception); } // In contrast to resource endpoints, we don't include the request body for operations because they are usually very long. - var requestException = - new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, null, exception.InnerException); + var requestException = new InvalidRequestBodyException(exception.GenericMessage, exception.SpecificMessage, null, exception.InnerException); if (exception.AtomicOperationIndex != null) { - foreach (var error in requestException.Errors) + foreach (Error error in requestException.Errors) { error.Source.Pointer = $"/atomic:operations[{exception.AtomicOperationIndex}]"; } @@ -111,7 +108,7 @@ private InvalidRequestBodyException ToInvalidRequestBodyException(JsonApiSeriali return requestException; } - + private bool RequiresRequestBody(string requestMethod) { if (requestMethod == HttpMethods.Post || requestMethod == HttpMethods.Patch) @@ -154,31 +151,30 @@ private static void AssertHasRequestBody(object model, string body) private void ValidateIncomingResourceType(object model, HttpRequest httpRequest) { - var endpointResourceType = GetResourceTypeFromEndpoint(); + Type endpointResourceType = GetResourceTypeFromEndpoint(); + if (endpointResourceType == null) { return; } - var bodyResourceTypes = GetResourceTypesFromRequestBody(model); - foreach (var bodyResourceType in bodyResourceTypes) + IEnumerable bodyResourceTypes = GetResourceTypesFromRequestBody(model); + + foreach (Type bodyResourceType in bodyResourceTypes) { if (!endpointResourceType.IsAssignableFrom(bodyResourceType)) { - var resourceFromEndpoint = _resourceContextProvider.GetResourceContext(endpointResourceType); - var resourceFromBody = _resourceContextProvider.GetResourceContext(bodyResourceType); + ResourceContext resourceFromEndpoint = _resourceContextProvider.GetResourceContext(endpointResourceType); + ResourceContext resourceFromBody = _resourceContextProvider.GetResourceContext(bodyResourceType); - throw new ResourceTypeMismatchException(new HttpMethod(httpRequest.Method), - httpRequest.Path, resourceFromEndpoint, resourceFromBody); + throw new ResourceTypeMismatchException(new HttpMethod(httpRequest.Method), httpRequest.Path, resourceFromEndpoint, resourceFromBody); } } } private Type GetResourceTypeFromEndpoint() { - return _request.Kind == EndpointKind.Primary - ? _request.PrimaryResource.ResourceType - : _request.SecondaryResource?.ResourceType; + return _request.Kind == EndpointKind.Primary ? _request.PrimaryResource.ResourceType : _request.SecondaryResource?.ResourceType; } private IEnumerable GetResourceTypesFromRequestBody(object model) @@ -194,6 +190,7 @@ private IEnumerable GetResourceTypesFromRequestBody(object model) private void ValidateRequestIncludesId(object model, string body) { bool hasMissingId = model is IEnumerable list ? HasMissingId(list) : HasMissingId(model); + if (hasMissingId) { throw new InvalidRequestBodyException("Request body must include 'id' element.", null, body); @@ -204,7 +201,7 @@ private void ValidatePrimaryIdValue(object model, PathString requestPath) { if (_request.Kind == EndpointKind.Primary) { - if (TryGetId(model, out var bodyId) && bodyId != _request.PrimaryId) + if (TryGetId(model, out string bodyId) && bodyId != _request.PrimaryId) { throw new ResourceIdMismatchException(bodyId, _request.PrimaryId, requestPath); } @@ -224,7 +221,7 @@ private bool HasMissingId(object model) /// private bool HasMissingId(IEnumerable models) { - foreach (var model in models) + foreach (object model in models) { if (TryGetId(model, out string id) && id == null) { diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs index a575ce59d8..189f2ede64 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializationException.cs @@ -13,8 +13,7 @@ public class JsonApiSerializationException : Exception public string SpecificMessage { get; } public int? AtomicOperationIndex { get; } - public JsonApiSerializationException(string genericMessage, string specificMessage, - Exception innerException = null, int? atomicOperationIndex = null) + public JsonApiSerializationException(string genericMessage, string specificMessage, Exception innerException = null, int? atomicOperationIndex = null) : base(genericMessage, innerException) { GenericMessage = genericMessage; diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index c925825a80..bb37d29087 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Net; using System.Net.Http; using System.Text; @@ -8,6 +9,7 @@ using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Formatters; @@ -16,8 +18,8 @@ namespace JsonApiDotNetCore.Serialization { /// - /// Formats the response data used (see https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-3.0). - /// It was intended to have as little dependencies as possible in formatting layer for greater extensibility. + /// Formats the response data used (see https://docs.microsoft.com/en-us/aspnet/core/web-api/advanced/formatting?view=aspnetcore-3.0). It was intended to + /// have as little dependencies as possible in formatting layer for greater extensibility. /// [PublicAPI] public class JsonApiWriter : IJsonApiWriter @@ -41,24 +43,25 @@ public async Task WriteAsync(OutputFormatterWriteContext context) { ArgumentGuard.NotNull(context, nameof(context)); - var response = context.HttpContext.Response; + HttpResponse response = context.HttpContext.Response; response.ContentType = _serializer.ContentType; - await using var writer = context.WriterFactory(response.Body, Encoding.UTF8); + await using TextWriter writer = context.WriterFactory(response.Body, Encoding.UTF8); string responseContent; + try { - responseContent = SerializeResponse(context.Object, (HttpStatusCode) response.StatusCode); + responseContent = SerializeResponse(context.Object, (HttpStatusCode)response.StatusCode); } catch (Exception exception) { - var errorDocument = _exceptionHandler.HandleException(exception); + ErrorDocument errorDocument = _exceptionHandler.HandleException(exception); responseContent = _serializer.Serialize(errorDocument); - response.StatusCode = (int) errorDocument.GetErrorStatusCode(); + response.StatusCode = (int)errorDocument.GetErrorStatusCode(); } - var url = context.HttpContext.Request.GetEncodedUrl(); + string url = context.HttpContext.Request.GetEncodedUrl(); _traceWriter.LogMessage(() => $"Sending {response.StatusCode} response for request at '{url}' with body: <<{responseContent}>>"); await writer.WriteAsync(responseContent); @@ -79,15 +82,14 @@ private string SerializeResponse(object contextObject, HttpStatusCode statusCode throw new UnsuccessfulActionResultException(statusCode); } - if (statusCode == HttpStatusCode.NoContent || statusCode == HttpStatusCode.ResetContent || - statusCode == HttpStatusCode.NotModified) + if (statusCode == HttpStatusCode.NoContent || statusCode == HttpStatusCode.ResetContent || statusCode == HttpStatusCode.NotModified) { // Prevent exception from Kestrel server, caused by writing data:null json response. return null; } } - var contextObjectWrapped = WrapErrors(contextObject); + object contextObjectWrapped = WrapErrors(contextObject); return _serializer.Serialize(contextObjectWrapped); } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Error.cs b/src/JsonApiDotNetCore/Serialization/Objects/Error.cs index 545c71da54..b33f79bba8 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/Error.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/Error.cs @@ -7,17 +7,12 @@ namespace JsonApiDotNetCore.Serialization.Objects { /// - /// Provides additional information about a problem encountered while performing an operation. - /// Error objects MUST be returned as an array keyed by errors in the top level of a JSON:API document. + /// Provides additional information about a problem encountered while performing an operation. Error objects MUST be returned as an array keyed by errors + /// in the top level of a JSON:API document. /// [PublicAPI] public sealed class Error { - public Error(HttpStatusCode statusCode) - { - StatusCode = statusCode; - } - /// /// A unique identifier for this particular occurrence of the problem. /// @@ -30,8 +25,6 @@ public Error(HttpStatusCode statusCode) [JsonProperty] public ErrorLinks Links { get; set; } = new ErrorLinks(); - public bool ShouldSerializeLinks() => Links?.About != null; - /// /// The HTTP status code applicable to this problem. /// @@ -52,7 +45,8 @@ public string Status public string Code { get; set; } /// - /// A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization. + /// A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of + /// localization. /// [JsonProperty] public string Title { get; set; } @@ -69,14 +63,30 @@ public string Status [JsonProperty] public ErrorSource Source { get; set; } = new ErrorSource(); - public bool ShouldSerializeSource() => Source != null && (Source.Pointer != null || Source.Parameter != null); - /// /// An object containing non-standard meta-information (key/value pairs) about the error. /// [JsonProperty] public ErrorMeta Meta { get; set; } = new ErrorMeta(); - public bool ShouldSerializeMeta() => Meta != null && Meta.Data.Any(); + public Error(HttpStatusCode statusCode) + { + StatusCode = statusCode; + } + + public bool ShouldSerializeLinks() + { + return Links?.About != null; + } + + public bool ShouldSerializeSource() + { + return Source != null && (Source.Pointer != null || Source.Parameter != null); + } + + public bool ShouldSerializeMeta() + { + return Meta != null && Meta.Data.Any(); + } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs index 67d0e82a27..d558b94186 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs @@ -30,17 +30,14 @@ public ErrorDocument(IEnumerable errors) public HttpStatusCode GetErrorStatusCode() { - var statusCodes = Errors - .Select(e => (int)e.StatusCode) - .Distinct() - .ToArray(); + int[] statusCodes = Errors.Select(e => (int)e.StatusCode).Distinct().ToArray(); if (statusCodes.Length == 1) { return (HttpStatusCode)statusCodes[0]; } - var statusCode = int.Parse(statusCodes.Max().ToString()[0] + "00"); + int statusCode = int.Parse(statusCodes.Max().ToString()[0] + "00"); return (HttpStatusCode)statusCode; } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs index 9c4477dd02..1589089719 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs @@ -22,8 +22,7 @@ public void IncludeExceptionStackTrace(Exception exception) } else { - Data["StackTrace"] = exception.ToString() - .Split("\n", int.MaxValue, StringSplitOptions.RemoveEmptyEntries); + Data["StackTrace"] = exception.ToString().Split("\n", int.MaxValue, StringSplitOptions.RemoveEmptyEntries); } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs index 0e1686fbdf..88cdc1812d 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs @@ -5,7 +5,8 @@ namespace JsonApiDotNetCore.Serialization.Objects public sealed class ErrorSource { /// - /// Optional. A JSON Pointer [RFC6901] to the associated resource in the request document [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute]. + /// Optional. A JSON Pointer [RFC6901] to the associated resource in the request document [e.g. "/data" for a primary data object, or + /// "/data/attributes/title" for a specific attribute]. /// [JsonProperty] public string Pointer { get; set; } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs b/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs index 40292cfb36..27ef8d0690 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs @@ -7,8 +7,21 @@ namespace JsonApiDotNetCore.Serialization.Objects { [PublicAPI] - public abstract class ExposableData where TResource : class + public abstract class ExposableData + where TResource : class { + private bool IsEmpty => !HasManyData && SingleData == null; + + private bool HasManyData => IsManyData && ManyData.Any(); + + /// + /// Internally used to indicate if the document's primary data should still be serialized when it's value is null. This is used when a single resource is + /// requested but not present (eg /articles/1/author). + /// + internal bool IsPopulated { get; private set; } + + internal bool HasResource => IsPopulated && !IsEmpty; + /// /// See "primary data" in https://jsonapi.org/format/#document-top-level. /// @@ -19,24 +32,6 @@ public object Data set => SetPrimaryData(value); } - /// - /// See https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm. - /// - /// - /// Moving this method to the derived class where it is needed only in the - /// case of would make more sense, but - /// Newtonsoft does not support this. - /// - public bool ShouldSerializeData() - { - if (GetType() == typeof(RelationshipEntry)) - { - return IsPopulated; - } - - return true; - } - /// /// Internally used for "single" primary data. /// @@ -56,21 +51,24 @@ public bool ShouldSerializeData() public bool IsManyData { get; private set; } /// - /// Internally used to indicate if the document's primary data - /// should still be serialized when it's value is null. This is used when - /// a single resource is requested but not present (eg /articles/1/author). + /// See https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm. /// - internal bool IsPopulated { get; private set; } - - internal bool HasResource => IsPopulated && !IsEmpty; - - private bool IsEmpty => !HasManyData && SingleData == null; + /// + /// Moving this method to the derived class where it is needed only in the case of would make more sense, but Newtonsoft + /// does not support this. + /// + public bool ShouldSerializeData() + { + if (GetType() == typeof(RelationshipEntry)) + { + return IsPopulated; + } - private bool HasManyData => IsManyData && ManyData.Any(); + return true; + } /// - /// Gets the "single" or "many" data depending on which one was - /// assigned in this document. + /// Gets the "single" or "many" data depending on which one was assigned in this document. /// protected object GetPrimaryData() { @@ -88,6 +86,7 @@ protected object GetPrimaryData() protected void SetPrimaryData(object value) { IsPopulated = true; + if (value is JObject jObject) { SingleData = jObject.ToObject(); @@ -99,6 +98,7 @@ protected void SetPrimaryData(object value) else if (value != null) { IsManyData = true; + if (value is JArray jArray) { ManyData = jArray.ToObject>(); diff --git a/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs index eb868ab837..98eb1a07df 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs @@ -29,12 +29,39 @@ public sealed class TopLevelLinks public string Next { get; set; } // http://www.newtonsoft.com/json/help/html/ConditionalProperties.htm - public bool ShouldSerializeSelf() => !string.IsNullOrEmpty(Self); - public bool ShouldSerializeRelated() => !string.IsNullOrEmpty(Related); - public bool ShouldSerializeDescribedBy() => !string.IsNullOrEmpty(DescribedBy); - public bool ShouldSerializeFirst() => !string.IsNullOrEmpty(First); - public bool ShouldSerializeLast() => !string.IsNullOrEmpty(Last); - public bool ShouldSerializePrev() => !string.IsNullOrEmpty(Prev); - public bool ShouldSerializeNext() => !string.IsNullOrEmpty(Next); + public bool ShouldSerializeSelf() + { + return !string.IsNullOrEmpty(Self); + } + + public bool ShouldSerializeRelated() + { + return !string.IsNullOrEmpty(Related); + } + + public bool ShouldSerializeDescribedBy() + { + return !string.IsNullOrEmpty(DescribedBy); + } + + public bool ShouldSerializeFirst() + { + return !string.IsNullOrEmpty(First); + } + + public bool ShouldSerializeLast() + { + return !string.IsNullOrEmpty(Last); + } + + public bool ShouldSerializePrev() + { + return !string.IsNullOrEmpty(Prev); + } + + public bool ShouldSerializeNext() + { + return !string.IsNullOrEmpty(Next); + } } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs index 746e12654b..df0e66f562 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -15,7 +16,7 @@ namespace JsonApiDotNetCore.Serialization { /// - /// Server deserializer implementation of the . + /// Server deserializer implementation of the . /// [PublicAPI] public class RequestDeserializer : BaseDeserializer, IJsonApiDeserializer @@ -25,13 +26,8 @@ public class RequestDeserializer : BaseDeserializer, IJsonApiDeserializer private readonly IJsonApiRequest _request; private readonly IJsonApiOptions _options; - public RequestDeserializer( - IResourceContextProvider resourceContextProvider, - IResourceFactory resourceFactory, - ITargetedFields targetedFields, - IHttpContextAccessor httpContextAccessor, - IJsonApiRequest request, - IJsonApiOptions options) + public RequestDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields, + IHttpContextAccessor httpContextAccessor, IJsonApiRequest request, IJsonApiOptions options) : base(resourceContextProvider, resourceFactory) { ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); @@ -60,7 +56,7 @@ public object Deserialize(string body) return DeserializeOperationsDocument(body); } - var instance = DeserializeBody(body); + object instance = DeserializeBody(body); AssertResourceIdIsNotTargeted(_targetedFields); @@ -72,7 +68,7 @@ private object DeserializeOperationsDocument(string body) JToken bodyToken = LoadJToken(body); var document = bodyToken.ToObject(); - if (document?.Operations == null || !document.Operations.Any()) + if ((document?.Operations).IsNullOrEmpty()) { throw new JsonApiSerializationException("No operations found.", null); } @@ -86,9 +82,9 @@ private object DeserializeOperationsDocument(string body) var operations = new List(); AtomicOperationIndex = 0; - foreach (var operation in document.Operations) + foreach (AtomicOperationObject operation in document.Operations) { - var container = DeserializeOperation(operation); + OperationContainer container = DeserializeOperation(operation); operations.Add(container); AtomicOperationIndex++; @@ -104,7 +100,8 @@ private OperationContainer DeserializeOperation(AtomicOperationObject operation) AssertHasNoHref(operation); - var kind = GetOperationKind(operation); + OperationKind kind = GetOperationKind(operation); + switch (kind) { case OperationKind.CreateResource: @@ -118,8 +115,7 @@ private OperationContainer DeserializeOperation(AtomicOperationObject operation) } } - bool requireToManyRelationship = - kind == OperationKind.AddToRelationship || kind == OperationKind.RemoveFromRelationship; + bool requireToManyRelationship = kind == OperationKind.AddToRelationship || kind == OperationKind.RemoveFromRelationship; return ParseForRelationshipOperation(operation, kind, requireToManyRelationship); } @@ -129,8 +125,7 @@ private void AssertHasNoHref(AtomicOperationObject operation) { if (operation.Href != null) { - throw new JsonApiSerializationException("Usage of the 'href' element is not supported.", null, - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Usage of the 'href' element is not supported.", null, atomicOperationIndex: AtomicOperationIndex); } } @@ -150,36 +145,30 @@ private OperationKind GetOperationKind(AtomicOperationObject operation) } case AtomicOperationCode.Update: { - return operation.Ref?.Relationship != null - ? OperationKind.SetRelationship - : OperationKind.UpdateResource; + return operation.Ref?.Relationship != null ? OperationKind.SetRelationship : OperationKind.UpdateResource; } case AtomicOperationCode.Remove: { if (operation.Ref == null) { - throw new JsonApiSerializationException("The 'ref' element is required.", null, - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("The 'ref' element is required.", null, atomicOperationIndex: AtomicOperationIndex); } - return operation.Ref.Relationship != null - ? OperationKind.RemoveFromRelationship - : OperationKind.DeleteResource; + return operation.Ref.Relationship != null ? OperationKind.RemoveFromRelationship : OperationKind.DeleteResource; } } throw new NotSupportedException($"Unknown operation code '{operation.Code}'."); } - private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperationObject operation, - OperationKind kind) + private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperationObject operation, OperationKind kind) { - var resourceObject = GetRequiredSingleDataForResourceOperation(operation); + ResourceObject resourceObject = GetRequiredSingleDataForResourceOperation(operation); AssertElementHasType(resourceObject, "data"); AssertElementHasIdOrLid(resourceObject, "data", kind != OperationKind.CreateResource); - var primaryResourceContext = GetExistingResourceContext(resourceObject.Type); + ResourceContext primaryResourceContext = GetExistingResourceContext(resourceObject.Type); AssertCompatibleId(resourceObject, primaryResourceContext.IdentityType); @@ -190,12 +179,11 @@ private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperati AssertElementHasType(operation.Ref, "ref"); AssertElementHasIdOrLid(operation.Ref, "ref", true); - var resourceContextInRef = GetExistingResourceContext(operation.Ref.Type); + ResourceContext resourceContextInRef = GetExistingResourceContext(operation.Ref.Type); if (resourceContextInRef != primaryResourceContext) { - throw new JsonApiSerializationException( - "Resource type mismatch between 'ref.type' and 'data.type' element.", + throw new JsonApiSerializationException("Resource type mismatch between 'ref.type' and 'data.type' element.", $"Expected resource of type '{resourceContextInRef.PublicName}' in 'data.type', instead of '{primaryResourceContext.PublicName}'.", atomicOperationIndex: AtomicOperationIndex); } @@ -210,9 +198,10 @@ private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperati PrimaryResource = primaryResourceContext, OperationKind = kind }; + _request.CopyFrom(request); - var primaryResource = ParseResourceObject(operation.SingleData); + IIdentifiable primaryResource = ParseResourceObject(operation.SingleData); request.PrimaryId = primaryResource.StringId; _request.CopyFrom(request); @@ -232,15 +221,13 @@ private ResourceObject GetRequiredSingleDataForResourceOperation(AtomicOperation { if (operation.Data == null) { - throw new JsonApiSerializationException("The 'data' element is required.", null, - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("The 'data' element is required.", null, atomicOperationIndex: AtomicOperationIndex); } if (operation.SingleData == null) { - throw new JsonApiSerializationException( - "Expected single data element for create/update resource operation.", - null, atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Expected single data element for create/update resource operation.", null, + atomicOperationIndex: AtomicOperationIndex); } return operation.SingleData; @@ -251,21 +238,18 @@ private void AssertElementHasType(ResourceIdentifierObject resourceIdentifierObj { if (resourceIdentifierObject.Type == null) { - throw new JsonApiSerializationException($"The '{elementPath}.type' element is required.", null, - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException($"The '{elementPath}.type' element is required.", null, atomicOperationIndex: AtomicOperationIndex); } } - private void AssertElementHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, string elementPath, - bool isRequired) + private void AssertElementHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, string elementPath, bool isRequired) { bool hasNone = resourceIdentifierObject.Id == null && resourceIdentifierObject.Lid == null; bool hasBoth = resourceIdentifierObject.Id != null && resourceIdentifierObject.Lid != null; if (isRequired ? hasNone || hasBoth : hasBoth) { - throw new JsonApiSerializationException( - $"The '{elementPath}.id' or '{elementPath}.lid' element is required.", null, + throw new JsonApiSerializationException($"The '{elementPath}.id' or '{elementPath}.lid' element is required.", null, atomicOperationIndex: AtomicOperationIndex); } } @@ -285,39 +269,32 @@ private void AssertCompatibleId(ResourceIdentifierObject resourceIdentifierObjec } } - private void AssertSameIdentityInRefData(AtomicOperationObject operation, - ResourceIdentifierObject resourceIdentifierObject) + private void AssertSameIdentityInRefData(AtomicOperationObject operation, ResourceIdentifierObject resourceIdentifierObject) { - if (operation.Ref.Id != null && resourceIdentifierObject.Id != null && - resourceIdentifierObject.Id != operation.Ref.Id) + if (operation.Ref.Id != null && resourceIdentifierObject.Id != null && resourceIdentifierObject.Id != operation.Ref.Id) { - throw new JsonApiSerializationException( - "Resource ID mismatch between 'ref.id' and 'data.id' element.", + throw new JsonApiSerializationException("Resource ID mismatch between 'ref.id' and 'data.id' element.", $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentifierObject.Id}'.", atomicOperationIndex: AtomicOperationIndex); } - if (operation.Ref.Lid != null && resourceIdentifierObject.Lid != null && - resourceIdentifierObject.Lid != operation.Ref.Lid) + if (operation.Ref.Lid != null && resourceIdentifierObject.Lid != null && resourceIdentifierObject.Lid != operation.Ref.Lid) { - throw new JsonApiSerializationException( - "Resource local ID mismatch between 'ref.lid' and 'data.lid' element.", + throw new JsonApiSerializationException("Resource local ID mismatch between 'ref.lid' and 'data.lid' element.", $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentifierObject.Lid}'.", atomicOperationIndex: AtomicOperationIndex); } if (operation.Ref.Id != null && resourceIdentifierObject.Lid != null) { - throw new JsonApiSerializationException( - "Resource identity mismatch between 'ref.id' and 'data.lid' element.", + throw new JsonApiSerializationException("Resource identity mismatch between 'ref.id' and 'data.lid' element.", $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentifierObject.Lid}' in 'data.lid'.", atomicOperationIndex: AtomicOperationIndex); } if (operation.Ref.Lid != null && resourceIdentifierObject.Id != null) { - throw new JsonApiSerializationException( - "Resource identity mismatch between 'ref.lid' and 'data.id' element.", + throw new JsonApiSerializationException("Resource identity mismatch between 'ref.lid' and 'data.id' element.", $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentifierObject.Id}' in 'data.id'.", atomicOperationIndex: AtomicOperationIndex); } @@ -328,11 +305,11 @@ private OperationContainer ParseForDeleteResourceOperation(AtomicOperationObject AssertElementHasType(operation.Ref, "ref"); AssertElementHasIdOrLid(operation.Ref, "ref", true); - var primaryResourceContext = GetExistingResourceContext(operation.Ref.Type); + ResourceContext primaryResourceContext = GetExistingResourceContext(operation.Ref.Type); AssertCompatibleId(operation.Ref, primaryResourceContext.IdentityType); - var primaryResource = ResourceFactory.CreateInstance(primaryResourceContext.ResourceType); + IIdentifiable primaryResource = ResourceFactory.CreateInstance(primaryResourceContext.ResourceType); primaryResource.StringId = operation.Ref.Id; primaryResource.LocalId = operation.Ref.Lid; @@ -348,31 +325,28 @@ private OperationContainer ParseForDeleteResourceOperation(AtomicOperationObject return new OperationContainer(kind, primaryResource, new TargetedFields(), request); } - private OperationContainer ParseForRelationshipOperation(AtomicOperationObject operation, OperationKind kind, - bool requireToMany) + private OperationContainer ParseForRelationshipOperation(AtomicOperationObject operation, OperationKind kind, bool requireToMany) { AssertElementHasType(operation.Ref, "ref"); AssertElementHasIdOrLid(operation.Ref, "ref", true); - var primaryResourceContext = GetExistingResourceContext(operation.Ref.Type); + ResourceContext primaryResourceContext = GetExistingResourceContext(operation.Ref.Type); AssertCompatibleId(operation.Ref, primaryResourceContext.IdentityType); - var primaryResource = ResourceFactory.CreateInstance(primaryResourceContext.ResourceType); + IIdentifiable primaryResource = ResourceFactory.CreateInstance(primaryResourceContext.ResourceType); primaryResource.StringId = operation.Ref.Id; primaryResource.LocalId = operation.Ref.Lid; - var relationship = GetExistingRelationship(operation.Ref, primaryResourceContext); + RelationshipAttribute relationship = GetExistingRelationship(operation.Ref, primaryResourceContext); if (requireToMany && relationship is HasOneAttribute) { - throw new JsonApiSerializationException( - $"Only to-many relationships can be targeted in '{operation.Code.ToString().Camelize()}' operations.", - $"Relationship '{operation.Ref.Relationship}' must be a to-many relationship.", - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException($"Only to-many relationships can be targeted in '{operation.Code.ToString().Camelize()}' operations.", + $"Relationship '{operation.Ref.Relationship}' must be a to-many relationship.", atomicOperationIndex: AtomicOperationIndex); } - var secondaryResourceContext = ResourceContextProvider.GetResourceContext(relationship.RightType); + ResourceContext secondaryResourceContext = ResourceContextProvider.GetResourceContext(relationship.RightType); var request = new JsonApiRequest { @@ -385,6 +359,7 @@ private OperationContainer ParseForRelationshipOperation(AtomicOperationObject o IsCollection = relationship is HasManyAttribute, OperationKind = kind }; + _request.CopyFrom(request); _targetedFields.Relationships.Add(relationship); @@ -400,16 +375,13 @@ private OperationContainer ParseForRelationshipOperation(AtomicOperationObject o return new OperationContainer(kind, primaryResource, targetedFields, request); } - private RelationshipAttribute GetExistingRelationship(AtomicReference reference, - ResourceContext resourceContext) + private RelationshipAttribute GetExistingRelationship(AtomicReference reference, ResourceContext resourceContext) { - var relationship = resourceContext.Relationships.FirstOrDefault(attribute => - attribute.PublicName == reference.Relationship); + RelationshipAttribute relationship = resourceContext.Relationships.FirstOrDefault(attribute => attribute.PublicName == reference.Relationship); if (relationship == null) { - throw new JsonApiSerializationException( - "The referenced relationship does not exist.", + throw new JsonApiSerializationException("The referenced relationship does not exist.", $"Resource of type '{reference.Type}' does not contain a relationship named '{reference.Relationship}'.", atomicOperationIndex: AtomicOperationIndex); } @@ -417,25 +389,22 @@ private RelationshipAttribute GetExistingRelationship(AtomicReference reference, return relationship; } - private void ParseDataForRelationship(RelationshipAttribute relationship, - ResourceContext secondaryResourceContext, - AtomicOperationObject operation, IIdentifiable primaryResource) + private void ParseDataForRelationship(RelationshipAttribute relationship, ResourceContext secondaryResourceContext, AtomicOperationObject operation, + IIdentifiable primaryResource) { if (relationship is HasOneAttribute) { if (operation.ManyData != null) { - throw new JsonApiSerializationException( - "Expected single data element for to-one relationship.", - $"Expected single data element for '{relationship.PublicName}' relationship.", - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Expected single data element for to-one relationship.", + $"Expected single data element for '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } if (operation.SingleData != null) { ValidateSingleDataForRelationship(operation.SingleData, secondaryResourceContext, "data"); - var secondaryResource = ParseResourceObject(operation.SingleData); + IIdentifiable secondaryResource = ParseResourceObject(operation.SingleData); relationship.SetValue(primaryResource, secondaryResource); } } @@ -443,47 +412,41 @@ private void ParseDataForRelationship(RelationshipAttribute relationship, { if (operation.ManyData == null) { - throw new JsonApiSerializationException( - "Expected data[] element for to-many relationship.", - $"Expected data[] element for '{relationship.PublicName}' relationship.", - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Expected data[] element for to-many relationship.", + $"Expected data[] element for '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } var secondaryResources = new List(); - foreach (var resourceObject in operation.ManyData) + foreach (ResourceObject resourceObject in operation.ManyData) { ValidateSingleDataForRelationship(resourceObject, secondaryResourceContext, "data[]"); - var secondaryResource = ParseResourceObject(resourceObject); + IIdentifiable secondaryResource = ParseResourceObject(resourceObject); secondaryResources.Add(secondaryResource); } - var rightResources = - TypeHelper.CopyToTypedCollection(secondaryResources, relationship.Property.PropertyType); + IEnumerable rightResources = TypeHelper.CopyToTypedCollection(secondaryResources, relationship.Property.PropertyType); relationship.SetValue(primaryResource, rightResources); } } - private void ValidateSingleDataForRelationship(ResourceObject dataResourceObject, - ResourceContext resourceContext, string elementPath) + private void ValidateSingleDataForRelationship(ResourceObject dataResourceObject, ResourceContext resourceContext, string elementPath) { AssertElementHasType(dataResourceObject, elementPath); AssertElementHasIdOrLid(dataResourceObject, elementPath, true); - var resourceContextInData = GetExistingResourceContext(dataResourceObject.Type); + ResourceContext resourceContextInData = GetExistingResourceContext(dataResourceObject.Type); AssertCompatibleType(resourceContextInData, resourceContext, elementPath); AssertCompatibleId(dataResourceObject, resourceContextInData.IdentityType); } - private void AssertCompatibleType(ResourceContext resourceContextInData, ResourceContext resourceContextInRef, - string elementPath) + private void AssertCompatibleType(ResourceContext resourceContextInData, ResourceContext resourceContextInRef, string elementPath) { if (!resourceContextInData.ResourceType.IsAssignableFrom(resourceContextInRef.ResourceType)) { - throw new JsonApiSerializationException( - $"Resource type mismatch between 'ref.relationship' and '{elementPath}.type' element.", + throw new JsonApiSerializationException($"Resource type mismatch between 'ref.relationship' and '{elementPath}.type' element.", $"Expected resource of type '{resourceContextInRef.PublicName}' in '{elementPath}.type', instead of '{resourceContextInData.PublicName}'.", atomicOperationIndex: AtomicOperationIndex); } @@ -491,23 +454,26 @@ private void AssertCompatibleType(ResourceContext resourceContextInData, Resourc private void AssertResourceIdIsNotTargeted(ITargetedFields targetedFields) { - if (!_request.IsReadOnly && - targetedFields.Attributes.Any(attribute => attribute.Property.Name == nameof(Identifiable.Id))) + if (!_request.IsReadOnly && targetedFields.Attributes.Any(attribute => attribute.Property.Name == nameof(Identifiable.Id))) { - throw new JsonApiSerializationException("Resource ID is read-only.", null, - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Resource ID is read-only.", null, atomicOperationIndex: AtomicOperationIndex); } } /// - /// Additional processing required for server deserialization. Flags a - /// processed attribute or relationship as updated using . + /// Additional processing required for server deserialization. Flags a processed attribute or relationship as updated using + /// . /// - /// The resource that was constructed from the document's body. - /// The metadata for the exposed field. - /// Relationship data for . Is null when is not a . - protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, - RelationshipEntry data = null) + /// + /// The resource that was constructed from the document's body. + /// + /// + /// The metadata for the exposed field. + /// + /// + /// Relationship data for . Is null when is not a . + /// + protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) { bool isCreatingResource = IsCreatingResource(); bool isUpdatingResource = IsUpdatingResource(); @@ -516,18 +482,14 @@ protected override void AfterProcessField(IIdentifiable resource, ResourceFieldA { if (isCreatingResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowCreate)) { - throw new JsonApiSerializationException( - "Setting the initial value of the requested attribute is not allowed.", - $"Setting the initial value of '{attr.PublicName}' is not allowed.", - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Setting the initial value of the requested attribute is not allowed.", + $"Setting the initial value of '{attr.PublicName}' is not allowed.", atomicOperationIndex: AtomicOperationIndex); } if (isUpdatingResource && !attr.Capabilities.HasFlag(AttrCapabilities.AllowChange)) { - throw new JsonApiSerializationException( - "Changing the value of the requested attribute is not allowed.", - $"Changing the value of '{attr.PublicName}' is not allowed.", - atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException("Changing the value of the requested attribute is not allowed.", + $"Changing the value of '{attr.PublicName}' is not allowed.", atomicOperationIndex: AtomicOperationIndex); } _targetedFields.Attributes.Add(attr); @@ -542,16 +504,14 @@ private bool IsCreatingResource() { return _request.Kind == EndpointKind.AtomicOperations ? _request.OperationKind == OperationKind.CreateResource - : _request.Kind == EndpointKind.Primary && - _httpContextAccessor.HttpContext.Request.Method == HttpMethod.Post.Method; + : _request.Kind == EndpointKind.Primary && _httpContextAccessor.HttpContext.Request.Method == HttpMethod.Post.Method; } private bool IsUpdatingResource() { return _request.Kind == EndpointKind.AtomicOperations ? _request.OperationKind == OperationKind.UpdateResource - : _request.Kind == EndpointKind.Primary && - _httpContextAccessor.HttpContext.Request.Method == HttpMethod.Patch.Method; + : _request.Kind == EndpointKind.Primary && _httpContextAccessor.HttpContext.Request.Method == HttpMethod.Patch.Method; } } } diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs index 70f5fca56b..fb0b60f5fd 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; using Newtonsoft.Json; @@ -12,16 +13,16 @@ namespace JsonApiDotNetCore.Serialization { /// - /// Server serializer implementation of for resources of a specific type. + /// Server serializer implementation of for resources of a specific type. /// /// - /// Because in JsonApiDotNetCore every JSON:API request is associated with exactly one - /// resource (the primary resource, see ), - /// the serializer can leverage this information using generics. - /// See for how this is instantiated. + /// Because in JsonApiDotNetCore every JSON:API request is associated with exactly one resource (the primary resource, see + /// ), the serializer can leverage this information using generics. See + /// for how this is instantiated. /// - /// Type of the resource associated with the scope of the request - /// for which this serializer is used. + /// + /// Type of the resource associated with the scope of the request for which this serializer is used. + /// [PublicAPI] public class ResponseSerializer : BaseSerializer, IJsonApiSerializer where TResource : class, IIdentifiable @@ -36,12 +37,8 @@ public class ResponseSerializer : BaseSerializer, IJsonApiSerializer /// public string ContentType { get; } = HeaderConstants.MediaType; - public ResponseSerializer(IMetaBuilder metaBuilder, - ILinkBuilder linkBuilder, - IIncludedResourceObjectBuilder includedBuilder, - IFieldsToSerialize fieldsToSerialize, - IResourceObjectBuilder resourceObjectBuilder, - IJsonApiOptions options) + public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, + IFieldsToSerialize fieldsToSerialize, IResourceObjectBuilder resourceObjectBuilder, IJsonApiOptions options) : base(resourceObjectBuilder) { ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder)); @@ -81,22 +78,26 @@ public string Serialize(object content) private string SerializeErrorDocument(ErrorDocument errorDocument) { - return SerializeObject(errorDocument, _options.SerializerSettings, serializer => { serializer.ApplyErrorSettings(); }); + return SerializeObject(errorDocument, _options.SerializerSettings, serializer => + { + serializer.ApplyErrorSettings(); + }); } /// - /// Converts a single resource into a serialized . + /// Converts a single resource into a serialized . /// /// /// This method is internal instead of private for easier testability. /// internal string SerializeSingle(IIdentifiable resource) { - var attributes = _fieldsToSerialize.GetAttributes(_primaryResourceType); - var relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType); + IReadOnlyCollection attributes = _fieldsToSerialize.GetAttributes(_primaryResourceType); + IReadOnlyCollection relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType); + + Document document = Build(resource, attributes, relationships); + ResourceObject resourceObject = document.SingleData; - var document = Build(resource, attributes, relationships); - var resourceObject = document.SingleData; if (resourceObject != null) { resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); @@ -104,24 +105,29 @@ internal string SerializeSingle(IIdentifiable resource) AddTopLevelObjects(document); - return SerializeObject(document, _options.SerializerSettings, serializer => { serializer.NullValueHandling = NullValueHandling.Include; }); + return SerializeObject(document, _options.SerializerSettings, serializer => + { + serializer.NullValueHandling = NullValueHandling.Include; + }); } /// - /// Converts a collection of resources into a serialized . + /// Converts a collection of resources into a serialized . /// /// /// This method is internal instead of private for easier testability. /// internal string SerializeMany(IReadOnlyCollection resources) { - var attributes = _fieldsToSerialize.GetAttributes(_primaryResourceType); - var relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType); + IReadOnlyCollection attributes = _fieldsToSerialize.GetAttributes(_primaryResourceType); + IReadOnlyCollection relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType); + + Document document = Build(resources, attributes, relationships); - var document = Build(resources, attributes, relationships); foreach (ResourceObject resourceObject in document.ManyData) { - var links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); + ResourceLinks links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); + if (links == null) { break; @@ -132,12 +138,14 @@ internal string SerializeMany(IReadOnlyCollection resources) AddTopLevelObjects(document); - return SerializeObject(document, _options.SerializerSettings, serializer => { serializer.NullValueHandling = NullValueHandling.Include; }); + return SerializeObject(document, _options.SerializerSettings, serializer => + { + serializer.NullValueHandling = NullValueHandling.Include; + }); } /// - /// Adds top-level objects that are only added to a document in the case - /// of server-side serialization. + /// Adds top-level objects that are only added to a document in the case of server-side serialization. /// private void AddTopLevelObjects(Document document) { diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs index 4b6b1c12b5..5ddc248a4d 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializerFactory.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCore.Serialization { /// - /// A factory class to abstract away the initialization of the serializer from the - /// ASP.NET Core formatter pipeline. + /// A factory class to abstract away the initialization of the serializer from the ASP.NET Core formatter pipeline. /// [PublicAPI] public class ResponseSerializerFactory : IJsonApiSerializerFactory @@ -26,7 +25,7 @@ public ResponseSerializerFactory(IJsonApiRequest request, IRequestScopedServiceP } /// - /// Initializes the server serializer using the associated with the current request. + /// Initializes the server serializer using the associated with the current request. /// public IJsonApiSerializer GetSerializer() { @@ -35,17 +34,17 @@ public IJsonApiSerializer GetSerializer() return (IJsonApiSerializer)_provider.GetRequiredService(typeof(AtomicOperationsResponseSerializer)); } - var targetType = GetDocumentType(); + Type targetType = GetDocumentType(); - var serializerType = typeof(ResponseSerializer<>).MakeGenericType(targetType); - var serializer = _provider.GetRequiredService(serializerType); + Type serializerType = typeof(ResponseSerializer<>).MakeGenericType(targetType); + object serializer = _provider.GetRequiredService(serializerType); return (IJsonApiSerializer)serializer; } private Type GetDocumentType() { - var resourceContext = _request.SecondaryResource ?? _request.PrimaryResource; + ResourceContext resourceContext = _request.SecondaryResource ?? _request.PrimaryResource; return resourceContext.ResourceType; } } diff --git a/src/JsonApiDotNetCore/Services/AsyncCollectionExtensions.cs b/src/JsonApiDotNetCore/Services/AsyncCollectionExtensions.cs index be11ea908e..e93b294a22 100644 --- a/src/JsonApiDotNetCore/Services/AsyncCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Services/AsyncCollectionExtensions.cs @@ -13,7 +13,7 @@ public static async Task AddRangeAsync(this ICollection source, IAsyncEnum ArgumentGuard.NotNull(source, nameof(source)); ArgumentGuard.NotNull(elementsToAdd, nameof(elementsToAdd)); - await foreach (var missingResource in elementsToAdd.WithCancellation(cancellationToken)) + await foreach (T missingResource in elementsToAdd.WithCancellation(cancellationToken)) { source.Add(missingResource); } @@ -25,7 +25,7 @@ public static async Task> ToListAsync(this IAsyncEnumerable source var list = new List(); - await foreach (var element in source.WithCancellation(cancellationToken)) + await foreach (T element in source.WithCancellation(cancellationToken)) { list.Add(element); } diff --git a/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs b/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs index ca1e968767..5bbc74ae71 100644 --- a/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/IAddToRelationshipService.cs @@ -11,7 +11,8 @@ namespace JsonApiDotNetCore.Services /// public interface IAddToRelationshipService : IAddToRelationshipService where TResource : class, IIdentifiable - { } + { + } /// [PublicAPI] @@ -21,10 +22,19 @@ public interface IAddToRelationshipService /// /// Handles a JSON:API request to add resources to a to-many relationship. /// - /// The identifier of the primary resource. - /// The relationship to add resources to. - /// The set of resources to add to the relationship. - /// Propagates notification that request handling should be canceled. - Task AddToToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken); + /// + /// The identifier of the primary resource. + /// + /// + /// The relationship to add resources to. + /// + /// + /// The set of resources to add to the relationship. + /// + /// + /// Propagates notification that request handling should be canceled. + /// + Task AddToToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, + CancellationToken cancellationToken); } } diff --git a/src/JsonApiDotNetCore/Services/ICreateService.cs b/src/JsonApiDotNetCore/Services/ICreateService.cs index 5022604cb3..af735de513 100644 --- a/src/JsonApiDotNetCore/Services/ICreateService.cs +++ b/src/JsonApiDotNetCore/Services/ICreateService.cs @@ -7,7 +7,8 @@ namespace JsonApiDotNetCore.Services /// public interface ICreateService : ICreateService where TResource : class, IIdentifiable - { } + { + } /// public interface ICreateService diff --git a/src/JsonApiDotNetCore/Services/IDeleteService.cs b/src/JsonApiDotNetCore/Services/IDeleteService.cs index 5dcaebdf19..b3a801208d 100644 --- a/src/JsonApiDotNetCore/Services/IDeleteService.cs +++ b/src/JsonApiDotNetCore/Services/IDeleteService.cs @@ -9,7 +9,8 @@ namespace JsonApiDotNetCore.Services /// public interface IDeleteService : IDeleteService where TResource : class, IIdentifiable - { } + { + } /// public interface IDeleteService diff --git a/src/JsonApiDotNetCore/Services/IGetAllService.cs b/src/JsonApiDotNetCore/Services/IGetAllService.cs index eee7963a18..bab5aeab31 100644 --- a/src/JsonApiDotNetCore/Services/IGetAllService.cs +++ b/src/JsonApiDotNetCore/Services/IGetAllService.cs @@ -8,7 +8,8 @@ namespace JsonApiDotNetCore.Services /// public interface IGetAllService : IGetAllService where TResource : class, IIdentifiable - { } + { + } /// public interface IGetAllService diff --git a/src/JsonApiDotNetCore/Services/IGetByIdService.cs b/src/JsonApiDotNetCore/Services/IGetByIdService.cs index 42a23351d5..d383cf7afc 100644 --- a/src/JsonApiDotNetCore/Services/IGetByIdService.cs +++ b/src/JsonApiDotNetCore/Services/IGetByIdService.cs @@ -7,7 +7,8 @@ namespace JsonApiDotNetCore.Services /// public interface IGetByIdService : IGetByIdService where TResource : class, IIdentifiable - { } + { + } /// public interface IGetByIdService diff --git a/src/JsonApiDotNetCore/Services/IGetRelationshipService.cs b/src/JsonApiDotNetCore/Services/IGetRelationshipService.cs index 575f8ca6bf..191457172d 100644 --- a/src/JsonApiDotNetCore/Services/IGetRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/IGetRelationshipService.cs @@ -9,7 +9,8 @@ namespace JsonApiDotNetCore.Services /// public interface IGetRelationshipService : IGetRelationshipService where TResource : class, IIdentifiable - { } + { + } /// public interface IGetRelationshipService diff --git a/src/JsonApiDotNetCore/Services/IGetSecondaryService.cs b/src/JsonApiDotNetCore/Services/IGetSecondaryService.cs index b035aa6327..949de5a5ac 100644 --- a/src/JsonApiDotNetCore/Services/IGetSecondaryService.cs +++ b/src/JsonApiDotNetCore/Services/IGetSecondaryService.cs @@ -9,14 +9,16 @@ namespace JsonApiDotNetCore.Services /// public interface IGetSecondaryService : IGetSecondaryService where TResource : class, IIdentifiable - { } + { + } /// public interface IGetSecondaryService where TResource : class, IIdentifiable { /// - /// Handles a JSON:API request to retrieve a single resource or a collection of resources for a secondary endpoint, such as /articles/1/author or /articles/1/revisions. + /// Handles a JSON:API request to retrieve a single resource or a collection of resources for a secondary endpoint, such as /articles/1/author or + /// /articles/1/revisions. /// Task GetSecondaryAsync(TId id, string relationshipName, CancellationToken cancellationToken); } diff --git a/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs b/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs index 56b3d5137b..c322732cc7 100644 --- a/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/IRemoveFromRelationshipService.cs @@ -10,7 +10,8 @@ namespace JsonApiDotNetCore.Services /// public interface IRemoveFromRelationshipService : IRemoveFromRelationshipService where TResource : class, IIdentifiable - { } + { + } /// public interface IRemoveFromRelationshipService @@ -19,10 +20,19 @@ public interface IRemoveFromRelationshipService /// /// Handles a JSON:API request to remove resources from a to-many relationship. /// - /// The identifier of the primary resource. - /// The relationship to remove resources from. - /// The set of resources to remove from the relationship. - /// Propagates notification that request handling should be canceled. - Task RemoveFromToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken); + /// + /// The identifier of the primary resource. + /// + /// + /// The relationship to remove resources from. + /// + /// + /// The set of resources to remove from the relationship. + /// + /// + /// Propagates notification that request handling should be canceled. + /// + Task RemoveFromToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, + CancellationToken cancellationToken); } } diff --git a/src/JsonApiDotNetCore/Services/IResourceCommandService.cs b/src/JsonApiDotNetCore/Services/IResourceCommandService.cs index a769f90f4c..334fdb26fa 100644 --- a/src/JsonApiDotNetCore/Services/IResourceCommandService.cs +++ b/src/JsonApiDotNetCore/Services/IResourceCommandService.cs @@ -5,30 +5,29 @@ namespace JsonApiDotNetCore.Services /// /// Groups write operations. /// - /// The resource type. - public interface IResourceCommandService : - ICreateService, - IAddToRelationshipService, - IUpdateService, - ISetRelationshipService, - IDeleteService, - IRemoveFromRelationshipService, - IResourceCommandService + /// + /// The resource type. + /// + public interface IResourceCommandService + : ICreateService, IAddToRelationshipService, IUpdateService, ISetRelationshipService, + IDeleteService, IRemoveFromRelationshipService, IResourceCommandService where TResource : class, IIdentifiable - { } + { + } /// /// Groups write operations. /// - /// The resource type. - /// The resource identifier type. - public interface IResourceCommandService : - ICreateService, - IAddToRelationshipService, - IUpdateService, - ISetRelationshipService, - IDeleteService, - IRemoveFromRelationshipService + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// + public interface IResourceCommandService + : ICreateService, IAddToRelationshipService, IUpdateService, ISetRelationshipService, + IDeleteService, IRemoveFromRelationshipService where TResource : class, IIdentifiable - { } + { + } } diff --git a/src/JsonApiDotNetCore/Services/IResourceQueryService.cs b/src/JsonApiDotNetCore/Services/IResourceQueryService.cs index dd337a6bfc..07c89e8643 100644 --- a/src/JsonApiDotNetCore/Services/IResourceQueryService.cs +++ b/src/JsonApiDotNetCore/Services/IResourceQueryService.cs @@ -5,26 +5,28 @@ namespace JsonApiDotNetCore.Services /// /// Groups read operations. /// - /// The resource type. - public interface IResourceQueryService : - IGetAllService, - IGetByIdService, - IGetRelationshipService, - IGetSecondaryService, - IResourceQueryService + /// + /// The resource type. + /// + public interface IResourceQueryService + : IGetAllService, IGetByIdService, IGetRelationshipService, IGetSecondaryService, + IResourceQueryService where TResource : class, IIdentifiable - { } + { + } /// /// Groups read operations. /// - /// The resource type. - /// The resource identifier type. - public interface IResourceQueryService : - IGetAllService, - IGetByIdService, - IGetRelationshipService, - IGetSecondaryService + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// + public interface IResourceQueryService + : IGetAllService, IGetByIdService, IGetRelationshipService, IGetSecondaryService where TResource : class, IIdentifiable - { } + { + } } diff --git a/src/JsonApiDotNetCore/Services/IResourceService.cs b/src/JsonApiDotNetCore/Services/IResourceService.cs index 126f6b43b1..eb1a744c1b 100644 --- a/src/JsonApiDotNetCore/Services/IResourceService.cs +++ b/src/JsonApiDotNetCore/Services/IResourceService.cs @@ -5,19 +5,25 @@ namespace JsonApiDotNetCore.Services /// /// Represents the foundational Resource Service layer in the JsonApiDotNetCore architecture that uses a Resource Repository for data access. /// - /// The resource type. - public interface IResourceService - : IResourceCommandService, IResourceQueryService, IResourceService + /// + /// The resource type. + /// + public interface IResourceService : IResourceCommandService, IResourceQueryService, IResourceService where TResource : class, IIdentifiable - { } + { + } /// /// Represents the foundational Resource Service layer in the JsonApiDotNetCore architecture that uses a Resource Repository for data access. /// - /// The resource type. - /// The resource identifier type. - public interface IResourceService - : IResourceCommandService, IResourceQueryService + /// + /// The resource type. + /// + /// + /// The resource identifier type. + /// + public interface IResourceService : IResourceCommandService, IResourceQueryService where TResource : class, IIdentifiable - { } + { + } } diff --git a/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs b/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs index 46ec2153f1..28904f1a28 100644 --- a/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs +++ b/src/JsonApiDotNetCore/Services/ISetRelationshipService.cs @@ -9,7 +9,8 @@ namespace JsonApiDotNetCore.Services /// public interface ISetRelationshipService : ISetRelationshipService where TResource : class, IIdentifiable - { } + { + } /// public interface ISetRelationshipService @@ -18,10 +19,18 @@ public interface ISetRelationshipService /// /// Handles a JSON:API request to perform a complete replacement of a relationship on an existing resource. /// - /// The identifier of the primary resource. - /// The relationship for which to perform a complete replacement. - /// The resource or set of resources to assign to the relationship. - /// Propagates notification that request handling should be canceled. + /// + /// The identifier of the primary resource. + /// + /// + /// The relationship for which to perform a complete replacement. + /// + /// + /// The resource or set of resources to assign to the relationship. + /// + /// + /// Propagates notification that request handling should be canceled. + /// Task SetRelationshipAsync(TId primaryId, string relationshipName, object secondaryResourceIds, CancellationToken cancellationToken); } } diff --git a/src/JsonApiDotNetCore/Services/IUpdateService.cs b/src/JsonApiDotNetCore/Services/IUpdateService.cs index 13d3b590b1..83e88f6e96 100644 --- a/src/JsonApiDotNetCore/Services/IUpdateService.cs +++ b/src/JsonApiDotNetCore/Services/IUpdateService.cs @@ -7,15 +7,16 @@ namespace JsonApiDotNetCore.Services /// public interface IUpdateService : IUpdateService where TResource : class, IIdentifiable - { } + { + } /// public interface IUpdateService where TResource : class, IIdentifiable { /// - /// Handles a JSON:API request to update the attributes and/or relationships of an existing resource. - /// Only the values of sent attributes are replaced. And only the values of sent relationships are replaced. + /// Handles a JSON:API request to update the attributes and/or relationships of an existing resource. Only the values of sent attributes are replaced. + /// And only the values of sent relationships are replaced. /// Task UpdateAsync(TId id, TResource resource, CancellationToken cancellationToken); } diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index 6b08585445..0b472d9d6e 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -12,6 +12,7 @@ using JsonApiDotNetCore.Hooks.Internal.Execution; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; +using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -21,8 +22,7 @@ namespace JsonApiDotNetCore.Services { /// [PublicAPI] - public class JsonApiResourceService : - IResourceService + public class JsonApiResourceService : IResourceService where TResource : class, IIdentifiable { private readonly IResourceRepositoryAccessor _repositoryAccessor; @@ -34,15 +34,9 @@ public class JsonApiResourceService : private readonly IResourceChangeTracker _resourceChangeTracker; private readonly IResourceHookExecutorFacade _hookExecutor; - public JsonApiResourceService( - IResourceRepositoryAccessor repositoryAccessor, - IQueryLayerComposer queryLayerComposer, - IPaginationContext paginationContext, - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IJsonApiRequest request, - IResourceChangeTracker resourceChangeTracker, - IResourceHookExecutorFacade hookExecutor) + public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, + IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, + IResourceChangeTracker resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) { ArgumentGuard.NotNull(repositoryAccessor, nameof(repositoryAccessor)); ArgumentGuard.NotNull(queryLayerComposer, nameof(queryLayerComposer)); @@ -72,7 +66,7 @@ public virtual async Task> GetAsync(CancellationT if (_options.IncludeTotalResourceCount) { - var topFilter = _queryLayerComposer.GetTopFilterFromConstraints(_request.PrimaryResource); + FilterExpression topFilter = _queryLayerComposer.GetTopFilterFromConstraints(_request.PrimaryResource); _paginationContext.TotalResourceCount = await _repositoryAccessor.CountAsync(topFilter, cancellationToken); if (_paginationContext.TotalResourceCount == 0) @@ -81,8 +75,8 @@ public virtual async Task> GetAsync(CancellationT } } - var queryLayer = _queryLayerComposer.ComposeFromConstraints(_request.PrimaryResource); - var resources = await _repositoryAccessor.GetAsync(queryLayer, cancellationToken); + QueryLayer queryLayer = _queryLayerComposer.ComposeFromConstraints(_request.PrimaryResource); + IReadOnlyCollection resources = await _repositoryAccessor.GetAsync(queryLayer, cancellationToken); if (queryLayer.Pagination?.PageSize != null && queryLayer.Pagination.PageSize.Value == resources.Count) { @@ -96,11 +90,14 @@ public virtual async Task> GetAsync(CancellationT /// public virtual async Task GetAsync(TId id, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id}); + _traceWriter.LogMethodStart(new + { + id + }); _hookExecutor.BeforeReadSingle(id, ResourcePipeline.GetSingle); - var primaryResource = await TryGetPrimaryResourceByIdAsync(id, TopFieldSelection.PreserveExisting, cancellationToken); + TResource primaryResource = await TryGetPrimaryResourceByIdAsync(id, TopFieldSelection.PreserveExisting, cancellationToken); AssertPrimaryResourceExists(primaryResource); _hookExecutor.AfterReadSingle(primaryResource, ResourcePipeline.GetSingle); @@ -112,13 +109,18 @@ public virtual async Task GetAsync(TId id, CancellationToken cancella /// public virtual async Task GetSecondaryAsync(TId id, string relationshipName, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id, relationshipName}); + _traceWriter.LogMethodStart(new + { + id, + relationshipName + }); + AssertHasRelationship(_request.Relationship, relationshipName); _hookExecutor.BeforeReadSingle(id, ResourcePipeline.GetRelationship); - var secondaryLayer = _queryLayerComposer.ComposeFromConstraints(_request.SecondaryResource); - var primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship); + QueryLayer secondaryLayer = _queryLayerComposer.ComposeFromConstraints(_request.SecondaryResource); + QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship); if (_request.IsCollection && _options.IncludeTotalResourceCount) { @@ -128,17 +130,16 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN // And we should call BlogResourceDefinition.OnApplyFilter to filter out soft-deleted blogs and translate from equals('IsDeleted','false') to equals('Blog.IsDeleted','false') } - var primaryResources = await _repositoryAccessor.GetAsync(primaryLayer, cancellationToken); + IReadOnlyCollection primaryResources = await _repositoryAccessor.GetAsync(primaryLayer, cancellationToken); - var primaryResource = primaryResources.SingleOrDefault(); + TResource primaryResource = primaryResources.SingleOrDefault(); AssertPrimaryResourceExists(primaryResource); _hookExecutor.AfterReadSingle(primaryResource, ResourcePipeline.GetRelationship); - var secondaryResourceOrResources = _request.Relationship.GetValue(primaryResource); + object secondaryResourceOrResources = _request.Relationship.GetValue(primaryResource); - if (secondaryResourceOrResources is ICollection secondaryResources && - secondaryLayer.Pagination?.PageSize?.Value == secondaryResources.Count) + if (secondaryResourceOrResources is ICollection secondaryResources && secondaryLayer.Pagination?.PageSize?.Value == secondaryResources.Count) { _paginationContext.IsPageFull = true; } @@ -149,7 +150,11 @@ public virtual async Task GetSecondaryAsync(TId id, string relationshipN /// public virtual async Task GetRelationshipAsync(TId id, string relationshipName, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id, relationshipName}); + _traceWriter.LogMethodStart(new + { + id, + relationshipName + }); ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); @@ -157,17 +162,17 @@ public virtual async Task GetRelationshipAsync(TId id, string relationsh _hookExecutor.BeforeReadSingle(id, ResourcePipeline.GetRelationship); - var secondaryLayer = _queryLayerComposer.ComposeSecondaryLayerForRelationship(_request.SecondaryResource); - var primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship); + QueryLayer secondaryLayer = _queryLayerComposer.ComposeSecondaryLayerForRelationship(_request.SecondaryResource); + QueryLayer primaryLayer = _queryLayerComposer.WrapLayerForSecondaryEndpoint(secondaryLayer, _request.PrimaryResource, id, _request.Relationship); - var primaryResources = await _repositoryAccessor.GetAsync(primaryLayer, cancellationToken); + IReadOnlyCollection primaryResources = await _repositoryAccessor.GetAsync(primaryLayer, cancellationToken); - var primaryResource = primaryResources.SingleOrDefault(); + TResource primaryResource = primaryResources.SingleOrDefault(); AssertPrimaryResourceExists(primaryResource); _hookExecutor.AfterReadSingle(primaryResource, ResourcePipeline.GetRelationship); - var secondaryResourceOrResources = _request.Relationship.GetValue(primaryResource); + object secondaryResourceOrResources = _request.Relationship.GetValue(primaryResource); return _hookExecutor.OnReturnRelationship(secondaryResourceOrResources); } @@ -175,11 +180,14 @@ public virtual async Task GetRelationshipAsync(TId id, string relationsh /// public virtual async Task CreateAsync(TResource resource, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {resource}); + _traceWriter.LogMethodStart(new + { + resource + }); ArgumentGuard.NotNull(resource, nameof(resource)); - var resourceFromRequest = resource; + TResource resourceFromRequest = resource; _resourceChangeTracker.SetRequestedAttributeValues(resourceFromRequest); _hookExecutor.BeforeCreate(resourceFromRequest); @@ -196,7 +204,9 @@ public virtual async Task CreateAsync(TResource resource, Cancellatio { if (!Equals(resourceFromRequest.Id, default(TId))) { - var existingResource = await TryGetPrimaryResourceByIdAsync(resourceFromRequest.Id, TopFieldSelection.OnlyIdAttribute, cancellationToken); + TResource existingResource = + await TryGetPrimaryResourceByIdAsync(resourceFromRequest.Id, TopFieldSelection.OnlyIdAttribute, cancellationToken); + if (existingResource != null) { throw new ResourceAlreadyExistsException(resourceFromRequest.StringId, _request.PrimaryResource.PublicName); @@ -207,7 +217,9 @@ public virtual async Task CreateAsync(TResource resource, Cancellatio throw; } - var resourceFromDatabase = await TryGetPrimaryResourceByIdAsync(resourceForDatabase.Id, TopFieldSelection.WithAllAttributes, cancellationToken); + TResource resourceFromDatabase = + await TryGetPrimaryResourceByIdAsync(resourceForDatabase.Id, TopFieldSelection.WithAllAttributes, cancellationToken); + AssertPrimaryResourceExists(resourceFromDatabase); _hookExecutor.AfterCreate(resourceFromDatabase); @@ -215,6 +227,7 @@ public virtual async Task CreateAsync(TResource resource, Cancellatio _resourceChangeTracker.SetFinallyStoredAttributeValues(resourceFromDatabase); bool hasImplicitChanges = _resourceChangeTracker.HasImplicitChanges(); + if (!hasImplicitChanges) { return null; @@ -228,12 +241,14 @@ private async Task AssertResourcesToAssignInRelationshipsExistAsync(TResource re { var missingResources = new List(); - foreach (var (queryLayer, relationship) in _queryLayerComposer.ComposeForGetTargetedSecondaryResourceIds(resource)) + foreach ((QueryLayer queryLayer, RelationshipAttribute relationship) in _queryLayerComposer.ComposeForGetTargetedSecondaryResourceIds(resource)) { object rightValue = relationship.GetValue(resource); ICollection rightResourceIds = TypeHelper.ExtractResources(rightValue); - var missingResourcesInRelationship = GetMissingRightResourcesAsync(queryLayer, relationship, rightResourceIds, cancellationToken); + IAsyncEnumerable missingResourcesInRelationship = + GetMissingRightResourcesAsync(queryLayer, relationship, rightResourceIds, cancellationToken); + await missingResources.AddRangeAsync(missingResourcesInRelationship, cancellationToken); } @@ -243,29 +258,33 @@ private async Task AssertResourcesToAssignInRelationshipsExistAsync(TResource re } } - private async IAsyncEnumerable GetMissingRightResourcesAsync( - QueryLayer existingRightResourceIdsQueryLayer, RelationshipAttribute relationship, - ICollection rightResourceIds, [EnumeratorCancellation] CancellationToken cancellationToken) + private async IAsyncEnumerable GetMissingRightResourcesAsync(QueryLayer existingRightResourceIdsQueryLayer, + RelationshipAttribute relationship, ICollection rightResourceIds, [EnumeratorCancellation] CancellationToken cancellationToken) { - var existingResources = await _repositoryAccessor.GetAsync( + IReadOnlyCollection existingResources = await _repositoryAccessor.GetAsync( existingRightResourceIdsQueryLayer.ResourceContext.ResourceType, existingRightResourceIdsQueryLayer, cancellationToken); - var existingResourceIds = existingResources.Select(resource => resource.StringId).ToArray(); + string[] existingResourceIds = existingResources.Select(resource => resource.StringId).ToArray(); - foreach (var rightResourceId in rightResourceIds) + foreach (IIdentifiable rightResourceId in rightResourceIds) { if (!existingResourceIds.Contains(rightResourceId.StringId)) { - yield return new MissingResourceInRelationship(relationship.PublicName, - existingRightResourceIdsQueryLayer.ResourceContext.PublicName, rightResourceId.StringId); + yield return new MissingResourceInRelationship(relationship.PublicName, existingRightResourceIdsQueryLayer.ResourceContext.PublicName, + rightResourceId.StringId); } } } /// - public async Task AddToToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken) + public async Task AddToToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, + CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {primaryId, secondaryResourceIds}); + _traceWriter.LogMethodStart(new + { + primaryId, + secondaryResourceIds + }); ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); @@ -287,7 +306,7 @@ public async Task AddToToManyRelationshipAsync(TId primaryId, string relationshi } catch (DataStoreUpdateException) { - var primaryResource = await TryGetPrimaryResourceByIdAsync(primaryId, TopFieldSelection.OnlyIdAttribute, cancellationToken); + TResource primaryResource = await TryGetPrimaryResourceByIdAsync(primaryId, TopFieldSelection.OnlyIdAttribute, cancellationToken); AssertPrimaryResourceExists(primaryResource); await AssertResourcesExistAsync(secondaryResourceIds, cancellationToken); @@ -299,23 +318,25 @@ public async Task AddToToManyRelationshipAsync(TId primaryId, string relationshi private async Task RemoveExistingIdsFromSecondarySetAsync(TId primaryId, ISet secondaryResourceIds, HasManyThroughAttribute hasManyThrough, CancellationToken cancellationToken) { - var queryLayer = _queryLayerComposer.ComposeForHasMany(hasManyThrough, primaryId, secondaryResourceIds); - var primaryResources = await _repositoryAccessor.GetAsync(queryLayer, cancellationToken); - - var primaryResource = primaryResources.FirstOrDefault(); + QueryLayer queryLayer = _queryLayerComposer.ComposeForHasMany(hasManyThrough, primaryId, secondaryResourceIds); + IReadOnlyCollection primaryResources = await _repositoryAccessor.GetAsync(queryLayer, cancellationToken); + + TResource primaryResource = primaryResources.FirstOrDefault(); AssertPrimaryResourceExists(primaryResource); - var rightValue = _request.Relationship.GetValue(primaryResource); - var existingRightResourceIds = TypeHelper.ExtractResources(rightValue); + object rightValue = _request.Relationship.GetValue(primaryResource); + ICollection existingRightResourceIds = TypeHelper.ExtractResources(rightValue); secondaryResourceIds.ExceptWith(existingRightResourceIds); } private async Task AssertResourcesExistAsync(ICollection secondaryResourceIds, CancellationToken cancellationToken) { - var queryLayer = _queryLayerComposer.ComposeForGetRelationshipRightIds(_request.Relationship, secondaryResourceIds); + QueryLayer queryLayer = _queryLayerComposer.ComposeForGetRelationshipRightIds(_request.Relationship, secondaryResourceIds); + + List missingResources = + await GetMissingRightResourcesAsync(queryLayer, _request.Relationship, secondaryResourceIds, cancellationToken).ToListAsync(cancellationToken); - var missingResources = await GetMissingRightResourcesAsync(queryLayer, _request.Relationship, secondaryResourceIds, cancellationToken).ToListAsync(cancellationToken); if (missingResources.Any()) { throw new ResourcesInRelationshipsNotFoundException(missingResources); @@ -325,11 +346,15 @@ private async Task AssertResourcesExistAsync(ICollection secondar /// public virtual async Task UpdateAsync(TId id, TResource resource, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id, resource}); + _traceWriter.LogMethodStart(new + { + id, + resource + }); ArgumentGuard.NotNull(resource, nameof(resource)); - var resourceFromRequest = resource; + TResource resourceFromRequest = resource; _resourceChangeTracker.SetRequestedAttributeValues(resourceFromRequest); _hookExecutor.BeforeUpdateResource(resourceFromRequest); @@ -356,6 +381,7 @@ public virtual async Task UpdateAsync(TId id, TResource resource, Can _resourceChangeTracker.SetFinallyStoredAttributeValues(afterResourceFromDatabase); bool hasImplicitChanges = _resourceChangeTracker.HasImplicitChanges(); + if (!hasImplicitChanges) { return null; @@ -368,7 +394,12 @@ public virtual async Task UpdateAsync(TId id, TResource resource, Can /// public virtual async Task SetRelationshipAsync(TId primaryId, string relationshipName, object secondaryResourceIds, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {primaryId, relationshipName, secondaryResourceIds}); + _traceWriter.LogMethodStart(new + { + primaryId, + relationshipName, + secondaryResourceIds + }); ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); @@ -394,7 +425,10 @@ public virtual async Task SetRelationshipAsync(TId primaryId, string relationshi /// public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {id}); + _traceWriter.LogMethodStart(new + { + id + }); _hookExecutor.BeforeDelete(id); @@ -404,7 +438,7 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke } catch (DataStoreUpdateException) { - var primaryResource = await TryGetPrimaryResourceByIdAsync(id, TopFieldSelection.OnlyIdAttribute, cancellationToken); + TResource primaryResource = await TryGetPrimaryResourceByIdAsync(id, TopFieldSelection.OnlyIdAttribute, cancellationToken); AssertPrimaryResourceExists(primaryResource); throw; } @@ -413,9 +447,15 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke } /// - public async Task RemoveFromToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, CancellationToken cancellationToken) + public async Task RemoveFromToManyRelationshipAsync(TId primaryId, string relationshipName, ISet secondaryResourceIds, + CancellationToken cancellationToken) { - _traceWriter.LogMethodStart(new {primaryId, relationshipName, secondaryResourceIds}); + _traceWriter.LogMethodStart(new + { + primaryId, + relationshipName, + secondaryResourceIds + }); ArgumentGuard.NotNull(relationshipName, nameof(relationshipName)); ArgumentGuard.NotNull(secondaryResourceIds, nameof(secondaryResourceIds)); @@ -433,15 +473,15 @@ public async Task RemoveFromToManyRelationshipAsync(TId primaryId, string relati private async Task TryGetPrimaryResourceByIdAsync(TId id, TopFieldSelection fieldSelection, CancellationToken cancellationToken) { - var primaryLayer = _queryLayerComposer.ComposeForGetById(id, _request.PrimaryResource, fieldSelection); + QueryLayer primaryLayer = _queryLayerComposer.ComposeForGetById(id, _request.PrimaryResource, fieldSelection); - var primaryResources = await _repositoryAccessor.GetAsync(primaryLayer, cancellationToken); + IReadOnlyCollection primaryResources = await _repositoryAccessor.GetAsync(primaryLayer, cancellationToken); return primaryResources.SingleOrDefault(); } private async Task GetPrimaryResourceForUpdateAsync(TId id, CancellationToken cancellationToken) { - var queryLayer = _queryLayerComposer.ComposeForUpdate(id, _request.PrimaryResource); + QueryLayer queryLayer = _queryLayerComposer.ComposeForUpdate(id, _request.PrimaryResource); var resource = await _repositoryAccessor.GetForUpdateAsync(queryLayer, cancellationToken); AssertPrimaryResourceExists(resource); @@ -470,23 +510,17 @@ private void AssertHasRelationship(RelationshipAttribute relationship, string na /// /// Represents the foundational Resource Service layer in the JsonApiDotNetCore architecture that uses a Resource Repository for data access. /// - /// The resource type. + /// + /// The resource type. + /// [PublicAPI] - public class JsonApiResourceService : JsonApiResourceService, - IResourceService + public class JsonApiResourceService : JsonApiResourceService, IResourceService where TResource : class, IIdentifiable { - public JsonApiResourceService( - IResourceRepositoryAccessor repositoryAccessor, - IQueryLayerComposer queryLayerComposer, - IPaginationContext paginationContext, - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IJsonApiRequest request, - IResourceChangeTracker resourceChangeTracker, - IResourceHookExecutorFacade hookExecutor) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, - resourceChangeTracker, hookExecutor) + public JsonApiResourceService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, + IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, + IResourceChangeTracker resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, hookExecutor) { } } diff --git a/src/JsonApiDotNetCore/TypeHelper.cs b/src/JsonApiDotNetCore/TypeHelper.cs index c3b004979a..b648a9387c 100644 --- a/src/JsonApiDotNetCore/TypeHelper.cs +++ b/src/JsonApiDotNetCore/TypeHelper.cs @@ -13,7 +13,11 @@ internal static class TypeHelper { private static readonly Type[] HashSetCompatibleCollectionTypes = { - typeof(HashSet<>), typeof(ICollection<>), typeof(ISet<>), typeof(IEnumerable<>), typeof(IReadOnlyCollection<>) + typeof(HashSet<>), + typeof(ICollection<>), + typeof(ISet<>), + typeof(IEnumerable<>), + typeof(IReadOnlyCollection<>) }; public static object ConvertType(object value, Type type) @@ -31,12 +35,14 @@ public static object ConvertType(object value, Type type) } Type runtimeType = value.GetType(); + if (type == runtimeType || type.IsAssignableFrom(runtimeType)) { return value; } string stringValue = value.ToString(); + if (string.IsNullOrEmpty(stringValue)) { return GetDefaultValue(type); @@ -50,19 +56,19 @@ public static object ConvertType(object value, Type type) if (nonNullableType == typeof(Guid)) { Guid convertedValue = Guid.Parse(stringValue); - return isNullableTypeRequested ? (Guid?) convertedValue : convertedValue; + return isNullableTypeRequested ? (Guid?)convertedValue : convertedValue; } if (nonNullableType == typeof(DateTimeOffset)) { DateTimeOffset convertedValue = DateTimeOffset.Parse(stringValue); - return isNullableTypeRequested ? (DateTimeOffset?) convertedValue : convertedValue; + return isNullableTypeRequested ? (DateTimeOffset?)convertedValue : convertedValue; } if (nonNullableType == typeof(TimeSpan)) { TimeSpan convertedValue = TimeSpan.Parse(stringValue); - return isNullableTypeRequested ? (TimeSpan?) convertedValue : convertedValue; + return isNullableTypeRequested ? (TimeSpan?)convertedValue : convertedValue; } if (nonNullableType.IsEnum) @@ -76,11 +82,10 @@ public static object ConvertType(object value, Type type) // https://bradwilson.typepad.com/blog/2008/07/creating-nullab.html return Convert.ChangeType(stringValue, nonNullableType); } - catch (Exception exception) when (exception is FormatException || exception is OverflowException || - exception is InvalidCastException || exception is ArgumentException) + catch (Exception exception) when (exception is FormatException || exception is OverflowException || exception is InvalidCastException || + exception is ArgumentException) { - throw new FormatException( - $"Failed to convert '{value}' of type '{runtimeType.Name}' to type '{type.Name}'.", exception); + throw new FormatException($"Failed to convert '{value}' of type '{runtimeType.Name}' to type '{type.Name}'.", exception); } } @@ -111,8 +116,7 @@ public static Type TryGetCollectionElementType(Type type) } /// - /// Gets the property info that is referenced in the NavigationAction expression. - /// Credits: https://stackoverflow.com/a/17116267/4441216 + /// Gets the property info that is referenced in the NavigationAction expression. Credits: https://stackoverflow.com/a/17116267/4441216 /// public static PropertyInfo ParseNavigationExpression(Expression> navigationExpression) { @@ -147,20 +151,29 @@ public static PropertyInfo ParseNavigationExpression(Expression /// Creates an instance of the specified generic type /// - /// The instance of the parameterized generic type - /// Generic type parameters to be used in open type. - /// Constructor arguments to be provided in instantiation. - /// Open generic type + /// + /// The instance of the parameterized generic type + /// + /// + /// Generic type parameters to be used in open type. + /// + /// + /// Constructor arguments to be provided in instantiation. + /// + /// + /// Open generic type + /// private static object CreateInstanceOfOpenType(Type openType, Type[] parameters, params object[] constructorArguments) { - var parameterizedType = openType.MakeGenericType(parameters); + Type parameterizedType = openType.MakeGenericType(parameters); return Activator.CreateInstance(parameterizedType, constructorArguments); } /// - /// Helper method that "unboxes" the TValue from the relationship dictionary into + /// Helper method that "unboxes" the TValue from the relationship dictionary into /// - public static Dictionary> ConvertRelationshipDictionary(Dictionary relationships) + public static Dictionary> ConvertRelationshipDictionary( + Dictionary relationships) { return relationships.ToDictionary(pair => pair.Key, pair => (HashSet)pair.Value); } @@ -168,7 +181,8 @@ public static Dictionary> ConvertRelat /// /// Converts a dictionary of AttrAttributes to the underlying PropertyInfo that is referenced /// - public static Dictionary> ConvertAttributeDictionary(IEnumerable attributes, HashSet resources) + public static Dictionary> ConvertAttributeDictionary(IEnumerable attributes, + HashSet resources) { return attributes.ToDictionary(attr => attr.Property, _ => resources); } @@ -176,10 +190,18 @@ public static Dictionary> ConvertAttributeDicti /// /// Creates an instance of the specified generic type /// - /// The instance of the parameterized generic type - /// Generic type parameter to be used in open type. - /// Constructor arguments to be provided in instantiation. - /// Open generic type + /// + /// The instance of the parameterized generic type + /// + /// + /// Generic type parameter to be used in open type. + /// + /// + /// Constructor arguments to be provided in instantiation. + /// + /// + /// Open generic type + /// public static object CreateInstanceOfOpenType(Type openType, Type parameter, params object[] constructorArguments) { return CreateInstanceOfOpenType(openType, parameter.AsArray(), constructorArguments); @@ -190,13 +212,17 @@ public static object CreateInstanceOfOpenType(Type openType, Type parameter, par /// public static object CreateInstanceOfOpenType(Type openType, Type parameter, bool hasInternalConstructor, params object[] constructorArguments) { - Type[] parameters = {parameter}; + Type[] parameters = + { + parameter + }; + if (!hasInternalConstructor) { return CreateInstanceOfOpenType(openType, parameters, constructorArguments); } - var parameterizedType = openType.MakeGenericType(parameters); + Type parameterizedType = openType.MakeGenericType(parameters); // note that if for whatever reason the constructor of AffectedResource is set from // internal to public, this will throw an error, as it is looking for a non-public one. return Activator.CreateInstance(parameterizedType, BindingFlags.NonPublic | BindingFlags.Instance, null, constructorArguments, null); @@ -205,15 +231,19 @@ public static object CreateInstanceOfOpenType(Type openType, Type parameter, boo /// /// Reflectively instantiates a list of a certain type. /// - /// The list of the target type - /// The target type + /// + /// The list of the target type + /// + /// + /// The target type + /// public static IList CreateListFor(Type elementType) { return (IList)CreateInstanceOfOpenType(typeof(List<>), elementType); } /// - /// Reflectively instantiates a hashset of a certain type. + /// Reflectively instantiates a hashset of a certain type. /// public static IEnumerable CreateHashSetFor(Type type, object elements) { @@ -245,14 +275,14 @@ public static Type ToConcreteCollectionType(Type collectionType) } /// - /// Indicates whether a instance can be assigned to the specified type, - /// for example IList{Article} -> false or ISet{Article} -> true. + /// Indicates whether a instance can be assigned to the specified type, for example IList{Article} -> false or ISet{Article} -> + /// true. /// public static bool TypeCanContainHashSet(Type collectionType) { if (collectionType.IsGenericType) { - var openCollectionType = collectionType.GetGenericTypeDefinition(); + Type openCollectionType = collectionType.GetGenericTypeDefinition(); return HashSetCompatibleCollectionTypes.Contains(openCollectionType); } @@ -260,11 +290,12 @@ public static bool TypeCanContainHashSet(Type collectionType) } /// - /// Gets the type (such as Guid or int) of the Id property on a type that implements . + /// Gets the type (such as Guid or int) of the Id property on a type that implements . /// public static Type GetIdType(Type resourceType) { - var property = resourceType.GetProperty(nameof(Identifiable.Id)); + PropertyInfo property = resourceType.GetProperty(nameof(Identifiable.Id)); + if (property == null) { throw new ArgumentException($"Type '{resourceType.Name}' does not have 'Id' property."); @@ -303,8 +334,7 @@ public static object CreateInstance(Type type) } catch (Exception exception) { - throw new InvalidOperationException( - $"Failed to create an instance of '{type.FullName}' using its default constructor.", exception); + throw new InvalidOperationException($"Failed to create an instance of '{type.FullName}' using its default constructor.", exception); } } @@ -321,8 +351,8 @@ public static IList CopyToList(IEnumerable copyFrom, Type elementType, Converter if (elementConverter != null) { - var converted = copyFrom.Cast().Select(element => elementConverter(element)); - return (IList) CopyToTypedCollection(converted, collectionType); + IEnumerable converted = copyFrom.Cast().Select(element => elementConverter(element)); + return (IList)CopyToTypedCollection(converted, collectionType); } return (IList)CopyToTypedCollection(copyFrom, collectionType); @@ -331,19 +361,23 @@ public static IList CopyToList(IEnumerable copyFrom, Type elementType, Converter /// /// Creates a collection instance based on the specified collection type and copies the specified elements into it. /// - /// Source to copy from. - /// Target collection type, for example: typeof(List{Article}) or typeof(ISet{Person}). + /// + /// Source to copy from. + /// + /// + /// Target collection type, for example: typeof(List{Article}) or typeof(ISet{Person}). + /// public static IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType) { ArgumentGuard.NotNull(source, nameof(source)); ArgumentGuard.NotNull(collectionType, nameof(collectionType)); - var concreteCollectionType = ToConcreteCollectionType(collectionType); + Type concreteCollectionType = ToConcreteCollectionType(collectionType); dynamic concreteCollectionInstance = CreateInstance(concreteCollectionType); - foreach (var item in source) + foreach (object item in source) { - concreteCollectionInstance.Add((dynamic) item); + concreteCollectionInstance.Add((dynamic)item); } return concreteCollectionInstance; diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 4f568d9555..ecdc3569d6 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -57,14 +57,14 @@ public void Can_add_resources_from_assembly_to_graph() // Act facade.DiscoverResources(); - + // Assert - var resourceGraph = _resourceGraphBuilder.Build(); - - var personResource = resourceGraph.GetResourceContext(typeof(Person)); + IResourceGraph resourceGraph = _resourceGraphBuilder.Build(); + + ResourceContext personResource = resourceGraph.GetResourceContext(typeof(Person)); personResource.Should().NotBeNull(); - var articleResource = resourceGraph.GetResourceContext(typeof(Article)); + ResourceContext articleResource = resourceGraph.GetResourceContext(typeof(Article)); articleResource.Should().NotBeNull(); } @@ -77,11 +77,11 @@ public void Can_add_resource_from_current_assembly_to_graph() // Act facade.DiscoverResources(); - + // Assert - var resourceGraph = _resourceGraphBuilder.Build(); + IResourceGraph resourceGraph = _resourceGraphBuilder.Build(); - var resource = resourceGraph.GetResourceContext(typeof(TestResource)); + ResourceContext resource = resourceGraph.GetResourceContext(typeof(TestResource)); resource.Should().NotBeNull(); } @@ -91,12 +91,12 @@ public void Can_add_resource_service_from_current_assembly_to_container() // Arrange var facade = new ServiceDiscoveryFacade(_services, _resourceGraphBuilder, _options, LoggerFactory); facade.AddCurrentAssembly(); - + // Act facade.DiscoverInjectables(); // Assert - var services = _services.BuildServiceProvider(); + ServiceProvider services = _services.BuildServiceProvider(); var resourceService = services.GetRequiredService>(); resourceService.Should().BeOfType(); @@ -113,7 +113,7 @@ public void Can_add_resource_repository_from_current_assembly_to_container() facade.DiscoverInjectables(); // Assert - var services = _services.BuildServiceProvider(); + ServiceProvider services = _services.BuildServiceProvider(); var resourceRepository = services.GetRequiredService>(); resourceRepository.Should().BeOfType(); @@ -130,7 +130,7 @@ public void Can_add_resource_definition_from_current_assembly_to_container() facade.DiscoverInjectables(); // Assert - var services = _services.BuildServiceProvider(); + ServiceProvider services = _services.BuildServiceProvider(); var resourceDefinition = services.GetRequiredService>(); resourceDefinition.Should().BeOfType(); @@ -149,29 +149,24 @@ public void Can_add_resource_hooks_definition_from_current_assembly_to_container facade.DiscoverInjectables(); // Assert - var services = _services.BuildServiceProvider(); + ServiceProvider services = _services.BuildServiceProvider(); var resourceHooksDefinition = services.GetRequiredService>(); resourceHooksDefinition.Should().BeOfType(); } [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class TestResource : Identifiable { } + public sealed class TestResource : Identifiable + { + } [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class TestResourceService : JsonApiResourceService { - public TestResourceService( - IResourceRepositoryAccessor repositoryAccessor, - IQueryLayerComposer queryLayerComposer, - IPaginationContext paginationContext, - IJsonApiOptions options, - ILoggerFactory loggerFactory, - IJsonApiRequest request, - IResourceChangeTracker resourceChangeTracker, - IResourceHookExecutorFacade hookExecutor) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, - resourceChangeTracker, hookExecutor) + public TestResourceService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, + IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, + IResourceChangeTracker resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, hookExecutor) { } } @@ -179,27 +174,29 @@ public TestResourceService( [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class TestResourceRepository : EntityFrameworkCoreRepository { - public TestResourceRepository( - ITargetedFields targetedFields, - IDbContextResolver contextResolver, - IResourceGraph resourceGraph, - IResourceFactory resourceFactory, - IEnumerable constraintProviders, - ILoggerFactory loggerFactory) + public TestResourceRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) - { } + { + } } [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class TestResourceHooksDefinition : ResourceHooksDefinition { - public TestResourceHooksDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public TestResourceHooksDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) + { + } } [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class TestResourceDefinition : JsonApiResourceDefinition { - public TestResourceDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { } + public TestResourceDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) + { + } } } } diff --git a/test/JsonApiDotNetCoreExampleTests/ExampleIntegrationTestContext.cs b/test/JsonApiDotNetCoreExampleTests/ExampleIntegrationTestContext.cs index 311d18b4d1..50e796e08d 100644 --- a/test/JsonApiDotNetCoreExampleTests/ExampleIntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/ExampleIntegrationTestContext.cs @@ -7,8 +7,12 @@ namespace JsonApiDotNetCoreExampleTests /// /// A test context for tests that reference the JsonApiDotNetCoreExample project. /// - /// The server Startup class, which can be defined in the test project. - /// The EF Core database context, which can be defined in the test project. + /// + /// The server Startup class, which can be defined in the test project. + /// + /// + /// The EF Core database context, which can be defined in the test project. + /// public sealed class ExampleIntegrationTestContext : BaseIntegrationTestContext where TStartup : class where TDbContext : DbContext diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs index bcd75da764..ba68a661b0 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -25,8 +26,8 @@ public AtomicConstrainedOperationsControllerTests(ExampleIntegrationTestContext< public async Task Can_create_resources_for_matching_resource_type() { // Arrange - var newTitle1 = _fakers.MusicTrack.Generate().Title; - var newTitle2 = _fakers.MusicTrack.Generate().Title; + string newTitle1 = _fakers.MusicTrack.Generate().Title; + string newTitle2 = _fakers.MusicTrack.Generate().Title; var requestBody = new { @@ -62,7 +63,8 @@ public async Task Can_create_resources_for_matching_resource_type() const string route = "/operations/musicTracks/create"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -95,14 +97,14 @@ public async Task Cannot_create_resource_for_mismatching_resource_type() const string route = "/operations/musicTracks/create"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); @@ -113,7 +115,7 @@ public async Task Cannot_create_resource_for_mismatching_resource_type() public async Task Cannot_update_resources_for_matching_resource_type() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -143,14 +145,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations/musicTracks/create"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); @@ -161,8 +163,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_add_to_ToMany_relationship_for_matching_resource_type() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); - var existingPerformer = _fakers.Performer.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + Performer existingPerformer = _fakers.Performer.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -198,14 +200,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations/musicTracks/create"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs index d4a69eb31a..21b4fd4ac5 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs @@ -19,8 +19,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Contro [Route("/operations/musicTracks/create")] public sealed class CreateMusicTrackOperationsController : JsonApiOperationsController { - public CreateMusicTrackOperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IOperationsProcessor processor, IJsonApiRequest request, ITargetedFields targetedFields) + public CreateMusicTrackOperationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IOperationsProcessor processor, + IJsonApiRequest request, ITargetedFields targetedFields) : base(options, loggerFactory, processor, request, targetedFields) { } @@ -35,7 +35,8 @@ public override async Task PostOperationsAsync(IList operations) { int index = 0; - foreach (var operation in operations) + + foreach (OperationContainer operation in operations) { if (operation.Kind != OperationKind.CreateResource || operation.Resource.GetType() != typeof(MusicTrack)) { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs index 7c008892e3..d40d65b836 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Extensions; @@ -13,8 +14,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Creating { - public sealed class AtomicCreateResourceTests - : IClassFixture, OperationsDbContext>> + public sealed class AtomicCreateResourceTests : IClassFixture, OperationsDbContext>> { private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); @@ -30,8 +30,8 @@ public AtomicCreateResourceTests(ExampleIntegrationTestContext(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -68,11 +69,11 @@ public async Task Can_create_resource() responseDocument.Results[0].SingleData.Attributes["bornAt"].Should().BeCloseTo(newBornAt); responseDocument.Results[0].SingleData.Relationships.Should().BeNull(); - var newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var performerInDatabase = await dbContext.Performers.FirstWithIdAsync(newPerformerId); + Performer performerInDatabase = await dbContext.Performers.FirstWithIdAsync(newPerformerId); performerInDatabase.ArtistName.Should().Be(newArtistName); performerInDatabase.BornAt.Should().BeCloseTo(newBornAt); @@ -85,9 +86,10 @@ public async Task Can_create_resources() // Arrange const int elementCount = 5; - var newTracks = _fakers.MusicTrack.Generate(elementCount); + List newTracks = _fakers.MusicTrack.Generate(elementCount); var operationElements = new List(elementCount); + for (int index = 0; index < elementCount; index++) { operationElements.Add(new @@ -115,7 +117,8 @@ public async Task Can_create_resources() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -124,7 +127,7 @@ public async Task Can_create_resources() for (int index = 0; index < elementCount; index++) { - var singleData = responseDocument.Results[index].SingleData; + ResourceObject singleData = responseDocument.Results[index].SingleData; singleData.Should().NotBeNull(); singleData.Type.Should().Be("musicTracks"); @@ -135,19 +138,18 @@ public async Task Can_create_resources() singleData.Relationships.Should().NotBeEmpty(); } - var newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.SingleData.Id)); + IEnumerable newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.SingleData.Id)); await _testContext.RunOnDatabaseAsync(async dbContext => { - var tracksInDatabase = await dbContext.MusicTracks - .Where(musicTrack => newTrackIds.Contains(musicTrack.Id)) - .ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.Where(musicTrack => newTrackIds.Contains(musicTrack.Id)).ToListAsync(); tracksInDatabase.Should().HaveCount(elementCount); for (int index = 0; index < elementCount; index++) { - var trackInDatabase = tracksInDatabase.Single(musicTrack => musicTrack.Id == Guid.Parse(responseDocument.Results[index].SingleData.Id)); + MusicTrack trackInDatabase = + tracksInDatabase.Single(musicTrack => musicTrack.Id == Guid.Parse(responseDocument.Results[index].SingleData.Id)); trackInDatabase.Title.Should().Be(newTracks[index].Title); trackInDatabase.LengthInSeconds.Should().BeApproximately(newTracks[index].LengthInSeconds); @@ -185,7 +187,8 @@ public async Task Can_create_resource_without_attributes_or_relationships() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -197,11 +200,11 @@ public async Task Can_create_resource_without_attributes_or_relationships() responseDocument.Results[0].SingleData.Attributes["bornAt"].Should().BeCloseTo(default(DateTimeOffset)); responseDocument.Results[0].SingleData.Relationships.Should().BeNull(); - var newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var performerInDatabase = await dbContext.Performers.FirstWithIdAsync(newPerformerId); + Performer performerInDatabase = await dbContext.Performers.FirstWithIdAsync(newPerformerId); performerInDatabase.ArtistName.Should().BeNull(); performerInDatabase.BornAt.Should().Be(default); @@ -212,7 +215,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_resource_with_unknown_attribute() { // Arrange - var newName = _fakers.Playlist.Generate().Name; + string newName = _fakers.Playlist.Generate().Name; var requestBody = new { @@ -237,7 +240,8 @@ public async Task Can_create_resource_with_unknown_attribute() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -248,11 +252,11 @@ public async Task Can_create_resource_with_unknown_attribute() responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newName); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - var newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var performerInDatabase = await dbContext.Playlists.FirstWithIdAsync(newPlaylistId); + Playlist performerInDatabase = await dbContext.Playlists.FirstWithIdAsync(newPlaylistId); performerInDatabase.Name.Should().Be(newName); }); @@ -291,7 +295,8 @@ public async Task Can_create_resource_with_unknown_relationship() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -302,11 +307,11 @@ public async Task Can_create_resource_with_unknown_relationship() responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - var newLyricId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newLyricId = long.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var lyricInDatabase = await dbContext.Lyrics.FirstWithIdAsync(newLyricId); + Lyric lyricInDatabase = await dbContext.Lyrics.FirstWithIdAsync(newLyricId); lyricInDatabase.Should().NotBeNull(); }); @@ -316,7 +321,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_resource_with_client_generated_ID() { // Arrange - var newTitle = _fakers.MusicTrack.Generate().Title; + string newTitle = _fakers.MusicTrack.Generate().Title; var requestBody = new { @@ -341,14 +346,14 @@ public async Task Cannot_create_resource_with_client_generated_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Specifying the resource ID in operations that create a resource is not allowed."); error.Detail.Should().BeNull(); @@ -374,14 +379,14 @@ public async Task Cannot_create_resource_for_href_element() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -410,14 +415,14 @@ public async Task Cannot_create_resource_for_ref_element() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.relationship' element is required."); error.Detail.Should().BeNull(); @@ -442,14 +447,14 @@ public async Task Cannot_create_resource_for_missing_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); @@ -480,14 +485,14 @@ public async Task Cannot_create_resource_for_missing_type() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); error.Detail.Should().BeNull(); @@ -516,14 +521,14 @@ public async Task Cannot_create_resource_for_unknown_type() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -534,7 +539,7 @@ public async Task Cannot_create_resource_for_unknown_type() public async Task Cannot_create_resource_for_array() { // Arrange - var newArtistName = _fakers.Performer.Generate().ArtistName; + string newArtistName = _fakers.Performer.Generate().ArtistName; var requestBody = new { @@ -561,14 +566,14 @@ public async Task Cannot_create_resource_for_array() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for create/update resource operation."); error.Detail.Should().BeNull(); @@ -601,14 +606,14 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Setting the initial value of the requested attribute is not allowed."); error.Detail.Should().Be("Setting the initial value of 'createdAt' is not allowed."); @@ -619,7 +624,7 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() public async Task Cannot_create_resource_with_readonly_attribute() { // Arrange - var newPlaylistName = _fakers.Playlist.Generate().Name; + string newPlaylistName = _fakers.Playlist.Generate().Name; var requestBody = new { @@ -644,14 +649,14 @@ public async Task Cannot_create_resource_with_readonly_attribute() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isArchived' is read-only."); @@ -684,14 +689,14 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'DateTimeOffset'. - Request body:"); @@ -702,11 +707,11 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() public async Task Can_create_resource_with_attributes_and_multiple_relationship_types() { // Arrange - var existingLyric = _fakers.Lyric.Generate(); - var existingCompany = _fakers.RecordCompany.Generate(); - var existingPerformer = _fakers.Performer.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); + Performer existingPerformer = _fakers.Performer.Generate(); - var newTitle = _fakers.MusicTrack.Generate().Title; + string newTitle = _fakers.MusicTrack.Generate().Title; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -766,7 +771,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -777,14 +783,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTitle); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var trackInDatabase = await dbContext.MusicTracks + MusicTrack trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Lyric) .Include(musicTrack => musicTrack.OwnedBy) .Include(musicTrack => musicTrack.Performers) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs index 8e11640cfe..0bb4edb8e0 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -17,13 +18,14 @@ public sealed class AtomicCreateResourceWithClientGeneratedIdTests private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); - public AtomicCreateResourceWithClientGeneratedIdTests(ExampleIntegrationTestContext, OperationsDbContext> testContext) + public AtomicCreateResourceWithClientGeneratedIdTests( + ExampleIntegrationTestContext, OperationsDbContext> testContext) { _testContext = testContext; testContext.ConfigureServicesAfterStartup(services => services.AddControllersFromExampleProject()); - var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); options.AllowClientGeneratedIds = true; } @@ -31,7 +33,7 @@ public AtomicCreateResourceWithClientGeneratedIdTests(ExampleIntegrationTestCont public async Task Can_create_resource_with_client_generated_guid_ID_having_side_effects() { // Arrange - var newLanguage = _fakers.TextLanguage.Generate(); + TextLanguage newLanguage = _fakers.TextLanguage.Generate(); newLanguage.Id = Guid.NewGuid(); var requestBody = new @@ -57,7 +59,8 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -71,7 +74,7 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ await _testContext.RunOnDatabaseAsync(async dbContext => { - var languageInDatabase = await dbContext.TextLanguages.FirstWithIdAsync(newLanguage.Id); + TextLanguage languageInDatabase = await dbContext.TextLanguages.FirstWithIdAsync(newLanguage.Id); languageInDatabase.IsoCode.Should().Be(newLanguage.IsoCode); }); @@ -81,7 +84,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_resource_with_client_generated_string_ID_having_no_side_effects() { // Arrange - var newTrack = _fakers.MusicTrack.Generate(); + MusicTrack newTrack = _fakers.MusicTrack.Generate(); newTrack.Id = Guid.NewGuid(); var requestBody = new @@ -108,7 +111,7 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_ const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -117,7 +120,7 @@ public async Task Can_create_resource_with_client_generated_string_ID_having_no_ await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(newTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(newTrack.Id); trackInDatabase.Title.Should().Be(newTrack.Title); trackInDatabase.LengthInSeconds.Should().BeApproximately(newTrack.LengthInSeconds); @@ -128,10 +131,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_resource_for_existing_client_generated_ID() { // Arrange - var existingLanguage = _fakers.TextLanguage.Generate(); + TextLanguage existingLanguage = _fakers.TextLanguage.Generate(); existingLanguage.Id = Guid.NewGuid(); - var languageToCreate = _fakers.TextLanguage.Generate(); + TextLanguage languageToCreate = _fakers.TextLanguage.Generate(); languageToCreate.Id = existingLanguage.Id; await _testContext.RunOnDatabaseAsync(async dbContext => @@ -163,14 +166,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error 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 '{languageToCreate.StringId}' already exists."); @@ -181,7 +184,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_resource_for_incompatible_ID() { // Arrange - var guid = Guid.NewGuid().ToString(); + string guid = Guid.NewGuid().ToString(); var requestBody = new { @@ -205,14 +208,14 @@ public async Task Cannot_create_resource_for_incompatible_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); @@ -243,14 +246,14 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index bbaedf4faa..dd6a0e90c6 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -16,7 +18,8 @@ public sealed class AtomicCreateResourceWithToManyRelationshipTests private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); - public AtomicCreateResourceWithToManyRelationshipTests(ExampleIntegrationTestContext, OperationsDbContext> testContext) + public AtomicCreateResourceWithToManyRelationshipTests( + ExampleIntegrationTestContext, OperationsDbContext> testContext) { _testContext = testContext; @@ -27,8 +30,8 @@ public AtomicCreateResourceWithToManyRelationshipTests(ExampleIntegrationTestCon public async Task Can_create_HasMany_relationship() { // Arrange - var existingPerformers = _fakers.Performer.Generate(2); - var newTitle = _fakers.MusicTrack.Generate().Title; + List existingPerformers = _fakers.Performer.Generate(2); + string newTitle = _fakers.MusicTrack.Generate().Title; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -77,7 +80,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -88,13 +92,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(newTrackId); trackInDatabase.Performers.Should().HaveCount(2); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingPerformers[0].Id); @@ -106,8 +108,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_HasManyThrough_relationship() { // Arrange - var existingTracks = _fakers.MusicTrack.Generate(3); - var newName = _fakers.Playlist.Generate().Name; + List existingTracks = _fakers.MusicTrack.Generate(3); + string newName = _fakers.Playlist.Generate().Name; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -161,7 +163,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -172,14 +175,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - var newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(newPlaylistId); @@ -229,14 +232,14 @@ public async Task Cannot_create_for_missing_relationship_type() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'performers' relationship."); @@ -279,14 +282,14 @@ public async Task Cannot_create_for_unknown_relationship_type() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -328,14 +331,14 @@ public async Task Cannot_create_for_missing_relationship_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); @@ -346,7 +349,7 @@ public async Task Cannot_create_for_missing_relationship_ID() public async Task Cannot_create_for_unknown_relationship_IDs() { // Arrange - var newTitle = _fakers.MusicTrack.Generate().Title; + string newTitle = _fakers.MusicTrack.Generate().Title; var requestBody = new { @@ -389,20 +392,20 @@ public async Task Cannot_create_for_unknown_relationship_IDs() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - var error1 = responseDocument.Errors[0]; + Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be("Related resource of type 'performers' with ID '12345678' in relationship 'performers' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - var error2 = responseDocument.Errors[1]; + Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be("Related resource of type 'performers' with ID '87654321' in relationship 'performers' does not exist."); @@ -445,14 +448,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); @@ -463,8 +466,8 @@ public async Task Cannot_create_on_relationship_type_mismatch() public async Task Can_create_with_duplicates() { // Arrange - var existingPerformer = _fakers.Performer.Generate(); - var newTitle = _fakers.MusicTrack.Generate().Title; + Performer existingPerformer = _fakers.Performer.Generate(); + string newTitle = _fakers.MusicTrack.Generate().Title; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -513,7 +516,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -524,13 +528,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(newTrackId); trackInDatabase.Performers.Should().HaveCount(1); trackInDatabase.Performers[0].Id.Should().Be(existingPerformer.Id); @@ -555,7 +557,7 @@ public async Task Cannot_create_with_null_data_in_HasMany_relationship() { performers = new { - data = (object) null + data = (object)null } } } @@ -566,14 +568,14 @@ public async Task Cannot_create_with_null_data_in_HasMany_relationship() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -598,7 +600,7 @@ public async Task Cannot_create_with_null_data_in_HasManyThrough_relationship() { tracks = new { - data = (object) null + data = (object)null } } } @@ -609,14 +611,14 @@ public async Task Cannot_create_with_null_data_in_HasManyThrough_relationship() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'tracks' relationship."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index 999e214e5c..84107d9b63 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -19,7 +20,8 @@ public sealed class AtomicCreateResourceWithToOneRelationshipTests private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); - public AtomicCreateResourceWithToOneRelationshipTests(ExampleIntegrationTestContext, OperationsDbContext> testContext) + public AtomicCreateResourceWithToOneRelationshipTests( + ExampleIntegrationTestContext, OperationsDbContext> testContext) { _testContext = testContext; @@ -30,7 +32,7 @@ public AtomicCreateResourceWithToOneRelationshipTests(ExampleIntegrationTestCont public async Task Can_create_OneToOne_relationship_from_principal_side() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -67,7 +69,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -78,13 +81,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - var newLyricId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newLyricId = long.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var lyricInDatabase = await dbContext.Lyrics - .Include(lyric => lyric.Track) - .FirstWithIdAsync(newLyricId); + Lyric lyricInDatabase = await dbContext.Lyrics.Include(lyric => lyric.Track).FirstWithIdAsync(newLyricId); lyricInDatabase.Track.Should().NotBeNull(); lyricInDatabase.Track.Id.Should().Be(existingTrack.Id); @@ -95,8 +96,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_OneToOne_relationship_from_dependent_side() { // Arrange - var existingLyric = _fakers.Lyric.Generate(); - var newTrackTitle = _fakers.MusicTrack.Generate().Title; + Lyric existingLyric = _fakers.Lyric.Generate(); + string newTrackTitle = _fakers.MusicTrack.Generate().Title; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -137,7 +138,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -148,13 +150,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Lyric) - .FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Lyric).FirstWithIdAsync(newTrackId); trackInDatabase.Lyric.Should().NotBeNull(); trackInDatabase.Lyric.Id.Should().Be(existingLyric.Id); @@ -167,8 +167,8 @@ public async Task Can_create_resources_with_ToOne_relationship() // Arrange const int elementCount = 5; - var existingCompany = _fakers.RecordCompany.Generate(); - var newTrackTitles = _fakers.MusicTrack.Generate(elementCount).Select(musicTrack => musicTrack.Title).ToArray(); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); + string[] newTrackTitles = _fakers.MusicTrack.Generate(elementCount).Select(musicTrack => musicTrack.Title).ToArray(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -177,6 +177,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); var operationElements = new List(elementCount); + for (int index = 0; index < elementCount; index++) { operationElements.Add(new @@ -212,7 +213,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -226,14 +228,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[index].SingleData.Attributes["title"].Should().Be(newTrackTitles[index]); } - var newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.SingleData.Id)); + IEnumerable newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.SingleData.Id)); await _testContext.RunOnDatabaseAsync(async dbContext => { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var tracksInDatabase = await dbContext.MusicTracks + List tracksInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) .Where(musicTrack => newTrackIds.Contains(musicTrack.Id)) .ToListAsync(); @@ -245,7 +247,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => for (int index = 0; index < elementCount; index++) { - var trackInDatabase = tracksInDatabase.Single(musicTrack => + MusicTrack trackInDatabase = tracksInDatabase.Single(musicTrack => musicTrack.Id == Guid.Parse(responseDocument.Results[index].SingleData.Id)); trackInDatabase.Title.Should().Be(newTrackTitles[index]); @@ -288,14 +290,14 @@ public async Task Cannot_create_for_missing_relationship_type() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'lyric' relationship."); @@ -335,14 +337,14 @@ public async Task Cannot_create_for_unknown_relationship_type() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -381,14 +383,14 @@ public async Task Cannot_create_for_missing_relationship_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); @@ -428,14 +430,14 @@ public async Task Cannot_create_with_unknown_relationship_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be("Related resource of type 'lyrics' with ID '12345678' in relationship 'lyric' does not exist."); @@ -475,14 +477,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); @@ -493,8 +495,8 @@ public async Task Cannot_create_on_relationship_type_mismatch() public async Task Can_create_resource_with_duplicate_relationship() { // Arrange - var existingCompany = _fakers.RecordCompany.Generate(); - var newTrackTitle = _fakers.MusicTrack.Generate().Title; + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); + string newTrackTitle = _fakers.MusicTrack.Generate().Title; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -540,12 +542,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var requestBodyText = JsonConvert.SerializeObject(requestBody).Replace("ownedBy_duplicate", "ownedBy"); + string requestBodyText = JsonConvert.SerializeObject(requestBody).Replace("ownedBy_duplicate", "ownedBy"); const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBodyText); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBodyText); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -556,13 +559,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(newTrackId); trackInDatabase.OwnedBy.Should().NotBeNull(); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); @@ -605,14 +606,14 @@ public async Task Cannot_create_with_data_array_in_relationship() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index 14464a777b..ffb3342364 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -12,8 +13,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Deleting { - public sealed class AtomicDeleteResourceTests - : IClassFixture, OperationsDbContext>> + public sealed class AtomicDeleteResourceTests : IClassFixture, OperationsDbContext>> { private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); @@ -29,7 +29,7 @@ public AtomicDeleteResourceTests(ExampleIntegrationTestContext { @@ -56,7 +56,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -65,7 +65,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var performerInDatabase = await dbContext.Performers.FirstWithIdOrDefaultAsync(existingPerformer.Id); + Performer performerInDatabase = await dbContext.Performers.FirstWithIdOrDefaultAsync(existingPerformer.Id); performerInDatabase.Should().BeNull(); }); @@ -77,7 +77,7 @@ public async Task Can_delete_existing_resources() // Arrange const int elementCount = 5; - var existingTracks = _fakers.MusicTrack.Generate(elementCount); + List existingTracks = _fakers.MusicTrack.Generate(elementCount); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -87,6 +87,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); var operationElements = new List(elementCount); + for (int index = 0; index < elementCount; index++) { operationElements.Add(new @@ -108,7 +109,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -117,8 +118,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var tracksInDatabase = await dbContext.MusicTracks - .ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().BeEmpty(); }); @@ -128,7 +128,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_delete_resource_with_OneToOne_relationship_from_principal_side() { // Arrange - var existingLyric = _fakers.Lyric.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); existingLyric.Track = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -156,7 +156,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -165,11 +165,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var lyricsInDatabase = await dbContext.Lyrics.FirstWithIdOrDefaultAsync(existingLyric.Id); + Lyric lyricsInDatabase = await dbContext.Lyrics.FirstWithIdOrDefaultAsync(existingLyric.Id); lyricsInDatabase.Should().BeNull(); - var trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(existingLyric.Track.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(existingLyric.Track.Id); trackInDatabase.Lyric.Should().BeNull(); }); @@ -179,7 +179,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_delete_resource_with_OneToOne_relationship_from_dependent_side() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Lyric = _fakers.Lyric.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -207,7 +207,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -216,11 +216,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var tracksInDatabase = await dbContext.MusicTracks.FirstWithIdOrDefaultAsync(existingTrack.Id); + MusicTrack tracksInDatabase = await dbContext.MusicTracks.FirstWithIdOrDefaultAsync(existingTrack.Id); tracksInDatabase.Should().BeNull(); - var lyricInDatabase = await dbContext.Lyrics.FirstWithIdAsync(existingTrack.Lyric.Id); + Lyric lyricInDatabase = await dbContext.Lyrics.FirstWithIdAsync(existingTrack.Lyric.Id); lyricInDatabase.Track.Should().BeNull(); }); @@ -230,7 +230,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_delete_existing_resource_with_HasMany_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Performers = _fakers.Performer.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -258,7 +258,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -267,11 +267,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks.FirstWithIdOrDefaultAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.FirstWithIdOrDefaultAsync(existingTrack.Id); trackInDatabase.Should().BeNull(); - var performersInDatabase = await dbContext.Performers.ToListAsync(); + List performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().ContainSingle(userAccount => userAccount.Id == existingTrack.Performers.ElementAt(0).Id); performersInDatabase.Should().ContainSingle(userAccount => userAccount.Id == existingTrack.Performers.ElementAt(1).Id); @@ -313,7 +313,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -322,12 +322,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var playlistInDatabase = await dbContext.Playlists.FirstWithIdOrDefaultAsync(existingPlaylistMusicTrack.Playlist.Id); + Playlist playlistInDatabase = await dbContext.Playlists.FirstWithIdOrDefaultAsync(existingPlaylistMusicTrack.Playlist.Id); playlistInDatabase.Should().BeNull(); - var playlistTracksInDatabase = await dbContext.PlaylistMusicTracks - .FirstOrDefaultAsync(playlistMusicTrack => playlistMusicTrack.Playlist.Id == existingPlaylistMusicTrack.Playlist.Id); + PlaylistMusicTrack playlistTracksInDatabase = await dbContext.PlaylistMusicTracks.FirstOrDefaultAsync(playlistMusicTrack => + playlistMusicTrack.Playlist.Id == existingPlaylistMusicTrack.Playlist.Id); playlistTracksInDatabase.Should().BeNull(); }); @@ -352,14 +352,14 @@ public async Task Cannot_delete_resource_for_href_element() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -384,14 +384,14 @@ public async Task Cannot_delete_resource_for_missing_ref_element() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref' element is required."); error.Detail.Should().BeNull(); @@ -420,14 +420,14 @@ public async Task Cannot_delete_resource_for_missing_type() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -457,14 +457,14 @@ public async Task Cannot_delete_resource_for_unknown_type() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -493,14 +493,14 @@ public async Task Cannot_delete_resource_for_missing_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -530,14 +530,14 @@ public async Task Cannot_delete_resource_for_unknown_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error 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 '99999999' does not exist."); @@ -548,7 +548,7 @@ public async Task Cannot_delete_resource_for_unknown_ID() public async Task Cannot_delete_resource_for_incompatible_ID() { // Arrange - var guid = Guid.NewGuid().ToString(); + string guid = Guid.NewGuid().ToString(); var requestBody = new { @@ -569,14 +569,14 @@ public async Task Cannot_delete_resource_for_incompatible_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int64'."); @@ -607,14 +607,14 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs index f2f9ccdc25..15ebcab128 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Resources; @@ -10,8 +11,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Links { - public sealed class AtomicAbsoluteLinksTests - : IClassFixture, OperationsDbContext>> + public sealed class AtomicAbsoluteLinksTests : IClassFixture, OperationsDbContext>> { private const string HostPrefix = "http://localhost"; @@ -34,8 +34,8 @@ public AtomicAbsoluteLinksTests(ExampleIntegrationTestContext { @@ -77,7 +77,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -86,7 +87,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string languageLink = HostPrefix + "/textLanguages/" + existingLanguage.StringId; - var singleData1 = responseDocument.Results[0].SingleData; + ResourceObject singleData1 = responseDocument.Results[0].SingleData; singleData1.Should().NotBeNull(); singleData1.Links.Should().NotBeNull(); singleData1.Links.Self.Should().Be(languageLink); @@ -97,7 +98,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string companyLink = HostPrefix + "/recordCompanies/" + existingCompany.StringId; - var singleData2 = responseDocument.Results[1].SingleData; + ResourceObject singleData2 = responseDocument.Results[1].SingleData; singleData2.Should().NotBeNull(); singleData2.Links.Should().NotBeNull(); singleData2.Links.Self.Should().Be(companyLink); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs index 1931b4725d..3fea795b67 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Resources; @@ -16,7 +17,8 @@ public sealed class AtomicRelativeLinksWithNamespaceTests { private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; - public AtomicRelativeLinksWithNamespaceTests(ExampleIntegrationTestContext, OperationsDbContext> testContext) + public AtomicRelativeLinksWithNamespaceTests( + ExampleIntegrationTestContext, OperationsDbContext> testContext) { _testContext = testContext; @@ -64,7 +66,8 @@ public async Task Create_resource_with_side_effects_returns_relative_links() const string route = "/api/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs index 197c8f51ed..af3c5ab285 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -10,8 +12,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.LocalIds { - public sealed class AtomicLocalIdTests - : IClassFixture, OperationsDbContext>> + public sealed class AtomicLocalIdTests : IClassFixture, OperationsDbContext>> { private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); @@ -27,8 +28,8 @@ public AtomicLocalIdTests(ExampleIntegrationTestContext(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -97,14 +99,12 @@ public async Task Can_create_resource_with_ToOne_relationship_using_local_ID() responseDocument.Results[1].SingleData.Lid.Should().BeNull(); responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); - var newCompanyId = short.Parse(responseDocument.Results[0].SingleData.Id); - var newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + short newCompanyId = short.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -119,8 +119,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_resource_with_OneToMany_relationship_using_local_ID() { // Arrange - var newPerformer = _fakers.Performer.Generate(); - var newTrackTitle = _fakers.MusicTrack.Generate().Title; + Performer newPerformer = _fakers.Performer.Generate(); + string newTrackTitle = _fakers.MusicTrack.Generate().Title; const string performerLocalId = "performer-1"; @@ -174,7 +174,8 @@ public async Task Can_create_resource_with_OneToMany_relationship_using_local_ID const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -192,14 +193,12 @@ public async Task Can_create_resource_with_OneToMany_relationship_using_local_ID responseDocument.Results[1].SingleData.Lid.Should().BeNull(); responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); - var newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); - var newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -214,8 +213,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_resource_with_ManyToMany_relationship_using_local_ID() { // Arrange - var newTrackTitle = _fakers.MusicTrack.Generate().Title; - var newPlaylistName = _fakers.Playlist.Generate().Name; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newPlaylistName = _fakers.Playlist.Generate().Name; const string trackLocalId = "track-1"; @@ -268,7 +267,8 @@ public async Task Can_create_resource_with_ManyToMany_relationship_using_local_I const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -285,15 +285,15 @@ public async Task Can_create_resource_with_ManyToMany_relationship_using_local_I responseDocument.Results[1].SingleData.Lid.Should().BeNull(); responseDocument.Results[1].SingleData.Attributes["name"].Should().Be(newPlaylistName); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - var newPlaylistId = long.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[1].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(newPlaylistId); @@ -354,14 +354,14 @@ public async Task Cannot_consume_local_ID_that_is_assigned_in_same_operation() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Local ID cannot be both defined and used within the same operation."); error.Detail.Should().Be("Local ID 'company-1' cannot be both defined and used within the same operation."); @@ -372,7 +372,7 @@ public async Task Cannot_consume_local_ID_that_is_assigned_in_same_operation() public async Task Cannot_reassign_local_ID() { // Arrange - var newPlaylistName = _fakers.Playlist.Generate().Name; + string newPlaylistName = _fakers.Playlist.Generate().Name; const string playlistLocalId = "playlist-1"; var requestBody = new @@ -420,14 +420,14 @@ public async Task Cannot_reassign_local_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Another local ID with the same name is already defined at this point."); error.Detail.Should().Be("Another local ID with name 'playlist-1' is already defined at this point."); @@ -438,8 +438,8 @@ public async Task Cannot_reassign_local_ID() public async Task Can_update_resource_using_local_ID() { // Arrange - var newTrackTitle = _fakers.MusicTrack.Generate().Title; - var newTrackGenre = _fakers.MusicTrack.Generate().Genre; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newTrackGenre = _fakers.MusicTrack.Generate().Genre; const string trackLocalId = "track-1"; @@ -479,7 +479,8 @@ public async Task Can_update_resource_using_local_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -494,11 +495,11 @@ public async Task Can_update_resource_using_local_ID() responseDocument.Results[1].Data.Should().BeNull(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); trackInDatabase.Genre.Should().Be(newTrackGenre); @@ -509,9 +510,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_relationships_using_local_ID() { // Arrange - var newTrackTitle = _fakers.MusicTrack.Generate().Title; - var newArtistName = _fakers.Performer.Generate().ArtistName; - var newCompanyName = _fakers.RecordCompany.Generate().Name; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newArtistName = _fakers.Performer.Generate().ArtistName; + string newCompanyName = _fakers.RecordCompany.Generate().Name; const string trackLocalId = "track-1"; const string performerLocalId = "performer-1"; @@ -597,7 +598,8 @@ public async Task Can_update_resource_with_relationships_using_local_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -621,16 +623,16 @@ public async Task Can_update_resource_with_relationships_using_local_ID() responseDocument.Results[3].Data.Should().BeNull(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - var newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); - var newCompanyId = short.Parse(responseDocument.Results[2].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); + short newCompanyId = short.Parse(responseDocument.Results[2].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var trackInDatabase = await dbContext.MusicTracks + MusicTrack trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.OwnedBy) .Include(musicTrack => musicTrack.Performers) .FirstWithIdAsync(newTrackId); @@ -653,8 +655,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_ToOne_relationship_using_local_ID() { // Arrange - var newTrackTitle = _fakers.MusicTrack.Generate().Title; - var newCompanyName = _fakers.RecordCompany.Generate().Name; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newCompanyName = _fakers.RecordCompany.Generate().Name; const string trackLocalId = "track-1"; const string companyLocalId = "company-1"; @@ -710,7 +712,8 @@ public async Task Can_create_ToOne_relationship_using_local_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -729,14 +732,12 @@ public async Task Can_create_ToOne_relationship_using_local_ID() responseDocument.Results[2].Data.Should().BeNull(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - var newCompanyId = short.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + short newCompanyId = short.Parse(responseDocument.Results[1].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -750,8 +751,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_OneToMany_relationship_using_local_ID() { // Arrange - var newTrackTitle = _fakers.MusicTrack.Generate().Title; - var newArtistName = _fakers.Performer.Generate().ArtistName; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newArtistName = _fakers.Performer.Generate().ArtistName; const string trackLocalId = "track-1"; const string performerLocalId = "performer-1"; @@ -810,7 +811,8 @@ public async Task Can_create_OneToMany_relationship_using_local_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -829,14 +831,12 @@ public async Task Can_create_OneToMany_relationship_using_local_ID() responseDocument.Results[2].Data.Should().BeNull(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - var newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -850,8 +850,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_ManyToMany_relationship_using_local_ID() { // Arrange - var newPlaylistName = _fakers.Playlist.Generate().Name; - var newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newPlaylistName = _fakers.Playlist.Generate().Name; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; const string playlistLocalId = "playlist-1"; const string trackLocalId = "track-1"; @@ -910,7 +910,8 @@ public async Task Can_create_ManyToMany_relationship_using_local_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -929,15 +930,15 @@ public async Task Can_create_ManyToMany_relationship_using_local_ID() responseDocument.Results[2].Data.Should().BeNull(); - var newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); - var newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(newPlaylistId); @@ -957,10 +958,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_OneToMany_relationship_using_local_ID() { // Arrange - var existingPerformer = _fakers.Performer.Generate(); + Performer existingPerformer = _fakers.Performer.Generate(); - var newTrackTitle = _fakers.MusicTrack.Generate().Title; - var newArtistName = _fakers.Performer.Generate().ArtistName; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newArtistName = _fakers.Performer.Generate().ArtistName; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1039,7 +1040,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -1058,14 +1060,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[2].Data.Should().BeNull(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - var newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -1079,10 +1079,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_ManyToMany_relationship_using_local_ID() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); - var newPlaylistName = _fakers.Playlist.Generate().Name; - var newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newPlaylistName = _fakers.Playlist.Generate().Name; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1161,7 +1161,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -1180,15 +1181,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[2].Data.Should().BeNull(); - var newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); - var newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(newPlaylistId); @@ -1208,10 +1209,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_add_to_OneToMany_relationship_using_local_ID() { // Arrange - var existingPerformer = _fakers.Performer.Generate(); + Performer existingPerformer = _fakers.Performer.Generate(); - var newTrackTitle = _fakers.MusicTrack.Generate().Title; - var newArtistName = _fakers.Performer.Generate().ArtistName; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newArtistName = _fakers.Performer.Generate().ArtistName; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1290,7 +1291,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -1309,14 +1311,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[2].Data.Should().BeNull(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - var newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -1334,10 +1334,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_add_to_ManyToMany_relationship_using_local_ID() { // Arrange - var existingTracks = _fakers.MusicTrack.Generate(2); + List existingTracks = _fakers.MusicTrack.Generate(2); - var newPlaylistName = _fakers.Playlist.Generate().Name; - var newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newPlaylistName = _fakers.Playlist.Generate().Name; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; const string playlistLocalId = "playlist-1"; const string trackLocalId = "track-1"; @@ -1434,7 +1434,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -1455,15 +1456,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[3].Data.Should().BeNull(); - var newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); - var newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(newPlaylistId); @@ -1484,11 +1485,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_remove_from_OneToMany_relationship_using_local_ID() { // Arrange - var existingPerformer = _fakers.Performer.Generate(); + Performer existingPerformer = _fakers.Performer.Generate(); - var newTrackTitle = _fakers.MusicTrack.Generate().Title; - var newArtistName1 = _fakers.Performer.Generate().ArtistName; - var newArtistName2 = _fakers.Performer.Generate().ArtistName; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newArtistName1 = _fakers.Performer.Generate().ArtistName; + string newArtistName2 = _fakers.Performer.Generate().ArtistName; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1596,7 +1597,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -1620,13 +1622,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Results[3].Data.Should().BeNull(); - var newTrackId = Guid.Parse(responseDocument.Results[2].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[2].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(newTrackId); trackInDatabase.Title.Should().Be(newTrackTitle); @@ -1640,7 +1640,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_remove_from_ManyToMany_relationship_using_local_ID() { // Arrange - var existingPlaylist = _fakers.Playlist.Generate(); + Playlist existingPlaylist = _fakers.Playlist.Generate(); + existingPlaylist.PlaylistMusicTracks = new[] { new PlaylistMusicTrack @@ -1653,7 +1654,7 @@ public async Task Can_remove_from_ManyToMany_relationship_using_local_ID() } }; - var newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; const string trackLocalId = "track-1"; @@ -1740,7 +1741,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -1763,7 +1765,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(existingPlaylist.Id); @@ -1780,7 +1782,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_delete_resource_using_local_ID() { // Arrange - var newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; const string trackLocalId = "track-1"; @@ -1816,7 +1818,8 @@ public async Task Can_delete_resource_using_local_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -1830,11 +1833,11 @@ public async Task Can_delete_resource_using_local_ID() responseDocument.Results[1].Data.Should().BeNull(); - var newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks.FirstWithIdOrDefaultAsync(newTrackId); + MusicTrack trackInDatabase = await dbContext.MusicTracks.FirstWithIdOrDefaultAsync(newTrackId); trackInDatabase.Should().BeNull(); }); @@ -1872,14 +1875,14 @@ public async Task Cannot_consume_unassigned_local_ID_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); @@ -1921,14 +1924,14 @@ public async Task Cannot_consume_unassigned_local_ID_in_data_element() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); @@ -1939,7 +1942,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_data_element() public async Task Cannot_consume_unassigned_local_ID_in_data_array() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1984,14 +1987,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); @@ -2040,14 +2043,14 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_elemen const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); @@ -2099,14 +2102,14 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_array( const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); @@ -2158,14 +2161,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_same_operation() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'track-1' belongs to resource type 'musicTracks' instead of 'recordCompanies'."); @@ -2215,14 +2218,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'musicTracks'."); @@ -2275,14 +2278,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_data_element() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'playlists'."); @@ -2293,7 +2296,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_data_element() public async Task Cannot_consume_local_ID_of_different_type_in_data_array() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); const string companyLocalId = "company-1"; @@ -2349,14 +2352,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'performers'."); @@ -2367,7 +2370,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data_element() { // Arrange - var newPlaylistName = _fakers.Playlist.Generate().Name; + string newPlaylistName = _fakers.Playlist.Generate().Name; const string playlistLocalId = "playlist-1"; @@ -2422,14 +2425,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'playlist-1' belongs to resource type 'playlists' instead of 'recordCompanies'."); @@ -2492,14 +2495,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'musicTracks'."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Lyric.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Lyric.cs index 31e9e5106a..c3c155fde7 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Lyric.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Lyric.cs @@ -19,7 +19,7 @@ public sealed class Lyric : Identifiable [Attr(Capabilities = AttrCapabilities.None)] public DateTimeOffset CreatedAt { get; set; } - + [HasOne] public MusicTrack Track { get; set; } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs index ecbfb911e2..a0df52450c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Extensions; @@ -12,8 +13,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Meta { - public sealed class AtomicResourceMetaTests - : IClassFixture, OperationsDbContext>> + public sealed class AtomicResourceMetaTests : IClassFixture, OperationsDbContext>> { private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); @@ -35,8 +35,8 @@ public AtomicResourceMetaTests(ExampleIntegrationTestContext(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -92,7 +93,7 @@ public async Task Returns_resource_meta_in_create_resource_with_side_effects() public async Task Returns_top_level_meta_in_update_resource_with_side_effects() { // Arrange - var existingLanguage = _fakers.TextLanguage.Generate(); + TextLanguage existingLanguage = _fakers.TextLanguage.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -122,7 +123,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs index 65f95ff844..09a59d5507 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMeta.cs @@ -21,4 +21,4 @@ public IReadOnlyDictionary GetMeta() }; } } -} \ No newline at end of file +} diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs index bb6d147bcc..ae24a50a4a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization; @@ -13,8 +14,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Meta { - public sealed class AtomicResponseMetaTests - : IClassFixture, OperationsDbContext>> + public sealed class AtomicResponseMetaTests : IClassFixture, OperationsDbContext>> { private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); @@ -56,7 +56,8 @@ public async Task Returns_top_level_meta_in_create_resource_with_side_effects() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -65,7 +66,7 @@ public async Task Returns_top_level_meta_in_create_resource_with_side_effects() responseDocument.Meta["license"].Should().Be("MIT"); responseDocument.Meta["projectUrl"].Should().Be("https://github.com/json-api-dotnet/JsonApiDotNetCore/"); - var versionArray = ((IEnumerable) responseDocument.Meta["versions"]).Select(token => token.ToString()).ToArray(); + string[] versionArray = ((IEnumerable)responseDocument.Meta["versions"]).Select(token => token.ToString()).ToArray(); versionArray.Should().HaveCount(4); versionArray.Should().Contain("v4.0.0"); @@ -78,7 +79,7 @@ public async Task Returns_top_level_meta_in_create_resource_with_side_effects() public async Task Returns_top_level_meta_in_update_resource_with_side_effects() { // Arrange - var existingLanguage = _fakers.TextLanguage.Generate(); + TextLanguage existingLanguage = _fakers.TextLanguage.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -108,7 +109,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -117,7 +119,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Meta["license"].Should().Be("MIT"); responseDocument.Meta["projectUrl"].Should().Be("https://github.com/json-api-dotnet/JsonApiDotNetCore/"); - var versionArray = ((IEnumerable) responseDocument.Meta["versions"]).Select(token => token.ToString()).ToArray(); + string[] versionArray = ((IEnumerable)responseDocument.Meta["versions"]).Select(token => token.ToString()).ToArray(); versionArray.Should().HaveCount(4); versionArray.Should().Contain("v4.0.0"); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs index 27e13c0b7e..8c9546742e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/MusicTrackMetaDefinition.cs @@ -9,7 +9,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Meta [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class MusicTrackMetaDefinition : JsonApiResourceDefinition { - public MusicTrackMetaDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public MusicTrackMetaDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs index ace9c8828f..f59c5d3991 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Meta/TextLanguageMetaDefinition.cs @@ -11,7 +11,8 @@ public sealed class TextLanguageMetaDefinition : JsonApiResourceDefinition, OperationsDbContext>> + public sealed class AtomicRequestBodyTests : IClassFixture, OperationsDbContext>> { private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; @@ -28,14 +29,14 @@ public async Task Cannot_process_for_missing_request_body() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, null); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, null); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -43,10 +44,10 @@ public async Task Cannot_process_for_missing_request_body() await _testContext.RunOnDatabaseAsync(async dbContext => { - var performersInDatabase = await dbContext.Performers.ToListAsync(); + List performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().BeEmpty(); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().BeEmpty(); }); } @@ -60,14 +61,14 @@ public async Task Cannot_process_for_broken_JSON_request_body() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Unexpected end of content while loading JObject."); @@ -86,14 +87,14 @@ public async Task Cannot_process_empty_operations_array() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: No operations found."); error.Detail.Should().BeNull(); @@ -101,10 +102,10 @@ public async Task Cannot_process_empty_operations_array() await _testContext.RunOnDatabaseAsync(async dbContext => { - var performersInDatabase = await dbContext.Performers.ToListAsync(); + List performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().BeEmpty(); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().BeEmpty(); }); } @@ -134,14 +135,14 @@ public async Task Cannot_process_for_unknown_operation_code() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Error converting value \"merge\" to type"); @@ -149,10 +150,10 @@ public async Task Cannot_process_for_unknown_operation_code() await _testContext.RunOnDatabaseAsync(async dbContext => { - var performersInDatabase = await dbContext.Performers.ToListAsync(); + List performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().BeEmpty(); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().BeEmpty(); }); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs index 2d497077c4..84c9f239b7 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -27,7 +28,7 @@ public MaximumOperationsPerRequestTests(ExampleIntegrationTestContext(); + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); options.MaximumOperationsPerRequest = 2; var requestBody = new @@ -61,14 +62,14 @@ public async Task Cannot_process_more_operations_than_maximum() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request exceeds the maximum number of operations."); error.Detail.Should().Be("The number of operations in this request (3) is higher than 2."); @@ -79,7 +80,7 @@ public async Task Cannot_process_more_operations_than_maximum() public async Task Can_process_operations_same_as_maximum() { // Arrange - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); options.MaximumOperationsPerRequest = 2; var requestBody = new @@ -114,7 +115,7 @@ public async Task Can_process_operations_same_as_maximum() const string route = "/operations"; // Act - var (httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -124,12 +125,13 @@ public async Task Can_process_operations_same_as_maximum() public async Task Can_process_high_number_of_operations_when_unconstrained() { // Arrange - var options = (JsonApiOptions) _testContext.Factory.Services.GetRequiredService(); + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); options.MaximumOperationsPerRequest = null; const int elementCount = 100; var operationElements = new List(elementCount); + for (int index = 0; index < elementCount; index++) { operationElements.Add(new @@ -153,7 +155,7 @@ public async Task Can_process_high_number_of_operations_when_unconstrained() const string route = "/operations"; // Act - var (httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs index 5f6e19fc92..24d3f851dc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -48,20 +49,20 @@ public async Task Cannot_create_resource_with_multiple_violations() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(2); - var error1 = responseDocument.Errors[0]; + Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Title field is required."); error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/title"); - var error2 = responseDocument.Errors[1]; + Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); @@ -72,8 +73,8 @@ public async Task Cannot_create_resource_with_multiple_violations() public async Task Can_create_resource_with_annotated_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); - var newPlaylistName = _fakers.Playlist.Generate().Name; + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + string newPlaylistName = _fakers.Playlist.Generate().Name; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -117,21 +118,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - var newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(newPlaylistId); @@ -148,7 +150,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_update_resource_with_multiple_violations() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -169,7 +171,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingTrack.StringId, attributes = new { - title = (string) null, + title = (string)null, lengthInSeconds = -1 } } @@ -180,20 +182,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(2); - var error1 = responseDocument.Errors[0]; + Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Title field is required."); error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/title"); - var error2 = responseDocument.Errors[1]; + Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); @@ -204,8 +206,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_omitted_required_attribute() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); - var newTrackGenre = _fakers.MusicTrack.Generate().Genre; + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + string newTrackGenre = _fakers.MusicTrack.Generate().Genre; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -236,7 +238,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -245,7 +247,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(existingTrack.Id); trackInDatabase.Title.Should().Be(existingTrack.Title); trackInDatabase.Genre.Should().Be(newTrackGenre); @@ -256,8 +258,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_annotated_relationship() { // Arrange - var existingPlaylist = _fakers.Playlist.Generate(); - var existingTrack = _fakers.MusicTrack.Generate(); + Playlist existingPlaylist = _fakers.Playlist.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -298,7 +300,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -310,7 +312,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(existingPlaylist.Id); @@ -327,8 +329,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_ToOne_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); - var existingCompany = _fakers.RecordCompany.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -361,7 +363,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -370,9 +372,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Should().NotBeNull(); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); @@ -383,8 +383,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_ToMany_relationship() { // Arrange - var existingPlaylist = _fakers.Playlist.Generate(); - var existingTrack = _fakers.MusicTrack.Generate(); + Playlist existingPlaylist = _fakers.Playlist.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -420,7 +420,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -432,7 +432,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(existingPlaylist.Id); @@ -462,7 +462,7 @@ public async Task Validates_all_operations_before_execution_starts() id = 99999999, attributes = new { - name = (string) null + name = (string)null } } }, @@ -484,26 +484,26 @@ public async Task Validates_all_operations_before_execution_starts() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(3); - var error1 = responseDocument.Errors[0]; + Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Name field is required."); error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/name"); - var error2 = responseDocument.Errors[1]; + Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The Title field is required."); error2.Source.Pointer.Should().Be("/atomic:operations[1]/data/attributes/title"); - var error3 = responseDocument.Errors[2]; + Error error3 = responseDocument.Errors[2]; error3.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error3.Title.Should().Be("Input validation failed."); error3.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTrack.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTrack.cs index 991d999f1d..94733028d9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTrack.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTrack.cs @@ -28,8 +28,8 @@ public sealed class MusicTrack : Identifiable public DateTimeOffset ReleasedAt { get; set; } [HasOne] - public Lyric Lyric { get; set;} - + public Lyric Lyric { get; set; } + [HasOne] public RecordCompany OwnedBy { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTracksController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTracksController.cs index 9cdbe029ac..ed2ffd6044 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTracksController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/MusicTracksController.cs @@ -8,8 +8,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations { public sealed class MusicTracksController : JsonApiController { - public MusicTracksController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public MusicTracksController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs index 839c6cd7b8..c7ab6d943c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/OperationsDbContext.cs @@ -24,7 +24,11 @@ public OperationsDbContext(DbContextOptions options) protected override void OnModelCreating(ModelBuilder builder) { builder.Entity() - .HasKey(playlistMusicTrack => new {playlistMusicTrack.PlaylistId, playlistMusicTrack.MusicTrackId}); + .HasKey(playlistMusicTrack => new + { + playlistMusicTrack.PlaylistId, + playlistMusicTrack.MusicTrackId + }); builder.Entity() .HasOne(musicTrack => musicTrack.Lyric) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs index 4b71f7215c..2e41befd2e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Extensions; @@ -14,8 +16,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.QueryStrings { - public sealed class AtomicQueryStringTests - : IClassFixture, OperationsDbContext>> + public sealed class AtomicQueryStringTests : IClassFixture, OperationsDbContext>> { private static readonly DateTime FrozenTime = 30.July(2018).At(13, 46, 12); @@ -30,11 +31,15 @@ public AtomicQueryStringTests(ExampleIntegrationTestContext(new FrozenSystemClock {UtcNow = FrozenTime}); + services.AddSingleton(new FrozenSystemClock + { + UtcNow = FrozenTime + }); + services.AddScoped, MusicTrackReleaseDefinition>(); }); - var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); options.AllowQueryStringOverrideForSerializerDefaultValueHandling = true; options.AllowQueryStringOverrideForSerializerNullValueHandling = true; } @@ -64,14 +69,14 @@ public async Task Cannot_include_on_operations_endpoint() const string route = "/operations?include=recordCompanies"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'include' cannot be used at this endpoint."); @@ -103,14 +108,14 @@ public async Task Cannot_filter_on_operations_endpoint() const string route = "/operations?filter=equals(id,'1')"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'filter' cannot be used at this endpoint."); @@ -142,14 +147,14 @@ public async Task Cannot_sort_on_operations_endpoint() const string route = "/operations?sort=-id"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'sort' cannot be used at this endpoint."); @@ -181,14 +186,14 @@ public async Task Cannot_use_pagination_number_on_operations_endpoint() const string route = "/operations?page[number]=1"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'page[number]' cannot be used at this endpoint."); @@ -220,14 +225,14 @@ public async Task Cannot_use_pagination_size_on_operations_endpoint() const string route = "/operations?page[size]=1"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'page[size]' cannot be used at this endpoint."); @@ -259,14 +264,14 @@ public async Task Cannot_use_sparse_fieldset_on_operations_endpoint() const string route = "/operations?fields[recordCompanies]=id"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'fields[recordCompanies]' cannot be used at this endpoint."); @@ -277,7 +282,7 @@ public async Task Cannot_use_sparse_fieldset_on_operations_endpoint() public async Task Can_use_Queryable_handler_on_resource_endpoint() { // Arrange - var musicTracks = _fakers.MusicTrack.Generate(3); + List musicTracks = _fakers.MusicTrack.Generate(3); musicTracks[0].ReleasedAt = FrozenTime.AddMonths(5); musicTracks[1].ReleasedAt = FrozenTime.AddMonths(-5); musicTracks[2].ReleasedAt = FrozenTime.AddMonths(-1); @@ -292,7 +297,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/musicTracks?isRecentlyReleased=true"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -305,7 +310,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_use_Queryable_handler_on_operations_endpoint() { // Arrange - var newTrackTitle = _fakers.MusicTrack.Generate().Title; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; var requestBody = new { @@ -329,18 +334,20 @@ public async Task Cannot_use_Queryable_handler_on_operations_endpoint() const string route = "/operations?isRecentlyReleased=true"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Unknown query string parameter."); + error.Detail.Should().Be("Query string parameter 'isRecentlyReleased' is unknown. " + "Set 'AllowUnknownQueryStringParameters' to 'true' in options to ignore unknown parameters."); + error.Source.Parameter.Should().Be("isRecentlyReleased"); } @@ -348,8 +355,8 @@ public async Task Cannot_use_Queryable_handler_on_operations_endpoint() public async Task Can_use_defaults_on_operations_endpoint() { // Arrange - var newTrackTitle = _fakers.MusicTrack.Generate().Title; - var newTrackLength = _fakers.MusicTrack.Generate().LengthInSeconds; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; + decimal? newTrackLength = _fakers.MusicTrack.Generate().LengthInSeconds; var requestBody = new { @@ -374,7 +381,8 @@ public async Task Can_use_defaults_on_operations_endpoint() const string route = "/operations?defaults=false"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -391,8 +399,8 @@ public async Task Can_use_defaults_on_operations_endpoint() public async Task Can_use_nulls_on_operations_endpoint() { // Arrange - var newTrackTitle = _fakers.MusicTrack.Generate().Title; - var newTrackLength = _fakers.MusicTrack.Generate().LengthInSeconds; + string newTrackTitle = _fakers.MusicTrack.Generate().Title; + decimal? newTrackLength = _fakers.MusicTrack.Generate().LengthInSeconds; var requestBody = new { @@ -417,7 +425,8 @@ public async Task Can_use_nulls_on_operations_endpoint() const string route = "/operations?nulls=false"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs index d3869ba33d..857f68ef25 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/QueryStrings/MusicTrackReleaseDefinition.cs @@ -32,13 +32,11 @@ public override QueryStringParameterHandlers OnRegisterQueryableHand private IQueryable FilterOnRecentlyReleased(IQueryable source, StringValues parameterValue) { - var tracks = source; + IQueryable tracks = source; if (bool.Parse(parameterValue)) { - tracks = tracks.Where(musicTrack => - musicTrack.ReleasedAt < _systemClock.UtcNow && - musicTrack.ReleasedAt > _systemClock.UtcNow.AddMonths(-3)); + tracks = tracks.Where(musicTrack => musicTrack.ReleasedAt < _systemClock.UtcNow && musicTrack.ReleasedAt > _systemClock.UtcNow.AddMonths(-3)); } return tracks; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/AtomicSparseFieldSetResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/AtomicSparseFieldSetResourceDefinitionTests.cs index 01ce190904..de4c1ecaaf 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/AtomicSparseFieldSetResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/ResourceDefinitions/AtomicSparseFieldSetResourceDefinitionTests.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Resources; @@ -38,7 +40,7 @@ public async Task Hides_text_in_create_resource_with_side_effects() provider.CanViewText = false; provider.HitCount = 0; - var newLyrics = _fakers.Lyric.Generate(2); + List newLyrics = _fakers.Lyric.Generate(2); var requestBody = new { @@ -76,7 +78,8 @@ public async Task Hides_text_in_create_resource_with_side_effects() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -100,7 +103,7 @@ public async Task Hides_text_in_update_resource_with_side_effects() provider.CanViewText = false; provider.HitCount = 0; - var existingLyrics = _fakers.Lyric.Generate(2); + List existingLyrics = _fakers.Lyric.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -142,7 +145,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs index ad624f6189..e3ff40fb65 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs @@ -1,4 +1,7 @@ +using System; +using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -9,8 +12,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Transactions { - public sealed class AtomicRollbackTests - : IClassFixture, OperationsDbContext>> + public sealed class AtomicRollbackTests : IClassFixture, OperationsDbContext>> { private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); @@ -26,9 +28,9 @@ public AtomicRollbackTests(ExampleIntegrationTestContext { @@ -84,14 +86,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be("Related resource of type 'performers' with ID '99999999' in relationship 'performers' does not exist."); @@ -99,10 +101,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var performersInDatabase = await dbContext.Performers.ToListAsync(); + List performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().BeEmpty(); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().BeEmpty(); }); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs index 4b18f53178..899ee2510d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -61,14 +62,14 @@ public async Task Cannot_use_non_transactional_repository() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error 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."); @@ -100,14 +101,14 @@ public async Task Cannot_use_transactional_repository_without_active_transaction const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error 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."); @@ -139,14 +140,14 @@ public async Task Cannot_use_distributed_transaction() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error 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."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs index 4417ee9d3b..ef08acc7fe 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/LyricRepository.cs @@ -16,9 +16,8 @@ public sealed class LyricRepository : EntityFrameworkCoreRepository public override Guid? TransactionId => _extraDbContext.Database.CurrentTransaction.TransactionId; - public LyricRepository(ExtraDbContext extraDbContext, ITargetedFields targetedFields, - IDbContextResolver contextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, - IEnumerable constraintProviders, ILoggerFactory loggerFactory) + public LyricRepository(ExtraDbContext extraDbContext, ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) { _extraDbContext = extraDbContext; diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs index 554f6b6d3e..612333855e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/MusicTrackRepository.cs @@ -14,9 +14,8 @@ public sealed class MusicTrackRepository : EntityFrameworkCoreRepository null; - public MusicTrackRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, - IResourceGraph resourceGraph, IResourceFactory resourceFactory, - IEnumerable constraintProviders, ILoggerFactory loggerFactory) + public MusicTrackRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs index f22a3d8846..555c9ba955 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Transactions/PerformerRepository.cs @@ -58,8 +58,7 @@ public Task AddToToManyRelationshipAsync(int primaryId, ISet seco throw new NotImplementedException(); } - public Task RemoveFromToManyRelationshipAsync(Performer primaryResource, ISet secondaryResourceIds, - CancellationToken cancellationToken) + public Task RemoveFromToManyRelationshipAsync(Performer primaryResource, ISet secondaryResourceIds, CancellationToken cancellationToken) { throw new NotImplementedException(); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index 3a0f791045..87c407cd41 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore; @@ -29,8 +30,8 @@ public AtomicAddToToManyRelationshipTests(ExampleIntegrationTestContext { @@ -63,14 +64,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted in 'add' operations."); error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); @@ -80,10 +81,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_add_to_HasMany_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Performers = _fakers.Performer.Generate(1); - var existingPerformers = _fakers.Performer.Generate(2); + List existingPerformers = _fakers.Performer.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -138,7 +139,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -147,9 +148,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(3); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingTrack.Performers[0].Id); @@ -162,7 +161,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_add_to_HasManyThrough_relationship() { // Arrange - var existingPlaylist = _fakers.Playlist.Generate(); + Playlist existingPlaylist = _fakers.Playlist.Generate(); + existingPlaylist.PlaylistMusicTracks = new List { new PlaylistMusicTrack @@ -171,7 +171,7 @@ public async Task Can_add_to_HasManyThrough_relationship() } }; - var existingTracks = _fakers.MusicTrack.Generate(2); + List existingTracks = _fakers.MusicTrack.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -226,7 +226,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -238,7 +238,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(existingPlaylist.Id); @@ -274,14 +274,14 @@ public async Task Cannot_add_for_href_element() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -311,14 +311,14 @@ public async Task Cannot_add_for_missing_type_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -349,14 +349,14 @@ public async Task Cannot_add_for_unknown_type_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -386,14 +386,14 @@ public async Task Cannot_add_for_missing_ID_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -404,7 +404,7 @@ public async Task Cannot_add_for_missing_ID_in_ref() public async Task Cannot_add_for_unknown_ID_in_ref() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -440,14 +440,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error 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 'recordCompanies' with ID '9999' does not exist."); @@ -479,14 +479,14 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -516,14 +516,14 @@ public async Task Cannot_add_for_missing_relationship_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.relationship' element is required."); error.Detail.Should().BeNull(); @@ -554,14 +554,14 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); @@ -572,7 +572,7 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() public async Task Cannot_add_for_null_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -593,7 +593,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingTrack.StringId, relationship = "performers" }, - data = (object) null + data = (object)null } } }; @@ -601,14 +601,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -646,14 +646,14 @@ public async Task Cannot_add_for_missing_type_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); error.Detail.Should().BeNull(); @@ -692,14 +692,14 @@ public async Task Cannot_add_for_unknown_type_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -737,14 +737,14 @@ public async Task Cannot_add_for_missing_ID_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -784,14 +784,14 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -802,8 +802,8 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() public async Task Cannot_add_for_unknown_IDs_in_data() { // Arrange - var existingCompany = _fakers.RecordCompany.Generate(); - var trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); + Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -844,20 +844,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - var error1 = responseDocument.Errors[0]; + Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - var error2 = responseDocument.Errors[1]; + Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -868,7 +868,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_add_for_relationship_mismatch_between_ref_and_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -904,14 +904,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); @@ -922,7 +922,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_add_with_empty_data_array() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Performers = _fakers.Performer.Generate(1); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -952,7 +952,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -961,9 +961,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(1); trackInDatabase.Performers[0].Id.Should().Be(existingTrack.Performers[0].Id); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index 50de26773d..4ba1b44634 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore; @@ -29,7 +30,7 @@ public AtomicRemoveFromToManyRelationshipTests(ExampleIntegrationTestContext @@ -63,14 +64,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted in 'remove' operations."); error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); @@ -80,7 +81,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_remove_from_HasMany_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Performers = _fakers.Performer.Generate(3); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -136,7 +137,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -145,14 +146,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(1); trackInDatabase.Performers[0].Id.Should().Be(existingTrack.Performers[1].Id); - var performersInDatabase = await dbContext.Performers.ToListAsync(); + List performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().HaveCount(3); }); } @@ -161,7 +160,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_remove_from_HasManyThrough_relationship() { // Arrange - var existingPlaylist = _fakers.Playlist.Generate(); + Playlist existingPlaylist = _fakers.Playlist.Generate(); + existingPlaylist.PlaylistMusicTracks = new List { new PlaylistMusicTrack @@ -231,7 +231,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -243,7 +243,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(existingPlaylist.Id); @@ -254,7 +254,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => playlistInDatabase.PlaylistMusicTracks.Should().HaveCount(1); playlistInDatabase.PlaylistMusicTracks[0].MusicTrack.Id.Should().Be(existingPlaylist.PlaylistMusicTracks[1].MusicTrack.Id); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().HaveCount(3); }); } @@ -278,14 +278,14 @@ public async Task Cannot_remove_for_href_element() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -315,14 +315,14 @@ public async Task Cannot_remove_for_missing_type_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -353,14 +353,14 @@ public async Task Cannot_remove_for_unknown_type_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -390,14 +390,14 @@ public async Task Cannot_remove_for_missing_ID_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -408,7 +408,7 @@ public async Task Cannot_remove_for_missing_ID_in_ref() public async Task Cannot_remove_for_unknown_ID_in_ref() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -444,14 +444,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error 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 'recordCompanies' with ID '9999' does not exist."); @@ -483,14 +483,14 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -521,14 +521,14 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); @@ -539,7 +539,7 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() public async Task Cannot_remove_for_null_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -560,7 +560,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingTrack.StringId, relationship = "performers" }, - data = (object) null + data = (object)null } } }; @@ -568,14 +568,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -613,14 +613,14 @@ public async Task Cannot_remove_for_missing_type_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); error.Detail.Should().BeNull(); @@ -659,14 +659,14 @@ public async Task Cannot_remove_for_unknown_type_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -704,14 +704,14 @@ public async Task Cannot_remove_for_missing_ID_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -751,14 +751,14 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -769,8 +769,8 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() public async Task Cannot_remove_for_unknown_IDs_in_data() { // Arrange - var existingCompany = _fakers.RecordCompany.Generate(); - var trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); + Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -811,20 +811,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - var error1 = responseDocument.Errors[0]; + Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - var error2 = responseDocument.Errors[1]; + Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -835,7 +835,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_remove_for_relationship_mismatch_between_ref_and_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -871,14 +871,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); @@ -889,7 +889,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_remove_with_empty_data_array() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Performers = _fakers.Performer.Generate(1); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -920,7 +920,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -929,9 +929,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(1); trackInDatabase.Performers[0].Id.Should().Be(existingTrack.Performers[0].Id); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index 108a070664..9eca84249c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore; @@ -29,7 +30,7 @@ public AtomicReplaceToManyRelationshipTests(ExampleIntegrationTestContext @@ -60,7 +61,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -69,13 +70,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().BeEmpty(); - var performersInDatabase = await dbContext.Performers.ToListAsync(); + List performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().HaveCount(2); }); } @@ -84,7 +83,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_clear_HasManyThrough_relationship() { // Arrange - var existingPlaylist = _fakers.Playlist.Generate(); + Playlist existingPlaylist = _fakers.Playlist.Generate(); + existingPlaylist.PlaylistMusicTracks = new List { new PlaylistMusicTrack @@ -125,7 +125,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -137,7 +137,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(existingPlaylist.Id); @@ -147,7 +147,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => playlistInDatabase.PlaylistMusicTracks.Should().BeEmpty(); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().HaveCount(2); }); } @@ -156,10 +156,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_HasMany_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Performers = _fakers.Performer.Generate(1); - var existingPerformers = _fakers.Performer.Generate(2); + List existingPerformers = _fakers.Performer.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -202,7 +202,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -211,15 +211,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(2); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingPerformers[0].Id); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingPerformers[1].Id); - var performersInDatabase = await dbContext.Performers.ToListAsync(); + List performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().HaveCount(3); }); } @@ -228,7 +226,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_HasManyThrough_relationship() { // Arrange - var existingPlaylist = _fakers.Playlist.Generate(); + Playlist existingPlaylist = _fakers.Playlist.Generate(); + existingPlaylist.PlaylistMusicTracks = new List { new PlaylistMusicTrack @@ -237,7 +236,7 @@ public async Task Can_replace_HasManyThrough_relationship() } }; - var existingTracks = _fakers.MusicTrack.Generate(2); + List existingTracks = _fakers.MusicTrack.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -280,7 +279,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -292,7 +291,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(existingPlaylist.Id); @@ -304,7 +303,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => playlistInDatabase.PlaylistMusicTracks.Should().ContainSingle(playlistMusicTrack => playlistMusicTrack.MusicTrack.Id == existingTracks[0].Id); playlistInDatabase.PlaylistMusicTracks.Should().ContainSingle(playlistMusicTrack => playlistMusicTrack.MusicTrack.Id == existingTracks[1].Id); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().HaveCount(3); }); } @@ -328,14 +327,14 @@ public async Task Cannot_replace_for_href_element() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -365,14 +364,14 @@ public async Task Cannot_replace_for_missing_type_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -403,14 +402,14 @@ public async Task Cannot_replace_for_unknown_type_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -440,14 +439,14 @@ public async Task Cannot_replace_for_missing_ID_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -458,7 +457,7 @@ public async Task Cannot_replace_for_missing_ID_in_ref() public async Task Cannot_replace_for_unknown_ID_in_ref() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -494,14 +493,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error 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 'recordCompanies' with ID '9999' does not exist."); @@ -512,9 +511,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_replace_for_incompatible_ID_in_ref() { // Arrange - var guid = Guid.NewGuid().ToString(); + string guid = Guid.NewGuid().ToString(); - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -550,14 +549,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int16'."); @@ -589,14 +588,14 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -627,14 +626,14 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); @@ -645,7 +644,7 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() public async Task Cannot_replace_for_null_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -666,7 +665,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingTrack.StringId, relationship = "performers" }, - data = (object) null + data = (object)null } } }; @@ -674,14 +673,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -719,14 +718,14 @@ public async Task Cannot_replace_for_missing_type_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); error.Detail.Should().BeNull(); @@ -765,14 +764,14 @@ public async Task Cannot_replace_for_unknown_type_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -810,14 +809,14 @@ public async Task Cannot_replace_for_missing_ID_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -857,14 +856,14 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -875,8 +874,8 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() public async Task Cannot_replace_for_unknown_IDs_in_data() { // Arrange - var existingCompany = _fakers.RecordCompany.Generate(); - var trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); + Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -917,20 +916,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - var error1 = responseDocument.Errors[0]; + Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - var error2 = responseDocument.Errors[1]; + Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -941,7 +940,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_replace_for_incompatible_ID_in_data() { // Arrange - var existingCompany = _fakers.RecordCompany.Generate(); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -977,14 +976,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); @@ -995,7 +994,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_replace_for_relationship_mismatch_between_ref_and_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1031,14 +1030,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 216878d00e..54365fd4da 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -27,7 +29,7 @@ public AtomicUpdateToOneRelationshipTests(ExampleIntegrationTestContext @@ -50,7 +52,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingLyric.StringId, relationship = "track" }, - data = (object) null + data = (object)null } } }; @@ -58,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -67,13 +69,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var lyricInDatabase = await dbContext.Lyrics - .Include(lyric => lyric.Track) - .FirstWithIdAsync(existingLyric.Id); + Lyric lyricInDatabase = await dbContext.Lyrics.Include(lyric => lyric.Track).FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Should().BeNull(); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().HaveCount(1); }); } @@ -82,7 +82,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_clear_OneToOne_relationship_from_dependent_side() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Lyric = _fakers.Lyric.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -105,7 +105,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingTrack.StringId, relationship = "lyric" }, - data = (object) null + data = (object)null } } }; @@ -113,7 +113,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -122,13 +122,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Lyric) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Lyric).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Should().BeNull(); - var lyricsInDatabase = await dbContext.Lyrics.ToListAsync(); + List lyricsInDatabase = await dbContext.Lyrics.ToListAsync(); lyricsInDatabase.Should().HaveCount(1); }); } @@ -137,7 +135,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_clear_ManyToOne_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.OwnedBy = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -160,7 +158,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingTrack.StringId, relationship = "ownedBy" }, - data = (object) null + data = (object)null } } }; @@ -168,7 +166,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -177,13 +175,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Should().BeNull(); - var companiesInDatabase = await dbContext.RecordCompanies.ToListAsync(); + List companiesInDatabase = await dbContext.RecordCompanies.ToListAsync(); companiesInDatabase.Should().HaveCount(1); }); } @@ -192,8 +188,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_OneToOne_relationship_from_principal_side() { // Arrange - var existingLyric = _fakers.Lyric.Generate(); - var existingTrack = _fakers.MusicTrack.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -226,7 +222,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -235,9 +231,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var lyricInDatabase = await dbContext.Lyrics - .Include(lyric => lyric.Track) - .FirstWithIdAsync(existingLyric.Id); + Lyric lyricInDatabase = await dbContext.Lyrics.Include(lyric => lyric.Track).FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Id.Should().Be(existingTrack.Id); }); @@ -247,8 +241,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_OneToOne_relationship_from_dependent_side() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); - var existingLyric = _fakers.Lyric.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -281,7 +275,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -290,9 +284,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Lyric) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Lyric).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Id.Should().Be(existingLyric.Id); }); @@ -302,8 +294,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_ManyToOne_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); - var existingCompany = _fakers.RecordCompany.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -336,7 +328,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -345,9 +337,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); }); @@ -357,10 +347,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_OneToOne_relationship_from_principal_side() { // Arrange - var existingLyric = _fakers.Lyric.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); existingLyric.Track = _fakers.MusicTrack.Generate(); - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -394,7 +384,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -403,13 +393,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var lyricInDatabase = await dbContext.Lyrics - .Include(lyric => lyric.Track) - .FirstWithIdAsync(existingLyric.Id); + Lyric lyricInDatabase = await dbContext.Lyrics.Include(lyric => lyric.Track).FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Id.Should().Be(existingTrack.Id); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().HaveCount(2); }); } @@ -418,10 +406,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_OneToOne_relationship_from_dependent_side() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Lyric = _fakers.Lyric.Generate(); - var existingLyric = _fakers.Lyric.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -455,7 +443,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -464,13 +452,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Lyric) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Lyric).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Id.Should().Be(existingLyric.Id); - var lyricsInDatabase = await dbContext.Lyrics.ToListAsync(); + List lyricsInDatabase = await dbContext.Lyrics.ToListAsync(); lyricsInDatabase.Should().HaveCount(2); }); } @@ -479,10 +465,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_ManyToOne_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.OwnedBy = _fakers.RecordCompany.Generate(); - var existingCompany = _fakers.RecordCompany.Generate(); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -516,7 +502,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -525,13 +511,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); - var companiesInDatabase = await dbContext.RecordCompanies.ToListAsync(); + List companiesInDatabase = await dbContext.RecordCompanies.ToListAsync(); companiesInDatabase.Should().HaveCount(2); }); } @@ -555,14 +539,14 @@ public async Task Cannot_create_for_href_element() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -592,14 +576,14 @@ public async Task Cannot_create_for_missing_type_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -630,14 +614,14 @@ public async Task Cannot_create_for_unknown_type_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -667,14 +651,14 @@ public async Task Cannot_create_for_missing_ID_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -687,7 +671,7 @@ public async Task Cannot_create_for_unknown_ID_in_ref() // Arrange string missingTrackId = Guid.NewGuid().ToString(); - var existingLyric = _fakers.Lyric.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -720,14 +704,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error 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 'musicTracks' with ID '{missingTrackId}' does not exist."); @@ -738,7 +722,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_for_incompatible_ID_in_ref() { // Arrange - var existingLyric = _fakers.Lyric.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -771,14 +755,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); @@ -810,14 +794,14 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -848,14 +832,14 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); @@ -866,7 +850,7 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() public async Task Cannot_create_for_array_in_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -902,14 +886,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); @@ -944,14 +928,14 @@ public async Task Cannot_create_for_missing_type_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); error.Detail.Should().BeNull(); @@ -987,14 +971,14 @@ public async Task Cannot_create_for_unknown_type_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -1029,14 +1013,14 @@ public async Task Cannot_create_for_missing_ID_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -1073,14 +1057,14 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -1091,7 +1075,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() public async Task Cannot_create_for_unknown_ID_in_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1124,14 +1108,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be("Related resource of type 'lyrics' with ID '99999999' in relationship 'lyric' does not exist."); @@ -1142,7 +1126,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_for_incompatible_ID_in_data() { // Arrange - var existingLyric = _fakers.Lyric.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1175,14 +1159,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); @@ -1193,7 +1177,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_for_relationship_mismatch_between_ref_and_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1226,14 +1210,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data.type' element."); error.Detail.Should().Be("Expected resource of type 'lyrics' in 'data.type', instead of 'playlists'."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 1b4c51d93b..b356585dd2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore; @@ -29,7 +30,7 @@ public AtomicReplaceToManyRelationshipTests(ExampleIntegrationTestContext @@ -65,7 +66,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -74,13 +75,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().BeEmpty(); - var performersInDatabase = await dbContext.Performers.ToListAsync(); + List performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().HaveCount(2); }); } @@ -89,7 +88,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_clear_HasManyThrough_relationship() { // Arrange - var existingPlaylist = _fakers.Playlist.Generate(); + Playlist existingPlaylist = _fakers.Playlist.Generate(); + existingPlaylist.PlaylistMusicTracks = new List { new PlaylistMusicTrack @@ -135,7 +135,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -147,7 +147,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(existingPlaylist.Id); @@ -157,7 +157,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => playlistInDatabase.PlaylistMusicTracks.Should().BeEmpty(); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().HaveCount(2); }); } @@ -166,10 +166,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_HasMany_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Performers = _fakers.Performer.Generate(1); - var existingPerformers = _fakers.Performer.Generate(2); + List existingPerformers = _fakers.Performer.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -217,7 +217,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -226,15 +226,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Performers) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Performers).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Performers.Should().HaveCount(2); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingPerformers[0].Id); trackInDatabase.Performers.Should().ContainSingle(performer => performer.Id == existingPerformers[1].Id); - var performersInDatabase = await dbContext.Performers.ToListAsync(); + List performersInDatabase = await dbContext.Performers.ToListAsync(); performersInDatabase.Should().HaveCount(3); }); } @@ -243,7 +241,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_HasManyThrough_relationship() { // Arrange - var existingPlaylist = _fakers.Playlist.Generate(); + Playlist existingPlaylist = _fakers.Playlist.Generate(); + existingPlaylist.PlaylistMusicTracks = new List { new PlaylistMusicTrack @@ -252,7 +251,7 @@ public async Task Can_replace_HasManyThrough_relationship() } }; - var existingTracks = _fakers.MusicTrack.Generate(2); + List existingTracks = _fakers.MusicTrack.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -300,7 +299,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -312,7 +311,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var playlistInDatabase = await dbContext.Playlists + Playlist playlistInDatabase = await dbContext.Playlists .Include(playlist => playlist.PlaylistMusicTracks) .ThenInclude(playlistMusicTrack => playlistMusicTrack.MusicTrack) .FirstWithIdAsync(existingPlaylist.Id); @@ -324,7 +323,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => playlistInDatabase.PlaylistMusicTracks.Should().ContainSingle(playlistMusicTrack => playlistMusicTrack.MusicTrack.Id == existingTracks[0].Id); playlistInDatabase.PlaylistMusicTracks.Should().ContainSingle(playlistMusicTrack => playlistMusicTrack.MusicTrack.Id == existingTracks[1].Id); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().HaveCount(3); }); } @@ -333,7 +332,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_replace_for_null_relationship_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -356,7 +355,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { performers = new { - data = (object) null + data = (object)null } } } @@ -367,14 +366,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -417,14 +416,14 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'tracks' relationship."); @@ -468,14 +467,14 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -518,14 +517,14 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); @@ -570,14 +569,14 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); @@ -588,8 +587,8 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() public async Task Cannot_replace_for_unknown_IDs_in_relationship_data() { // Arrange - var existingCompany = _fakers.RecordCompany.Generate(); - var trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); + Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -635,20 +634,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - var error1 = responseDocument.Errors[0]; + Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - var error2 = responseDocument.Errors[1]; + Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -659,7 +658,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_for_relationship_mismatch() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -700,14 +699,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 626fa31e02..b870ca48b3 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Extensions; @@ -13,8 +14,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.AtomicOperations.Updating.Resources { - public sealed class AtomicUpdateResourceTests - : IClassFixture, OperationsDbContext>> + public sealed class AtomicUpdateResourceTests : IClassFixture, OperationsDbContext>> { private readonly ExampleIntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new OperationsFakers(); @@ -32,8 +32,8 @@ public async Task Can_update_resources() // Arrange const int elementCount = 5; - var existingTracks = _fakers.MusicTrack.Generate(elementCount); - var newTrackTitles = _fakers.MusicTrack.Generate(elementCount).Select(musicTrack => musicTrack.Title).ToArray(); + List existingTracks = _fakers.MusicTrack.Generate(elementCount); + string[] newTrackTitles = _fakers.MusicTrack.Generate(elementCount).Select(musicTrack => musicTrack.Title).ToArray(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -43,6 +43,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); var operationElements = new List(elementCount); + for (int index = 0; index < elementCount; index++) { operationElements.Add(new @@ -68,7 +69,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -77,14 +78,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var tracksInDatabase = await dbContext.MusicTracks - .ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().HaveCount(elementCount); for (int index = 0; index < elementCount; index++) { - var trackInDatabase = tracksInDatabase.Single(musicTrack => musicTrack.Id == existingTracks[index].Id); + MusicTrack trackInDatabase = tracksInDatabase.Single(musicTrack => musicTrack.Id == existingTracks[index].Id); trackInDatabase.Title.Should().Be(newTrackTitles[index]); trackInDatabase.Genre.Should().Be(existingTracks[index].Genre); @@ -96,7 +96,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_without_attributes_or_relationships() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.OwnedBy = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -130,7 +130,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -139,9 +139,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Title.Should().Be(existingTrack.Title); trackInDatabase.Genre.Should().Be(existingTrack.Genre); @@ -155,8 +153,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_unknown_attribute() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); - var newTitle = _fakers.MusicTrack.Generate().Title; + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + string newTitle = _fakers.MusicTrack.Generate().Title; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -188,7 +186,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -197,7 +195,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.FirstWithIdAsync(existingTrack.Id); trackInDatabase.Title.Should().Be(newTitle); }); @@ -207,7 +205,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_unknown_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -245,7 +243,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -257,10 +255,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_partially_update_resource_without_side_effects() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.OwnedBy = _fakers.RecordCompany.Generate(); - var newGenre = _fakers.MusicTrack.Generate().Genre; + string newGenre = _fakers.MusicTrack.Generate().Genre; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -291,7 +289,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -300,9 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Title.Should().Be(existingTrack.Title); trackInDatabase.LengthInSeconds.Should().Be(existingTrack.LengthInSeconds); @@ -318,13 +314,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_completely_update_resource_without_side_effects() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.OwnedBy = _fakers.RecordCompany.Generate(); - var newTitle = _fakers.MusicTrack.Generate().Title; - var newLengthInSeconds = _fakers.MusicTrack.Generate().LengthInSeconds; - var newGenre = _fakers.MusicTrack.Generate().Genre; - var newReleasedAt = _fakers.MusicTrack.Generate().ReleasedAt; + string newTitle = _fakers.MusicTrack.Generate().Title; + decimal? newLengthInSeconds = _fakers.MusicTrack.Generate().LengthInSeconds; + string newGenre = _fakers.MusicTrack.Generate().Genre; + DateTimeOffset newReleasedAt = _fakers.MusicTrack.Generate().ReleasedAt; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -358,7 +354,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -367,9 +363,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Title.Should().Be(newTitle); trackInDatabase.LengthInSeconds.Should().Be(newLengthInSeconds); @@ -385,8 +379,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_side_effects() { // Arrange - var existingLanguage = _fakers.TextLanguage.Generate(); - var newIsoCode = _fakers.TextLanguage.Generate().IsoCode; + TextLanguage existingLanguage = _fakers.TextLanguage.Generate(); + string newIsoCode = _fakers.TextLanguage.Generate().IsoCode; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -417,7 +411,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -431,7 +426,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var languageInDatabase = await dbContext.TextLanguages.FirstWithIdAsync(existingLanguage.Id); + TextLanguage languageInDatabase = await dbContext.TextLanguages.FirstWithIdAsync(existingLanguage.Id); languageInDatabase.IsoCode.Should().Be(newIsoCode); }); @@ -441,7 +436,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_resource_with_side_effects_hides_relationship_data_in_response() { // Arrange - var existingLanguage = _fakers.TextLanguage.Generate(); + TextLanguage existingLanguage = _fakers.TextLanguage.Generate(); existingLanguage.Lyrics = _fakers.Lyric.Generate(1); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -469,7 +464,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = + await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -499,14 +495,14 @@ public async Task Cannot_update_resource_for_href_element() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -517,8 +513,8 @@ public async Task Cannot_update_resource_for_href_element() public async Task Can_update_resource_for_ref_element() { // Arrange - var existingPerformer = _fakers.Performer.Generate(); - var newArtistName = _fakers.Performer.Generate().ArtistName; + Performer existingPerformer = _fakers.Performer.Generate(); + string newArtistName = _fakers.Performer.Generate().ArtistName; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -554,7 +550,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -563,7 +559,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var performerInDatabase = await dbContext.Performers.FirstWithIdAsync(existingPerformer.Id); + Performer performerInDatabase = await dbContext.Performers.FirstWithIdAsync(existingPerformer.Id); performerInDatabase.ArtistName.Should().Be(newArtistName); performerInDatabase.BornAt.Should().BeCloseTo(existingPerformer.BornAt); @@ -603,14 +599,14 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -650,14 +646,14 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -699,14 +695,14 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -731,14 +727,14 @@ public async Task Cannot_update_resource_for_missing_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); @@ -773,14 +769,14 @@ public async Task Cannot_update_resource_for_missing_type_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); error.Detail.Should().BeNull(); @@ -815,14 +811,14 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -859,14 +855,14 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -877,7 +873,7 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() public async Task Cannot_update_resource_for_array_in_data() { // Arrange - var existingPerformer = _fakers.Performer.Generate(); + Performer existingPerformer = _fakers.Performer.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -911,14 +907,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for create/update resource operation."); error.Detail.Should().BeNull(); @@ -959,14 +955,14 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.type' and 'data.type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data.type', instead of 'playlists'."); @@ -1007,14 +1003,14 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource ID mismatch between 'ref.id' and 'data.id' element."); error.Detail.Should().Be("Expected resource with ID '12345678' in 'data.id', instead of '87654321'."); @@ -1055,14 +1051,14 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource local ID mismatch between 'ref.lid' and 'data.lid' element."); error.Detail.Should().Be("Expected resource with local ID 'local-1' in 'data.lid', instead of 'local-2'."); @@ -1103,14 +1099,14 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.id' and 'data.lid' element."); error.Detail.Should().Be("Expected resource with ID '12345678' in 'data.id', instead of 'local-1' in 'data.lid'."); @@ -1151,14 +1147,14 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.lid' and 'data.id' element."); error.Detail.Should().Be("Expected resource with local ID 'local-1' in 'data.lid', instead of '12345678' in 'data.id'."); @@ -1194,14 +1190,14 @@ public async Task Cannot_update_resource_for_unknown_type() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -1237,14 +1233,14 @@ public async Task Cannot_update_resource_for_unknown_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error 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 '99999999' does not exist."); @@ -1255,7 +1251,7 @@ public async Task Cannot_update_resource_for_unknown_ID() public async Task Cannot_update_resource_for_incompatible_ID() { // Arrange - var guid = Guid.NewGuid().ToString(); + string guid = Guid.NewGuid().ToString(); var requestBody = new { @@ -1284,14 +1280,14 @@ public async Task Cannot_update_resource_for_incompatible_ID() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); @@ -1302,7 +1298,7 @@ public async Task Cannot_update_resource_for_incompatible_ID() public async Task Cannot_update_resource_attribute_with_blocked_capability() { // Arrange - var existingLyric = _fakers.Lyric.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1333,14 +1329,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); error.Detail.Should().Be("Changing the value of 'createdAt' is not allowed."); @@ -1351,7 +1347,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_update_resource_with_readonly_attribute() { // Arrange - var existingPlaylist = _fakers.Playlist.Generate(); + Playlist existingPlaylist = _fakers.Playlist.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1382,14 +1378,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isArchived' is read-only."); @@ -1400,7 +1396,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_change_ID_of_existing_resource() { // Arrange - var existingCompany = _fakers.RecordCompany.Generate(); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1431,14 +1427,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); error.Detail.Should().BeNull(); @@ -1449,7 +1445,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_update_resource_with_incompatible_attribute_value() { // Arrange - var existingPerformer = _fakers.Performer.Generate(); + Performer existingPerformer = _fakers.Performer.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1480,14 +1476,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'DateTimeOffset'. - Request body:"); @@ -1498,16 +1494,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_attributes_and_multiple_relationship_types() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Lyric = _fakers.Lyric.Generate(); existingTrack.OwnedBy = _fakers.RecordCompany.Generate(); existingTrack.Performers = _fakers.Performer.Generate(1); - var newGenre = _fakers.MusicTrack.Generate().Genre; + string newGenre = _fakers.MusicTrack.Generate().Genre; - var existingLyric = _fakers.Lyric.Generate(); - var existingCompany = _fakers.RecordCompany.Generate(); - var existingPerformer = _fakers.Performer.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); + Performer existingPerformer = _fakers.Performer.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1568,7 +1564,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -1580,7 +1576,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var trackInDatabase = await dbContext.MusicTracks + MusicTrack trackInDatabase = await dbContext.MusicTracks .Include(musicTrack => musicTrack.Lyric) .Include(musicTrack => musicTrack.OwnedBy) .Include(musicTrack => musicTrack.Performers) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index f3069f8f07..52f58e2506 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -27,7 +29,7 @@ public AtomicUpdateToOneRelationshipTests(ExampleIntegrationTestContext @@ -52,7 +54,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { track = new { - data = (object) null + data = (object)null } } } @@ -63,7 +65,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -72,13 +74,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var lyricInDatabase = await dbContext.Lyrics - .Include(lyric => lyric.Track) - .FirstWithIdAsync(existingLyric.Id); + Lyric lyricInDatabase = await dbContext.Lyrics.Include(lyric => lyric.Track).FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Should().BeNull(); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().HaveCount(1); }); } @@ -87,7 +87,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_clear_OneToOne_relationship_from_dependent_side() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Lyric = _fakers.Lyric.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -112,7 +112,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { lyric = new { - data = (object) null + data = (object)null } } } @@ -123,7 +123,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -132,13 +132,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Lyric) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Lyric).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Should().BeNull(); - var lyricsInDatabase = await dbContext.Lyrics.ToListAsync(); + List lyricsInDatabase = await dbContext.Lyrics.ToListAsync(); lyricsInDatabase.Should().HaveCount(1); }); } @@ -147,7 +145,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_clear_ManyToOne_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.OwnedBy = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -172,7 +170,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { ownedBy = new { - data = (object) null + data = (object)null } } } @@ -183,7 +181,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -192,13 +190,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Should().BeNull(); - var companiesInDatabase = await dbContext.RecordCompanies.ToListAsync(); + List companiesInDatabase = await dbContext.RecordCompanies.ToListAsync(); companiesInDatabase.Should().HaveCount(1); }); } @@ -207,8 +203,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_OneToOne_relationship_from_principal_side() { // Arrange - var existingLyric = _fakers.Lyric.Generate(); - var existingTrack = _fakers.MusicTrack.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -246,7 +242,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -255,9 +251,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var lyricInDatabase = await dbContext.Lyrics - .Include(lyric => lyric.Track) - .FirstWithIdAsync(existingLyric.Id); + Lyric lyricInDatabase = await dbContext.Lyrics.Include(lyric => lyric.Track).FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Id.Should().Be(existingTrack.Id); }); @@ -267,8 +261,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_OneToOne_relationship_from_dependent_side() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); - var existingLyric = _fakers.Lyric.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -306,7 +300,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -315,9 +309,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Lyric) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Lyric).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Id.Should().Be(existingLyric.Id); }); @@ -327,8 +319,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_ManyToOne_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); - var existingCompany = _fakers.RecordCompany.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -366,7 +358,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -375,9 +367,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); }); @@ -387,10 +377,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_OneToOne_relationship_from_principal_side() { // Arrange - var existingLyric = _fakers.Lyric.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); existingLyric.Track = _fakers.MusicTrack.Generate(); - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -429,7 +419,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -438,13 +428,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var lyricInDatabase = await dbContext.Lyrics - .Include(lyric => lyric.Track) - .FirstWithIdAsync(existingLyric.Id); + Lyric lyricInDatabase = await dbContext.Lyrics.Include(lyric => lyric.Track).FirstWithIdAsync(existingLyric.Id); lyricInDatabase.Track.Id.Should().Be(existingTrack.Id); - var tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); + List tracksInDatabase = await dbContext.MusicTracks.ToListAsync(); tracksInDatabase.Should().HaveCount(2); }); } @@ -453,10 +441,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_OneToOne_relationship_from_dependent_side() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.Lyric = _fakers.Lyric.Generate(); - var existingLyric = _fakers.Lyric.Generate(); + Lyric existingLyric = _fakers.Lyric.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -495,7 +483,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -504,13 +492,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.Lyric) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.Lyric).FirstWithIdAsync(existingTrack.Id); trackInDatabase.Lyric.Id.Should().Be(existingLyric.Id); - var lyricsInDatabase = await dbContext.Lyrics.ToListAsync(); + List lyricsInDatabase = await dbContext.Lyrics.ToListAsync(); lyricsInDatabase.Should().HaveCount(2); }); } @@ -519,10 +505,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_replace_ManyToOne_relationship() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); existingTrack.OwnedBy = _fakers.RecordCompany.Generate(); - var existingCompany = _fakers.RecordCompany.Generate(); + RecordCompany existingCompany = _fakers.RecordCompany.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -561,7 +547,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -570,13 +556,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var trackInDatabase = await dbContext.MusicTracks - .Include(musicTrack => musicTrack.OwnedBy) - .FirstWithIdAsync(existingTrack.Id); + MusicTrack trackInDatabase = await dbContext.MusicTracks.Include(musicTrack => musicTrack.OwnedBy).FirstWithIdAsync(existingTrack.Id); trackInDatabase.OwnedBy.Id.Should().Be(existingCompany.Id); - var companiesInDatabase = await dbContext.RecordCompanies.ToListAsync(); + List companiesInDatabase = await dbContext.RecordCompanies.ToListAsync(); companiesInDatabase.Should().HaveCount(2); }); } @@ -585,7 +569,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_for_array_in_relationship_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -626,14 +610,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); @@ -673,14 +657,14 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'track' relationship."); @@ -721,14 +705,14 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); @@ -768,14 +752,14 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); @@ -817,14 +801,14 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); @@ -835,7 +819,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() public async Task Cannot_create_for_unknown_ID_in_relationship_data() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -873,14 +857,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be("Related resource of type 'lyrics' with ID '99999999' in relationship 'lyric' does not exist."); @@ -891,7 +875,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_for_relationship_mismatch() { // Arrange - var existingTrack = _fakers.MusicTrack.Generate(); + MusicTrack existingTrack = _fakers.MusicTrack.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -929,14 +913,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Car.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Car.cs index eccd7acda0..5263e4e362 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Car.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Car.cs @@ -15,7 +15,8 @@ public override string Id get => $"{RegionId}:{LicensePlate}"; set { - var elements = value.Split(':'); + string[] elements = value.Split(':'); + if (elements.Length == 2) { if (int.TryParse(elements[0], out int regionId)) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs index 2da565d34e..ee0ae50c6f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs @@ -11,11 +11,11 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys { /// - /// Rewrites an expression tree, updating all references to with - /// the combination of and . + /// Rewrites an expression tree, updating all references to with the combination of and + /// . /// /// - /// This enables queries to use , which is not mapped in the database. + /// This enables queries to use , which is not mapped in the database. /// internal sealed class CarExpressionRewriter : QueryExpressionRewriter { @@ -24,23 +24,19 @@ internal sealed class CarExpressionRewriter : QueryExpressionRewriter public CarExpressionRewriter(IResourceContextProvider resourceContextProvider) { - var carResourceContext = resourceContextProvider.GetResourceContext(); + ResourceContext carResourceContext = resourceContextProvider.GetResourceContext(); - _regionIdAttribute = - carResourceContext.Attributes.Single(attribute => - attribute.Property.Name == nameof(Car.RegionId)); + _regionIdAttribute = carResourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Car.RegionId)); - _licensePlateAttribute = - carResourceContext.Attributes.Single(attribute => - attribute.Property.Name == nameof(Car.LicensePlate)); + _licensePlateAttribute = carResourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Car.LicensePlate)); } public override QueryExpression VisitComparison(ComparisonExpression expression, object argument) { - if (expression.Left is ResourceFieldChainExpression leftChain && - expression.Right is LiteralConstantExpression rightConstant) + if (expression.Left is ResourceFieldChainExpression leftChain && expression.Right is LiteralConstantExpression rightConstant) { PropertyInfo leftProperty = leftChain.Fields.Last().Property; + if (IsCarId(leftProperty)) { if (expression.Operator != ComparisonOperator.Equals) @@ -58,9 +54,10 @@ public override QueryExpression VisitComparison(ComparisonExpression expression, public override QueryExpression VisitEqualsAnyOf(EqualsAnyOfExpression expression, object argument) { PropertyInfo property = expression.TargetAttribute.Fields.Last().Property; + if (IsCarId(property)) { - var carStringIds = expression.Constants.Select(constant => constant.Value).ToArray(); + string[] carStringIds = expression.Constants.Select(constant => constant.Value).ToArray(); return RewriteFilterOnCarStringIds(expression.TargetAttribute, carStringIds); } @@ -70,6 +67,7 @@ public override QueryExpression VisitEqualsAnyOf(EqualsAnyOfExpression expressio public override QueryExpression VisitMatchText(MatchTextExpression expression, object argument) { PropertyInfo property = expression.TargetAttribute.Fields.Last().Property; + if (IsCarId(property)) { throw new NotSupportedException("Partial text matching on Car IDs is not possible."); @@ -83,34 +81,34 @@ private static bool IsCarId(PropertyInfo property) return property.Name == nameof(Identifiable.Id) && property.DeclaringType == typeof(Car); } - private QueryExpression RewriteFilterOnCarStringIds(ResourceFieldChainExpression existingCarIdChain, - IEnumerable carStringIds) + private QueryExpression RewriteFilterOnCarStringIds(ResourceFieldChainExpression existingCarIdChain, IEnumerable carStringIds) { var outerTerms = new List(); - foreach (var carStringId in carStringIds) + foreach (string carStringId in carStringIds) { var tempCar = new Car { StringId = carStringId }; - var keyComparison = - CreateEqualityComparisonOnCompositeKey(existingCarIdChain, tempCar.RegionId, tempCar.LicensePlate); + QueryExpression keyComparison = CreateEqualityComparisonOnCompositeKey(existingCarIdChain, tempCar.RegionId, tempCar.LicensePlate); outerTerms.Add(keyComparison); } return outerTerms.Count == 1 ? outerTerms[0] : new LogicalExpression(LogicalOperator.Or, outerTerms); } - private QueryExpression CreateEqualityComparisonOnCompositeKey(ResourceFieldChainExpression existingCarIdChain, - long regionIdValue, string licensePlateValue) + private QueryExpression CreateEqualityComparisonOnCompositeKey(ResourceFieldChainExpression existingCarIdChain, long regionIdValue, + string licensePlateValue) { - var regionIdChain = ReplaceLastAttributeInChain(existingCarIdChain, _regionIdAttribute); + ResourceFieldChainExpression regionIdChain = ReplaceLastAttributeInChain(existingCarIdChain, _regionIdAttribute); + var regionIdComparison = new ComparisonExpression(ComparisonOperator.Equals, regionIdChain, new LiteralConstantExpression(regionIdValue.ToString())); - var licensePlateChain = ReplaceLastAttributeInChain(existingCarIdChain, _licensePlateAttribute); + ResourceFieldChainExpression licensePlateChain = ReplaceLastAttributeInChain(existingCarIdChain, _licensePlateAttribute); + var licensePlateComparison = new ComparisonExpression(ComparisonOperator.Equals, licensePlateChain, new LiteralConstantExpression(licensePlateValue)); @@ -125,15 +123,14 @@ public override QueryExpression VisitSort(SortExpression expression, object argu { var newSortElements = new List(); - foreach (var sortElement in expression.Elements) + foreach (SortElementExpression sortElement in expression.Elements) { if (IsSortOnCarId(sortElement)) { - var regionIdSort = ReplaceLastAttributeInChain(sortElement.TargetAttribute, _regionIdAttribute); + ResourceFieldChainExpression regionIdSort = ReplaceLastAttributeInChain(sortElement.TargetAttribute, _regionIdAttribute); newSortElements.Add(new SortElementExpression(regionIdSort, sortElement.IsAscending)); - var licensePlateSort = - ReplaceLastAttributeInChain(sortElement.TargetAttribute, _licensePlateAttribute); + ResourceFieldChainExpression licensePlateSort = ReplaceLastAttributeInChain(sortElement.TargetAttribute, _licensePlateAttribute); newSortElements.Add(new SortElementExpression(licensePlateSort, sortElement.IsAscending)); } else @@ -150,6 +147,7 @@ private static bool IsSortOnCarId(SortElementExpression sortElement) if (sortElement.TargetAttribute != null) { PropertyInfo property = sortElement.TargetAttribute.Fields.Last().Property; + if (IsCarId(property)) { return true; @@ -159,10 +157,9 @@ private static bool IsSortOnCarId(SortElementExpression sortElement) return false; } - private static ResourceFieldChainExpression ReplaceLastAttributeInChain( - ResourceFieldChainExpression resourceFieldChain, AttrAttribute attribute) + private static ResourceFieldChainExpression ReplaceLastAttributeInChain(ResourceFieldChainExpression resourceFieldChain, AttrAttribute attribute) { - var fields = resourceFieldChain.Fields.ToList(); + List fields = resourceFieldChain.Fields.ToList(); fields[^1] = attribute; return new ResourceFieldChainExpression(fields); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs index a167ae4fff..8142d8d378 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarRepository.cs @@ -15,8 +15,7 @@ public sealed class CarRepository : EntityFrameworkCoreRepository { private readonly IResourceGraph _resourceGraph; - public CarRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, - IResourceGraph resourceGraph, IResourceFactory resourceFactory, + public CarRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) { @@ -35,13 +34,13 @@ private void RecursiveRewriteFilterInLayer(QueryLayer queryLayer) if (queryLayer.Filter != null) { var writer = new CarExpressionRewriter(_resourceGraph); - queryLayer.Filter = (FilterExpression) writer.Visit(queryLayer.Filter, null); + queryLayer.Filter = (FilterExpression)writer.Visit(queryLayer.Filter, null); } if (queryLayer.Sort != null) { var writer = new CarExpressionRewriter(_resourceGraph); - queryLayer.Sort = (SortExpression) writer.Visit(queryLayer.Sort, null); + queryLayer.Sort = (SortExpression)writer.Visit(queryLayer.Sort, null); } if (queryLayer.Projection != null) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarsController.cs index f264c043e3..982f72d625 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CarsController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys { public sealed class CarsController : JsonApiController { - public CarsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public CarsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs index 602dd90e15..13609a17ec 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeDbContext.cs @@ -20,7 +20,11 @@ public CompositeDbContext(DbContextOptions options) protected override void OnModelCreating(ModelBuilder builder) { builder.Entity() - .HasKey(car => new {car.RegionId, car.LicensePlate}); + .HasKey(car => new + { + car.RegionId, + car.LicensePlate + }); builder.Entity() .HasOne(engine => engine.Car) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs index b8a74e6232..f4453b50c7 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -13,8 +14,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys { - public sealed class CompositeKeyTests - : IClassFixture, CompositeDbContext>> + public sealed class CompositeKeyTests : IClassFixture, CompositeDbContext>> { private readonly ExampleIntegrationTestContext, CompositeDbContext> _testContext; @@ -27,7 +27,7 @@ public CompositeKeyTests(ExampleIntegrationTestContext(); }); - var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); options.AllowClientGeneratedIds = true; } @@ -51,7 +51,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/cars?filter=any(id,'123:AA-BB-11','999:XX-YY-22')"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -77,10 +77,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/cars/" + car.StringId; + string route = "/cars/" + car.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -109,7 +109,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/cars?sort=id"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -138,7 +138,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/cars?fields[cars]=id"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -172,7 +172,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/cars"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -181,8 +181,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var carInDatabase = await dbContext.Cars - .FirstOrDefaultAsync(car => car.RegionId == 123 && car.LicensePlate == "AA-BB-11"); + Car carInDatabase = await dbContext.Cars.FirstOrDefaultAsync(car => car.RegionId == 123 && car.LicensePlate == "AA-BB-11"); carInDatabase.Should().NotBeNull(); carInDatabase.Id.Should().Be("123:AA-BB-11"); @@ -231,10 +230,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/engines/" + existingEngine.StringId; + string route = "/engines/" + existingEngine.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -243,9 +242,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var engineInDatabase = await dbContext.Engines - .Include(engine => engine.Car) - .FirstWithIdAsync(existingEngine.Id); + Engine engineInDatabase = await dbContext.Engines.Include(engine => engine.Car).FirstWithIdAsync(existingEngine.Id); engineInDatabase.Car.Should().NotBeNull(); engineInDatabase.Car.Id.Should().Be(existingCar.StringId); @@ -283,16 +280,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { car = new { - data = (object) null + data = (object)null } } } }; - var route = "/engines/" + existingEngine.StringId; + string route = "/engines/" + existingEngine.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -301,9 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var engineInDatabase = await dbContext.Engines - .Include(engine => engine.Car) - .FirstWithIdAsync(existingEngine.Id); + Engine engineInDatabase = await dbContext.Engines.Include(engine => engine.Car).FirstWithIdAsync(existingEngine.Id); engineInDatabase.Car.Should().BeNull(); }); @@ -350,10 +345,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = $"/dealerships/{existingDealership.StringId}/relationships/inventory"; + string route = $"/dealerships/{existingDealership.StringId}/relationships/inventory"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -362,9 +357,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var dealershipInDatabase = await dbContext.Dealerships - .Include(dealership => dealership.Inventory) - .FirstWithIdOrDefaultAsync(existingDealership.Id); + Dealership dealershipInDatabase = await dbContext.Dealerships + .Include(dealership => dealership.Inventory).FirstWithIdOrDefaultAsync(existingDealership.Id); dealershipInDatabase.Inventory.Should().HaveCount(1); dealershipInDatabase.Inventory.Should().ContainSingle(car => car.Id == existingDealership.Inventory.ElementAt(1).Id); @@ -379,6 +373,7 @@ public async Task Can_add_to_OneToMany_relationship() { Address = "Dam 1, 1012JS Amsterdam, the Netherlands" }; + var existingCar = new Car { RegionId = 123, @@ -404,10 +399,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = $"/dealerships/{existingDealership.StringId}/relationships/inventory"; + string route = $"/dealerships/{existingDealership.StringId}/relationships/inventory"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -416,9 +411,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var dealershipInDatabase = await dbContext.Dealerships - .Include(dealership => dealership.Inventory) - .FirstWithIdOrDefaultAsync(existingDealership.Id); + Dealership dealershipInDatabase = await dbContext.Dealerships + .Include(dealership => dealership.Inventory).FirstWithIdOrDefaultAsync(existingDealership.Id); dealershipInDatabase.Inventory.Should().HaveCount(1); dealershipInDatabase.Inventory.Should().ContainSingle(car => car.Id == existingCar.Id); @@ -446,6 +440,7 @@ public async Task Can_replace_OneToMany_relationship() } } }; + var existingCar = new Car { RegionId = 789, @@ -473,14 +468,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => type = "cars", id = "789:EE-FF-33" } - } }; - var route = $"/dealerships/{existingDealership.StringId}/relationships/inventory"; + string route = $"/dealerships/{existingDealership.StringId}/relationships/inventory"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -489,9 +483,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var dealershipInDatabase = await dbContext.Dealerships - .Include(dealership => dealership.Inventory) - .FirstWithIdOrDefaultAsync(existingDealership.Id); + Dealership dealershipInDatabase = await dbContext.Dealerships + .Include(dealership => dealership.Inventory).FirstWithIdOrDefaultAsync(existingDealership.Id); dealershipInDatabase.Inventory.Should().HaveCount(2); dealershipInDatabase.Inventory.Should().ContainSingle(car => car.Id == existingCar.Id); @@ -527,17 +520,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = $"/dealerships/{existingDealership.StringId}/relationships/inventory"; + string route = $"/dealerships/{existingDealership.StringId}/relationships/inventory"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be("Related resource of type 'cars' with ID '999:XX-YY-22' in relationship 'inventory' does not exist."); @@ -560,10 +553,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/cars/" + existingCar.StringId; + string route = "/cars/" + existingCar.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -572,8 +565,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var carInDatabase = await dbContext.Cars - .FirstOrDefaultAsync(car => car.RegionId == existingCar.RegionId && car.LicensePlate == existingCar.LicensePlate); + Car carInDatabase = + await dbContext.Cars.FirstOrDefaultAsync(car => car.RegionId == existingCar.RegionId && car.LicensePlate == existingCar.LicensePlate); carInDatabase.Should().BeNull(); }); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Dealership.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Dealership.cs index 8ac8d4e50d..aaa1449254 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Dealership.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Dealership.cs @@ -11,7 +11,7 @@ public sealed class Dealership : Identifiable [Attr] public string Address { get; set; } - [HasMany] + [HasMany] public ISet Inventory { get; set; } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/DealershipsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/DealershipsController.cs index 53b4f281e1..7301033afd 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/DealershipsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/DealershipsController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys { public sealed class DealershipsController : JsonApiController { - public DealershipsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public DealershipsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Engine.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Engine.cs index b58c3b53ec..8ccbada031 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Engine.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/Engine.cs @@ -10,7 +10,7 @@ public sealed class Engine : Identifiable [Attr] public string SerialCode { get; set; } - [HasOne] + [HasOne] public Car Car { get; set; } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/EnginesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/EnginesController.cs index 4833292cd8..b4371cd63d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/EnginesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CompositeKeys/EnginesController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CompositeKeys { public sealed class EnginesController : JsonApiController { - public EnginesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public EnginesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs index a892129ddc..25ad63189e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using FluentAssertions; @@ -10,8 +11,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ContentNegotiation { - public sealed class AcceptHeaderTests - : IClassFixture, PolicyDbContext>> + public sealed class AcceptHeaderTests : IClassFixture, PolicyDbContext>> { private readonly ExampleIntegrationTestContext, PolicyDbContext> _testContext; @@ -31,7 +31,7 @@ public async Task Permits_no_Accept_headers() var acceptHeaders = new MediaTypeWithQualityHeaderValue[0]; // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -66,7 +66,7 @@ public async Task Permits_no_Accept_headers_at_operations_endpoint() var acceptHeaders = new MediaTypeWithQualityHeaderValue[0]; // Act - var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType, acceptHeaders); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType, acceptHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -78,14 +78,14 @@ public async Task Permits_global_wildcard_in_Accept_headers() // Arrange const string route = "/policies"; - var acceptHeaders = new[] + MediaTypeWithQualityHeaderValue[] acceptHeaders = { MediaTypeWithQualityHeaderValue.Parse("text/html"), MediaTypeWithQualityHeaderValue.Parse("*/*") }; // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -97,14 +97,14 @@ public async Task Permits_application_wildcard_in_Accept_headers() // Arrange const string route = "/policies"; - var acceptHeaders = new[] + MediaTypeWithQualityHeaderValue[] acceptHeaders = { MediaTypeWithQualityHeaderValue.Parse("text/html;q=0.8"), MediaTypeWithQualityHeaderValue.Parse("application/*;q=0.2") }; // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -116,7 +116,7 @@ public async Task Permits_JsonApi_without_parameters_in_Accept_headers() // Arrange const string route = "/policies"; - var acceptHeaders = new[] + MediaTypeWithQualityHeaderValue[] acceptHeaders = { MediaTypeWithQualityHeaderValue.Parse("text/html"), MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some"), @@ -126,7 +126,7 @@ public async Task Permits_JsonApi_without_parameters_in_Accept_headers() }; // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route, acceptHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -158,17 +158,17 @@ public async Task Permits_JsonApi_with_AtomicOperations_extension_in_Accept_head const string route = "/operations"; const string contentType = HeaderConstants.AtomicOperationsMediaType; - var acceptHeaders = new[] + MediaTypeWithQualityHeaderValue[] acceptHeaders = { MediaTypeWithQualityHeaderValue.Parse("text/html"), MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some"), MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType), MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected"), - MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType+";ext=\"https://jsonapi.org/ext/atomic\"; q=0.2") + MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + ";ext=\"https://jsonapi.org/ext/atomic\"; q=0.2") }; // Act - var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType, acceptHeaders); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType, acceptHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -180,7 +180,7 @@ public async Task Denies_JsonApi_with_parameters_in_Accept_headers() // Arrange const string route = "/policies"; - var acceptHeaders = new[] + MediaTypeWithQualityHeaderValue[] acceptHeaders = { MediaTypeWithQualityHeaderValue.Parse("text/html"), MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some"), @@ -190,14 +190,14 @@ public async Task Denies_JsonApi_with_parameters_in_Accept_headers() }; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route, acceptHeaders); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route, acceptHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotAcceptable); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotAcceptable); error.Title.Should().Be("The specified Accept header value does not contain any supported media types."); error.Detail.Should().Be("Please include 'application/vnd.api+json' in the Accept header values."); @@ -229,20 +229,21 @@ public async Task Denies_JsonApi_in_Accept_headers_at_operations_endpoint() const string route = "/operations"; const string contentType = HeaderConstants.AtomicOperationsMediaType; - var acceptHeaders = new[] + MediaTypeWithQualityHeaderValue[] acceptHeaders = { MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType) }; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType, acceptHeaders); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = + await _testContext.ExecutePostAsync(route, requestBody, contentType, acceptHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotAcceptable); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotAcceptable); error.Title.Should().Be("The specified Accept header value does not contain any supported media types."); error.Detail.Should().Be("Please include 'application/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic\"' in the Accept header values."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs index f32001e4a1..3aba63977a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Middleware; @@ -9,8 +10,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ContentNegotiation { - public sealed class ContentTypeHeaderTests - : IClassFixture, PolicyDbContext>> + public sealed class ContentTypeHeaderTests : IClassFixture, PolicyDbContext>> { private readonly ExampleIntegrationTestContext, PolicyDbContext> _testContext; @@ -28,7 +28,7 @@ public async Task Returns_JsonApi_ContentType_header() const string route = "/policies"; // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -61,7 +61,7 @@ public async Task Returns_JsonApi_ContentType_header_with_AtomicOperations_exten const string route = "/operations"; // Act - var (httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -88,14 +88,15 @@ public async Task Denies_unknown_ContentType_header() const string contentType = "text/html"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = + await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'text/html' for the Content-Type header value."); @@ -122,7 +123,7 @@ public async Task Permits_JsonApi_ContentType_header() // Act // ReSharper disable once RedundantArgumentDefaultValue - var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -155,7 +156,7 @@ public async Task Permits_JsonApi_ContentType_header_with_AtomicOperations_exten const string contentType = HeaderConstants.AtomicOperationsMediaType; // Act - var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -181,14 +182,15 @@ public async Task Denies_JsonApi_ContentType_header_with_profile() const string contentType = HeaderConstants.MediaType + "; profile=something"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = + await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); @@ -214,14 +216,15 @@ public async Task Denies_JsonApi_ContentType_header_with_extension() const string contentType = HeaderConstants.MediaType + "; ext=something"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = + await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); @@ -247,14 +250,15 @@ public async Task Denies_JsonApi_ContentType_header_with_AtomicOperations_extens const string contentType = HeaderConstants.AtomicOperationsMediaType; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = + await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); @@ -280,14 +284,15 @@ public async Task Denies_JsonApi_ContentType_header_with_CharSet() const string contentType = HeaderConstants.MediaType + "; charset=ISO-8859-4"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = + await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); @@ -313,14 +318,15 @@ public async Task Denies_JsonApi_ContentType_header_with_unknown_parameter() const string contentType = HeaderConstants.MediaType + "; unknown=unexpected"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = + await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); @@ -354,7 +360,8 @@ public async Task Denies_JsonApi_ContentType_header_at_operations_endpoint() // Act // ReSharper disable once RedundantArgumentDefaultValue - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = + await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); @@ -363,7 +370,7 @@ public async Task Denies_JsonApi_ContentType_header_at_operations_endpoint() string detail = $"Please specify '{HeaderConstants.AtomicOperationsMediaType}' instead of '{contentType}' for the Content-Type header value."; - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be(detail); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PoliciesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PoliciesController.cs index d99ab9bd6a..5cf5119f08 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PoliciesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ContentNegotiation/PoliciesController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ContentNegotiation { public sealed class PoliciesController : JsonApiController { - public PoliciesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public PoliciesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs index daffab38a9..dc1d6e3670 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -8,8 +9,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ControllerActionResults { - public sealed class ActionResultTests - : IClassFixture, ActionResultDbContext>> + public sealed class ActionResultTests : IClassFixture, ActionResultDbContext>> { private readonly ExampleIntegrationTestContext, ActionResultDbContext> _testContext; @@ -30,10 +30,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/toothbrushes/" + toothbrush.StringId; + string route = "/toothbrushes/" + toothbrush.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -46,17 +46,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Converts_empty_ActionResult_to_error_collection() { // Arrange - var route = "/toothbrushes/" + BaseToothbrushesController.EmptyActionResultId; + string route = "/toothbrushes/" + BaseToothbrushesController.EmptyActionResultId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("NotFound"); error.Detail.Should().BeNull(); @@ -66,17 +66,17 @@ public async Task Converts_empty_ActionResult_to_error_collection() public async Task Converts_ActionResult_with_error_object_to_error_collection() { // Arrange - var route = "/toothbrushes/" + BaseToothbrushesController.ActionResultWithErrorObjectId; + string route = "/toothbrushes/" + BaseToothbrushesController.ActionResultWithErrorObjectId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("No toothbrush with that ID exists."); error.Detail.Should().BeNull(); @@ -86,17 +86,17 @@ public async Task Converts_ActionResult_with_error_object_to_error_collection() public async Task Cannot_convert_ActionResult_with_string_parameter_to_error_collection() { // Arrange - var route = "/toothbrushes/" + BaseToothbrushesController.ActionResultWithStringParameter; + string route = "/toothbrushes/" + BaseToothbrushesController.ActionResultWithStringParameter; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); error.Title.Should().Be("An unhandled error occurred while processing this request."); error.Detail.Should().Be("Data being returned must be errors or resources."); @@ -106,17 +106,17 @@ public async Task Cannot_convert_ActionResult_with_string_parameter_to_error_col public async Task Converts_ObjectResult_with_error_object_to_error_collection() { // Arrange - var route = "/toothbrushes/" + BaseToothbrushesController.ObjectResultWithErrorObjectId; + string route = "/toothbrushes/" + BaseToothbrushesController.ObjectResultWithErrorObjectId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadGateway); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadGateway); error.Title.Should().BeNull(); error.Detail.Should().BeNull(); @@ -126,27 +126,27 @@ public async Task Converts_ObjectResult_with_error_object_to_error_collection() public async Task Converts_ObjectResult_with_error_objects_to_error_collection() { // Arrange - var route = "/toothbrushes/" + BaseToothbrushesController.ObjectResultWithErrorCollectionId; + string route = "/toothbrushes/" + BaseToothbrushesController.ObjectResultWithErrorCollectionId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(3); - var error1 = responseDocument.Errors[0]; + Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.PreconditionFailed); error1.Title.Should().BeNull(); error1.Detail.Should().BeNull(); - var error2 = responseDocument.Errors[1]; + Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.Unauthorized); error2.Title.Should().BeNull(); error2.Detail.Should().BeNull(); - var error3 = responseDocument.Errors[2]; + Error error3 = responseDocument.Errors[2]; error3.StatusCode.Should().Be(HttpStatusCode.ExpectationFailed); error3.Title.Should().Be("This is not a very great request."); error3.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs index e8d6f252f4..00fb4ac4dc 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs @@ -18,8 +18,7 @@ public abstract class BaseToothbrushesController : BaseJsonApiController resourceService) + protected BaseToothbrushesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } @@ -60,6 +59,7 @@ public override async Task GetAsync(int id, CancellationToken can Title = "This is not a very great request." } }; + return Error(errors); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ToothbrushesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ToothbrushesController.cs index 8cd4e1d646..a16b083db4 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ToothbrushesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ControllerActionResults/ToothbrushesController.cs @@ -9,8 +9,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ControllerActionResults { public sealed class ToothbrushesController : BaseToothbrushesController { - public ToothbrushesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public ToothbrushesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs index 494a9b66e0..8bf3ab0a72 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -8,8 +9,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CustomRoutes { - public sealed class ApiControllerAttributeTests - : IClassFixture, CustomRouteDbContext>> + public sealed class ApiControllerAttributeTests : IClassFixture, CustomRouteDbContext>> { private readonly ExampleIntegrationTestContext, CustomRouteDbContext> _testContext; @@ -25,14 +25,14 @@ public async Task ApiController_attribute_transforms_NotFound_action_result_with const string route = "/world-civilians/missing"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.Links.About.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.4"); } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CiviliansController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CiviliansController.cs index e8a730e15c..7a0ef9bf3b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CiviliansController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CiviliansController.cs @@ -13,8 +13,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CustomRoutes [Route("world-civilians")] public sealed class CiviliansController : JsonApiController { - public CiviliansController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public CiviliansController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs index aaebbce763..7bee8663e2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs @@ -1,5 +1,7 @@ +using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -9,8 +11,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.CustomRoutes { - public sealed class CustomRouteTests - : IClassFixture, CustomRouteDbContext>> + public sealed class CustomRouteTests : IClassFixture, CustomRouteDbContext>> { private const string HostPrefix = "http://localhost"; @@ -26,7 +27,7 @@ public CustomRouteTests(ExampleIntegrationTestContext { @@ -34,10 +35,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/world-api/civilization/popular/towns/" + town.StringId; + string route = "/world-api/civilization/popular/towns/" + town.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -58,7 +59,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_get_resources_at_custom_action_method() { // Arrange - var town = _fakers.Town.Generate(7); + List town = _fakers.Town.Generate(7); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -70,7 +71,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/world-api/civilization/popular/towns/largest-5"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Town.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Town.cs index ba0ba27fe2..1242def3a3 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Town.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/Town.cs @@ -13,7 +13,7 @@ public sealed class Town : Identifiable [Attr] public double Latitude { get; set; } - + [Attr] public double Longitude { get; set; } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/TownsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/TownsController.cs index a39b9c96c9..4f65adaaf2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/TownsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/CustomRoutes/TownsController.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -17,8 +18,7 @@ public sealed class TownsController : JsonApiController { private readonly CustomRouteDbContext _dbContext; - public TownsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService, CustomRouteDbContext dbContext) + public TownsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService, CustomRouteDbContext dbContext) : base(options, loggerFactory, resourceService) { _dbContext = dbContext; @@ -27,11 +27,9 @@ public TownsController(IJsonApiOptions options, ILoggerFactory loggerFactory, [HttpGet("largest-{count}")] public async Task GetLargestTownsAsync(int count, CancellationToken cancellationToken) { - var query = _dbContext.Towns - .OrderByDescending(town => town.Civilians.Count) - .Take(count); + IQueryable query = _dbContext.Towns.OrderByDescending(town => town.Civilians.Count).Take(count); - var results = await query.ToListAsync(cancellationToken); + List results = await query.ToListAsync(cancellationToken); return Ok(results); } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Building.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Building.cs index 79af5eac30..48334083aa 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Building.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/Building.cs @@ -47,7 +47,7 @@ public string PrimaryDoorColor [EagerLoad] public Door PrimaryDoor { get; set; } - + [EagerLoad] public Door SecondaryDoor { get; set; } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs index 78501d46b2..8bf5086e06 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingRepository.cs @@ -13,16 +13,15 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class BuildingRepository : EntityFrameworkCoreRepository { - public BuildingRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, - IResourceGraph resourceGraph, IResourceFactory resourceFactory, - IEnumerable constraintProviders, ILoggerFactory loggerFactory) + public BuildingRepository(ITargetedFields targetedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, + IResourceFactory resourceFactory, IEnumerable constraintProviders, ILoggerFactory loggerFactory) : base(targetedFields, contextResolver, resourceGraph, resourceFactory, constraintProviders, loggerFactory) { } public override async Task GetForCreateAsync(int id, CancellationToken cancellationToken) { - var building = await base.GetForCreateAsync(id, cancellationToken); + Building building = await base.GetForCreateAsync(id, cancellationToken); // Must ensure that an instance exists for this required relationship, so that POST succeeds. building.PrimaryDoor = new Door(); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingsController.cs index 4a0b9bb366..be28a84035 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/BuildingsController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { public sealed class BuildingsController : JsonApiController { - public BuildingsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public BuildingsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs index 2acabc76c4..862c6d12fd 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -10,8 +11,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { - public sealed class EagerLoadingTests - : IClassFixture, EagerLoadingDbContext>> + public sealed class EagerLoadingTests : IClassFixture, EagerLoadingDbContext>> { private readonly ExampleIntegrationTestContext, EagerLoadingDbContext> _testContext; private readonly EagerLoadingFakers _fakers = new EagerLoadingFakers(); @@ -30,7 +30,7 @@ public EagerLoadingTests(ExampleIntegrationTestContext await dbContext.SaveChangesAsync(); }); - var route = "/buildings/" + building.StringId; + string route = "/buildings/" + building.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -61,7 +61,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_get_primary_resource_with_nested_eager_loads() { // Arrange - var street = _fakers.Street.Generate(); + Street street = _fakers.Street.Generate(); street.Buildings = _fakers.Building.Generate(2); street.Buildings[0].Windows = _fakers.Window.Generate(2); @@ -77,10 +77,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/streets/" + street.StringId; + string route = "/streets/" + street.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -97,7 +97,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_get_primary_resource_with_fieldset() { // Arrange - var street = _fakers.Street.Generate(); + Street street = _fakers.Street.Generate(); street.Buildings = _fakers.Building.Generate(1); street.Buildings[0].Windows = _fakers.Window.Generate(3); street.Buildings[0].PrimaryDoor = _fakers.Door.Generate(); @@ -108,10 +108,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/streets/{street.StringId}?fields[streets]=windowTotalCount"; + string route = $"/streets/{street.StringId}?fields[streets]=windowTotalCount"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -127,7 +127,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_get_primary_resource_with_includes() { // Arrange - var state = _fakers.State.Generate(); + State state = _fakers.State.Generate(); state.Cities = _fakers.City.Generate(1); state.Cities[0].Streets = _fakers.Street.Generate(1); state.Cities[0].Streets[0].Buildings = _fakers.Building.Generate(1); @@ -140,10 +140,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/states/{state.StringId}?include=cities.streets"; + string route = $"/states/{state.StringId}?include=cities.streets"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -169,7 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_get_secondary_resources_with_include_and_fieldsets() { // Arrange - var state = _fakers.State.Generate(); + State state = _fakers.State.Generate(); state.Cities = _fakers.City.Generate(1); state.Cities[0].Streets = _fakers.Street.Generate(1); state.Cities[0].Streets[0].Buildings = _fakers.Building.Generate(1); @@ -183,10 +183,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/states/{state.StringId}/cities?include=streets&fields[cities]=name&fields[streets]=doorTotalCount,windowTotalCount"; + string route = $"/states/{state.StringId}/cities?include=streets&fields[cities]=name&fields[streets]=doorTotalCount,windowTotalCount"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -210,7 +210,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_resource() { // Arrange - var newBuilding = _fakers.Building.Generate(); + Building newBuilding = _fakers.Building.Generate(); var requestBody = new { @@ -227,7 +227,7 @@ public async Task Can_create_resource() const string route = "/buildings"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -238,14 +238,14 @@ public async Task Can_create_resource() responseDocument.SingleData.Attributes["primaryDoorColor"].Should().BeNull(); responseDocument.SingleData.Attributes["secondaryDoorColor"].Should().BeNull(); - var newBuildingId = int.Parse(responseDocument.SingleData.Id); + int newBuildingId = int.Parse(responseDocument.SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var buildingInDatabase = await dbContext.Buildings + Building buildingInDatabase = await dbContext.Buildings .Include(building => building.PrimaryDoor) .Include(building => building.SecondaryDoor) .Include(building => building.Windows) @@ -266,13 +266,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource() { // Arrange - var existingBuilding = _fakers.Building.Generate(); + Building existingBuilding = _fakers.Building.Generate(); existingBuilding.PrimaryDoor = _fakers.Door.Generate(); existingBuilding.SecondaryDoor = _fakers.Door.Generate(); existingBuilding.Windows = _fakers.Window.Generate(2); - var newBuildingNumber = _fakers.Building.Generate().Number; - var newPrimaryDoorColor = _fakers.Door.Generate().Color; + string newBuildingNumber = _fakers.Building.Generate().Number; + string newPrimaryDoorColor = _fakers.Door.Generate().Color; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -294,10 +294,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/buildings/" + existingBuilding.StringId; + string route = "/buildings/" + existingBuilding.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -309,7 +309,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true - var buildingInDatabase = await dbContext.Buildings + Building buildingInDatabase = await dbContext.Buildings .Include(building => building.PrimaryDoor) .Include(building => building.SecondaryDoor) .Include(building => building.Windows) @@ -331,7 +331,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_delete_resource() { // Arrange - var existingBuilding = _fakers.Building.Generate(); + Building existingBuilding = _fakers.Building.Generate(); existingBuilding.PrimaryDoor = _fakers.Door.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -340,10 +340,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/buildings/" + existingBuilding.StringId; + string route = "/buildings/" + existingBuilding.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -352,7 +352,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var buildingInDatabase = await dbContext.Buildings.FirstWithIdOrDefaultAsync(existingBuilding.Id); + Building buildingInDatabase = await dbContext.Buildings.FirstWithIdOrDefaultAsync(existingBuilding.Id); buildingInDatabase.Should().BeNull(); }); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StatesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StatesController.cs index 28c8b795b8..bbaf58cfb2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StatesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StatesController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { public sealed class StatesController : JsonApiController { - public StatesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public StatesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StreetsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StreetsController.cs index e6b4eda7e1..04b4275495 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StreetsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/EagerLoading/StreetsController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.EagerLoading { public sealed class StreetsController : JsonApiController { - public StreetsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public StreetsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs index 7013299c6a..2b63cf8a1e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticleService.cs @@ -16,22 +16,19 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ExceptionHandling [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class ConsumerArticleService : JsonApiResourceService { - internal const string UnavailableArticlePrefix = "X"; - private const string SupportEmailAddress = "company@email.com"; + internal const string UnavailableArticlePrefix = "X"; public ConsumerArticleService(IResourceRepositoryAccessor repositoryAccessor, IQueryLayerComposer queryLayerComposer, - IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, - IJsonApiRequest request, IResourceChangeTracker resourceChangeTracker, - IResourceHookExecutorFacade hookExecutor) - : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, - resourceChangeTracker, hookExecutor) + IPaginationContext paginationContext, IJsonApiOptions options, ILoggerFactory loggerFactory, IJsonApiRequest request, + IResourceChangeTracker resourceChangeTracker, IResourceHookExecutorFacade hookExecutor) + : base(repositoryAccessor, queryLayerComposer, paginationContext, options, loggerFactory, request, resourceChangeTracker, hookExecutor) { } public override async Task GetAsync(int id, CancellationToken cancellationToken) { - var consumerArticle = await base.GetAsync(id, cancellationToken); + ConsumerArticle consumerArticle = await base.GetAsync(id, cancellationToken); if (consumerArticle.Code.StartsWith(UnavailableArticlePrefix, StringComparison.Ordinal)) { diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticlesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticlesController.cs index dcc0ad7e8e..7b36586706 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticlesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ConsumerArticlesController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ExceptionHandling { public sealed class ConsumerArticlesController : JsonApiController { - public ConsumerArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public ConsumerArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs index b86bd84787..93801ce51c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs @@ -1,5 +1,7 @@ +using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -14,8 +16,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ExceptionHandling { - public sealed class ExceptionHandlerTests - : IClassFixture, ErrorDbContext>> + public sealed class ExceptionHandlerTests : IClassFixture, ErrorDbContext>> { private readonly ExampleIntegrationTestContext, ErrorDbContext> _testContext; @@ -67,17 +68,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/consumerArticles/" + consumerArticle.StringId; + string route = "/consumerArticles/" + consumerArticle.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Gone); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Gone); error.Title.Should().Be("The requested article is no longer available."); error.Detail.Should().Be("Article with code 'X123' is no longer available."); @@ -103,22 +104,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/throwingArticles/" + throwingArticle.StringId; + string route = "/throwingArticles/" + throwingArticle.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); error.Title.Should().Be("An unhandled error occurred while processing this request."); error.Detail.Should().Be("Exception has been thrown by the target of an invocation."); - var stackTraceLines = ((JArray) error.Meta.Data["stackTrace"]).Select(token => token.Value()); + IEnumerable stackTraceLines = ((JArray)error.Meta.Data["stackTrace"]).Select(token => token.Value()); stackTraceLines.Should().ContainMatch("* System.InvalidOperationException: Article status could not be determined.*"); loggerFactory.Logger.Messages.Should().HaveCount(1); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ThrowingArticlesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ThrowingArticlesController.cs index 6616498f85..f2e6def6ed 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ThrowingArticlesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ExceptionHandling/ThrowingArticlesController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ExceptionHandling { public sealed class ThrowingArticlesController : JsonApiController { - public ThrowingArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public ThrowingArticlesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGalleriesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGalleriesController.cs index d7383df1eb..4ed2ccb73e 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGalleriesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/ArtGalleriesController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS { public sealed class ArtGalleriesController : JsonApiController { - public ArtGalleriesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public ArtGalleriesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingFakers.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingFakers.cs index 428c27ad69..0bbe24f429 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingFakers.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingFakers.cs @@ -2,6 +2,9 @@ using Bogus; using TestBuildingBlocks; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS { internal sealed class HostingFakers : FakerContainer diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs index e3e35b7f72..954b59fa5f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/HostingTests.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -8,8 +9,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS { - public sealed class HostingTests - : IClassFixture, HostingDbContext>> + public sealed class HostingTests : IClassFixture, HostingDbContext>> { private const string HostPrefix = "http://localhost"; @@ -25,7 +25,7 @@ public HostingTests(ExampleIntegrationTestContext @@ -38,7 +38,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/iis-application-virtual-directory/public-api/artGalleries?include=paintings"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -71,7 +71,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_primary_resources_with_include_on_custom_route_returns_links() { // Arrange - var painting = _fakers.Painting.Generate(); + Painting painting = _fakers.Painting.Generate(); painting.ExposedAt = _fakers.ArtGallery.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -84,7 +84,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/iis-application-virtual-directory/custom/path/to/paintings?include=exposedAt"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/PaintingsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/PaintingsController.cs index ced2427169..5a6a51da2f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/PaintingsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/HostingInIIS/PaintingsController.cs @@ -7,11 +7,11 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.HostingInIIS { - [DisableRoutingConvention, Route("custom/path/to/paintings")] + [DisableRoutingConvention] + [Route("custom/path/to/paintings")] public sealed class PaintingsController : JsonApiController { - public PaintingsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public PaintingsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/BankAccountsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/BankAccountsController.cs index 91793dfc8c..c7751fde1a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/BankAccountsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/BankAccountsController.cs @@ -6,8 +6,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation { public sealed class BankAccountsController : ObfuscatedIdentifiableController { - public BankAccountsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public BankAccountsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/DebitCardsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/DebitCardsController.cs index b72cea109e..d17fb2b016 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/DebitCardsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/DebitCardsController.cs @@ -6,8 +6,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation { public sealed class DebitCardsController : ObfuscatedIdentifiableController { - public DebitCardsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public DebitCardsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs index cc563b5f4b..19dfbea6b0 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs @@ -32,15 +32,16 @@ public static int Decode(string value) private static string FromHexString(string hexString) { - List bytes = new List(hexString.Length / 2); + var bytes = new List(hexString.Length / 2); + for (int index = 0; index < hexString.Length; index += 2) { - var hexChar = hexString.Substring(index, 2); + string hexChar = hexString.Substring(index, 2); byte bt = byte.Parse(hexChar, NumberStyles.HexNumber); bytes.Add(bt); } - var chars = Encoding.ASCII.GetChars(bytes.ToArray()); + char[] chars = Encoding.ASCII.GetChars(bytes.ToArray()); return new string(chars); } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs index 71e130bb4b..99da5dacb9 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -9,8 +11,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation { - public sealed class IdObfuscationTests - : IClassFixture, ObfuscationDbContext>> + public sealed class IdObfuscationTests : IClassFixture, ObfuscationDbContext>> { private readonly ExampleIntegrationTestContext, ObfuscationDbContext> _testContext; private readonly ObfuscationFakers _fakers = new ObfuscationFakers(); @@ -24,7 +25,7 @@ public IdObfuscationTests(ExampleIntegrationTestContext accounts = _fakers.BankAccount.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -33,10 +34,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/bankAccounts?filter=equals(id,'{accounts[1].StringId}')"; + string route = $"/bankAccounts?filter=equals(id,'{accounts[1].StringId}')"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -49,7 +50,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_filter_any_in_primary_resources() { // Arrange - var accounts = _fakers.BankAccount.Generate(2); + List accounts = _fakers.BankAccount.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -58,10 +59,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/bankAccounts?filter=any(id,'{accounts[1].StringId}','{HexadecimalCodec.Encode(99999999)}')"; + string route = $"/bankAccounts?filter=any(id,'{accounts[1].StringId}','{HexadecimalCodec.Encode(99999999)}')"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -77,14 +78,14 @@ public async Task Cannot_get_primary_resource_for_invalid_ID() const string route = "/bankAccounts/not-a-hex-value"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Invalid ID value."); error.Detail.Should().Be("The value 'not-a-hex-value' is not a valid hexadecimal value."); @@ -94,7 +95,7 @@ public async Task Cannot_get_primary_resource_for_invalid_ID() public async Task Can_get_primary_resource_by_ID() { // Arrange - var card = _fakers.DebitCard.Generate(); + DebitCard card = _fakers.DebitCard.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -102,10 +103,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/debitCards/" + card.StringId; + string route = "/debitCards/" + card.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -118,7 +119,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_get_secondary_resources() { // Arrange - var account = _fakers.BankAccount.Generate(); + BankAccount account = _fakers.BankAccount.Generate(); account.Cards = _fakers.DebitCard.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -127,10 +128,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/bankAccounts/{account.StringId}/cards"; + string route = $"/bankAccounts/{account.StringId}/cards"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -144,7 +145,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_include_resource_with_sparse_fieldset() { // Arrange - var account = _fakers.BankAccount.Generate(); + BankAccount account = _fakers.BankAccount.Generate(); account.Cards = _fakers.DebitCard.Generate(1); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -153,10 +154,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/bankAccounts/{account.StringId}?include=cards&fields[debitCards]=ownerName"; + string route = $"/bankAccounts/{account.StringId}?include=cards&fields[debitCards]=ownerName"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -174,7 +175,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_get_relationship() { // Arrange - var account = _fakers.BankAccount.Generate(); + BankAccount account = _fakers.BankAccount.Generate(); account.Cards = _fakers.DebitCard.Generate(1); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -183,10 +184,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/bankAccounts/{account.StringId}/relationships/cards"; + string route = $"/bankAccounts/{account.StringId}/relationships/cards"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -199,8 +200,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_resource_with_relationship() { // Arrange - var existingAccount = _fakers.BankAccount.Generate(); - var newCard = _fakers.DebitCard.Generate(); + BankAccount existingAccount = _fakers.BankAccount.Generate(); + DebitCard newCard = _fakers.DebitCard.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -235,7 +236,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/debitCards"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -243,13 +244,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.SingleData.Attributes["ownerName"].Should().Be(newCard.OwnerName); responseDocument.SingleData.Attributes["pinCode"].Should().Be(newCard.PinCode); - var newCardId = HexadecimalCodec.Decode(responseDocument.SingleData.Id); + int newCardId = HexadecimalCodec.Decode(responseDocument.SingleData.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { - var cardInDatabase = await dbContext.DebitCards - .Include(card => card.Account) - .FirstWithIdAsync(newCardId); + DebitCard cardInDatabase = await dbContext.DebitCards.Include(card => card.Account).FirstWithIdAsync(newCardId); cardInDatabase.OwnerName.Should().Be(newCard.OwnerName); cardInDatabase.PinCode.Should().Be(newCard.PinCode); @@ -264,12 +263,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_update_resource_with_relationship() { // Arrange - var existingAccount = _fakers.BankAccount.Generate(); + BankAccount existingAccount = _fakers.BankAccount.Generate(); existingAccount.Cards = _fakers.DebitCard.Generate(1); - var existingCard = _fakers.DebitCard.Generate(); + DebitCard existingCard = _fakers.DebitCard.Generate(); - var newIban = _fakers.BankAccount.Generate().Iban; + string newIban = _fakers.BankAccount.Generate().Iban; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -303,11 +302,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } } }; - - var route = "/bankAccounts/" + existingAccount.StringId; + + string route = "/bankAccounts/" + existingAccount.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -316,9 +315,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var accountInDatabase = await dbContext.BankAccounts - .Include(account => account.Cards) - .FirstWithIdAsync(existingAccount.Id); + BankAccount accountInDatabase = await dbContext.BankAccounts.Include(account => account.Cards).FirstWithIdAsync(existingAccount.Id); accountInDatabase.Iban.Should().Be(newIban); @@ -326,17 +323,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => accountInDatabase.Cards[0].Id.Should().Be(existingCard.Id); accountInDatabase.Cards[0].StringId.Should().Be(existingCard.StringId); }); - } [Fact] public async Task Can_add_to_ToMany_relationship() { // Arrange - var existingAccount = _fakers.BankAccount.Generate(); + BankAccount existingAccount = _fakers.BankAccount.Generate(); existingAccount.Cards = _fakers.DebitCard.Generate(1); - var existingDebitCard = _fakers.DebitCard.Generate(); + DebitCard existingDebitCard = _fakers.DebitCard.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -355,11 +351,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } } }; - - var route = $"/bankAccounts/{existingAccount.StringId}/relationships/cards"; + + string route = $"/bankAccounts/{existingAccount.StringId}/relationships/cards"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -368,9 +364,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var accountInDatabase = await dbContext.BankAccounts - .Include(account => account.Cards) - .FirstWithIdAsync(existingAccount.Id); + BankAccount accountInDatabase = await dbContext.BankAccounts.Include(account => account.Cards).FirstWithIdAsync(existingAccount.Id); accountInDatabase.Cards.Should().HaveCount(2); }); @@ -380,7 +374,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_remove_from_ToMany_relationship() { // Arrange - var existingAccount = _fakers.BankAccount.Generate(); + BankAccount existingAccount = _fakers.BankAccount.Generate(); existingAccount.Cards = _fakers.DebitCard.Generate(2); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -400,11 +394,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } } }; - - var route = $"/bankAccounts/{existingAccount.StringId}/relationships/cards"; + + string route = $"/bankAccounts/{existingAccount.StringId}/relationships/cards"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -413,9 +407,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var accountInDatabase = await dbContext.BankAccounts - .Include(account => account.Cards) - .FirstWithIdAsync(existingAccount.Id); + BankAccount accountInDatabase = await dbContext.BankAccounts.Include(account => account.Cards).FirstWithIdAsync(existingAccount.Id); accountInDatabase.Cards.Should().HaveCount(1); }); @@ -425,7 +417,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_delete_resource() { // Arrange - var existingAccount = _fakers.BankAccount.Generate(); + BankAccount existingAccount = _fakers.BankAccount.Generate(); existingAccount.Cards = _fakers.DebitCard.Generate(1); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -434,10 +426,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/bankAccounts/" + existingAccount.StringId; + string route = "/bankAccounts/" + existingAccount.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -446,9 +438,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - var accountInDatabase = await dbContext.BankAccounts - .Include(account => account.Cards) - .FirstWithIdOrDefaultAsync(existingAccount.Id); + BankAccount accountInDatabase = await dbContext.BankAccounts.Include(account => account.Cards).FirstWithIdOrDefaultAsync(existingAccount.Id); accountInDatabase.Should().BeNull(); }); @@ -458,19 +448,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_delete_missing_resource() { // Arrange - var stringId = HexadecimalCodec.Encode(99999999); + string stringId = HexadecimalCodec.Encode(99999999); - var route = "/bankAccounts/" + stringId; + string route = "/bankAccounts/" + stringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error 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 'bankAccounts' with ID '{stringId}' does not exist."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs index c762873886..ee6e3de278 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs @@ -13,8 +13,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.IdObfuscation public abstract class ObfuscatedIdentifiableController : BaseJsonApiController where TResource : class, IIdentifiable { - protected ObfuscatedIdentifiableController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + protected ObfuscatedIdentifiableController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } @@ -53,8 +52,8 @@ public override Task PostAsync([FromBody] TResource resource, Can } [HttpPost("{id}/relationships/{relationshipName}")] - public Task PostRelationshipAsync(string id, string relationshipName, - [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken) + public Task PostRelationshipAsync(string id, string relationshipName, [FromBody] ISet secondaryResourceIds, + CancellationToken cancellationToken) { int idValue = HexadecimalCodec.Decode(id); return base.PostRelationshipAsync(idValue, relationshipName, secondaryResourceIds, cancellationToken); @@ -68,8 +67,8 @@ public Task PatchAsync(string id, [FromBody] TResource resource, } [HttpPatch("{id}/relationships/{relationshipName}")] - public Task PatchRelationshipAsync(string id, string relationshipName, - [FromBody] object secondaryResourceIds, CancellationToken cancellationToken) + public Task PatchRelationshipAsync(string id, string relationshipName, [FromBody] object secondaryResourceIds, + CancellationToken cancellationToken) { int idValue = HexadecimalCodec.Decode(id); return base.PatchRelationshipAsync(idValue, relationshipName, secondaryResourceIds, cancellationToken); @@ -83,8 +82,8 @@ public Task DeleteAsync(string id, CancellationToken cancellation } [HttpDelete("{id}/relationships/{relationshipName}")] - public Task DeleteRelationshipAsync(string id, string relationshipName, - [FromBody] ISet secondaryResourceIds, CancellationToken cancellationToken) + public Task DeleteRelationshipAsync(string id, string relationshipName, [FromBody] ISet secondaryResourceIds, + CancellationToken cancellationToken) { int idValue = HexadecimalCodec.Decode(id); return base.DeleteRelationshipAsync(idValue, relationshipName, secondaryResourceIds, cancellationToken); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs index 36ca78da28..fdca0b77dd 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -29,7 +30,7 @@ public AbsoluteLinksWithNamespaceTests(ExampleIntegrationTestContext), typeof(NeverSameResourceChangeTracker<>)); }); - var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); options.IncludeTotalResourceCount = true; } @@ -37,7 +38,7 @@ public AbsoluteLinksWithNamespaceTests(ExampleIntegrationTestContext { @@ -45,10 +46,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/api/photoAlbums/" + album.StringId; + string route = "/api/photoAlbums/" + album.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -70,7 +71,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_primary_resources_with_include_returns_absolute_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -83,7 +84,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/api/photoAlbums?include=photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -114,7 +115,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_secondary_resource_returns_absolute_links() { // Arrange - var photo = _fakers.Photo.Generate(); + Photo photo = _fakers.Photo.Generate(); photo.Album = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -123,10 +124,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/api/photos/{photo.StringId}/album"; + string route = $"/api/photos/{photo.StringId}/album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -150,7 +151,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_secondary_resources_returns_absolute_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -159,10 +160,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/api/photoAlbums/{album.StringId}/photos"; + string route = $"/api/photoAlbums/{album.StringId}/photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -186,7 +187,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_HasOne_relationship_returns_absolute_links() { // Arrange - var photo = _fakers.Photo.Generate(); + Photo photo = _fakers.Photo.Generate(); photo.Album = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -195,10 +196,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/api/photos/{photo.StringId}/relationships/album"; + string route = $"/api/photos/{photo.StringId}/relationships/album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -219,7 +220,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_HasMany_relationship_returns_absolute_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -228,10 +229,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/api/photoAlbums/{album.StringId}/relationships/photos"; + string route = $"/api/photoAlbums/{album.StringId}/relationships/photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -252,7 +253,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Create_resource_with_side_effects_and_include_returns_absolute_links() { // Arrange - var existingPhoto = _fakers.Photo.Generate(); + Photo existingPhoto = _fakers.Photo.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -285,7 +286,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/api/photoAlbums?include=photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -315,8 +316,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_resource_with_side_effects_and_include_returns_absolute_links() { // Arrange - var existingPhoto = _fakers.Photo.Generate(); - var existingAlbum = _fakers.PhotoAlbum.Generate(); + Photo existingPhoto = _fakers.Photo.Generate(); + PhotoAlbum existingAlbum = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -344,10 +345,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = $"/api/photos/{existingPhoto.StringId}?include=album"; + string route = $"/api/photos/{existingPhoto.StringId}?include=album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs index 37faa2c92a..e625ae5c55 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -29,7 +30,7 @@ public AbsoluteLinksWithoutNamespaceTests(ExampleIntegrationTestContext), typeof(NeverSameResourceChangeTracker<>)); }); - var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); options.IncludeTotalResourceCount = true; } @@ -37,7 +38,7 @@ public AbsoluteLinksWithoutNamespaceTests(ExampleIntegrationTestContext { @@ -45,10 +46,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/photoAlbums/" + album.StringId; + string route = "/photoAlbums/" + album.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -70,7 +71,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_primary_resources_with_include_returns_absolute_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -83,7 +84,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/photoAlbums?include=photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -114,7 +115,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_secondary_resource_returns_absolute_links() { // Arrange - var photo = _fakers.Photo.Generate(); + Photo photo = _fakers.Photo.Generate(); photo.Album = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -123,10 +124,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/photos/{photo.StringId}/album"; + string route = $"/photos/{photo.StringId}/album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -150,7 +151,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_secondary_resources_returns_absolute_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -159,10 +160,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/photoAlbums/{album.StringId}/photos"; + string route = $"/photoAlbums/{album.StringId}/photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -186,7 +187,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_HasOne_relationship_returns_absolute_links() { // Arrange - var photo = _fakers.Photo.Generate(); + Photo photo = _fakers.Photo.Generate(); photo.Album = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -195,10 +196,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/photos/{photo.StringId}/relationships/album"; + string route = $"/photos/{photo.StringId}/relationships/album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -219,7 +220,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_HasMany_relationship_returns_absolute_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -228,10 +229,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/photoAlbums/{album.StringId}/relationships/photos"; + string route = $"/photoAlbums/{album.StringId}/relationships/photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -252,7 +253,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Create_resource_with_side_effects_and_include_returns_absolute_links() { // Arrange - var existingPhoto = _fakers.Photo.Generate(); + Photo existingPhoto = _fakers.Photo.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -285,7 +286,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/photoAlbums?include=photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -315,8 +316,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_resource_with_side_effects_and_include_returns_absolute_links() { // Arrange - var existingPhoto = _fakers.Photo.Generate(); - var existingAlbum = _fakers.PhotoAlbum.Generate(); + Photo existingPhoto = _fakers.Photo.Generate(); + PhotoAlbum existingAlbum = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -344,10 +345,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = $"/photos/{existingPhoto.StringId}?include=album"; + string route = $"/photos/{existingPhoto.StringId}?include=album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinkInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinkInclusionTests.cs index 760a1ebb8c..b7838e77e1 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinkInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/LinkInclusionTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -8,8 +9,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links { - public sealed class LinkInclusionTests - : IClassFixture, LinksDbContext>> + public sealed class LinkInclusionTests : IClassFixture, LinksDbContext>> { private readonly ExampleIntegrationTestContext, LinksDbContext> _testContext; private readonly LinksFakers _fakers = new LinksFakers(); @@ -23,7 +23,7 @@ public LinkInclusionTests(ExampleIntegrationTestContext await dbContext.SaveChangesAsync(); }); - var route = $"/photoLocations/{location.StringId}?include=photo,album"; + string route = $"/photoLocations/{location.StringId}?include=photo,album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoAlbumsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoAlbumsController.cs index 8d13f66b99..bc40b8e1c0 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoAlbumsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoAlbumsController.cs @@ -8,8 +8,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links { public sealed class PhotoAlbumsController : JsonApiController { - public PhotoAlbumsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public PhotoAlbumsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoLocationsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoLocationsController.cs index 1398ee84b1..0da524de7f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoLocationsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotoLocationsController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links { public sealed class PhotoLocationsController : JsonApiController { - public PhotoLocationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public PhotoLocationsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotosController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotosController.cs index e0dcb9a316..b7b2660b6d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotosController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/PhotosController.cs @@ -8,8 +8,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Links { public sealed class PhotosController : JsonApiController { - public PhotosController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public PhotosController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs index 7dbd40ae0f..91fa367b04 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -27,7 +28,7 @@ public RelativeLinksWithNamespaceTests(ExampleIntegrationTestContext), typeof(NeverSameResourceChangeTracker<>)); }); - var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); options.IncludeTotalResourceCount = true; } @@ -35,7 +36,7 @@ public RelativeLinksWithNamespaceTests(ExampleIntegrationTestContext { @@ -43,10 +44,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/api/photoAlbums/" + album.StringId; + string route = "/api/photoAlbums/" + album.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -68,7 +69,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_primary_resources_with_include_returns_relative_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -81,7 +82,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/api/photoAlbums?include=photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -112,7 +113,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_secondary_resource_returns_relative_links() { // Arrange - var photo = _fakers.Photo.Generate(); + Photo photo = _fakers.Photo.Generate(); photo.Album = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -121,10 +122,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/api/photos/{photo.StringId}/album"; + string route = $"/api/photos/{photo.StringId}/album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -148,7 +149,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_secondary_resources_returns_relative_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -157,10 +158,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/api/photoAlbums/{album.StringId}/photos"; + string route = $"/api/photoAlbums/{album.StringId}/photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -184,7 +185,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_HasOne_relationship_returns_relative_links() { // Arrange - var photo = _fakers.Photo.Generate(); + Photo photo = _fakers.Photo.Generate(); photo.Album = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -193,10 +194,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/api/photos/{photo.StringId}/relationships/album"; + string route = $"/api/photos/{photo.StringId}/relationships/album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -217,7 +218,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_HasMany_relationship_returns_relative_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -226,10 +227,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/api/photoAlbums/{album.StringId}/relationships/photos"; + string route = $"/api/photoAlbums/{album.StringId}/relationships/photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -250,7 +251,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Create_resource_with_side_effects_and_include_returns_relative_links() { // Arrange - var existingPhoto = _fakers.Photo.Generate(); + Photo existingPhoto = _fakers.Photo.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -283,7 +284,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/api/photoAlbums?include=photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -313,8 +314,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_resource_with_side_effects_and_include_returns_relative_links() { // Arrange - var existingPhoto = _fakers.Photo.Generate(); - var existingAlbum = _fakers.PhotoAlbum.Generate(); + Photo existingPhoto = _fakers.Photo.Generate(); + PhotoAlbum existingAlbum = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -342,10 +343,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = $"/api/photos/{existingPhoto.StringId}?include=album"; + string route = $"/api/photos/{existingPhoto.StringId}?include=album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs index b42f2b8c1b..13acd8c735 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -27,7 +28,7 @@ public RelativeLinksWithoutNamespaceTests(ExampleIntegrationTestContext), typeof(NeverSameResourceChangeTracker<>)); }); - var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); options.IncludeTotalResourceCount = true; } @@ -35,7 +36,7 @@ public RelativeLinksWithoutNamespaceTests(ExampleIntegrationTestContext { @@ -43,10 +44,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = "/photoAlbums/" + album.StringId; + string route = "/photoAlbums/" + album.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -68,7 +69,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_primary_resources_with_include_returns_relative_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -81,7 +82,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/photoAlbums?include=photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -112,7 +113,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_secondary_resource_returns_relative_links() { // Arrange - var photo = _fakers.Photo.Generate(); + Photo photo = _fakers.Photo.Generate(); photo.Album = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -121,10 +122,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/photos/{photo.StringId}/album"; + string route = $"/photos/{photo.StringId}/album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -148,7 +149,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_secondary_resources_returns_relative_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -157,10 +158,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/photoAlbums/{album.StringId}/photos"; + string route = $"/photoAlbums/{album.StringId}/photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -184,7 +185,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_HasOne_relationship_returns_relative_links() { // Arrange - var photo = _fakers.Photo.Generate(); + Photo photo = _fakers.Photo.Generate(); photo.Album = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -193,10 +194,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/photos/{photo.StringId}/relationships/album"; + string route = $"/photos/{photo.StringId}/relationships/album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -217,7 +218,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Get_HasMany_relationship_returns_relative_links() { // Arrange - var album = _fakers.PhotoAlbum.Generate(); + PhotoAlbum album = _fakers.PhotoAlbum.Generate(); album.Photos = _fakers.Photo.Generate(1).ToHashSet(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -226,10 +227,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/photoAlbums/{album.StringId}/relationships/photos"; + string route = $"/photoAlbums/{album.StringId}/relationships/photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -250,7 +251,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Create_resource_with_side_effects_and_include_returns_relative_links() { // Arrange - var existingPhoto = _fakers.Photo.Generate(); + Photo existingPhoto = _fakers.Photo.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -283,7 +284,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/photoAlbums?include=photos"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -313,8 +314,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Update_resource_with_side_effects_and_include_returns_relative_links() { // Arrange - var existingPhoto = _fakers.Photo.Generate(); - var existingAlbum = _fakers.PhotoAlbum.Generate(); + Photo existingPhoto = _fakers.Photo.Generate(); + PhotoAlbum existingAlbum = _fakers.PhotoAlbum.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -342,10 +343,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = $"/photos/{existingPhoto.StringId}?include=album"; + string route = $"/photos/{existingPhoto.StringId}?include=album"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditEntriesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditEntriesController.cs index 5a1e4d74e7..b825a3fb0b 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditEntriesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/AuditEntriesController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Logging { public sealed class AuditEntriesController : JsonApiController { - public AuditEntriesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public AuditEntriesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs index 44ba7be6c4..9e9c1085df 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Logging/LoggingTests.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCoreExampleTests.Startups; @@ -10,8 +11,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Logging { - public sealed class LoggingTests - : IClassFixture, AuditDbContext>> + public sealed class LoggingTests : IClassFixture, AuditDbContext>> { private readonly ExampleIntegrationTestContext, AuditDbContext> _testContext; private readonly AuditFakers _fakers = new AuditFakers(); @@ -48,7 +48,7 @@ public async Task Logs_request_body_at_Trace_level() var loggerFactory = _testContext.Factory.Services.GetRequiredService(); loggerFactory.Logger.Clear(); - var newEntry = _fakers.AuditEntry.Generate(); + AuditEntry newEntry = _fakers.AuditEntry.Generate(); var requestBody = new { @@ -67,7 +67,7 @@ public async Task Logs_request_body_at_Trace_level() const string route = "/auditEntries"; // Act - var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -89,14 +89,14 @@ public async Task Logs_response_body_at_Trace_level() const string route = "/auditEntries"; // Act - var (httpResponse, _) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); loggerFactory.Logger.Messages.Should().NotBeEmpty(); - loggerFactory.Logger.Messages.Should().ContainSingle(message => message.LogLevel == LogLevel.Trace && + loggerFactory.Logger.Messages.Should().ContainSingle(message => message.LogLevel == LogLevel.Trace && message.Text.StartsWith("Sending 200 response for request at 'http://localhost/auditEntries' with body: <<", StringComparison.Ordinal)); } @@ -113,7 +113,7 @@ public async Task Logs_invalid_request_body_error_at_Information_level() const string route = "/auditEntries"; // Act - var (httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ProductFamiliesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ProductFamiliesController.cs index 94b827c2f7..a276769c63 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ProductFamiliesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ProductFamiliesController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta { public sealed class ProductFamiliesController : JsonApiController { - public ProductFamiliesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public ProductFamiliesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs index 951f752d20..c47ab9aaa2 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Resources; @@ -10,8 +12,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta { - public sealed class ResourceMetaTests - : IClassFixture, SupportDbContext>> + public sealed class ResourceMetaTests : IClassFixture, SupportDbContext>> { private readonly ExampleIntegrationTestContext, SupportDbContext> _testContext; private readonly SupportFakers _fakers = new SupportFakers(); @@ -30,7 +31,7 @@ public ResourceMetaTests(ExampleIntegrationTestContext tickets = _fakers.SupportTicket.Generate(3); tickets[0].Description = "Critical: " + tickets[0].Description; tickets[2].Description = "Critical: " + tickets[2].Description; @@ -44,7 +45,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/supportTickets"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -59,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Returns_resource_meta_from_ResourceDefinition_in_included_resources() { // Arrange - var family = _fakers.ProductFamily.Generate(); + ProductFamily family = _fakers.ProductFamily.Generate(); family.Tickets = _fakers.SupportTicket.Generate(1); family.Tickets[0].Description = "Critical: " + family.Tickets[0].Description; @@ -70,10 +71,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/productFamilies/{family.StringId}?include=tickets"; + string route = $"/productFamilies/{family.StringId}?include=tickets"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResponseMetaTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResponseMetaTests.cs index d98fd84e8d..5edbc027ee 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/ResponseMetaTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -10,8 +11,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta { - public sealed class ResponseMetaTests - : IClassFixture, SupportDbContext>> + public sealed class ResponseMetaTests : IClassFixture, SupportDbContext>> { private readonly ExampleIntegrationTestContext, SupportDbContext> _testContext; @@ -24,7 +24,7 @@ public ResponseMetaTests(ExampleIntegrationTestContext(); }); - var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); options.IncludeTotalResourceCount = false; } @@ -40,7 +40,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/supportTickets"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs index 1f3d1be0a2..53304cb09f 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketDefinition.cs @@ -9,7 +9,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class SupportTicketDefinition : JsonApiResourceDefinition { - public SupportTicketDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + public SupportTicketDefinition(IResourceGraph resourceGraph) + : base(resourceGraph) { } @@ -22,7 +23,7 @@ public override IDictionary GetMeta(SupportTicket resource) ["hasHighPriority"] = true }; } - + return base.GetMeta(resource); } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketsController.cs index ad155dc489..a08e11371c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/SupportTicketsController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta { public sealed class SupportTicketsController : JsonApiController { - public SupportTicketsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public SupportTicketsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs index e2aa06abab..15df5c055c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Meta/TopLevelCountTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -11,8 +12,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.Meta { - public sealed class TopLevelCountTests - : IClassFixture, SupportDbContext>> + public sealed class TopLevelCountTests : IClassFixture, SupportDbContext>> { private readonly ExampleIntegrationTestContext, SupportDbContext> _testContext; private readonly SupportFakers _fakers = new SupportFakers(); @@ -26,7 +26,7 @@ public TopLevelCountTests(ExampleIntegrationTestContext), typeof(NeverSameResourceChangeTracker<>)); }); - var options = (JsonApiOptions) testContext.Factory.Services.GetRequiredService(); + var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); options.IncludeTotalResourceCount = true; } @@ -34,7 +34,7 @@ public TopLevelCountTests(ExampleIntegrationTestContext { @@ -46,7 +46,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/supportTickets"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -67,7 +67,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/supportTickets"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -80,7 +80,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Hides_resource_count_in_create_resource_response() { // Arrange - var newDescription = _fakers.SupportTicket.Generate().Description; + string newDescription = _fakers.SupportTicket.Generate().Description; var requestBody = new { @@ -97,7 +97,7 @@ public async Task Hides_resource_count_in_create_resource_response() const string route = "/supportTickets"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -109,9 +109,9 @@ public async Task Hides_resource_count_in_create_resource_response() public async Task Hides_resource_count_in_update_resource_response() { // Arrange - var existingTicket = _fakers.SupportTicket.Generate(); + SupportTicket existingTicket = _fakers.SupportTicket.Generate(); - var newDescription = _fakers.SupportTicket.Generate().Description; + string newDescription = _fakers.SupportTicket.Generate().Description; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -132,10 +132,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/supportTickets/" + existingTicket.StringId; + string route = "/supportTickets/" + existingTicket.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateDbContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateDbContext.cs index 6de2695ae9..c130f596b6 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateDbContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateDbContext.cs @@ -11,7 +11,8 @@ public sealed class ModelStateDbContext : DbContext public DbSet Directories { get; set; } public DbSet Files { get; set; } - public ModelStateDbContext(DbContextOptions options) : base(options) + public ModelStateDbContext(DbContextOptions options) + : base(options) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateValidationTests.cs index 337caf9c9a..84c3bc694c 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/ModelStateValidationTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -9,7 +10,8 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ModelStateValidation { - public sealed class ModelStateValidationTests : IClassFixture, ModelStateDbContext>> + public sealed class ModelStateValidationTests + : IClassFixture, ModelStateDbContext>> { private readonly ExampleIntegrationTestContext, ModelStateDbContext> _testContext; @@ -37,14 +39,14 @@ public async Task Cannot_create_resource_with_omitted_required_attribute() const string route = "/systemDirectories"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The Name field is required."); @@ -62,7 +64,7 @@ public async Task Cannot_create_resource_with_null_for_required_attribute_value( type = "systemDirectories", attributes = new { - name = (string) null, + name = (string)null, isCaseSensitive = true } } @@ -71,14 +73,14 @@ public async Task Cannot_create_resource_with_null_for_required_attribute_value( const string route = "/systemDirectories"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The Name field is required."); @@ -105,14 +107,14 @@ public async Task Cannot_create_resource_with_invalid_attribute_value() const string route = "/systemDirectories"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); @@ -139,7 +141,7 @@ public async Task Can_create_resource_with_valid_attribute_value() const string route = "/systemDirectories"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -168,26 +170,26 @@ public async Task Cannot_create_resource_with_multiple_violations() const string route = "/systemDirectories"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(3); - var error1 = responseDocument.Errors[0]; + Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Name field is required."); error1.Source.Pointer.Should().Be("/data/attributes/name"); - var error2 = responseDocument.Errors[1]; + Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field SizeInBytes must be between 0 and 9223372036854775807."); error2.Source.Pointer.Should().Be("/data/attributes/sizeInBytes"); - var error3 = responseDocument.Errors[2]; + Error error3 = responseDocument.Errors[2]; error3.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error3.Title.Should().Be("Input validation failed."); error3.Detail.Should().Be("The IsCaseSensitive field is required."); @@ -272,7 +274,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/systemDirectories"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -288,7 +290,7 @@ public async Task Can_add_to_annotated_ToMany_relationship() // Arrange var directory = new SystemDirectory { - Name="Projects", + Name = "Projects", IsCaseSensitive = true }; @@ -319,7 +321,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/systemDirectories/{directory.StringId}/relationships/files"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -359,7 +361,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = "/systemDirectories/" + directory.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -391,7 +393,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = directory.StringId, attributes = new { - name = (string) null + name = (string)null } } }; @@ -399,14 +401,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = "/systemDirectories/" + directory.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The Name field is required."); @@ -445,14 +447,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = "/systemDirectories/" + directory.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); @@ -505,20 +507,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/systemDirectories/-1"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(2); - var error1 = responseDocument.Errors[0]; + Error error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The field Id must match the regular expression '^[0-9]+$'."); error1.Source.Pointer.Should().Be("/data/attributes/id"); - var error2 = responseDocument.Errors[1]; + Error error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field Id must match the regular expression '^[0-9]+$'."); @@ -557,7 +559,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = "/systemDirectories/" + directory.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -668,7 +670,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = "/systemDirectories/" + directory.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -727,7 +729,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = "/systemDirectories/" + directory.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -781,7 +783,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = "/systemDirectories/" + directory.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -828,7 +830,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = "/systemDirectories/" + directory.StringId + "/relationships/parent"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -884,7 +886,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = "/systemDirectories/" + directory.StringId + "/relationships/files"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); @@ -898,7 +900,7 @@ public async Task Can_remove_from_annotated_ToMany_relationship() // Arrange var directory = new SystemDirectory { - Name="Projects", + Name = "Projects", IsCaseSensitive = true, Files = new List { @@ -924,7 +926,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/systemDirectories/{directory.StringId}/relationships/files"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/NoModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/NoModelStateValidationTests.cs index 1780d32aba..56e600e6e0 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/NoModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/NoModelStateValidationTests.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -37,7 +38,7 @@ public async Task Can_create_resource_with_invalid_attribute_value() const string route = "/systemDirectories"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -78,7 +79,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = "/systemDirectories/" + directory.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectoriesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectoriesController.cs index 5228903c5d..63ee816e0d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectoriesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectoriesController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ModelStateValidation { public sealed class SystemDirectoriesController : JsonApiController { - public SystemDirectoriesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public SystemDirectoriesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectory.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectory.cs index f78848863a..d291370ae6 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectory.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemDirectory.cs @@ -26,10 +26,10 @@ public sealed class SystemDirectory : Identifiable [Range(typeof(long), "0", "9223372036854775807")] public long SizeInBytes { get; set; } - [HasMany] + [HasMany] public ICollection Subdirectories { get; set; } - [HasMany] + [HasMany] public ICollection Files { get; set; } [HasOne] @@ -38,7 +38,7 @@ public sealed class SystemDirectory : Identifiable [HasOne] public SystemDirectory AlsoSelf { get; set; } - [HasOne] + [HasOne] public SystemDirectory Parent { get; set; } } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemFilesController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemFilesController.cs index 9278f59766..0a18e248f1 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemFilesController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/ModelStateValidation/SystemFilesController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ModelStateValidation { public sealed class SystemFilesController : JsonApiController { - public SystemFilesController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public SystemFilesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/DivingBoardsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/DivingBoardsController.cs index 826249bb22..0e055f84b0 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/DivingBoardsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/DivingBoardsController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.NamingConventions { public sealed class DivingBoardsController : JsonApiController { - public DivingBoardsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public DivingBoardsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingTests.cs index 91efaed905..1f96742ed1 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/KebabCasingTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Net; +using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -8,8 +9,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.NamingConventions { - public sealed class KebabCasingTests - : IClassFixture, SwimmingDbContext>> + public sealed class KebabCasingTests : IClassFixture, SwimmingDbContext>> { private readonly ExampleIntegrationTestContext, SwimmingDbContext> _testContext; private readonly SwimmingFakers _fakers = new SwimmingFakers(); @@ -23,7 +23,7 @@ public KebabCasingTests(ExampleIntegrationTestContext pools = _fakers.SwimmingPool.Generate(2); pools[1].DivingBoards = _fakers.DivingBoard.Generate(1); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -36,7 +36,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/public-api/swimming-pools?include=diving-boards"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -61,7 +61,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_filter_secondary_resources_with_sparse_fieldset() { // Arrange - var pool = _fakers.SwimmingPool.Generate(); + SwimmingPool pool = _fakers.SwimmingPool.Generate(); pool.WaterSlides = _fakers.WaterSlide.Generate(2); pool.WaterSlides[0].LengthInMeters = 1; pool.WaterSlides[1].LengthInMeters = 5; @@ -72,11 +72,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - var route = $"/public-api/swimming-pools/{pool.StringId}/water-slides" + + string route = $"/public-api/swimming-pools/{pool.StringId}/water-slides" + "?filter=greaterThan(length-in-meters,'1')&fields[water-slides]=length-in-meters"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -91,7 +91,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Can_create_resource() { // Arrange - var newPool = _fakers.SwimmingPool.Generate(); + SwimmingPool newPool = _fakers.SwimmingPool.Generate(); var requestBody = new { @@ -108,7 +108,7 @@ public async Task Can_create_resource() const string route = "/public-api/swimming-pools"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -117,7 +117,7 @@ public async Task Can_create_resource() responseDocument.SingleData.Type.Should().Be("swimming-pools"); responseDocument.SingleData.Attributes["is-indoor"].Should().Be(newPool.IsIndoor); - var newPoolId = int.Parse(responseDocument.SingleData.Id); + int newPoolId = int.Parse(responseDocument.SingleData.Id); string poolLink = route + $"/{newPoolId}"; responseDocument.SingleData.Relationships.Should().NotBeEmpty(); @@ -128,7 +128,7 @@ public async Task Can_create_resource() await _testContext.RunOnDatabaseAsync(async dbContext => { - var poolInDatabase = await dbContext.SwimmingPools.FirstWithIdAsync(newPoolId); + SwimmingPool poolInDatabase = await dbContext.SwimmingPools.FirstWithIdAsync(newPoolId); poolInDatabase.IsIndoor.Should().Be(newPool.IsIndoor); }); @@ -143,14 +143,14 @@ public async Task Applies_casing_convention_on_error_stack_trace() const string route = "/public-api/swimming-pools"; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Meta.Data.Should().ContainKey("stack-trace"); @@ -160,7 +160,7 @@ public async Task Applies_casing_convention_on_error_stack_trace() public async Task Applies_casing_convention_on_source_pointer_from_ModelState() { // Arrange - var existingBoard = _fakers.DivingBoard.Generate(); + DivingBoard existingBoard = _fakers.DivingBoard.Generate(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -181,17 +181,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - var route = "/public-api/diving-boards/" + existingBoard.StringId; + string route = "/public-api/diving-boards/" + existingBoard.StringId; // Act - var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - var error = responseDocument.Errors[0]; + Error error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The field HeightInMeters must be between 1 and 20."); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingPoolsController.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingPoolsController.cs index 6af99a7ea4..760227f2ff 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingPoolsController.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NamingConventions/SwimmingPoolsController.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.NamingConventions { public sealed class SwimmingPoolsController : JsonApiController { - public SwimmingPoolsController(IJsonApiOptions options, ILoggerFactory loggerFactory, - IResourceService resourceService) + public SwimmingPoolsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs index db76e954ab..19934d7fc3 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/NonJsonApiControllers/NonJsonApiControllerTests.cs @@ -25,10 +25,10 @@ public async Task Get_skips_middleware_and_formatters() // Arrange using var request = new HttpRequestMessage(HttpMethod.Get, "/NonJsonApi"); - var client = _factory.CreateClient(); + HttpClient client = _factory.CreateClient(); // Act - var httpResponse = await client.SendAsync(request); + HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -53,10 +53,10 @@ public async Task Post_skips_middleware_and_formatters() } }; - var client = _factory.CreateClient(); + HttpClient client = _factory.CreateClient(); // Act - var httpResponse = await client.SendAsync(request); + HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -72,10 +72,10 @@ public async Task Post_skips_error_handler() // Arrange using var request = new HttpRequestMessage(HttpMethod.Post, "/NonJsonApi"); - var client = _factory.CreateClient(); + HttpClient client = _factory.CreateClient(); // Act - var httpResponse = await client.SendAsync(request); + HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); @@ -100,10 +100,10 @@ public async Task Put_skips_middleware_and_formatters() } }; - var client = _factory.CreateClient(); + HttpClient client = _factory.CreateClient(); // Act - var httpResponse = await client.SendAsync(request); + HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -119,10 +119,10 @@ public async Task Patch_skips_middleware_and_formatters() // Arrange using var request = new HttpRequestMessage(HttpMethod.Patch, "/NonJsonApi?name=Janice"); - var client = _factory.CreateClient(); + HttpClient client = _factory.CreateClient(); // Act - var httpResponse = await client.SendAsync(request); + HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -138,10 +138,10 @@ public async Task Delete_skips_middleware_and_formatters() // Arrange using var request = new HttpRequestMessage(HttpMethod.Delete, "/NonJsonApi"); - var client = _factory.CreateClient(); + HttpClient client = _factory.CreateClient(); // Act - var httpResponse = await client.SendAsync(request); + HttpResponseMessage httpResponse = await client.SendAsync(request); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Blog.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Blog.cs index 2edbb10c99..6bcd37d005 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Blog.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/Blog.cs @@ -9,7 +9,7 @@ namespace JsonApiDotNetCoreExampleTests.IntegrationTests.QueryStrings [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Blog : Identifiable { - [Attr] + [Attr] public string Title { get; set; } [Attr] diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPost.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPost.cs index dea9789436..8805bda98a 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPost.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/QueryStrings/BlogPost.cs @@ -24,6 +24,7 @@ public sealed class BlogPost : Identifiable [NotMapped] [HasManyThrough(nameof(BlogPostLabels))] public ISet